diff --git a/datamodel/amender.go b/datamodel/amender.go new file mode 100644 index 00000000..9a146e66 --- /dev/null +++ b/datamodel/amender.go @@ -0,0 +1,13 @@ +package datamodel + +type Amender interface { + NodeBuilder + + // Get returns the node at the specified path. It will not create any intermediate nodes because this is just a + // retrieval and not a modification operation. + Get(path Path) (Node, error) + + // Transform will do an in-place transformation of the node at the specified path and return its previous value. + // If `createParents = true`, any missing parents will be created, otherwise this function will return an error. + Transform(path Path, createParents bool) (Node, error) +} diff --git a/datamodel/node.go b/datamodel/node.go index 625f472d..80cad009 100644 --- a/datamodel/node.go +++ b/datamodel/node.go @@ -238,7 +238,7 @@ type NodePrototype interface { // volumes of data, detecting and using this feature can result in significant // performance savings. type NodePrototypeSupportingAmend interface { - AmendingBuilder(base Node) NodeBuilder + AmendingBuilder(base Node) Amender // FUTURE: probably also needs a `AmendingWithout(base Node, filter func(k,v) bool) NodeBuilder`, or similar. // ("deletion" based APIs are also possible but both more complicated in interfaces added, and prone to accidentally quadratic usage.) // FUTURE: there should be some stdlib `Copy` (?) methods that automatically look for this feature, and fallback if absent. diff --git a/node/basicnode/map.go b/node/basicnode/map.go index 9a86fc52..0f973978 100644 --- a/node/basicnode/map.go +++ b/node/basicnode/map.go @@ -3,6 +3,8 @@ package basicnode import ( "fmt" + "github.com/emirpasic/gods/maps/linkedhashmap" + "github.com/ipld/go-ipld-prime/datamodel" "github.com/ipld/go-ipld-prime/node/mixins" ) @@ -18,8 +20,7 @@ var ( // It can contain any kind of value. // plainMap is also embedded in the 'any' struct and usable from there. type plainMap struct { - m map[string]datamodel.Node // string key -- even if a runtime schema wrapper is using us for storage, we must have a comparable type here, and string is all we know. - t []plainMap__Entry // table for fast iteration, order keeping, and yielding pointers to enable alloc/conv amortization. + m *linkedhashmap.Map } type plainMap__Entry struct { @@ -34,11 +35,11 @@ func (plainMap) Kind() datamodel.Kind { return datamodel.Kind_Map } func (n *plainMap) LookupByString(key string) (datamodel.Node, error) { - v, exists := n.m[key] - if !exists { + if v, exists := n.m.Get(key); !exists { return nil, datamodel.ErrNotExists{Segment: datamodel.PathSegmentOfString(key)} + } else { + return v.(*plainMap__Entry).v, nil } - return v, nil } func (n *plainMap) LookupByNode(key datamodel.Node) (datamodel.Node, error) { ks, err := key.AsString() @@ -54,13 +55,14 @@ func (n *plainMap) LookupBySegment(seg datamodel.PathSegment) (datamodel.Node, e return n.LookupByString(seg.String()) } func (n *plainMap) MapIterator() datamodel.MapIterator { - return &plainMap_MapIterator{n, 0} + itr := n.m.Iterator() + return &plainMap_MapIterator{n, &itr, 0} } func (plainMap) ListIterator() datamodel.ListIterator { return nil } func (n *plainMap) Length() int64 { - return int64(len(n.t)) + return int64(n.m.Size()) } func (plainMap) IsAbsent() bool { return false @@ -92,6 +94,7 @@ func (plainMap) Prototype() datamodel.NodePrototype { type plainMap_MapIterator struct { n *plainMap + ni *linkedhashmap.Iterator idx int } @@ -99,13 +102,15 @@ func (itr *plainMap_MapIterator) Next() (k datamodel.Node, v datamodel.Node, _ e if itr.Done() { return nil, nil, datamodel.ErrIteratorOverread{} } - k = &itr.n.t[itr.idx].k - v = itr.n.t[itr.idx].v + itr.ni.Next() + entry := itr.ni.Value().(*plainMap__Entry) + k = &entry.k + v = entry.v itr.idx++ return } func (itr *plainMap_MapIterator) Done() bool { - return itr.idx >= len(itr.n.t) + return itr.idx >= itr.n.m.Size() } // -- NodePrototype --> @@ -167,8 +172,7 @@ func (na *plainMap__Assembler) BeginMap(sizeHint int64) (datamodel.MapAssembler, sizeHint = 0 } // Allocate storage space. - na.w.t = make([]plainMap__Entry, 0, sizeHint) - na.w.m = make(map[string]datamodel.Node, sizeHint) + na.w.m = linkedhashmap.New() // That's it; return self as the MapAssembler. We already have all the right methods on this structure. return na, nil } @@ -247,12 +251,12 @@ func (ma *plainMap__Assembler) AssembleEntry(k string) (datamodel.NodeAssembler, panic("misuse") } // Check for dup keys; error if so. - _, exists := ma.w.m[k] + _, exists := ma.w.m.Get(k) if exists { return nil, datamodel.ErrRepeatedMapKey{Key: plainString(k)} } ma.state = maState_midValue - ma.w.t = append(ma.w.t, plainMap__Entry{k: plainString(k)}) + ma.w.m.Put(k, &plainMap__Entry{k: plainString(k)}) // Make value assembler valid by giving it pointer back to whole 'ma'; yield it. ma.va.ma = ma return &ma.va, nil @@ -325,7 +329,7 @@ func (plainMap__KeyAssembler) AssignFloat(float64) error { func (mka *plainMap__KeyAssembler) AssignString(v string) error { // Check for dup keys; error if so. // (And, backtrack state to accepting keys again so we don't get eternally wedged here.) - _, exists := mka.ma.w.m[v] + _, exists := mka.ma.w.m.Get(v) if exists { mka.ma.state = maState_initial mka.ma = nil // invalidate self to prevent further incorrect use. @@ -335,8 +339,7 @@ func (mka *plainMap__KeyAssembler) AssignString(v string) error { // we'll be doing map insertions after we get the value in hand. // (There's no need to delegate to another assembler for the key type, // because we're just at Data Model level here, which only regards plain strings.) - mka.ma.w.t = append(mka.ma.w.t, plainMap__Entry{}) - mka.ma.w.t[len(mka.ma.w.t)-1].k = plainString(v) + mka.ma.w.m.Put(v, &plainMap__Entry{k: plainString(v)}) // Update parent assembler state: clear to proceed. mka.ma.state = maState_expectValue mka.ma = nil // invalidate self to prevent further incorrect use. @@ -403,9 +406,10 @@ func (mva *plainMap__ValueAssembler) AssignLink(v datamodel.Link) error { return mva.AssignNode(&vb) } func (mva *plainMap__ValueAssembler) AssignNode(v datamodel.Node) error { - l := len(mva.ma.w.t) - 1 - mva.ma.w.t[l].v = v - mva.ma.w.m[string(mva.ma.w.t[l].k)] = v + itr := mva.ma.w.m.Iterator() + itr.Last() + val := itr.Value().(*plainMap__Entry) + val.v = v mva.ma.state = maState_initial mva.ma = nil // invalidate self to prevent further incorrect use. return nil diff --git a/storage/benchmarks/go.sum b/storage/benchmarks/go.sum index 17d422b0..ac0e53d9 100644 --- a/storage/benchmarks/go.sum +++ b/storage/benchmarks/go.sum @@ -6,8 +6,6 @@ github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZx github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= diff --git a/traversal/amendAny.go b/traversal/amendAny.go deleted file mode 100644 index 79d1ebb6..00000000 --- a/traversal/amendAny.go +++ /dev/null @@ -1,129 +0,0 @@ -package traversal - -import "github.com/ipld/go-ipld-prime/datamodel" - -var ( - _ datamodel.Node = &anyAmender{} - _ Amender = &anyAmender{} -) - -type anyAmender struct { - amendCfg -} - -func (opts AmendOptions) newAnyAmender(base datamodel.Node, parent Amender, create bool) Amender { - // If the base node is already an any-amender, reuse it but reset `parent` and `created`. - if amd, castOk := base.(*anyAmender); castOk { - return &anyAmender{amendCfg{&opts, amd.base, parent, create}} - } else { - return &anyAmender{amendCfg{&opts, base, parent, create}} - } -} - -// -- Node --> - -func (a *anyAmender) Kind() datamodel.Kind { - return a.base.Kind() -} - -func (a *anyAmender) LookupByString(key string) (datamodel.Node, error) { - return a.base.LookupByString(key) -} - -func (a *anyAmender) LookupByNode(key datamodel.Node) (datamodel.Node, error) { - return a.base.LookupByNode(key) -} - -func (a *anyAmender) LookupByIndex(idx int64) (datamodel.Node, error) { - return a.base.LookupByIndex(idx) -} - -func (a *anyAmender) LookupBySegment(seg datamodel.PathSegment) (datamodel.Node, error) { - return a.base.LookupBySegment(seg) -} - -func (a *anyAmender) MapIterator() datamodel.MapIterator { - return a.base.MapIterator() -} - -func (a *anyAmender) ListIterator() datamodel.ListIterator { - return a.base.ListIterator() -} - -func (a *anyAmender) Length() int64 { - return a.base.Length() -} - -func (a *anyAmender) IsAbsent() bool { - return a.base.IsAbsent() -} - -func (a *anyAmender) IsNull() bool { - return a.base.IsNull() -} - -func (a *anyAmender) AsBool() (bool, error) { - return a.base.AsBool() -} - -func (a *anyAmender) AsInt() (int64, error) { - return a.base.AsInt() -} - -func (a *anyAmender) AsFloat() (float64, error) { - return a.base.AsFloat() -} - -func (a *anyAmender) AsString() (string, error) { - return a.base.AsString() -} - -func (a *anyAmender) AsBytes() ([]byte, error) { - return a.base.AsBytes() -} - -func (a *anyAmender) AsLink() (datamodel.Link, error) { - return a.base.AsLink() -} - -func (a *anyAmender) Prototype() datamodel.NodePrototype { - return a.base.Prototype() -} - -// -- Amender --> - -func (a *anyAmender) Get(prog *Progress, path datamodel.Path, trackProgress bool) (datamodel.Node, error) { - // If the base node is an amender, use it, otherwise return the base node. - if amd, castOk := a.base.(Amender); castOk { - return amd.Get(prog, path, trackProgress) - } - return a.base, nil -} - -func (a *anyAmender) Transform(prog *Progress, path datamodel.Path, fn TransformFn, createParents bool) (datamodel.Node, error) { - // Allow the base node to be replaced. - if path.Len() == 0 { - prevNode := a.Build() - if newNode, err := fn(*prog, prevNode); err != nil { - return nil, err - } else { - // Go through `newAnyAmender` in case `newNode` is already an any-amender. - *a = *a.opts.newAnyAmender(newNode, a.parent, a.created).(*anyAmender) - return prevNode, nil - } - } - // If the base node is an amender, use it, otherwise panic. - if amd, castOk := a.base.(Amender); castOk { - return amd.Transform(prog, path, fn, createParents) - } - panic("misuse") -} - -func (a *anyAmender) Build() datamodel.Node { - // `anyAmender` is also a `Node`. - return (datamodel.Node)(a) -} - -func (a *anyAmender) isCreated() bool { - return a.created -} diff --git a/traversal/amendLink.go b/traversal/amendLink.go deleted file mode 100644 index 2e8b3a00..00000000 --- a/traversal/amendLink.go +++ /dev/null @@ -1,246 +0,0 @@ -package traversal - -import ( - "fmt" - - "github.com/ipld/go-ipld-prime/datamodel" - "github.com/ipld/go-ipld-prime/linking" - "github.com/ipld/go-ipld-prime/node/basicnode" - "github.com/ipld/go-ipld-prime/node/mixins" -) - -var ( - _ datamodel.Node = &linkAmender{} - _ Amender = &linkAmender{} -) - -type linkAmender struct { - amendCfg - - nLink datamodel.Link // newest link: can be `nil` if a transformation occurred, but it hasn't been recomputed - pLink datamodel.Link // previous link: will always have a valid value, even if not the latest - child Amender - linkCtx linking.LinkContext - linkSys linking.LinkSystem -} - -func (opts AmendOptions) newLinkAmender(base datamodel.Node, parent Amender, create bool) Amender { - // If the base node is already a link-amender, reuse the mutation state but reset `parent` and `created`. - if amd, castOk := base.(*linkAmender); castOk { - la := &linkAmender{amendCfg{&opts, amd.base, parent, create}, amd.nLink, amd.pLink, amd.child, amd.linkCtx, amd.linkSys} - // Make a copy of the child amender so that it has its own mutation state - if la.child != nil { - child := la.child.Build() - la.child = opts.newAmender(child, la, child.Kind(), false) - } - return la - } else { - // Start with fresh state because existing metadata could not be reused. - link, err := base.AsLink() - if err != nil { - panic("misuse") - } - // `linkCtx` and `linkSys` can be defaulted since they're only needed for recomputing the link after a - // transformation occurs, and such a transformation would have populated them correctly. - return &linkAmender{amendCfg{&opts, base, parent, create}, link, link, nil, linking.LinkContext{}, linking.LinkSystem{}} - } -} - -// -- Node --> - -func (a *linkAmender) Kind() datamodel.Kind { - return datamodel.Kind_Link -} - -func (a *linkAmender) LookupByString(key string) (datamodel.Node, error) { - return mixins.Link{TypeName: "linkAmender"}.LookupByString(key) -} - -func (a *linkAmender) LookupByNode(key datamodel.Node) (datamodel.Node, error) { - return mixins.Link{TypeName: "linkAmender"}.LookupByNode(key) -} - -func (a *linkAmender) LookupByIndex(idx int64) (datamodel.Node, error) { - return mixins.Link{TypeName: "linkAmender"}.LookupByIndex(idx) -} - -func (a *linkAmender) LookupBySegment(seg datamodel.PathSegment) (datamodel.Node, error) { - return mixins.Link{TypeName: "link"}.LookupBySegment(seg) -} - -func (a *linkAmender) MapIterator() datamodel.MapIterator { - return nil -} - -func (a *linkAmender) ListIterator() datamodel.ListIterator { - return nil -} - -func (a *linkAmender) Length() int64 { - return -1 -} - -func (a *linkAmender) IsAbsent() bool { - return false -} - -func (a *linkAmender) IsNull() bool { - return false -} - -func (a *linkAmender) AsBool() (bool, error) { - return mixins.Link{TypeName: "linkAmender"}.AsBool() -} - -func (a *linkAmender) AsInt() (int64, error) { - return mixins.Link{TypeName: "linkAmender"}.AsInt() -} - -func (a *linkAmender) AsFloat() (float64, error) { - return mixins.Link{TypeName: "linkAmender"}.AsFloat() -} - -func (a *linkAmender) AsString() (string, error) { - return mixins.Link{TypeName: "linkAmender"}.AsString() -} - -func (a *linkAmender) AsBytes() ([]byte, error) { - return mixins.Link{TypeName: "linkAmender"}.AsBytes() -} - -func (a *linkAmender) AsLink() (datamodel.Link, error) { - return a.computeLink() -} - -func (a *linkAmender) Prototype() datamodel.NodePrototype { - return basicnode.Prototype.Link -} - -// -- Amender --> - -func (a *linkAmender) Get(prog *Progress, path datamodel.Path, trackProgress bool) (datamodel.Node, error) { - // Check the budget - if prog.Budget != nil { - if prog.Budget.LinkBudget <= 0 { - return nil, &ErrBudgetExceeded{BudgetKind: "link", Path: prog.Path, Link: a.validLink()} - } - prog.Budget.LinkBudget-- - } - err := a.loadLink(prog, trackProgress) - if err != nil { - return nil, err - } - if path.Len() == 0 { - return a.Build(), nil - } - return a.child.Get(prog, path, trackProgress) -} - -func (a *linkAmender) Transform(prog *Progress, path datamodel.Path, fn TransformFn, createParents bool) (datamodel.Node, error) { - // Allow the base node to be replaced. - if path.Len() == 0 { - prevNode := a.Build() - if newNode, err := fn(*prog, prevNode); err != nil { - return nil, err - } else if newNode.Kind() != datamodel.Kind_Link { - return nil, fmt.Errorf("transform: cannot transform root into incompatible type: %q", newNode.Kind()) - } else { - // Go through `newLinkAmender` in case `newNode` is already a link-amender. - *a = *a.opts.newLinkAmender(newNode, a.parent, a.created).(*linkAmender) - return prevNode, nil - } - } - err := a.loadLink(prog, true) - if err != nil { - return nil, err - } - childVal, err := a.child.Transform(prog, path, fn, createParents) - if err != nil { - return nil, err - } - if a.opts.LazyLinkUpdate { - // Reset the link and lazily compute it when it is needed instead of on every transformation. - a.nLink = nil - } else { - newLink, err := a.linkSys.Store(a.linkCtx, a.nLink.Prototype(), a.child.Build()) - if err != nil { - return nil, fmt.Errorf("transform: error storing transformed node at %q: %w", prog.Path, err) - } - a.nLink = newLink - } - return childVal, nil -} - -func (a *linkAmender) Build() datamodel.Node { - // `linkAmender` is also a `Node`. - return (datamodel.Node)(a) -} - -func (a *linkAmender) isCreated() bool { - return a.created -} - -// validLink will return a valid `Link`, whether the base value, an intermediate recomputed value, or the latest value. -func (a *linkAmender) validLink() datamodel.Link { - if a.nLink == nil { - return a.pLink - } - return a.nLink -} - -func (a *linkAmender) computeLink() (datamodel.Link, error) { - // `nLink` can be `nil` if lazy link computation is enabled and the child node has been transformed, but the updated - // link has not yet been requested (and thus not recomputed). - if a.nLink == nil { - // We've already validated that `base` is a valid `Link` and so we don't care about a conversion error here. - baseLink, _ := a.base.AsLink() - lp := baseLink.Prototype() - // `nLink` will only be `nil` if a transformation made it "dirty", indicating that it needs to be recomputed. In - // this case, `child` will always have a valid value since it would have already been loaded/updated, so we - // don't need to check. - newLink, err := a.linkSys.ComputeLink(lp, a.child.Build()) - if err != nil { - return nil, err - } - a.nLink = newLink - } - return a.nLink, nil -} - -func (a *linkAmender) loadLink(prog *Progress, trackProgress bool) error { - if a.child == nil { - // Check the budget - if prog.Budget != nil { - if prog.Budget.LinkBudget <= 0 { - return &ErrBudgetExceeded{BudgetKind: "link", Path: prog.Path, Link: a.validLink()} - } - prog.Budget.LinkBudget-- - } - // Put together the context info we'll offer to the loader and prototypeChooser. - a.linkCtx = linking.LinkContext{ - Ctx: prog.Cfg.Ctx, - LinkPath: prog.Path, - LinkNode: a.Build(), - ParentNode: a.parent.Build(), - } - a.linkSys = prog.Cfg.LinkSystem - // `child` will only be `nil` if it was never loaded. In this case, `nLink` will always be valid, so we don't - // need to check. - // Pick what in-memory format we will build. - np, err := prog.Cfg.LinkTargetNodePrototypeChooser(a.nLink, a.linkCtx) - if err != nil { - return fmt.Errorf("error traversing node at %q: could not load link %q: %w", prog.Path, a.nLink, err) - } - // Load link - child, err := a.linkSys.Load(a.linkCtx, a.nLink, np) - if err != nil { - return fmt.Errorf("error traversing node at %q: could not load link %q: %w", prog.Path, a.nLink, err) - } - a.child = a.opts.newAmender(child, a, child.Kind(), false) - if trackProgress { - prog.LastBlock.Path = prog.Path - prog.LastBlock.Link = a.nLink - } - } - return nil -} diff --git a/traversal/amendList.go b/traversal/amendList.go deleted file mode 100644 index 01492165..00000000 --- a/traversal/amendList.go +++ /dev/null @@ -1,345 +0,0 @@ -package traversal - -import ( - "fmt" - "github.com/emirpasic/gods/lists/arraylist" - - "github.com/ipld/go-ipld-prime/datamodel" - "github.com/ipld/go-ipld-prime/node/basicnode" - "github.com/ipld/go-ipld-prime/node/mixins" -) - -var ( - _ datamodel.Node = &listAmender{} - _ Amender = &listAmender{} -) - -type listElement struct { - baseIdx int - elem datamodel.Node -} - -type listAmender struct { - amendCfg - - mods arraylist.List -} - -func (opts AmendOptions) newListAmender(base datamodel.Node, parent Amender, create bool) Amender { - // If the base node is already a list-amender, reuse the mutation state but reset `parent` and `created`. - if amd, castOk := base.(*listAmender); castOk { - return &listAmender{amendCfg{&opts, amd.base, parent, create}, amd.mods} - } else { - // Start with fresh state because existing metadata could not be reused. - var elems []interface{} - if base != nil { - elems = make([]interface{}, base.Length()) - for i := range elems { - elems[i] = listElement{i, nil} - } - } else { - elems = make([]interface{}, 0) - } - return &listAmender{amendCfg{&opts, base, parent, create}, *arraylist.New(elems...)} - } -} - -// -- Node --> - -func (a *listAmender) Kind() datamodel.Kind { - return datamodel.Kind_List -} - -func (a *listAmender) LookupByString(key string) (datamodel.Node, error) { - return mixins.List{TypeName: "listAmender"}.LookupByString(key) -} - -func (a *listAmender) LookupByNode(key datamodel.Node) (datamodel.Node, error) { - return mixins.List{TypeName: "listAmender"}.LookupByNode(key) -} - -func (a *listAmender) LookupByIndex(idx int64) (datamodel.Node, error) { - seg := datamodel.PathSegmentOfInt(idx) - if mod, exists := a.mods.Get(int(idx)); exists { - child := mod.(listElement) - if child.elem == nil { - bn, err := a.base.LookupByIndex(int64(child.baseIdx)) - if err != nil { - return nil, err - } - child.elem = bn - return bn, nil - } - return child.elem, nil - } - return nil, datamodel.ErrNotExists{Segment: seg} -} - -func (a *listAmender) LookupBySegment(seg datamodel.PathSegment) (datamodel.Node, error) { - idx, err := seg.Index() - if err != nil { - return nil, datamodel.ErrInvalidSegmentForList{TroubleSegment: seg, Reason: err} - } - return a.LookupByIndex(idx) -} - -func (a *listAmender) MapIterator() datamodel.MapIterator { - return nil -} - -func (a *listAmender) ListIterator() datamodel.ListIterator { - modsItr := a.mods.Iterator() - return &listAmender_Iterator{a, &modsItr, 0} -} - -func (a *listAmender) Length() int64 { - return int64(a.mods.Size()) -} - -func (a *listAmender) IsAbsent() bool { - return false -} - -func (a *listAmender) IsNull() bool { - return false -} - -func (a *listAmender) AsBool() (bool, error) { - return mixins.Map{TypeName: "listAmender"}.AsBool() -} - -func (a *listAmender) AsInt() (int64, error) { - return mixins.Map{TypeName: "listAmender"}.AsInt() -} - -func (a *listAmender) AsFloat() (float64, error) { - return mixins.Map{TypeName: "listAmender"}.AsFloat() -} - -func (a *listAmender) AsString() (string, error) { - return mixins.Map{TypeName: "listAmender"}.AsString() -} - -func (a *listAmender) AsBytes() ([]byte, error) { - return mixins.Map{TypeName: "listAmender"}.AsBytes() -} - -func (a *listAmender) AsLink() (datamodel.Link, error) { - return mixins.Map{TypeName: "listAmender"}.AsLink() -} - -func (a *listAmender) Prototype() datamodel.NodePrototype { - return basicnode.Prototype.List -} - -type listAmender_Iterator struct { - amd *listAmender - modsItr *arraylist.Iterator - idx int -} - -func (itr *listAmender_Iterator) Next() (idx int64, v datamodel.Node, err error) { - if itr.Done() { - return -1, nil, datamodel.ErrIteratorOverread{} - } - if itr.modsItr.Next() { - idx = int64(itr.modsItr.Index()) - v, err = itr.amd.LookupByIndex(idx) - if err != nil { - return -1, nil, err - } - itr.idx++ - return - } - return -1, nil, datamodel.ErrIteratorOverread{} -} - -func (itr *listAmender_Iterator) Done() bool { - return int64(itr.idx) >= itr.amd.Length() -} - -// -- Amender --> - -func (a *listAmender) Get(prog *Progress, path datamodel.Path, trackProgress bool) (datamodel.Node, error) { - // If the root is requested, return the `Node` view of the amender. - if path.Len() == 0 { - return a.Build(), nil - } - // Check the budget - if prog.Budget != nil { - if prog.Budget.NodeBudget <= 0 { - return nil, &ErrBudgetExceeded{BudgetKind: "node", Path: prog.Path} - } - prog.Budget.NodeBudget-- - } - childSeg, remainingPath := path.Shift() - prog.Path = prog.Path.AppendSegment(childSeg) - childVal, err := a.LookupBySegment(childSeg) - // Since we're explicitly looking for a node, look for the child node in the current amender state and throw an - // error if it does not exist. - if err != nil { - return nil, err - } - childIdx, err := childSeg.Index() - if err != nil { - return nil, err - } - childAmender, err := a.storeChildAmender(childIdx, childVal, childVal.Kind(), false, trackProgress) - if err != nil { - return nil, err - } - return childAmender.Get(prog, remainingPath, trackProgress) -} - -func (a *listAmender) Transform(prog *Progress, path datamodel.Path, fn TransformFn, createParents bool) (datamodel.Node, error) { - // Allow the base node to be replaced. - if path.Len() == 0 { - prevNode := a.Build() - if newNode, err := fn(*prog, prevNode); err != nil { - return nil, err - } else if newNode.Kind() != datamodel.Kind_List { - return nil, fmt.Errorf("transform: cannot transform root into incompatible type: %q", newNode.Kind()) - } else { - // Go through `newListAmender` in case `newNode` is already a list-amender. - *a = *a.opts.newListAmender(newNode, a.parent, a.created).(*listAmender) - return prevNode, nil - } - } - // Check the budget - if prog.Budget != nil { - if prog.Budget.NodeBudget <= 0 { - return nil, &ErrBudgetExceeded{BudgetKind: "node", Path: prog.Path} - } - prog.Budget.NodeBudget-- - } - childSeg, remainingPath := path.Shift() - atLeaf := remainingPath.Len() == 0 - childIdx, err := childSeg.Index() - var childVal datamodel.Node - if err != nil { - if childSeg.String() == "-" { - // "-" indicates appending a new element to the end of the list. - childIdx = a.Length() - childSeg = datamodel.PathSegmentOfInt(childIdx) - } else { - return nil, datamodel.ErrInvalidSegmentForList{TroubleSegment: childSeg, Reason: err} - } - } else { - // Don't allow the index to be equal to the length if the segment was not "-". - if childIdx >= a.Length() { - return nil, fmt.Errorf("transform: cannot navigate path segment %q at %q because it is beyond the list bounds", childSeg, prog.Path) - } - // Only lookup the segment if it was within range of the list elements. If `childIdx` is equal to the length of - // the list, then we fall-through and append an element to the end of the list. - childVal, err = a.LookupBySegment(childSeg) - if err != nil { - // - Return any error other than "not exists". - // - If the child node does not exist and `createParents = true`, create the new hierarchy, otherwise throw - // an error. - // - Even if `createParent = false`, if we're at the leaf, don't throw an error because we don't need to - // create any more intermediate parent nodes. - if _, notFoundErr := err.(datamodel.ErrNotExists); !notFoundErr || !(atLeaf || createParents) { - return nil, fmt.Errorf("transform: parent position at %q did not exist (and createParents was false)", prog.Path) - } - } - } - prog.Path = prog.Path.AppendSegment(childSeg) - // The default behaviour will be to update the element at the specified index (if it exists). New list elements can - // be added in two cases: - // - If an element is being appended to the end of the list. - // - If the transformation of the target node results in a list of nodes, use the first node in the list to replace - // the target node and then "add" the rest after. This is a bit of an ugly hack but is required for compatibility - // with two conflicting sets of semantics - the current `FocusedTransform`, which (quite reasonably) does an - // in-place replacement of list elements, and JSON Patch (https://datatracker.ietf.org/doc/html/rfc6902), which - // does not specify list element replacement. The only "compliant" way to do this today is to first "remove" the - // target node and then "add" its replacement at the same index, which seems incredibly inefficient. - create := (childVal == nil) || atLeaf - if atLeaf { - if newChildVal, err := fn(*prog, childVal); err != nil { - return nil, err - } else if newChildVal == nil { - a.mods.Remove(int(childIdx)) - } else if _, err = a.storeChildAmender(childIdx, newChildVal, newChildVal.Kind(), create, true); err != nil { - return nil, err - } - return childVal, nil - } - // If we're not at the leaf yet, look ahead on the remaining path to determine what kind of intermediate parent - // node we need to create. - var childKind datamodel.Kind - if childVal == nil { - // If we're not at the leaf yet, look ahead on the remaining path to determine what kind of intermediate parent - // node we need to create. - nextChildSeg, _ := remainingPath.Shift() - if _, err = nextChildSeg.Index(); err == nil { - // As per the discussion [here](https://github.com/smrz2001/go-ipld-prime/pull/1#issuecomment-1143035685), - // this code assumes that if we're dealing with an integral path segment, it corresponds to a list index. - childKind = datamodel.Kind_List - } else { - // From the same discussion as above, any non-integral, intermediate path can be assumed to be a map key. - childKind = datamodel.Kind_Map - } - } else { - childKind = childVal.Kind() - } - childAmender, err := a.storeChildAmender(childIdx, childVal, childKind, create, true) - if err != nil { - return nil, err - } - return childAmender.Transform(prog, remainingPath, fn, createParents) -} - -func (a *listAmender) Build() datamodel.Node { - // `listAmender` is also a `Node`. - return (datamodel.Node)(a) -} - -func (a *listAmender) isCreated() bool { - return a.created -} - -func (a *listAmender) storeChildAmender(childIdx int64, n datamodel.Node, k datamodel.Kind, create bool, trackProgress bool) (Amender, error) { - if trackProgress { - var childAmender Amender - idx := int(childIdx) - if create && (n.Kind() == datamodel.Kind_List) && (n.Length() > 0) { - first, err := n.LookupByIndex(0) - if err != nil { - return nil, err - } - // The following logic uses a transformed list (if there is one) to perform both insertions (needed by JSON - // Patch) and replacements (needed by `FocusedTransform`), while also providing the flexibility to insert more - // than one element at a particular index in the list. - // - // Rules: - // - If appending to the end of the main list, all elements from the transformed list should be considered - // "created" because they did not exist before. - // - If updating at a particular index in the main list, however, use the first element from the transformed - // list to replace the existing element at that index in the main list, then insert the rest of the - // transformed list elements after. - // - // A special case to consider is that of a list element genuinely being a list itself. If that is the case, the - // transformation MUST wrap the element in another list so that, once unwrapped, the element can be replaced or - // inserted without affecting its semantics. Otherwise, the sub-list's elements will get expanded onto that - // index in the main list. - childAmender = a.opts.newAmender(first, a, first.Kind(), childIdx == a.Length()) - a.mods.Set(idx, listElement{-1, childAmender.Build()}) - if n.Length() > 1 { - elems := make([]interface{}, n.Length()-1) - for i := range elems { - next, err := n.LookupByIndex(int64(i) + 1) - if err != nil { - return nil, err - } - elems[i] = listElement{-1, a.opts.newAmender(next, a, next.Kind(), true).Build()} - } - a.mods.Insert(idx+1, elems...) - } - } else { - childAmender = a.opts.newAmender(n, a, k, create) - a.mods.Set(idx, listElement{-1, childAmender.Build()}) - } - return childAmender, nil - } - return a.opts.newAmender(n, a, k, create), nil -} diff --git a/traversal/amendMap.go b/traversal/amendMap.go deleted file mode 100644 index 55dff22c..00000000 --- a/traversal/amendMap.go +++ /dev/null @@ -1,339 +0,0 @@ -package traversal - -import ( - "fmt" - "github.com/emirpasic/gods/maps/linkedhashmap" - - "github.com/ipld/go-ipld-prime/datamodel" - "github.com/ipld/go-ipld-prime/node/basicnode" - "github.com/ipld/go-ipld-prime/node/mixins" -) - -var ( - _ datamodel.Node = &mapAmender{} - _ Amender = &mapAmender{} -) - -type mapAmender struct { - amendCfg - - // This is the information needed to present an accurate "effective" view of the base node and all accumulated - // modifications. - mods linkedhashmap.Map - // This is the count of children *present in the base node* that are removed. Knowing this count allows accurate - // traversal of the "effective" node view. - rems int - // This is the count of new children. If an added node is removed, this count should be decremented instead of - // `rems`. - adds int -} - -func (opts AmendOptions) newMapAmender(base datamodel.Node, parent Amender, create bool) Amender { - // If the base node is already a map-amender, reuse the mutation state but reset `parent` and `created`. - if amd, castOk := base.(*mapAmender); castOk { - return &mapAmender{amendCfg{&opts, amd.base, parent, create}, amd.mods, amd.rems, amd.adds} - } else { - // Start with fresh state because existing metadata could not be reused. - return &mapAmender{amendCfg{&opts, base, parent, create}, *linkedhashmap.New(), 0, 0} - } -} - -// -- Node --> - -func (a *mapAmender) Kind() datamodel.Kind { - return datamodel.Kind_Map -} - -func (a *mapAmender) LookupByString(key string) (datamodel.Node, error) { - seg := datamodel.PathSegmentOfString(key) - // Added/removed nodes override the contents of the base node - if mod, exists := a.mods.Get(seg); exists { - v := mod.(Amender).Build() - if v.IsNull() { - return nil, datamodel.ErrNotExists{Segment: seg} - } - return v, nil - } - // Fallback to base node - if a.base != nil { - return a.base.LookupByString(key) - } - return nil, datamodel.ErrNotExists{Segment: seg} -} - -func (a *mapAmender) LookupByNode(key datamodel.Node) (datamodel.Node, error) { - ks, err := key.AsString() - if err != nil { - return nil, err - } - return a.LookupByString(ks) -} - -func (a *mapAmender) LookupByIndex(idx int64) (datamodel.Node, error) { - return mixins.Map{TypeName: "mapAmender"}.LookupByIndex(idx) -} - -func (a *mapAmender) LookupBySegment(seg datamodel.PathSegment) (datamodel.Node, error) { - return a.LookupByString(seg.String()) -} - -func (a *mapAmender) MapIterator() datamodel.MapIterator { - var baseItr datamodel.MapIterator = nil - // If all children were removed from the base node, or no base node was specified, there is nothing to iterate - // over w.r.t. that node. - if (a.base != nil) && (int64(a.rems) < a.base.Length()) { - baseItr = a.base.MapIterator() - } - var modsItr *linkedhashmap.Iterator - if (a.rems != 0) || (a.adds != 0) { - itr := a.mods.Iterator() - modsItr = &itr - } - return &mapAmender_Iterator{a, modsItr, baseItr, 0} -} - -func (a *mapAmender) ListIterator() datamodel.ListIterator { - return nil -} - -func (a *mapAmender) Length() int64 { - length := int64(a.adds - a.rems) - if a.base != nil { - length = length + a.base.Length() - } - return length -} - -func (a *mapAmender) IsAbsent() bool { - return false -} - -func (a *mapAmender) IsNull() bool { - return false -} - -func (a *mapAmender) AsBool() (bool, error) { - return mixins.Map{TypeName: "mapAmender"}.AsBool() -} - -func (a *mapAmender) AsInt() (int64, error) { - return mixins.Map{TypeName: "mapAmender"}.AsInt() -} - -func (a *mapAmender) AsFloat() (float64, error) { - return mixins.Map{TypeName: "mapAmender"}.AsFloat() -} - -func (a *mapAmender) AsString() (string, error) { - return mixins.Map{TypeName: "mapAmender"}.AsString() -} - -func (a *mapAmender) AsBytes() ([]byte, error) { - return mixins.Map{TypeName: "mapAmender"}.AsBytes() -} - -func (a *mapAmender) AsLink() (datamodel.Link, error) { - return mixins.Map{TypeName: "mapAmender"}.AsLink() -} - -func (a *mapAmender) Prototype() datamodel.NodePrototype { - return basicnode.Prototype.Map -} - -type mapAmender_Iterator struct { - amd *mapAmender - modsItr *linkedhashmap.Iterator - baseItr datamodel.MapIterator - idx int -} - -func (itr *mapAmender_Iterator) Next() (k datamodel.Node, v datamodel.Node, _ error) { - if itr.Done() { - return nil, nil, datamodel.ErrIteratorOverread{} - } - if itr.baseItr != nil { - // Iterate over base node first to maintain ordering. - var err error - for !itr.baseItr.Done() { - k, v, err = itr.baseItr.Next() - if err != nil { - return nil, nil, err - } - ks, _ := k.AsString() - if err != nil { - return nil, nil, err - } - if mod, exists := itr.amd.mods.Get(datamodel.PathSegmentOfString(ks)); exists { - v = mod.(Amender).Build() - // Skip removed nodes - if v.IsNull() { - continue - } - // Fall-through and return wrapped nodes - } - // We found a "real" node to return, increment the counter. - itr.idx++ - return - } - } - if itr.modsItr != nil { - // Iterate over mods, skipping removed nodes. - for itr.modsItr.Next() { - key := itr.modsItr.Key() - amd := itr.modsItr.Value().(Amender) - k = basicnode.NewString(key.(datamodel.PathSegment).String()) - v = amd.Build() - // Skip removed nodes. - if v.IsNull() { - continue - } - // Skip "wrapper" nodes that represent existing sub-nodes in the hierarchy corresponding to an added leaf - // node. - if !amd.isCreated() { - continue - } - // We found a "real" node to return, increment the counter. - itr.idx++ - return - } - } - return nil, nil, datamodel.ErrIteratorOverread{} -} - -func (itr *mapAmender_Iterator) Done() bool { - // Iteration is complete when all source nodes have been processed (skipping removed nodes) and all mods have been - // processed. - return int64(itr.idx) >= itr.amd.Length() -} - -// -- Amender --> - -func (a *mapAmender) Get(prog *Progress, path datamodel.Path, trackProgress bool) (datamodel.Node, error) { - // If the root is requested, return the `Node` view of the amender. - if path.Len() == 0 { - return a.Build(), nil - } - // Check the budget - if prog.Budget != nil { - if prog.Budget.NodeBudget <= 0 { - return nil, &ErrBudgetExceeded{BudgetKind: "node", Path: prog.Path} - } - prog.Budget.NodeBudget-- - } - childSeg, remainingPath := path.Shift() - prog.Path = prog.Path.AppendSegment(childSeg) - childVal, err := a.LookupBySegment(childSeg) - // Since we're explicitly looking for a node, look for the child node in the current amender state and throw an - // error if it does not exist. - if err != nil { - return nil, err - } - return a.storeChildAmender(childSeg, childVal, childVal.Kind(), false, trackProgress).Get(prog, remainingPath, trackProgress) -} - -func (a *mapAmender) Transform(prog *Progress, path datamodel.Path, fn TransformFn, createParents bool) (datamodel.Node, error) { - // Allow the base node to be replaced. - if path.Len() == 0 { - prevNode := a.Build() - if newNode, err := fn(*prog, prevNode); err != nil { - return nil, err - } else if newNode.Kind() != datamodel.Kind_Map { - return nil, fmt.Errorf("transform: cannot transform root into incompatible type: %q", newNode.Kind()) - } else { - // Go through `newMapAmender` in case `newNode` is already a map-amender. - *a = *a.opts.newMapAmender(newNode, a.parent, a.created).(*mapAmender) - return prevNode, nil - } - } - // Check the budget - if prog.Budget != nil { - if prog.Budget.NodeBudget <= 0 { - return nil, &ErrBudgetExceeded{BudgetKind: "node", Path: prog.Path} - } - prog.Budget.NodeBudget-- - } - childSeg, remainingPath := path.Shift() - prog.Path = prog.Path.AppendSegment(childSeg) - atLeaf := remainingPath.Len() == 0 - childVal, err := a.LookupBySegment(childSeg) - if err != nil { - // - Return any error other than "not exists". - // - If the child node does not exist and `createParents = true`, create the new hierarchy, otherwise throw an - // error. - // - Even if `createParent = false`, if we're at the leaf, don't throw an error because we don't need to create - // any more intermediate parent nodes. - if _, notFoundErr := err.(datamodel.ErrNotExists); !notFoundErr || !(atLeaf || createParents) { - return nil, fmt.Errorf("transform: parent position at %q did not exist (and createParents was false)", prog.Path) - } - } - if atLeaf { - if newChildVal, err := fn(*prog, childVal); err != nil { - return nil, err - } else if newChildVal == nil { - // Use the "Null" node to indicate a removed child. - a.mods.Put(childSeg, a.opts.newAnyAmender(datamodel.Null, a, false)) - // If the child being removed didn't already exist, we could error out but we don't have to because the - // state will remain consistent. This operation is equivalent to adding a child then removing it, in which - // case we would have incremented then decremented `adds`, leaving it the same. - if childVal != nil { - // If the child node being removed is a new node previously added to the node hierarchy, decrement - // `adds`, otherwise increment `rems`. This allows us to retain knowledge about the "history" of the - // base hierarchy. - if amd, castOk := childVal.(Amender); castOk && amd.isCreated() { - a.adds-- - } else { - a.rems++ - } - } - } else { - // While building the nested amender tree, only count nodes as "added" when they didn't exist and had to be - // created to fill out the hierarchy. - create := false - if childVal == nil { - a.adds++ - create = true - } - a.storeChildAmender(childSeg, newChildVal, newChildVal.Kind(), create, true) - } - return childVal, nil - } - // While building the nested amender tree, only count nodes as "added" when they didn't exist and had to be created - // to fill out the hierarchy. - var childKind datamodel.Kind - create := false - if childVal == nil { - a.adds++ - create = true - // If we're not at the leaf yet, look ahead on the remaining path to determine what kind of intermediate parent - // node we need to create. - nextChildSeg, _ := remainingPath.Shift() - if _, err = nextChildSeg.Index(); err == nil { - // As per the discussion [here](https://github.com/smrz2001/go-ipld-prime/pull/1#issuecomment-1143035685), - // this code assumes that if we're dealing with an integral path segment, it corresponds to a list index. - childKind = datamodel.Kind_List - } else { - // From the same discussion as above, any non-integral, intermediate path can be assumed to be a map key. - childKind = datamodel.Kind_Map - } - } else { - childKind = childVal.Kind() - } - return a.storeChildAmender(childSeg, childVal, childKind, create, true).Transform(prog, remainingPath, fn, createParents) -} - -func (a *mapAmender) Build() datamodel.Node { - // `mapAmender` is also a `Node`. - return (datamodel.Node)(a) -} - -func (a *mapAmender) isCreated() bool { - return a.created -} - -func (a *mapAmender) storeChildAmender(seg datamodel.PathSegment, n datamodel.Node, k datamodel.Kind, create bool, trackProgress bool) Amender { - childAmender := a.opts.newAmender(n, a, k, create) - if trackProgress { - a.mods.Put(seg, childAmender) - } - return childAmender -} diff --git a/traversal/amender.go b/traversal/amender.go deleted file mode 100644 index 8c1aef48..00000000 --- a/traversal/amender.go +++ /dev/null @@ -1,58 +0,0 @@ -package traversal - -import "github.com/ipld/go-ipld-prime/datamodel" - -type Amender interface { - // Get returns the node at the specified path. It will not create any intermediate nodes because this is just a - // retrieval and not a modification operation. - Get(prog *Progress, path datamodel.Path, trackProgress bool) (datamodel.Node, error) - - // Transform will do an in-place transformation of the node at the specified path and return its previous value. - // If `createParents = true`, any missing parents will be created, otherwise this function will return an error. - Transform(prog *Progress, path datamodel.Path, fn TransformFn, createParents bool) (datamodel.Node, error) - - // Build returns a traversable node that can be used with existing codec implementations. An `Amender` does not - // *have* to be a `Node` although currently, all `Amender` implementations are also `Node`s. - Build() datamodel.Node - - // isCreated returns whether an amender was "added" to a hierarchy instead of just wrapping an existing child node. - isCreated() bool -} - -type AmendOptions struct { - // If true, will update `Link` nodes lazily on access instead of after every transformation requiring recomputation. - LazyLinkUpdate bool -} - -type amendCfg struct { - opts *AmendOptions - base datamodel.Node - parent Amender - created bool -} - -func NewAmender(base datamodel.Node) Amender { - return AmendOptions{}.NewAmender(base) -} - -// NewAmender returns a new amender of the right "type" (i.e. map, list, any) using the specified base node. -func (opts AmendOptions) NewAmender(base datamodel.Node) Amender { - // Do not allow externally creating a new amender without a base node to refer to. Amendment assumes that there is - // something to amend. - if base == nil { - panic("misuse") - } - return opts.newAmender(base, nil, base.Kind(), false) -} - -func (opts AmendOptions) newAmender(base datamodel.Node, parent Amender, kind datamodel.Kind, create bool) Amender { - if kind == datamodel.Kind_Map { - return opts.newMapAmender(base, parent, create) - } else if kind == datamodel.Kind_List { - return opts.newListAmender(base, parent, create) - } else if kind == datamodel.Kind_Link { - return opts.newLinkAmender(base, parent, create) - } else { - return opts.newAnyAmender(base, parent, create) - } -} diff --git a/traversal/common.go b/traversal/common.go index d01a69bd..f3c22780 100644 --- a/traversal/common.go +++ b/traversal/common.go @@ -44,18 +44,18 @@ func (prog *Progress) init() { // If it's a string or an int, that's it. // Any other case will panic. (If you're using this one keys returned by a MapIterator, though, you can ignore this possibility; // any compliant map implementation should've already rejected that data long ago, and should not be able to yield it to you from an iterator.) -//func asPathSegment(n datamodel.Node) datamodel.PathSegment { -// if n2, ok := n.(schema.TypedNode); ok { -// n = n2.Representation() -// } -// switch n.Kind() { -// case datamodel.Kind_String: -// s, _ := n.AsString() -// return datamodel.PathSegmentOfString(s) -// case datamodel.Kind_Int: -// i, _ := n.AsInt() -// return datamodel.PathSegmentOfInt(i) -// default: -// panic(fmt.Errorf("cannot get pathsegment from a %s", n.Kind())) -// } -//} +func asPathSegment(n datamodel.Node) datamodel.PathSegment { + if n2, ok := n.(schema.TypedNode); ok { + n = n2.Representation() + } + switch n.Kind() { + case datamodel.Kind_String: + s, _ := n.AsString() + return datamodel.PathSegmentOfString(s) + case datamodel.Kind_Int: + i, _ := n.AsInt() + return datamodel.PathSegmentOfInt(i) + default: + panic(fmt.Errorf("cannot get pathsegment from a %s", n.Kind())) + } +} diff --git a/traversal/focus.go b/traversal/focus.go index 7ed89edd..225a0994 100644 --- a/traversal/focus.go +++ b/traversal/focus.go @@ -1,6 +1,11 @@ package traversal -import "github.com/ipld/go-ipld-prime/datamodel" +import ( + "fmt" + + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/linking" +) // Focus traverses a Node graph according to a path, reaches a single Node, // and calls the given VisitFn on that reached node. @@ -79,7 +84,77 @@ func (prog Progress) Get(n datamodel.Node, p datamodel.Path) (datamodel.Node, er // For Get calls, trackProgress=false, which avoids some allocations for state tracking that's not needed by that call. func (prog *Progress) get(n datamodel.Node, p datamodel.Path, trackProgress bool) (datamodel.Node, error) { prog.init() - return NewAmender(n).Get(prog, p, trackProgress) + segments := p.Segments() + var prev datamodel.Node // for LinkContext + for i, seg := range segments { + // Check the budget! + if prog.Budget != nil { + prog.Budget.NodeBudget-- + if prog.Budget.NodeBudget <= 0 { + return nil, &ErrBudgetExceeded{BudgetKind: "node", Path: prog.Path} + } + } + // Traverse the segment. + switch n.Kind() { + case datamodel.Kind_Invalid: + panic(fmt.Errorf("invalid node encountered at %q", p.Truncate(i))) + case datamodel.Kind_Map: + next, err := n.LookupByString(seg.String()) + if err != nil { + return nil, fmt.Errorf("error traversing segment %q on node at %q: %w", seg, p.Truncate(i), err) + } + prev, n = n, next + case datamodel.Kind_List: + intSeg, err := seg.Index() + if err != nil { + return nil, fmt.Errorf("error traversing segment %q on node at %q: the segment cannot be parsed as a number and the node is a list", seg, p.Truncate(i)) + } + next, err := n.LookupByIndex(intSeg) + if err != nil { + return nil, fmt.Errorf("error traversing segment %q on node at %q: %w", seg, p.Truncate(i), err) + } + prev, n = n, next + default: + return nil, fmt.Errorf("cannot traverse node at %q: %w", p.Truncate(i), fmt.Errorf("cannot traverse terminals")) + } + // Dereference any links. + for n.Kind() == datamodel.Kind_Link { + lnk, _ := n.AsLink() + // Check the budget! + if prog.Budget != nil { + if prog.Budget.LinkBudget <= 0 { + return nil, &ErrBudgetExceeded{BudgetKind: "link", Path: prog.Path, Link: lnk} + } + prog.Budget.LinkBudget-- + } + // Put together the context info we'll offer to the loader and prototypeChooser. + lnkCtx := linking.LinkContext{ + Ctx: prog.Cfg.Ctx, + LinkPath: p.Truncate(i), + LinkNode: n, + ParentNode: prev, + } + // Pick what in-memory format we will build. + np, err := prog.Cfg.LinkTargetNodePrototypeChooser(lnk, lnkCtx) + if err != nil { + return nil, fmt.Errorf("error traversing node at %q: could not load link %q: %w", p.Truncate(i+1), lnk, err) + } + // Load link! + prev = n + n, err = prog.Cfg.LinkSystem.Load(lnkCtx, lnk, np) + if err != nil { + return nil, fmt.Errorf("error traversing node at %q: could not load link %q: %w", p.Truncate(i+1), lnk, err) + } + if trackProgress { + prog.LastBlock.Path = p.Truncate(i + 1) + prog.LastBlock.Link = lnk + } + } + } + if trackProgress { + prog.Path = prog.Path.Join(p) + } + return n, nil } // FocusedTransform traverses a datamodel.Node graph, reaches a single Node, @@ -122,9 +197,236 @@ func (prog *Progress) get(n datamodel.Node, p datamodel.Path, trackProgress bool // creating new values which are partial updates to existing values. func (prog Progress) FocusedTransform(n datamodel.Node, p datamodel.Path, fn TransformFn, createParents bool) (datamodel.Node, error) { prog.init() - a := NewAmender(n) - if _, err := a.Transform(&prog, p, fn, createParents); err != nil { + nb := n.Prototype().NewBuilder() + if err := prog.focusedTransform(n, nb, p, fn, createParents); err != nil { return nil, err } - return a.Build(), nil + return nb.Build(), nil +} + +// focusedTransform assumes that an update will actually happen, and as it recurses deeper, +// begins building an updated node tree. +// +// As implemented, this is not actually efficient if the update will be a no-op; it won't notice until it gets there. +func (prog Progress) focusedTransform(n datamodel.Node, na datamodel.NodeAssembler, p datamodel.Path, fn TransformFn, createParents bool) error { + at := prog.Path + // Base case: if we've reached the end of the path, do the replacement here. + // (Note: in some cases within maps, there is another branch that is the base case, for reasons involving removes.) + if p.Len() == 0 { + n2, err := fn(prog, n) + if err != nil { + return err + } + return na.AssignNode(n2) + } + seg, p2 := p.Shift() + // Check the budget! + if prog.Budget != nil { + if prog.Budget.NodeBudget <= 0 { + return &ErrBudgetExceeded{BudgetKind: "node", Path: prog.Path} + } + prog.Budget.NodeBudget-- + } + // Special branch for if we've entered createParent mode in an earlier step. + // This needs slightly different logic because there's no prior node to reference + // (and we wouldn't want to waste time creating a dummy one). + if n == nil { + ma, err := na.BeginMap(1) + if err != nil { + return err + } + prog.Path = at.AppendSegment(seg) + if err := ma.AssembleKey().AssignString(seg.String()); err != nil { + return err + } + if err := prog.focusedTransform(nil, ma.AssembleValue(), p2, fn, createParents); err != nil { + return err + } + return ma.Finish() + } + // Handle node based on kind. + // If it's a recursive kind (map or list), we'll be recursing on it. + // If it's a link, load it! And recurse on it. + // If it's a scalar kind (any of the rest), we'll... be erroring, actually; + // if we're at the end, it was already handled at the top of the function, + // so we only get to this case if we were expecting to go deeper. + switch n.Kind() { + case datamodel.Kind_Map: + ma, err := na.BeginMap(n.Length()) + if err != nil { + return err + } + // If we're approaching the end of the path, call the TransformFunc. + // We need to know if it returns nil (meaning: do a deletion) _before_ we do the AssembleKey step. + // (This results in the entire map branch having a different base case.) + var end bool + var n2 datamodel.Node + if p2.Len() == 0 { + end = true + n3, err := n.LookupBySegment(seg) + if n3 != datamodel.Absent && err != nil { // TODO badly need to simplify the standard treatment of "not found" here. Can't even fit it all in one line! See https://github.com/ipld/go-ipld-prime/issues/360. + if _, ok := err.(datamodel.ErrNotExists); !ok { + return err + } + } + prog.Path = at.AppendSegment(seg) + n2, err = fn(prog, n3) + if err != nil { + return err + } + } + // Copy children over. Replace the target (preserving its current position!) while doing this, if found. + // Note that we don't recurse into copying children (assuming AssignNode doesn't); this is as shallow/COW as the AssignNode implementation permits. + var replaced bool + for itr := n.MapIterator(); !itr.Done(); { + k, v, err := itr.Next() + if err != nil { + return err + } + if asPathSegment(k).Equals(seg) { // for the segment that's either update, update within, or being removed: + if end { // the last path segment in the overall instruction gets a different case because it may need to handle deletion + if n2 == nil { + replaced = true + continue // replace with nil means delete, which means continue early here: don't even copy the key. + } + } + // as long as we're not deleting, then this key will exist in the new data. + if err := ma.AssembleKey().AssignNode(k); err != nil { + return err + } + replaced = true + if n2 != nil { // if we already produced the replacement because we're at the end... + if err := ma.AssembleValue().AssignNode(n2); err != nil { + return err + } + } else { // ... otherwise, recurse: + prog.Path = at.AppendSegment(seg) + if err := prog.focusedTransform(v, ma.AssembleValue(), p2, fn, createParents); err != nil { + return err + } + } + } else { // for any other siblings of the target: just copy. + if err := ma.AssembleKey().AssignNode(k); err != nil { + return err + } + if err := ma.AssembleValue().AssignNode(v); err != nil { + return err + } + } + } + if replaced { + return ma.Finish() + } + // If we didn't find the target yet: append it. + // If we're at the end, always do this; + // if we're in the middle, only do this if createParents mode is enabled. + prog.Path = at.AppendSegment(seg) + if p.Len() > 1 && !createParents { + return fmt.Errorf("transform: parent position at %q did not exist (and createParents was false)", prog.Path) + } + if err := ma.AssembleKey().AssignString(seg.String()); err != nil { + return err + } + if err := prog.focusedTransform(nil, ma.AssembleValue(), p2, fn, createParents); err != nil { + return err + } + return ma.Finish() + case datamodel.Kind_List: + la, err := na.BeginList(n.Length()) + if err != nil { + return err + } + // First figure out if this path segment can apply to a list sanely at all. + // Simultaneously, get it in numeric format, so subsequent operations are cheaper. + ti, err := seg.Index() + if err != nil { + if seg.String() == "-" { + ti = -1 + } else { + return fmt.Errorf("transform: cannot navigate path segment %q at %q because a list is here", seg, prog.Path) + } + } + // Copy children over. Replace the target (preserving its current position!) while doing this, if found. + // Note that we don't recurse into copying children (assuming AssignNode doesn't); this is as shallow/COW as the AssignNode implementation permits. + var replaced bool + for itr := n.ListIterator(); !itr.Done(); { + i, v, err := itr.Next() + if err != nil { + return err + } + if ti == i { + prog.Path = prog.Path.AppendSegment(seg) + if err := prog.focusedTransform(v, la.AssembleValue(), p2, fn, createParents); err != nil { + return err + } + replaced = true + } else { + if err := la.AssembleValue().AssignNode(v); err != nil { + return err + } + } + } + if replaced { + return la.Finish() + } + // If we didn't find the target yet: hopefully this was an append operation; + // if it wasn't, then it's index out of bounds. We don't arbitrarily extend lists with filler. + if ti >= 0 { + return fmt.Errorf("transform: cannot navigate path segment %q at %q because it is beyond the list bounds", seg, prog.Path) + } + prog.Path = prog.Path.AppendSegment(datamodel.PathSegmentOfInt(n.Length())) + if err := prog.focusedTransform(nil, la.AssembleValue(), p2, fn, createParents); err != nil { + return err + } + return la.Finish() + case datamodel.Kind_Link: + lnk, _ := n.AsLink() + // Check the budget! + if prog.Budget != nil { + if prog.Budget.LinkBudget <= 0 { + return &ErrBudgetExceeded{BudgetKind: "link", Path: prog.Path, Link: lnk} + } + prog.Budget.LinkBudget-- + } + // Put together the context info we'll offer to the loader and prototypeChooser. + lnkCtx := linking.LinkContext{ + Ctx: prog.Cfg.Ctx, + LinkPath: prog.Path, + LinkNode: n, + ParentNode: nil, // TODO inconvenient that we don't have this. maybe this whole case should be a helper function. + } + // Pick what in-memory format we will build. + np, err := prog.Cfg.LinkTargetNodePrototypeChooser(lnk, lnkCtx) + if err != nil { + return fmt.Errorf("transform: error traversing node at %q: could not load link %q: %w", prog.Path, lnk, err) + } + // Load link! + // We'll use LinkSystem.Fill here rather than Load, + // because there's a nice opportunity to reuse the builder shortly. + nb := np.NewBuilder() + err = prog.Cfg.LinkSystem.Fill(lnkCtx, lnk, nb) + if err != nil { + return fmt.Errorf("transform: error traversing node at %q: could not load link %q: %w", prog.Path, lnk, err) + } + prog.LastBlock.Path = prog.Path + prog.LastBlock.Link = lnk + n = nb.Build() + // Recurse. + // Start a new builder for this, using the same prototype we just used for loading the link. + // (Or more specifically: this is an opportunity for just resetting a builder and reusing memory!) + // When we come back... we'll have to engage serialization and storage on the new node! + // Path isn't updated here (neither progress nor to-go). + nb.Reset() + if err := prog.focusedTransform(n, nb, p, fn, createParents); err != nil { + return err + } + n = nb.Build() + lnk, err = prog.Cfg.LinkSystem.Store(lnkCtx, lnk.Prototype(), n) + if err != nil { + return fmt.Errorf("transform: error storing transformed node at %q: %w", prog.Path, err) + } + return na.AssignLink(lnk) + default: + return fmt.Errorf("transform: parent position at %q was a scalar, cannot go deeper", prog.Path) + } } diff --git a/traversal/patch/benchmarking_test.go b/traversal/patch/benchmarking_test.go deleted file mode 100644 index 7c5fe447..00000000 --- a/traversal/patch/benchmarking_test.go +++ /dev/null @@ -1,263 +0,0 @@ -package patch - -import ( - "fmt" - "math/rand" - "strconv" - "testing" - - "github.com/ipld/go-ipld-prime" - "github.com/ipld/go-ipld-prime/codec" - "github.com/ipld/go-ipld-prime/codec/dagjson" - "github.com/ipld/go-ipld-prime/datamodel" - "github.com/ipld/go-ipld-prime/fluent/qp" - "github.com/ipld/go-ipld-prime/node/basicnode" - "github.com/ipld/go-ipld-prime/traversal" -) - -var addTests = []struct { - size int - num int -}{ - {size: 100, num: 1}, - {size: 100, num: 10}, - {size: 100, num: 100}, - {size: 1000, num: 10}, - {size: 1000, num: 100}, - {size: 1000, num: 1000}, - {size: 10000, num: 100}, - {size: 10000, num: 1000}, - {size: 10000, num: 10000}, -} - -var removeTests = []struct { - size int - num int -}{ - {size: 100, num: 1}, - {size: 100, num: 10}, - {size: 100, num: 100}, - {size: 1000, num: 10}, - {size: 1000, num: 100}, - {size: 1000, num: 1000}, - {size: 10000, num: 100}, - {size: 10000, num: 1000}, - {size: 10000, num: 10000}, -} - -var replaceTests = []struct { - size int - num int -}{ - {size: 100, num: 1}, - {size: 100, num: 10}, - {size: 100, num: 100}, - {size: 1000, num: 10}, - {size: 1000, num: 100}, - {size: 1000, num: 1000}, - {size: 10000, num: 100}, - {size: 10000, num: 1000}, - {size: 10000, num: 10000}, -} - -func BenchmarkAmend_Map_Add(b *testing.B) { - for _, v := range addTests { - b.Run(fmt.Sprintf("inputs: %v", v), func(b *testing.B) { - n, _ := qp.BuildMap(basicnode.Prototype.Any, int64(v.size), func(ma datamodel.MapAssembler) { - for i := 0; i < v.size; i++ { - qp.MapEntry(ma, "key-"+strconv.Itoa(i), qp.String("value-"+strconv.Itoa(i))) - } - }) - var err error - for r := 0; r < b.N; r++ { - a := traversal.NewAmender(n) - for i := 0; i < v.num; i++ { - _, err = EvalOne(a.Build(), Operation{ - Op: Op_Add, - Path: datamodel.ParsePath("/new-key-" + strconv.Itoa(i)), - Value: basicnode.NewString("new-value-" + strconv.Itoa(i)), - }) - if err != nil { - b.Fatalf("amend did not apply: %s", err) - } - } - _, err = ipld.Encode(a.Build(), dagjson.EncodeOptions{ - EncodeLinks: true, - EncodeBytes: true, - MapSortMode: codec.MapSortMode_None, - }.Encode) - if err != nil { - b.Errorf("failed to serialize result: %s", err) - } - } - }) - } -} - -func BenchmarkAmend_List_Add(b *testing.B) { - for _, v := range addTests { - b.Run(fmt.Sprintf("inputs: %v", v), func(b *testing.B) { - n, _ := qp.BuildList(basicnode.Prototype.Any, int64(v.size), func(la datamodel.ListAssembler) { - for i := 0; i < v.size; i++ { - qp.ListEntry(la, qp.String("entry-"+strconv.Itoa(i))) - } - }) - var err error - for r := 0; r < b.N; r++ { - a := traversal.NewAmender(n) - for i := 0; i < v.num; i++ { - _, err = EvalOne(a.Build(), Operation{ - Op: Op_Add, - Path: datamodel.ParsePath("/0"), // insert at the start for worst-case - Value: basicnode.NewString("new-entry-" + strconv.Itoa(i)), - }) - if err != nil { - b.Fatalf("amend did not apply: %s", err) - } - } - _, err = ipld.Encode(a.Build(), dagjson.EncodeOptions{ - EncodeLinks: true, - EncodeBytes: true, - MapSortMode: codec.MapSortMode_None, - }.Encode) - if err != nil { - b.Errorf("failed to serialize result: %s", err) - } - } - }) - } -} - -func BenchmarkAmend_Map_Remove(b *testing.B) { - for _, v := range removeTests { - b.Run(fmt.Sprintf("inputs: %v", v), func(b *testing.B) { - n, _ := qp.BuildMap(basicnode.Prototype.Any, int64(v.size), func(ma datamodel.MapAssembler) { - for i := 0; i < v.size; i++ { - qp.MapEntry(ma, "key-"+strconv.Itoa(i), qp.String("value-"+strconv.Itoa(i))) - } - }) - var err error - for r := 0; r < b.N; r++ { - a := traversal.NewAmender(n) - for i := 0; i < v.num; i++ { - _, err = EvalOne(a.Build(), Operation{ - Op: Op_Remove, - Path: datamodel.ParsePath("/key-" + strconv.Itoa(i)), - }) - if err != nil { - b.Fatalf("amend did not apply: %s", err) - } - } - _, err = ipld.Encode(a.Build(), dagjson.EncodeOptions{ - EncodeLinks: true, - EncodeBytes: true, - MapSortMode: codec.MapSortMode_None, - }.Encode) - if err != nil { - b.Errorf("failed to serialize result: %s", err) - } - } - }) - } -} - -func BenchmarkAmend_List_Remove(b *testing.B) { - for _, v := range removeTests { - b.Run(fmt.Sprintf("inputs: %v", v), func(b *testing.B) { - n, _ := qp.BuildList(basicnode.Prototype.Any, int64(v.size), func(la datamodel.ListAssembler) { - for i := 0; i < v.size; i++ { - qp.ListEntry(la, qp.String("entry-"+strconv.Itoa(i))) - } - }) - var err error - for r := 0; r < b.N; r++ { - a := traversal.NewAmender(n) - for i := 0; i < v.num; i++ { - _, err = EvalOne(a.Build(), Operation{ - Op: Op_Remove, - Path: datamodel.ParsePath("/0"), // remove from the start for worst-case - }) - if err != nil { - b.Fatalf("amend did not apply: %s", err) - } - } - _, err = ipld.Encode(a.Build(), dagjson.EncodeOptions{ - EncodeLinks: true, - EncodeBytes: true, - MapSortMode: codec.MapSortMode_None, - }.Encode) - if err != nil { - b.Errorf("failed to serialize result: %s", err) - } - } - }) - } -} - -func BenchmarkAmend_Map_Replace(b *testing.B) { - for _, v := range replaceTests { - b.Run(fmt.Sprintf("inputs: %v", v), func(b *testing.B) { - n, _ := qp.BuildMap(basicnode.Prototype.Any, int64(v.size), func(ma datamodel.MapAssembler) { - for i := 0; i < v.size; i++ { - qp.MapEntry(ma, "key-"+strconv.Itoa(i), qp.String("value-"+strconv.Itoa(i))) - } - }) - var err error - for r := 0; r < b.N; r++ { - a := traversal.NewAmender(n) - for i := 0; i < v.num; i++ { - _, err = EvalOne(a.Build(), Operation{ - Op: Op_Replace, - Path: datamodel.ParsePath("/key-" + strconv.Itoa(rand.Intn(v.size))), - Value: basicnode.NewString("new-value-" + strconv.Itoa(i)), - }) - if err != nil { - b.Fatalf("amend did not apply: %s", err) - } - } - _, err = ipld.Encode(a.Build(), dagjson.EncodeOptions{ - EncodeLinks: true, - EncodeBytes: true, - MapSortMode: codec.MapSortMode_None, - }.Encode) - if err != nil { - b.Errorf("failed to serialize result: %s", err) - } - } - }) - } -} - -func BenchmarkAmend_List_Replace(b *testing.B) { - for _, v := range replaceTests { - b.Run(fmt.Sprintf("inputs: %v", v), func(b *testing.B) { - n, _ := qp.BuildList(basicnode.Prototype.Any, int64(v.size), func(la datamodel.ListAssembler) { - for i := 0; i < v.size; i++ { - qp.ListEntry(la, qp.String("entry-"+strconv.Itoa(i))) - } - }) - var err error - for r := 0; r < b.N; r++ { - a := traversal.NewAmender(n) - for i := 0; i < v.num; i++ { - _, err = EvalOne(a.Build(), Operation{ - Op: Op_Replace, - Path: datamodel.ParsePath("/" + strconv.Itoa(rand.Intn(v.size))), - Value: basicnode.NewString("new-entry-" + strconv.Itoa(i)), - }) - if err != nil { - b.Fatalf("amend did not apply: %s", err) - } - } - _, err = ipld.Encode(a.Build(), dagjson.EncodeOptions{ - EncodeLinks: true, - EncodeBytes: true, - MapSortMode: codec.MapSortMode_None, - }.Encode) - if err != nil { - b.Errorf("failed to serialize result: %s", err) - } - } - }) - } -} diff --git a/traversal/patch/eval.go b/traversal/patch/eval.go index 0b31ed1d..9534033c 100644 --- a/traversal/patch/eval.go +++ b/traversal/patch/eval.go @@ -10,8 +10,6 @@ import ( "fmt" "github.com/ipld/go-ipld-prime/datamodel" - "github.com/ipld/go-ipld-prime/fluent/qp" - "github.com/ipld/go-ipld-prime/node/basicnode" "github.com/ipld/go-ipld-prime/traversal" ) @@ -34,90 +32,113 @@ type Operation struct { } func Eval(n datamodel.Node, ops []Operation) (datamodel.Node, error) { - a := traversal.NewAmender(n) // One Amender To Patch Them All - prog := traversal.Progress{} + var err error for _, op := range ops { - _, err := evalOne(&prog, a.Build(), op) + n, err = EvalOne(n, op) if err != nil { return nil, err } } - return a.Build(), nil + return n, nil } func EvalOne(n datamodel.Node, op Operation) (datamodel.Node, error) { - return evalOne(&traversal.Progress{}, n, op) -} - -func evalOne(prog *traversal.Progress, n datamodel.Node, op Operation) (datamodel.Node, error) { - // If the node being modified is already an `Amender` reuse it, otherwise create a fresh one. - var a traversal.Amender - if amd, castOk := n.(traversal.Amender); castOk { - a = amd - } else { - a = traversal.NewAmender(n) - } switch op.Op { case Op_Add: // The behavior of the 'add' op in jsonpatch varies based on if the parent of the target path is a list. - // If the parent of the target path is a list, then 'add' is really more of an 'insert': it should slide the - // rest of the values down. There's also a special case for "-", which means "append to the end of the list". + // If the parent of the target path is a list, then 'add' is really more of an 'insert': it should slide the rest of the values down. + // There's also a special case for "-", which means "append to the end of the list". // Otherwise, if the destination path exists, it's an error. (No upserting.) - if _, err := a.Transform(prog, op.Path, func(progress traversal.Progress, prev datamodel.Node) (datamodel.Node, error) { - if n.Kind() == datamodel.Kind_List { - // Since jsonpatch expects list "add" operations to insert the element, return the transformed list - // "[previous node, new node]" so that the transformation can expand this list at the specified index in - // the original list. This allows jsonpatch to continue inserting elements for "add" operations, while - // also allowing transformations that update list elements in place (default behavior), currently used - // by `FocusedTransform`. - return qp.BuildList(basicnode.Prototype.Any, 2, func(la datamodel.ListAssembler) { - qp.ListEntry(la, qp.Node(op.Value)) - qp.ListEntry(la, qp.Node(prev)) - }) + // Handling this requires looking at the parent of the destination node, so we split this into *two* traversal.FocusedTransform calls. + return traversal.FocusedTransform(n, op.Path.Pop(), func(prog traversal.Progress, parent datamodel.Node) (datamodel.Node, error) { + if parent.Kind() == datamodel.Kind_List { + seg := op.Path.Last() + var idx int64 + if seg.String() == "-" { + idx = -1 + } + var err error + idx, err = seg.Index() + if err != nil { + return nil, fmt.Errorf("patch-invalid-path-through-list: at %q", op.Path) // TODO error structuralization and review the code + } + + nb := parent.Prototype().NewBuilder() + la, err := nb.BeginList(parent.Length() + 1) + if err != nil { + return nil, err + } + for itr := n.ListIterator(); !itr.Done(); { + i, v, err := itr.Next() + if err != nil { + return nil, err + } + if idx == i { + la.AssembleValue().AssignNode(op.Value) + } + if err := la.AssembleValue().AssignNode(v); err != nil { + return nil, err + } + } + // TODO: is one-past-the-end supposed to be supported or supposed to be ruled out? + if idx == -1 { + la.AssembleValue().AssignNode(op.Value) + } + if err := la.Finish(); err != nil { + return nil, err + } + return nb.Build(), nil } - return op.Value, nil - }, true); err != nil { - return nil, err - } - case Op_Remove: - if _, err := a.Transform(prog, op.Path, func(progress traversal.Progress, node datamodel.Node) (datamodel.Node, error) { - return nil, nil - }, false); err != nil { + return prog.FocusedTransform(parent, datamodel.NewPath([]datamodel.PathSegment{op.Path.Last()}), func(prog traversal.Progress, point datamodel.Node) (datamodel.Node, error) { + if point != nil && !point.IsAbsent() { + return nil, fmt.Errorf("patch-target-exists: at %q", op.Path) // TODO error structuralization and review the code + } + return op.Value, nil + }, false) + }, false) + case "remove": + return traversal.FocusedTransform(n, op.Path, func(_ traversal.Progress, point datamodel.Node) (datamodel.Node, error) { + return nil, nil // Returning a nil value here means "remove what's here". + }, false) + case "replace": + // TODO i think you need a check that it's not landing under itself here + return traversal.FocusedTransform(n, op.Path, func(_ traversal.Progress, point datamodel.Node) (datamodel.Node, error) { + return op.Value, nil // is this right? what does FocusedTransform do re upsert? + }, false) + case "move": + // TODO i think you need a check that it's not landing under itself here + source, err := traversal.Get(n, op.From) + if err != nil { return nil, err } - case Op_Replace: - if _, err := a.Transform(prog, op.Path, func(progress traversal.Progress, node datamodel.Node) (datamodel.Node, error) { - return op.Value, nil - }, false); err != nil { + n, err := traversal.FocusedTransform(n, op.Path, func(_ traversal.Progress, point datamodel.Node) (datamodel.Node, error) { + return source, nil // is this right? what does FocusedTransform do re upsert? + }, false) + if err != nil { return nil, err } - case Op_Move: - if source, err := a.Transform(prog, op.From, func(progress traversal.Progress, node datamodel.Node) (datamodel.Node, error) { - // Returning `nil` will cause the target node to be deleted. - return nil, nil - }, false); err != nil { - return nil, err - } else if _, err := a.Transform(prog, op.Path, func(progress traversal.Progress, node datamodel.Node) (datamodel.Node, error) { - return source, nil - }, true); err != nil { + return traversal.FocusedTransform(n, op.From, func(_ traversal.Progress, point datamodel.Node) (datamodel.Node, error) { + return nil, nil // Returning a nil value here means "remove what's here". + }, false) + case "copy": + // TODO i think you need a check that it's not landing under itself here + source, err := traversal.Get(n, op.From) + if err != nil { return nil, err } - case Op_Copy: - if source, err := a.Get(prog, op.From, true); err != nil { - return nil, err - } else if _, err := a.Transform(prog, op.Path, func(progress traversal.Progress, node datamodel.Node) (datamodel.Node, error) { - return source, nil - }, true); err != nil { + return traversal.FocusedTransform(n, op.Path, func(_ traversal.Progress, point datamodel.Node) (datamodel.Node, error) { + return source, nil // is this right? what does FocusedTransform do re upsert? + }, false) + case "test": + point, err := traversal.Get(n, op.Path) + if err != nil { return nil, err } - case Op_Test: - if point, err := a.Get(prog, op.Path, true); err != nil { - return nil, err - } else if !datamodel.DeepEqual(point, op.Value) { - return nil, fmt.Errorf("test failed") // TODO real error handling and a code + if datamodel.DeepEqual(point, op.Value) { + return n, nil } + return n, fmt.Errorf("test failed") // TODO real error handling and a code default: - return nil, fmt.Errorf("misuse: invalid operation") // TODO real error handling and a code + return nil, fmt.Errorf("misuse: invalid operation: %s", op.Op) // TODO real error handling and a code } - return a.Build(), nil }