From 24469a6bfbc3de80b8558d68faa0b388c2188f82 Mon Sep 17 00:00:00 2001 From: Mohsin Zaidi <2236875+smrz2001@users.noreply.github.com> Date: Thu, 9 Jun 2022 20:40:41 -0400 Subject: [PATCH] feat: ipld amend --- go.mod | 1 + go.sum | 2 + traversal/amend/amender.go | 55 ++++ traversal/amend/amender_test.go | 92 ++++++ traversal/amend/any.go | 125 +++++++ traversal/amend/benchmarking_test.go | 473 +++++++++++++++++++++++++++ traversal/amend/eval.go | 57 ++++ traversal/amend/list.go | 295 +++++++++++++++++ traversal/amend/map.go | 347 ++++++++++++++++++++ 9 files changed, 1447 insertions(+) create mode 100644 traversal/amend/amender.go create mode 100644 traversal/amend/amender_test.go create mode 100644 traversal/amend/any.go create mode 100644 traversal/amend/benchmarking_test.go create mode 100644 traversal/amend/eval.go create mode 100644 traversal/amend/list.go create mode 100644 traversal/amend/map.go diff --git a/go.mod b/go.mod index fff740ee..e10d16c1 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/ipld/go-ipld-prime go 1.17 require ( + github.com/emirpasic/gods v1.18.1 github.com/frankban/quicktest v1.14.3 github.com/google/go-cmp v0.5.8 github.com/ipfs/go-cid v0.2.0 diff --git a/go.sum b/go.sum index d02e0b93..9f8dd418 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= diff --git a/traversal/amend/amender.go b/traversal/amend/amender.go new file mode 100644 index 00000000..ebfebd5e --- /dev/null +++ b/traversal/amend/amender.go @@ -0,0 +1,55 @@ +package amend + +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(path datamodel.Path) (datamodel.Node, error) + + // Add will add the specified Node at the specified path. If `createParents = true`, any missing parents will be + // created, otherwise this function will return an error. + Add(path datamodel.Path, value datamodel.Node, createParents bool) error + + // Remove will remove the node at the specified path and return its value. This is useful for implementing a "move" + // operation, where a node can be "removed" and then "added" at a different path. + Remove(path datamodel.Path) (datamodel.Node, error) + + // Replace will do an in-place replacement of the node at the specified path and return its previous value. + Replace(path datamodel.Path, value datamodel.Node) (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 +} + +// NewAmender returns a new amender of the right "type" (i.e. map, list, any) using the specified base node. +func 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 newAmender(base, nil, base.Kind(), false) +} + +func newAmender(base datamodel.Node, parent Amender, kind datamodel.Kind, create bool) Amender { + if kind == datamodel.Kind_Map { + return newMapAmender(base, parent, create) + } else if kind == datamodel.Kind_List { + return newListAmender(base, parent, create) + } else { + return newAnyAmender(base, parent, create) + } +} + +func isCreated(a Amender) bool { + if ma, castOk := a.(*mapAmender); castOk { + return ma.created + } else if la, castOk := a.(*listAmender); castOk { + return la.created + } else if aa, castOk := a.(*anyAmender); castOk { + return aa.created + } + panic("misuse") +} diff --git a/traversal/amend/amender_test.go b/traversal/amend/amender_test.go new file mode 100644 index 00000000..ba844ee3 --- /dev/null +++ b/traversal/amend/amender_test.go @@ -0,0 +1,92 @@ +package amend + +import ( + "bytes" + "encoding/json" + "os" + "strings" + "testing" + + qt "github.com/frankban/quicktest" + "github.com/warpfork/go-testmark" + + "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/traversal/patch" +) + +func TestSpecFixtures(t *testing.T) { + dir := "../../.ipld/specs/patch/fixtures/" + testOneSpecFixtureFile(t, dir+"fixtures-1.md") +} + +func testOneSpecFixtureFile(t *testing.T, filename string) { + doc, err := testmark.ReadFile(filename) + if os.IsNotExist(err) { + t.Skipf("not running spec suite: %s (did you clone the submodule with the data?)", err) + } + if err != nil { + t.Fatalf("spec file parse failed?!: %s", err) + } + + // Data hunk in this spec file are in "directories" of a test scenario each. + doc.BuildDirIndex() + + for _, dir := range doc.DirEnt.ChildrenList { + t.Run(dir.Name, func(t *testing.T) { + // Grab all the data hunks. + // Each "directory" contains three piece of data: + // - `initial` -- this is the "block". It's arbitrary example data. They're all in json (or dag-json) format, for simplicity. + // - `patch` -- this is a list of patch ops. Again, as json. + // - `result` -- this is the expected result object. Again, as json. + initialBlob := dir.Children["initial"].Hunk.Body + patchBlob := dir.Children["patch"].Hunk.Body + resultBlob := dir.Children["result"].Hunk.Body + + // Parse everything. + initial, err := ipld.Decode(initialBlob, dagjson.Decode) + if err != nil { + t.Fatalf("failed to parse fixture data: %s", err) + } + ops, err := patch.ParseBytes(patchBlob, dagjson.Decode) + if err != nil { + t.Fatalf("failed to parse fixture patch: %s", err) + } + // We don't actually keep the decoded result object. We're just gonna serialize the result and textually diff that instead. + _, err = ipld.Decode(resultBlob, dagjson.Decode) + if err != nil { + t.Fatalf("failed to parse fixture data: %s", err) + } + + // Do the thing! + actualResult, err := Eval(initial, ops) + if strings.HasSuffix(dir.Name, "-fail") { + if err == nil { + t.Fatalf("patch was expected to fail") + } else { + return + } + } else { + if err != nil { + t.Fatalf("patch did not apply: %s", err) + } + } + + // Serialize (and pretty print) result, so that we can diff it. + actualResultBlob, err := ipld.Encode(actualResult, dagjson.EncodeOptions{ + EncodeLinks: true, + EncodeBytes: true, + MapSortMode: codec.MapSortMode_None, + }.Encode) + if err != nil { + t.Errorf("failed to reserialize result: %s", err) + } + var actualResultBlobPretty bytes.Buffer + json.Indent(&actualResultBlobPretty, actualResultBlob, "", "\t") + + // Diff! + qt.Assert(t, actualResultBlobPretty.String()+"\n", qt.Equals, string(resultBlob)) + }) + } +} diff --git a/traversal/amend/any.go b/traversal/amend/any.go new file mode 100644 index 00000000..7fd55b93 --- /dev/null +++ b/traversal/amend/any.go @@ -0,0 +1,125 @@ +package amend + +import ( + "github.com/ipld/go-ipld-prime/datamodel" +) + +var ( + _ datamodel.Node = &anyAmender{} + _ Amender = &anyAmender{} +) + +type anyAmender struct { + base datamodel.Node + parent Amender + created bool +} + +func newAnyAmender(base datamodel.Node, parent Amender, create bool) Amender { + return &anyAmender{base, parent, create} +} + +func (a *anyAmender) Build() datamodel.Node { + // `anyAmender` is also a `Node`. + return (datamodel.Node)(a) +} + +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() +} + +func (a *anyAmender) Get(path datamodel.Path) (datamodel.Node, error) { + // If the base node is an amender, use it, otherwise panic. + if amd, castOk := a.base.(Amender); castOk { + return amd.Get(path) + } + panic("misuse") +} + +func (a *anyAmender) Add(path datamodel.Path, value datamodel.Node, createParents bool) error { + // If the base node is an amender, use it, otherwise panic. + if amd, castOk := a.base.(Amender); castOk { + return amd.Add(path, value, createParents) + } + panic("misuse") +} + +func (a *anyAmender) Remove(path datamodel.Path) (datamodel.Node, error) { + // If the base node is an amender, use it, otherwise panic. + if amd, castOk := a.base.(Amender); castOk { + return amd.Remove(path) + } + panic("misuse") +} + +func (a *anyAmender) Replace(path datamodel.Path, value datamodel.Node) (datamodel.Node, error) { + // If the base node is an amender, use it, otherwise panic. + if amd, castOk := a.base.(Amender); castOk { + return amd.Replace(path, value) + } + panic("misuse") +} diff --git a/traversal/amend/benchmarking_test.go b/traversal/amend/benchmarking_test.go new file mode 100644 index 00000000..f17671ef --- /dev/null +++ b/traversal/amend/benchmarking_test.go @@ -0,0 +1,473 @@ +package amend + +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/patch" +) + +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++ { + tn := n + a := NewAmender(tn) + for i := 0; i < v.num; i++ { + err = EvalOne(a, patch.Operation{ + Op: patch.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 BenchmarkPatch_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++ { + tn := n + for i := 0; i < v.num; i++ { + tn, err = patch.EvalOne(tn, patch.Operation{ + Op: patch.Op_Add, + Path: datamodel.ParsePath("/new-key-" + strconv.Itoa(i)), + Value: basicnode.NewString("new-value-" + strconv.Itoa(i)), + }) + if err != nil { + b.Fatalf("patch did not apply: %s", err) + } + } + _, err = ipld.Encode(tn, 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++ { + tn := n + a := NewAmender(tn) + for i := 0; i < v.num; i++ { + err = EvalOne(a, patch.Operation{ + Op: patch.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 BenchmarkPatch_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++ { + tn := n + for i := 0; i < v.num; i++ { + tn, err = patch.EvalOne(tn, patch.Operation{ + Op: patch.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("patch did not apply: %s", err) + } + } + _, err = ipld.Encode(tn, 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++ { + tn := n + a := NewAmender(tn) + for i := 0; i < v.num; i++ { + err = EvalOne(a, patch.Operation{ + Op: patch.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 BenchmarkPatch_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++ { + tn := n + for i := 0; i < v.num; i++ { + tn, err = patch.EvalOne(tn, patch.Operation{ + Op: patch.Op_Remove, + Path: datamodel.ParsePath("/key-" + strconv.Itoa(i)), + }) + if err != nil { + b.Fatalf("patch did not apply: %s", err) + } + } + _, err = ipld.Encode(tn, 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++ { + tn := n + a := NewAmender(tn) + for i := 0; i < v.num; i++ { + err = EvalOne(a, patch.Operation{ + Op: patch.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) + } + } + }) + } +} + +// TODO: Investigate panic +//func BenchmarkPatch_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++ { +// tn := n +// for i := 0; i < v.num; i++ { +// tn, err = patch.EvalOne(tn, patch.Operation{ +// Op: patch.Op_Remove, +// Path: datamodel.ParsePath("/0"), // remove from the start for worst-case +// }) +// if err != nil { +// b.Fatalf("patch did not apply: %s", err) +// } +// } +// output, err := ipld.Encode(tn, dagjson.EncodeOptions{ +// EncodeLinks: true, +// EncodeBytes: true, +// MapSortMode: codec.MapSortMode_None, +// }.Encode) +// log.Printf("json: %s", output) +// 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++ { + tn := n + a := NewAmender(tn) + for i := 0; i < v.num; i++ { + err = EvalOne(a, patch.Operation{ + Op: patch.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 BenchmarkPatch_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++ { + tn := n + for i := 0; i < v.num; i++ { + tn, err = patch.EvalOne(tn, patch.Operation{ + Op: patch.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("patch did not apply: %s", err) + } + } + _, err = ipld.Encode(tn, 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++ { + tn := n + a := NewAmender(tn) + for i := 0; i < v.num; i++ { + err = EvalOne(a, patch.Operation{ + Op: patch.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) + } + } + }) + } +} + +func BenchmarkPatch_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++ { + tn := n + for i := 0; i < v.num; i++ { + tn, err = patch.EvalOne(tn, patch.Operation{ + Op: patch.Op_Replace, + Path: datamodel.ParsePath("/" + strconv.Itoa(rand.Intn(v.size))), + Value: basicnode.NewString("new-entry-" + strconv.Itoa(i)), + }) + if err != nil { + b.Fatalf("patch did not apply: %s", err) + } + } + _, err = ipld.Encode(tn, 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/amend/eval.go b/traversal/amend/eval.go new file mode 100644 index 00000000..4cad647a --- /dev/null +++ b/traversal/amend/eval.go @@ -0,0 +1,57 @@ +package amend + +import ( + "fmt" + + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/traversal/patch" +) + +func Eval(n datamodel.Node, ops []patch.Operation) (datamodel.Node, error) { + var err error + a := NewAmender(n) // One Amender To Patch Them All + for _, op := range ops { + err = EvalOne(a, op) + if err != nil { + return nil, err + } + } + return a.Build(), nil +} + +func EvalOne(a Amender, op patch.Operation) error { + switch op.Op { + case patch.Op_Add: + return a.Add(op.Path, op.Value, true) + case patch.Op_Remove: + _, err := a.Remove(op.Path) + return err + case patch.Op_Replace: + _, err := a.Replace(op.Path, op.Value) + return err + case patch.Op_Move: + source, err := a.Remove(op.From) + if err != nil { + return err + } + // Similar to `replace` with the difference that the destination path might not exist and need to be created. + return a.Add(op.Path, source, true) + case patch.Op_Copy: + source, err := a.Get(op.From) + if err != nil { + return err + } + return a.Add(op.Path, source, false) + case patch.Op_Test: + point, err := a.Get(op.Path) + if err != nil { + return err + } + if datamodel.DeepEqual(point, op.Value) { + return nil + } + return fmt.Errorf("test failed") // TODO real error handling and a code + default: + return fmt.Errorf("misuse: invalid operation") // TODO real error handling and a code + } +} diff --git a/traversal/amend/list.go b/traversal/amend/list.go new file mode 100644 index 00000000..3e8672e0 --- /dev/null +++ b/traversal/amend/list.go @@ -0,0 +1,295 @@ +package amend + +import ( + "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 { + idx int + elem datamodel.Node +} + +type listAmender struct { + base datamodel.Node + parent Amender + created bool + mods *arraylist.List +} + +func newListAmender(base datamodel.Node, parent Amender, create bool) Amender { + var mods *arraylist.List + // If the base node is already a list-amender *for the same base node*, reuse the modification metadata because that + // encapsulates all accumulated modifications. + if amd, castOk := base.(*listAmender); castOk && (base == amd.base) { + mods = 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) + } + mods = arraylist.New(elems...) + } + return &listAmender{base, parent, create, mods} +} + +func (a *listAmender) Build() datamodel.Node { + // `listAmender` is also a `Node`. + return (datamodel.Node)(a) +} + +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 { + baseNode, err := a.base.LookupByIndex(int64(child.idx)) + if err != nil { + return nil, err + } + child.elem = baseNode + return baseNode, 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() +} + +func (a *listAmender) Get(path datamodel.Path) (datamodel.Node, error) { + childSeg, remainingPath := path.Shift() + childVal, err := a.LookupBySegment(childSeg) + atLeaf := remainingPath.Len() == 0 + // 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 := newAmender(childVal, a, childVal.Kind(), false) + a.mods.Set(int(childIdx), listElement{int(childIdx), childAmender.(datamodel.Node)}) + if atLeaf { + return childVal, nil + } else { + return childAmender.Get(remainingPath) + } +} + +func (a *listAmender) Add(path datamodel.Path, value datamodel.Node, createParents bool) error { + childSeg, remainingPath := path.Shift() + atLeaf := remainingPath.Len() == 0 + childIdx, err := childSeg.Index() + if err != nil { + return datamodel.ErrInvalidSegmentForList{TroubleSegment: childSeg, Reason: err} + } + // Allow the index to be equal to the length - this just means that a new element needs to be added to the end of + // the list (i.e. appended). + if childIdx > a.Length() { + return datamodel.ErrNotExists{Segment: childSeg} + } + childVal, err := a.LookupBySegment(childSeg) + if err != nil { + // - Return any error other than "not exists". + // - If the chile 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 err + } + } + // 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. + // In the case of a list, also consider a node "added" if we're at the leaf. Even if there already was a child at + // that index, it just means we need to "insert" a new node at the index. + create := false + if (childVal == nil) || atLeaf { + create = true + } + var childKind datamodel.Kind + if atLeaf { + childVal = value + childKind = value.Kind() + } else { + // 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 + } + } + // When adding to a list-amender we're *always* creating a new node, never "wrapping" an existing one. This is by + // virtue of list semantics, where an addition means inserting a new element, even if one already existed at the + // specified index. + childAmender := newAmender(childVal, a, childKind, create) + if create { + a.mods.Insert(int(childIdx), listElement{int(childIdx), childAmender.(datamodel.Node)}) + } else { + a.mods.Set(int(childIdx), listElement{int(childIdx), childAmender.(datamodel.Node)}) + } + if atLeaf { + return nil + } else { + return childAmender.Add(remainingPath, value, createParents) + } +} + +func (a *listAmender) Remove(path datamodel.Path) (datamodel.Node, error) { + childSeg, remainingPath := path.Shift() + childVal, err := a.LookupBySegment(childSeg) + atLeaf := remainingPath.Len() == 0 + // 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 + } + if atLeaf { + a.mods.Remove(int(childIdx)) + return childVal, nil + } else { + childAmender := newAmender(childVal, a, childVal.Kind(), false) + a.mods.Set(int(childIdx), listElement{int(childIdx), childAmender.(datamodel.Node)}) + return childAmender.Remove(remainingPath) + } +} + +func (a *listAmender) Replace(path datamodel.Path, value datamodel.Node) (datamodel.Node, error) { + childSeg, remainingPath := path.Shift() + childVal, err := a.LookupBySegment(childSeg) + atLeaf := remainingPath.Len() == 0 + // 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 := newAmender(childVal, a, childVal.Kind(), false) + a.mods.Set(int(childIdx), listElement{int(childIdx), childAmender.(datamodel.Node)}) + if atLeaf { + return childVal, nil + } else { + return childAmender.Replace(remainingPath, value) + } +} diff --git a/traversal/amend/map.go b/traversal/amend/map.go new file mode 100644 index 00000000..66344e7d --- /dev/null +++ b/traversal/amend/map.go @@ -0,0 +1,347 @@ +package amend + +import ( + "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 { + base datamodel.Node + parent Amender + created bool + // 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 newMapAmender(base datamodel.Node, parent Amender, create bool) Amender { + // If the base node is already a map-amender *for the same base node*, reuse the modification metadata but reset + // other information (viz. parent, created). + if amd, castOk := base.(*mapAmender); castOk && (base == amd.base) { + return &mapAmender{base, parent, create, amd.mods, amd.rems, amd.adds} + } else { + // Start with fresh state because existing metadata could not be reused. + return &mapAmender{base, parent, create, linkedhashmap.New(), 0, 0} + } +} + +func (a *mapAmender) Build() datamodel.Node { + // `mapAmender` is also a `Node`. + return (datamodel.Node)(a) +} + +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.(datamodel.Node) + 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.(datamodel.Node) + // 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() + k = basicnode.NewString(key.(datamodel.PathSegment).String()) + v = itr.modsItr.Value().(datamodel.Node) + // 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, castOk := v.(Amender); castOk && !isCreated(amd) { + 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() +} + +func (a *mapAmender) Get(path datamodel.Path) (datamodel.Node, error) { + childSeg, remainingPath := path.Shift() + childVal, err := a.LookupBySegment(childSeg) + atLeaf := remainingPath.Len() == 0 + // 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 + } + childAmender := newAmender(childVal, a, childVal.Kind(), false) + a.mods.Put(childSeg, childAmender) + if atLeaf { + return childVal, nil + } else { + return childAmender.Get(remainingPath) + } +} + +func (a *mapAmender) Add(path datamodel.Path, value datamodel.Node, createParents bool) error { + childSeg, remainingPath := path.Shift() + atLeaf := remainingPath.Len() == 0 + childVal, err := a.LookupBySegment(childSeg) + if err != nil { + // - Return any error other than "not exists". + // - If the chile 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 err + } + } + // 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 + } + var childKind datamodel.Kind + if atLeaf { + if childVal != nil { + // The leaf must not already exist. + return datamodel.ErrRepeatedMapKey{Key: basicnode.NewString(childSeg.String())} + } + childVal = value + childKind = value.Kind() + } else { + // 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 + } + } + childAmender := newAmender(childVal, a, childKind, create) + a.mods.Put(childSeg, childAmender) + if atLeaf { + return nil + } else { + return childAmender.Add(remainingPath, value, createParents) + } +} + +func (a *mapAmender) Remove(path datamodel.Path) (datamodel.Node, error) { + childSeg, remainingPath := path.Shift() + childVal, err := a.LookupBySegment(childSeg) + atLeaf := remainingPath.Len() == 0 + // 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 + } + if atLeaf { + // Use the "Null" node to indicate a removed child. + a.mods.Put(childSeg, datamodel.Null) + // If this parent node is an amender and present in the base hierarchy, increment `rems`, otherwise decrement + // `adds`. This allows us to retain knowledge about the "history" of the base hierarchy. + if ma, mapCastOk := childVal.(*mapAmender); mapCastOk { + if ma.base != nil { + a.rems++ + } else { + a.adds-- + } + } else if la, listCastOk := childVal.(*listAmender); listCastOk { + if la.base != nil { + a.rems++ + } else { + a.adds-- + } + } else { + a.rems++ + } + return childVal, nil + } else { + childAmender := newAmender(childVal, a, childVal.Kind(), false) + // No need to update `rems` since we haven't reached the parent whose child is being removed. + a.mods.Put(childSeg, childAmender) + return childAmender.Remove(remainingPath) + } +} + +func (a *mapAmender) Replace(path datamodel.Path, value datamodel.Node) (datamodel.Node, error) { + childSeg, remainingPath := path.Shift() + childVal, err := a.LookupBySegment(childSeg) + atLeaf := remainingPath.Len() == 0 + // 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 + } + var childKind datamodel.Kind + if atLeaf { + childVal = value + childKind = value.Kind() + } else if _, err := childSeg.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 + } + childAmender := newAmender(childVal, a, childKind, false) + a.mods.Put(childSeg, childAmender) + if atLeaf { + return childVal, nil + } else { + return childAmender.Replace(remainingPath, value) + } +}