From e3c116ab1d549aaaada0a8269843d7e38d445e51 Mon Sep 17 00:00:00 2001 From: Mohsin Zaidi <2236875+smrz2001@users.noreply.github.com> Date: Fri, 14 Jul 2023 14:02:58 -0400 Subject: [PATCH] feat: add map amender tests --- datamodel/amender.go | 14 +- node/basicnode/list.go | 35 +--- node/basicnode/map.go | 72 ++++--- node/basicnode/map_test.go | 379 +++++++++++++++++++++++++++++++++++++ traversal/walk.go | 7 +- 5 files changed, 438 insertions(+), 69 deletions(-) diff --git a/datamodel/amender.go b/datamodel/amender.go index 351455bf..abf0a987 100644 --- a/datamodel/amender.go +++ b/datamodel/amender.go @@ -20,17 +20,17 @@ type containerAmender interface { Empty() bool Length() int64 Clear() - Values() ([]Node, error) + Values() (Node, error) // returns a list node with the values NodeAmender } // MapAmender adds a map-like interface to NodeAmender type MapAmender interface { - Put(key string, value Node) (bool, error) - Get(key string) (Node, error) - Remove(key string) (bool, error) - Keys() ([]string, error) + Put(key Node, value Node) error + Get(key Node) (Node, error) + Remove(key Node) (bool, error) + Keys() (Node, error) // returns a list node with the keys containerAmender } @@ -39,8 +39,8 @@ type MapAmender interface { type ListAmender interface { Get(idx int64) (Node, error) Remove(idx int64) error - Append(values ...Node) error - Insert(idx int64, values ...Node) error + Append(values Node) error // accepts a list node + Insert(idx int64, values Node) error // accepts a list node Set(idx int64, value Node) error containerAmender diff --git a/node/basicnode/list.go b/node/basicnode/list.go index cdd45eaf..9d45eef2 100644 --- a/node/basicnode/list.go +++ b/node/basicnode/list.go @@ -284,12 +284,12 @@ func (nb *plainList__Builder) Remove(idx int64) error { return err } -func (nb *plainList__Builder) Append(values ...datamodel.Node) error { +func (nb *plainList__Builder) Append(values datamodel.Node) error { // Passing an index equal to the length of the list will append the passed values to the end of the list - return nb.Insert(nb.Length(), values...) + return nb.Insert(nb.Length(), values) } -func (nb *plainList__Builder) Insert(idx int64, values ...datamodel.Node) error { +func (nb *plainList__Builder) Insert(idx int64, values datamodel.Node) error { var ps datamodel.PathSegment if idx == nb.Length() { ps = datamodel.PathSegmentOfString("-") // indicates appending to the end of the list @@ -299,22 +299,7 @@ func (nb *plainList__Builder) Insert(idx int64, values ...datamodel.Node) error _, err := nb.Transform( datamodel.NewPath([]datamodel.PathSegment{ps}), func(_ datamodel.Node) (datamodel.NodeAmender, error) { - // Put all the passed values into a new list. That will result in these values being expanded at the - // specified index. - na := nb.Prototype().NewBuilder() - la, err := na.BeginList(int64(len(values))) - if err != nil { - return nil, err - } - for _, v := range values { - if err := la.AssembleValue().AssignNode(v); err != nil { - return nil, err - } - } - if err := la.Finish(); err != nil { - return nil, err - } - return Prototype.Any.AmendingBuilder(na.Build()), nil + return Prototype.Any.AmendingBuilder(values), nil }, ) return err @@ -336,16 +321,8 @@ func (nb *plainList__Builder) Clear() { nb.Reset() } -func (nb *plainList__Builder) Values() ([]datamodel.Node, error) { - values := make([]datamodel.Node, 0, nb.Length()) - for itr := nb.w.ListIterator(); !itr.Done(); { - _, v, err := itr.Next() - if err != nil { - return nil, err - } - values = append(values, v) - } - return values, nil +func (nb *plainList__Builder) Values() (datamodel.Node, error) { + return nb.Build(), nil } // -- NodeAssembler --> diff --git a/node/basicnode/map.go b/node/basicnode/map.go index 09413c52..be7a8f59 100644 --- a/node/basicnode/map.go +++ b/node/basicnode/map.go @@ -202,10 +202,7 @@ func (nb *plainMap__Builder) Transform(path datamodel.Path, transform datamodel. if string(v.k) == childKey { if newChildAmender == nil { delete(nb.w.m, childKey) - newT := make([]plainMap__Entry, nb.w.Length()-1) - copy(newT, nb.w.t[:idx]) - copy(newT[:idx], nb.w.t[idx+1:]) - nb.w.t = newT + nb.w.t = append(nb.w.t[:idx], nb.w.t[idx+1:]...) } else { nb.w.t[idx].v = newChildAmender nb.w.m[string(nb.w.t[idx].k)] = newChildAmender @@ -219,27 +216,35 @@ func (nb *plainMap__Builder) Transform(path datamodel.Path, transform datamodel. } } -func (nb *plainMap__Builder) Put(key string, value datamodel.Node) (bool, error) { - if prevNode, err := nb.Transform( - datamodel.NewPath([]datamodel.PathSegment{datamodel.PathSegmentOfString(key)}), +func (nb *plainMap__Builder) Put(key datamodel.Node, value datamodel.Node) error { + ks, err := key.AsString() + if err != nil { + return err + } + if _, err := nb.Transform( + datamodel.NewPath([]datamodel.PathSegment{datamodel.PathSegmentOfString(ks)}), func(_ datamodel.Node) (datamodel.NodeAmender, error) { return Prototype.Any.AmendingBuilder(value), nil }, ); err != nil { - return false, err + return err } else { // If there was no previous node, we just added a new node. - return prevNode == nil, nil + return nil } } -func (nb *plainMap__Builder) Get(key string) (datamodel.Node, error) { - return nb.w.LookupByString(key) +func (nb *plainMap__Builder) Get(key datamodel.Node) (datamodel.Node, error) { + return nb.w.LookupByNode(key) } -func (nb *plainMap__Builder) Remove(key string) (bool, error) { +func (nb *plainMap__Builder) Remove(key datamodel.Node) (bool, error) { + ks, err := key.AsString() + if err != nil { + return false, err + } if prevNode, err := nb.Transform( - datamodel.NewPath([]datamodel.PathSegment{datamodel.PathSegmentOfString(key)}), + datamodel.NewPath([]datamodel.PathSegment{datamodel.PathSegmentOfString(ks)}), func(_ datamodel.Node) (datamodel.NodeAmender, error) { return nil, nil }, @@ -251,20 +256,35 @@ func (nb *plainMap__Builder) Remove(key string) (bool, error) { } } -func (nb *plainMap__Builder) Keys() ([]string, error) { - keys := make([]string, 0, nb.Length()) +func (nb *plainMap__Builder) Keys() (datamodel.Node, error) { + return nb.toList(true) +} + +func (nb *plainMap__Builder) toList(keysOrValues bool) (datamodel.Node, error) { + // Create a new List node and initialize its storage + lb := Prototype.List.AmendingBuilder(nil) + _, err := lb.BeginList(nb.Length()) + if err != nil { + return nil, err + } + var idx int64 = 0 for itr := nb.w.MapIterator(); !itr.Done(); { - k, _, err := itr.Next() + k, v, err := itr.Next() if err != nil { return nil, err } - keyStr, err := k.AsString() - if err != nil { + var n datamodel.Node + if keysOrValues { + n = k + } else { + n = v + } + if err = lb.Set(idx, n); err != nil { return nil, err } - keys = append(keys, keyStr) + idx++ } - return keys, nil + return lb.Build(), nil } func (nb *plainMap__Builder) Empty() bool { @@ -279,16 +299,8 @@ func (nb *plainMap__Builder) Clear() { nb.Reset() } -func (nb *plainMap__Builder) Values() ([]datamodel.Node, error) { - values := make([]datamodel.Node, 0, nb.Length()) - for itr := nb.w.MapIterator(); !itr.Done(); { - _, v, err := itr.Next() - if err != nil { - return nil, err - } - values = append(values, v) - } - return values, nil +func (nb *plainMap__Builder) Values() (datamodel.Node, error) { + return nb.toList(false) } // -- NodeAssembler --> diff --git a/node/basicnode/map_test.go b/node/basicnode/map_test.go index 404f5dfd..121b26f5 100644 --- a/node/basicnode/map_test.go +++ b/node/basicnode/map_test.go @@ -308,3 +308,382 @@ func TestMapDupKeyError(t *testing.T) { qt.Check(t, err, qt.ErrorMatches, `cannot repeat map key "cat"`) } + +func TestMapAmendingBuilderNewNode(t *testing.T) { + // Create a map amender with an empty base node + amender := basicnode.Prototype.Map.AmendingBuilder(nil) + + err := amender.Put(basicnode.NewString("cat"), basicnode.NewString("meow")) + if err != nil { + t.Fatal(err) + } + // Retry adding the entry + err = amender.Put(basicnode.NewString("cat"), basicnode.NewString("meow")) + if err != nil { + t.Fatal(err) + } + err = amender.Put(basicnode.NewString("dog"), basicnode.NewString("bark")) + if err != nil { + t.Fatal(err) + } + err = amender.Put(basicnode.NewString("eel"), basicnode.NewString("zap")) + if err != nil { + t.Fatal(err) + } + + // compare the printed map + mapNode := amender.Build() + actual := printer.Sprint(mapNode) + + expect := `map{ + string{"cat"}: string{"meow"} + string{"dog"}: string{"bark"} + string{"eel"}: string{"zap"} +}` + qt.Check(t, expect, qt.Equals, actual) + + // Access values of map, using string + r, err := mapNode.LookupByString("cat") + if err != nil { + t.Fatal(err) + } + qt.Check(t, "meow", qt.Equals, must.String(r)) + + // Access values of map, using node + r, err = mapNode.LookupByNode(basicnode.NewString("dog")) + if err != nil { + t.Fatal(err) + } + qt.Check(t, "bark", qt.Equals, must.String(r)) + + // Access values of map, using PathSegment + r, err = mapNode.LookupBySegment(datamodel.ParsePathSegment("eel")) + if err != nil { + t.Fatal(err) + } + qt.Check(t, "zap", qt.Equals, must.String(r)) + + // Validate the node's prototype + np := mapNode.Prototype() + qt.Check(t, fmt.Sprintf("%T", np), qt.Equals, "basicnode.Prototype__Map") + + // Amend the map + err = amender.Put(basicnode.NewString("cat"), basicnode.NewString("purr")) + if err != nil { + t.Fatal(err) + } + + // Access updated value of map, using get + r, err = amender.Get(basicnode.NewString("cat")) + if err != nil { + t.Fatal(err) + } + qt.Check(t, "purr", qt.Equals, must.String(r)) + + expect = `map{ + string{"cat"}: string{"purr"} + string{"dog"}: string{"bark"} + string{"eel"}: string{"zap"} +}` + + // The original node should have been updated + actual = printer.Sprint(mapNode) + qt.Assert(t, expect, qt.Equals, actual) + + // Remove an entry + removed, err := amender.Remove(basicnode.NewString("cat")) + if err != nil { + t.Fatal(err) + } + qt.Assert(t, removed, qt.IsTrue, qt.Commentf("remove should have returned true")) + + expect = `map{ + string{"dog"}: string{"bark"} + string{"eel"}: string{"zap"} +}` + + // The original node should have been updated + actual = printer.Sprint(mapNode) + qt.Assert(t, expect, qt.Equals, actual) + + // Should not find "cat" + r, err = amender.Get(basicnode.NewString("cat")) + if _, notFoundErr := err.(datamodel.ErrNotExists); !notFoundErr { + t.Fatal(err) + } + + keys, err := amender.Keys() + if err != nil { + t.Fatal(err) + } + expect = `list{ + 0: string{"dog"} + 1: string{"eel"} +}` + actual = printer.Sprint(keys) + qt.Assert(t, expect, qt.Equals, actual) + + values, err := amender.Values() + if err != nil { + t.Fatal(err) + } + expect = `list{ + 0: string{"bark"} + 1: string{"zap"} +}` + actual = printer.Sprint(values) + qt.Assert(t, expect, qt.Equals, actual) +} + +func TestMapAmendingBuilderExistingNode(t *testing.T) { + b := basicnode.Prototype.Map.NewBuilder() + + // construct a map of three keys, using the MapBuilder + ma, err := b.BeginMap(3) + if err != nil { + t.Fatal(err) + } + a := ma.AssembleKey() + a.AssignString("cat") + a = ma.AssembleValue() + a.AssignString("meow") + + a, err = ma.AssembleEntry("dog") + if err != nil { + t.Fatal(err) + } + a.AssignString("bark") + + a = ma.AssembleKey() + a.AssignString("eel") + a = ma.AssembleValue() + a.AssignString("zap") + + err = ma.Finish() + if err != nil { + t.Fatal(err) + } + + // Wrap in an amending builder + amender := basicnode.Prototype.Map.AmendingBuilder(b.Build()) + + // Compare the printed map + mapNode := b.Build() + actual := printer.Sprint(mapNode) + + expect := `map{ + string{"cat"}: string{"meow"} + string{"dog"}: string{"bark"} + string{"eel"}: string{"zap"} +}` + qt.Check(t, expect, qt.Equals, actual) + + // Access values of map, using string + r, err := mapNode.LookupByString("cat") + if err != nil { + t.Fatal(err) + } + qt.Check(t, "meow", qt.Equals, must.String(r)) + + // Access values of map, using node + r, err = mapNode.LookupByNode(basicnode.NewString("dog")) + if err != nil { + t.Fatal(err) + } + qt.Check(t, "bark", qt.Equals, must.String(r)) + + // Access values of map, using PathSegment + r, err = mapNode.LookupBySegment(datamodel.ParsePathSegment("eel")) + if err != nil { + t.Fatal(err) + } + qt.Check(t, "zap", qt.Equals, must.String(r)) + + // Validate the node's prototype + np := mapNode.Prototype() + qt.Check(t, fmt.Sprintf("%T", np), qt.Equals, "basicnode.Prototype__Map") + + // Amend the map + err = amender.Put(basicnode.NewString("cat"), basicnode.NewString("purr")) + if err != nil { + t.Fatal(err) + } + + // Access updated value of map, using get + r, err = amender.Get(basicnode.NewString("cat")) + if err != nil { + t.Fatal(err) + } + qt.Check(t, "purr", qt.Equals, must.String(r)) + + expect = `map{ + string{"cat"}: string{"purr"} + string{"dog"}: string{"bark"} + string{"eel"}: string{"zap"} +}` + + // The original node should have been updated + actual = printer.Sprint(mapNode) + qt.Assert(t, expect, qt.Equals, actual) + + // Remove an entry + removed, err := amender.Remove(basicnode.NewString("cat")) + if err != nil { + t.Fatal(err) + } + qt.Assert(t, removed, qt.IsTrue, qt.Commentf("remove should have returned true")) + + expect = `map{ + string{"dog"}: string{"bark"} + string{"eel"}: string{"zap"} +}` + + // The original node should have been updated + actual = printer.Sprint(mapNode) + qt.Assert(t, expect, qt.Equals, actual) + + // Should not find "cat" + r, err = amender.Get(basicnode.NewString("cat")) + if _, notFoundErr := err.(datamodel.ErrNotExists); !notFoundErr { + t.Fatal(err) + } + + keys, err := amender.Keys() + if err != nil { + t.Fatal(err) + } + expect = `list{ + 0: string{"dog"} + 1: string{"eel"} +}` + actual = printer.Sprint(keys) + qt.Assert(t, expect, qt.Equals, actual) + + values, err := amender.Values() + if err != nil { + t.Fatal(err) + } + expect = `list{ + 0: string{"bark"} + 1: string{"zap"} +}` + actual = printer.Sprint(values) + qt.Assert(t, expect, qt.Equals, actual) +} + +func TestMapAmendingBuilderCopiedNode(t *testing.T) { + b := basicnode.Prototype.Map.NewBuilder() + + // Construct a map of three keys, using the MapBuilder + ma, err := b.BeginMap(3) + if err != nil { + t.Fatal(err) + } + a := ma.AssembleKey() + a.AssignString("cat") + a = ma.AssembleValue() + a.AssignString("meow") + + a, err = ma.AssembleEntry("dog") + if err != nil { + t.Fatal(err) + } + a.AssignString("bark") + + a = ma.AssembleKey() + a.AssignString("eel") + a = ma.AssembleValue() + a.AssignString("zap") + + err = ma.Finish() + if err != nil { + t.Fatal(err) + } + + origMapNode := b.Build() + // Copy the map using datamodel.Copy. AssignNode will copy pointers to internal values and will not return a fully + // standalone copy. + origAmender := basicnode.Prototype.Map.AmendingBuilder(nil) + err = datamodel.Copy(origMapNode, origAmender) + if err != nil { + t.Fatal(err) + } + + // Wrap in an amending builder + newAmender := basicnode.Prototype.Map.AmendingBuilder(origAmender.Build()) + + // Compare the printed map + newMapNode := newAmender.Build() + actual := printer.Sprint(newMapNode) + + expect := `map{ + string{"cat"}: string{"meow"} + string{"dog"}: string{"bark"} + string{"eel"}: string{"zap"} +}` + qt.Check(t, expect, qt.Equals, actual) + + // Amend the copied map + err = newAmender.Put(basicnode.NewString("cat"), basicnode.NewString("purr")) + if err != nil { + t.Fatal(err) + } + + expect = `map{ + string{"cat"}: string{"purr"} + string{"dog"}: string{"bark"} + string{"eel"}: string{"zap"} +}` + + // The new node should have been updated + actual = printer.Sprint(newMapNode) + qt.Assert(t, expect, qt.Equals, actual) + + // Remove an entry + removed, err := newAmender.Remove(basicnode.NewString("cat")) + if err != nil { + t.Fatal(err) + } + qt.Assert(t, removed, qt.IsTrue, qt.Commentf("remove should have returned true")) + + expect = `map{ + string{"dog"}: string{"bark"} + string{"eel"}: string{"zap"} +}` + + _, err = newAmender.Get(basicnode.NewString("cat")) + if _, notFoundErr := err.(datamodel.ErrNotExists); !notFoundErr { + t.Fatal(err) + } + + keys, err := newAmender.Keys() + if err != nil { + t.Fatal(err) + } + expect = `list{ + 0: string{"dog"} + 1: string{"eel"} +}` + actual = printer.Sprint(keys) + qt.Assert(t, expect, qt.Equals, actual) + + values, err := newAmender.Values() + if err != nil { + t.Fatal(err) + } + expect = `list{ + 0: string{"bark"} + 1: string{"zap"} +}` + actual = printer.Sprint(values) + qt.Assert(t, expect, qt.Equals, actual) + + // The original node should not have been updated + expect = `map{ + string{"cat"}: string{"meow"} + string{"dog"}: string{"bark"} + string{"eel"}: string{"zap"} +}` + actual = printer.Sprint(origMapNode) + qt.Assert(t, expect, qt.Equals, actual) +} diff --git a/traversal/walk.go b/traversal/walk.go index 812ee47f..dc49c458 100644 --- a/traversal/walk.go +++ b/traversal/walk.go @@ -3,6 +3,7 @@ package traversal import ( "errors" "fmt" + "github.com/ipld/go-ipld-prime/node/basicnode" "github.com/ipld/go-ipld-prime/datamodel" "github.com/ipld/go-ipld-prime/linking" @@ -747,16 +748,16 @@ func (prog Progress) walk_transform_iterateAmendableMap(n datamodel.Node, s sele if err != nil { return nil, err } - if _, err := mapAmender.Put(ps.String(), next); err != nil { + if err := mapAmender.Put(basicnode.NewString(ps.String()), next); err != nil { return nil, err } } else { - if _, err := mapAmender.Put(ps.String(), v); err != nil { + if err := mapAmender.Put(basicnode.NewString(ps.String()), v); err != nil { return nil, err } } } else { - if _, err := mapAmender.Put(ps.String(), v); err != nil { + if err := mapAmender.Put(basicnode.NewString(ps.String()), v); err != nil { return nil, err } }