Commit bbb0cbf3 authored by Eric Myhre's avatar Eric Myhre

Introduce 'quip' data building helpers.

parent 6de9f5cb
// quip is a package of quick ipld patterns.
//
// Most quip functions take a pointer to an error as their first argument.
// This has two purposes: if there's an error there, the quip function will do nothing;
// and if the quip function does something and creates an error, it puts it there.
// The effect of this is that most logic can be written very linearly.
//
// quip functions can be used to increase brevity without worrying about performance costs.
// None of the quip functions cause additional allocations in the course of their work.
// Benchmarks indicate no measurable speed penalties versus longhand manual error checking.
//
// This package is currently considered experimental, and may change.
// Feel free to use it, but be advised that code using it may require frequent
// updating until things settle; naming conventions in this package may be
// revised with relatively little warning.
package quip
import (
"github.com/ipld/go-ipld-prime"
)
// TODO:REVIEW: a few things about this outline and its symbol naming:
// - consistency/nomenclature check:
// - fluent package uses "BuildMap" at package scope. And then "CreateMap" on its NB facade (which takes a callback param). It's calling "NodeBuilder.BeginMap" internally, of course. (is fluent's current choice odd and deserving of revising?)
// - the "Build" vs "Create" prefixes here indicate methods that start with a builder or prototype and return a full node, vs methods that work on assemblers. (the fluent package doesn't have any methods that *don't* take callbacks, so there's no name component to talk about that.)
// - here currently we've used "BeginMap" for something that returns a MapAssembler (consistent with ipld.NB), and "BuildMap" takes a callback.
// - ditto the above bullet tree for List instead of Map.
// - many of these methods could have varying parameter types: ipld.Node forms, interface{} and dtrt forms, and callback-taking-assembler forms. They all need unique names.
// - this is the case for values, and similarly for keys (though for keys the most important ones are probably string | ipld.Node | pathSegment). The crossproduct there is sizable.
// - do we want to fill out this matrix completely?
// - what naming convention can we use to make this consistent?
// - do we actually want the non-callback/returns-MapAssembler BeginMap style to be something this package bothers to export?
// - hard to imagine actually wanting to use these.
// - since it turns out that the callbacks *do* get optimized quite reasonably by the compiler in common cases, there's no reason to avoid them.
// TODO: a few notable gaps in what's provided, which should improve terseness even more:
// - we don't have top-level functions for doing a full Build returning a Node (and saving you the np.NewBuilder preamble and nb.Build postamble). see naming issues discussion.
// - we don't have great shorthand for scalar assignment at the leaves. current best is a composition like `quip.AbsorbError(&err, na.AssignString(x))`.
// - do we want to make an `quip.Assign{Kind}(*error, *ipld.NodeAssembler, {kindPrimitive})` for each kind? seems like a lot of symbols.
// - there's also generally a whole `quip.MapEntry(... {...})` or `ListEntry` clause around *that*, with the single Absorb+Assign line in the middle. that's boilerplate that needs trimming as well.
// - so... twice as many Assign helpers, because it's kinda necessary to specialize them to map and list assemblers too? uff.
func AbsorbError(e *error, err error) {
if *e != nil {
return
}
if err != nil {
*e = err
}
}
func BeginMap(e *error, na ipld.NodeAssembler, sizeHint int64) ipld.MapAssembler {
if *e != nil {
return nil
}
ma, err := na.BeginMap(sizeHint)
if err != nil {
*e = err
return nil
}
return ma
}
func BuildMap(e *error, na ipld.NodeAssembler, sizeHint int64, fn func(ma ipld.MapAssembler)) {
if *e != nil {
return
}
ma, err := na.BeginMap(sizeHint)
if err != nil {
*e = err
return
}
fn(ma)
*e = ma.Finish()
}
func MapEntry(e *error, ma ipld.MapAssembler, k string, fn func(va ipld.NodeAssembler)) {
if *e != nil {
return
}
va, err := ma.AssembleEntry(k)
if err != nil {
*e = err
return
}
fn(va)
}
func BeginList(e *error, na ipld.NodeAssembler, sizeHint int64) ipld.ListAssembler {
if *e != nil {
return nil
}
la, err := na.BeginList(sizeHint)
if err != nil {
*e = err
return nil
}
return la
}
func BuildList(e *error, na ipld.NodeAssembler, sizeHint int64, fn func(la ipld.ListAssembler)) {
if *e != nil {
return
}
la, err := na.BeginList(sizeHint)
if err != nil {
*e = err
return
}
fn(la)
*e = la.Finish()
}
func ListEntry(e *error, la ipld.ListAssembler, fn func(va ipld.NodeAssembler)) {
if *e != nil {
return
}
fn(la.AssembleValue())
}
func CopyRange(e *error, la ipld.ListAssembler, src ipld.Node, start, end int64) {
if *e != nil {
return
}
if start >= src.Length() {
return
}
if end < 0 {
end = src.Length()
}
if end < start {
return
}
for i := start; i < end; i++ {
n, err := src.LookupByIndex(i)
if err != nil {
*e = err
return
}
if err := la.AssembleValue().AssignNode(n); err != nil {
*e = err
return
}
}
return
}
package quip_test
import (
"strings"
"testing"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagjson"
"github.com/ipld/go-ipld-prime/fluent"
"github.com/ipld/go-ipld-prime/fluent/quip"
basicnode "github.com/ipld/go-ipld-prime/node/basic"
)
func BenchmarkQuip(b *testing.B) {
var n ipld.Node
var err error
for i := 0; i < b.N; i++ {
n, err = f1()
}
_ = n
if err != nil {
b.Fatal(err)
}
}
func BenchmarkUnmarshal(b *testing.B) {
var n ipld.Node
var err error
serial := `[{
"destination": "/",
"type": "overlay",
"source": "none",
"options": [
"lowerdir=/",
"upperdir=/tmp/overlay-root/upper",
"workdir=/tmp/overlay-root/work"
]
}]`
r := strings.NewReader(serial)
for i := 0; i < b.N; i++ {
nb := basicnode.Prototype.Any.NewBuilder()
err = dagjson.Decoder(nb, r)
n = nb.Build()
r.Reset(serial)
}
_ = n
if err != nil {
b.Fatal(err)
}
}
func BenchmarkFluent(b *testing.B) {
var n ipld.Node
var err error
for i := 0; i < b.N; i++ {
n, err = fluent.BuildList(basicnode.Prototype.Any, -1, func(la fluent.ListAssembler) {
la.AssembleValue().CreateMap(4, func(ma fluent.MapAssembler) {
ma.AssembleEntry("destination").AssignString("/")
ma.AssembleEntry("type").AssignString("overlay")
ma.AssembleEntry("source").AssignString("none")
ma.AssembleEntry("options").CreateList(-1, func(la fluent.ListAssembler) {
la.AssembleValue().AssignString("lowerdir=" + "/")
la.AssembleValue().AssignString("upperdir=" + "/tmp/overlay-root/upper")
la.AssembleValue().AssignString("workdir=" + "/tmp/overlay-root/work")
})
})
})
}
_ = n
if err != nil {
b.Fatal(err)
}
}
func BenchmarkReflect(b *testing.B) {
var n ipld.Node
var err error
val := []interface{}{
map[string]interface{}{
"destination": "/",
"type": "overlay",
"source": "none",
"options": []string{
"lowerdir=/",
"upperdir=/tmp/overlay-root/upper",
"workdir=/tmp/overlay-root/work",
},
},
}
for i := 0; i < b.N; i++ {
n, err = fluent.Reflect(basicnode.Prototype.Any, val)
}
_ = n
if err != nil {
b.Fatal(err)
}
}
func BenchmarkReflectIncludingInitialization(b *testing.B) {
var n ipld.Node
var err error
for i := 0; i < b.N; i++ {
n, err = fluent.Reflect(basicnode.Prototype.Any, []interface{}{
map[string]interface{}{
"destination": "/",
"type": "overlay",
"source": "none",
"options": []string{
"lowerdir=/",
"upperdir=/tmp/overlay-root/upper",
"workdir=/tmp/overlay-root/work",
},
},
})
}
_ = n
if err != nil {
b.Fatal(err)
}
}
func BenchmarkAgonizinglyBare(b *testing.B) {
var n ipld.Node
var err error
for i := 0; i < b.N; i++ {
n, err = fab()
}
_ = n
if err != nil {
b.Fatal(err)
}
}
func fab() (ipld.Node, error) {
nb := basicnode.Prototype.Any.NewBuilder()
la1, err := nb.BeginList(-1)
if err != nil {
return nil, err
}
ma, err := la1.AssembleValue().BeginMap(4)
if err != nil {
return nil, err
}
va, err := ma.AssembleEntry("destination")
if err != nil {
return nil, err
}
err = va.AssignString("/")
if err != nil {
return nil, err
}
va, err = ma.AssembleEntry("type")
if err != nil {
return nil, err
}
err = va.AssignString("overlay")
if err != nil {
return nil, err
}
va, err = ma.AssembleEntry("source")
if err != nil {
return nil, err
}
err = va.AssignString("none")
if err != nil {
return nil, err
}
va, err = ma.AssembleEntry("options")
if err != nil {
return nil, err
}
la2, err := va.BeginList(-4)
if err != nil {
return nil, err
}
err = la2.AssembleValue().AssignString("lowerdir=" + "/")
if err != nil {
return nil, err
}
err = la2.AssembleValue().AssignString("upperdir=" + "/tmp/overlay-root/upper")
if err != nil {
return nil, err
}
err = la2.AssembleValue().AssignString("workdir=" + "/tmp/overlay-root/work")
if err != nil {
return nil, err
}
err = la2.Finish()
if err != nil {
return nil, err
}
err = ma.Finish()
if err != nil {
return nil, err
}
err = la1.Finish()
if err != nil {
return nil, err
}
return nb.Build(), nil
}
func f1() (_ ipld.Node, err error) {
nb := basicnode.Prototype.Any.NewBuilder()
quip.BuildList(&err, nb, -1, func(la ipld.ListAssembler) {
f2(la.AssembleValue(),
"/",
"overlay",
"none",
[]string{
"lowerdir=" + "/",
"upperdir=" + "/tmp/overlay-root/upper",
"workdir=" + "/tmp/overlay-root/work",
},
)
})
if err != nil {
return nil, err
}
return nb.Build(), nil
}
func f2(na ipld.NodeAssembler, a string, b string, c string, d []string) (err error) {
quip.BuildMap(&err, na, 4, func(ma ipld.MapAssembler) {
quip.MapEntry(&err, ma, "destination", func(va ipld.NodeAssembler) {
quip.AbsorbError(&err, va.AssignString(a))
})
quip.MapEntry(&err, ma, "type", func(va ipld.NodeAssembler) {
quip.AbsorbError(&err, va.AssignString(b))
})
quip.MapEntry(&err, ma, "source", func(va ipld.NodeAssembler) {
quip.AbsorbError(&err, va.AssignString(c))
})
quip.MapEntry(&err, ma, "options", func(va ipld.NodeAssembler) {
quip.BuildList(&err, va, int64(len(d)), func(la ipld.ListAssembler) {
for i := range d {
quip.ListEntry(&err, la, func(va ipld.NodeAssembler) {
quip.AbsorbError(&err, va.AssignString(d[i]))
})
}
})
})
})
return
}
package quip_test
import (
"os"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagjson"
"github.com/ipld/go-ipld-prime/fluent/quip"
basicnode "github.com/ipld/go-ipld-prime/node/basic"
)
func Example() {
nb := basicnode.Prototype.Any.NewBuilder()
var err error
quip.BuildMap(&err, nb, 4, func(ma ipld.MapAssembler) {
quip.MapEntry(&err, ma, "some key", func(va ipld.NodeAssembler) {
quip.AbsorbError(&err, va.AssignString("some value"))
})
quip.MapEntry(&err, ma, "another key", func(va ipld.NodeAssembler) {
quip.AbsorbError(&err, va.AssignString("another value"))
})
quip.MapEntry(&err, ma, "nested map", func(va ipld.NodeAssembler) {
quip.BuildMap(&err, va, 2, func(ma ipld.MapAssembler) {
quip.MapEntry(&err, ma, "deeper entries", func(va ipld.NodeAssembler) {
quip.AbsorbError(&err, va.AssignString("deeper values"))
})
quip.MapEntry(&err, ma, "more deeper entries", func(va ipld.NodeAssembler) {
quip.AbsorbError(&err, va.AssignString("more deeper values"))
})
})
})
quip.MapEntry(&err, ma, "nested list", func(va ipld.NodeAssembler) {
quip.BuildList(&err, va, 2, func(la ipld.ListAssembler) {
quip.ListEntry(&err, la, func(va ipld.NodeAssembler) {
quip.AbsorbError(&err, va.AssignInt(1))
})
quip.ListEntry(&err, la, func(va ipld.NodeAssembler) {
quip.AbsorbError(&err, va.AssignInt(2))
})
})
})
})
if err != nil {
panic(err)
}
n := nb.Build()
dagjson.Encoder(n, os.Stdout)
// Output:
// {
// "some key": "some value",
// "another key": "another value",
// "nested map": {
// "deeper entries": "deeper values",
// "more deeper entries": "more deeper values"
// },
// "nested list": [
// 1,
// 2
// ]
// }
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment