From 8984f185fd105e8a89f29b0d94777f06fdc74ed7 Mon Sep 17 00:00:00 2001 From: Mohsin Zaidi <2236875+smrz2001@users.noreply.github.com> Date: Thu, 6 Jul 2023 14:38:36 -0400 Subject: [PATCH] feat: add map/list amender interfaces --- datamodel/amender.go | 31 ++++++++++++ datamodel/node.go | 12 +++++ node/basicnode/list.go | 106 +++++++++++++++++++++++++++++++++++------ node/basicnode/map.go | 88 ++++++++++++++++++++++++++++++---- traversal/walk.go | 43 +++++++---------- 5 files changed, 231 insertions(+), 49 deletions(-) diff --git a/datamodel/amender.go b/datamodel/amender.go index e9b0bd2f..351455bf 100644 --- a/datamodel/amender.go +++ b/datamodel/amender.go @@ -14,3 +14,34 @@ type NodeAmender interface { // Transform returns the previous state of the target Node. Transform(path Path, transform AmendFn) (Node, error) } + +// containerAmender is an internal type for representing the interface for amendable containers (like maps and lists) +type containerAmender interface { + Empty() bool + Length() int64 + Clear() + Values() ([]Node, error) + + 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) + + containerAmender +} + +// ListAmender adds a list-like interface to NodeAmender +type ListAmender interface { + Get(idx int64) (Node, error) + Remove(idx int64) error + Append(values ...Node) error + Insert(idx int64, values ...Node) error + Set(idx int64, value Node) error + + containerAmender +} diff --git a/datamodel/node.go b/datamodel/node.go index 969f96ed..ee500d1d 100644 --- a/datamodel/node.go +++ b/datamodel/node.go @@ -246,6 +246,18 @@ type NodePrototypeSupportingAmend interface { // FUTURE: consider putting this (and others like it) in a `feature` package, if there begin to be enough of them and docs get crowded. } +// NodePrototypeSupportingMapAmend is a feature-detection interface that can be used on a NodePrototype to see if it's +// possible to update existing map-like nodes of this style. +type NodePrototypeSupportingMapAmend interface { + AmendingBuilder(base Node) MapAmender +} + +// NodePrototypeSupportingListAmend is a feature-detection interface that can be used on a NodePrototype to see if it's +// possible to update existing list-like nodes of this style. +type NodePrototypeSupportingListAmend interface { + AmendingBuilder(base Node) ListAmender +} + // MapIterator is an interface for traversing map nodes. // Sequential calls to Next() will yield key-value pairs; // Done() describes whether iteration should continue. diff --git a/node/basicnode/list.go b/node/basicnode/list.go index bf2b1330..cdd45eaf 100644 --- a/node/basicnode/list.go +++ b/node/basicnode/list.go @@ -9,11 +9,11 @@ import ( ) var ( - _ datamodel.Node = &plainList{} - _ datamodel.NodePrototype = Prototype__List{} - _ datamodel.NodePrototypeSupportingAmend = Prototype__List{} - _ datamodel.NodeBuilder = &plainList__Builder{} - _ datamodel.NodeAssembler = &plainList__Assembler{} + _ datamodel.Node = &plainList{} + _ datamodel.NodePrototype = Prototype__List{} + _ datamodel.NodePrototypeSupportingListAmend = Prototype__List{} + _ datamodel.NodeBuilder = &plainList__Builder{} + _ datamodel.NodeAssembler = &plainList__Assembler{} ) // plainList is a concrete type that provides a list-kind datamodel.Node. @@ -124,9 +124,9 @@ func (p Prototype__List) NewBuilder() datamodel.NodeBuilder { return p.AmendingBuilder(nil) } -// -- NodePrototypeSupportingAmend --> +// -- NodePrototypeSupportingListAmend --> -func (p Prototype__List) AmendingBuilder(base datamodel.Node) datamodel.NodeAmender { +func (p Prototype__List) AmendingBuilder(base datamodel.Node) datamodel.ListAmender { var w *plainList if base != nil { if baseList, castOk := base.(*plainList); !castOk { @@ -206,7 +206,7 @@ func (nb *plainList__Builder) Transform(path datamodel.Path, transform datamodel // - 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 + // with two conflicting sets of semantics - the current `focus` and `walk`, which (quite reasonably) do 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 inefficient. @@ -233,15 +233,15 @@ func (nb *plainList__Builder) storeChildAmender(childIdx int64, a datamodel.Node if (n.Kind() == datamodel.Kind_List) && (n.Length() > 0) { elems = make([]datamodel.NodeAmender, n.Length()) // 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 + // Patch) and replacements (needed by `focus` and `walk`), 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. + // - If appending to the end of the main list, all elements from the transformed list will be individually + // appended to the end of the list. + // - If updating at a particular index in the main list, 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 @@ -270,6 +270,84 @@ func (nb *plainList__Builder) storeChildAmender(childIdx int64, a datamodel.Node return nil } +func (nb *plainList__Builder) Get(idx int64) (datamodel.Node, error) { + return nb.w.LookupByIndex(idx) +} + +func (nb *plainList__Builder) Remove(idx int64) error { + _, err := nb.Transform( + datamodel.NewPath([]datamodel.PathSegment{datamodel.PathSegmentOfInt(idx)}), + func(_ datamodel.Node) (datamodel.NodeAmender, error) { + return nil, nil + }, + ) + return err +} + +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...) +} + +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 + } else { + ps = datamodel.PathSegmentOfInt(idx) + } + _, 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 err +} + +func (nb *plainList__Builder) Set(idx int64, value datamodel.Node) error { + return nb.Insert(idx, value) +} + +func (nb *plainList__Builder) Empty() bool { + return nb.Length() == 0 +} + +func (nb *plainList__Builder) Length() int64 { + return nb.w.Length() +} + +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 +} + // -- NodeAssembler --> type plainList__Assembler struct { diff --git a/node/basicnode/map.go b/node/basicnode/map.go index e35bf861..09413c52 100644 --- a/node/basicnode/map.go +++ b/node/basicnode/map.go @@ -9,11 +9,11 @@ import ( ) var ( - _ datamodel.Node = &plainMap{} - _ datamodel.NodePrototype = Prototype__Map{} - _ datamodel.NodePrototypeSupportingAmend = Prototype__Map{} - _ datamodel.NodeAmender = &plainMap__Builder{} - _ datamodel.NodeAssembler = &plainMap__Assembler{} + _ datamodel.Node = &plainMap{} + _ datamodel.NodePrototype = Prototype__Map{} + _ datamodel.NodePrototypeSupportingMapAmend = Prototype__Map{} + _ datamodel.NodeAmender = &plainMap__Builder{} + _ datamodel.NodeAssembler = &plainMap__Assembler{} ) // plainMap is a concrete type that provides a map-kind datamodel.Node. @@ -125,9 +125,9 @@ func (p Prototype__Map) NewBuilder() datamodel.NodeBuilder { return p.AmendingBuilder(nil) } -// -- NodePrototypeSupportingAmend --> +// -- NodePrototypeSupportingMapAmend --> -func (p Prototype__Map) AmendingBuilder(base datamodel.Node) datamodel.NodeAmender { +func (p Prototype__Map) AmendingBuilder(base datamodel.Node) datamodel.MapAmender { var b *plainMap if base != nil { if baseMap, castOk := base.(*plainMap); !castOk { @@ -158,7 +158,7 @@ func (nb *plainMap__Builder) Reset() { nb.w = &plainMap{} } -// -- NodeAmender --> +// -- MapAmender --> func (nb *plainMap__Builder) Transform(path datamodel.Path, transform datamodel.AmendFn) (datamodel.Node, error) { // Can only transform the root of the node or an immediate child. @@ -219,6 +219,78 @@ 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(_ datamodel.Node) (datamodel.NodeAmender, error) { + return Prototype.Any.AmendingBuilder(value), nil + }, + ); err != nil { + return false, err + } else { + // If there was no previous node, we just added a new node. + return prevNode == nil, nil + } +} + +func (nb *plainMap__Builder) Get(key string) (datamodel.Node, error) { + return nb.w.LookupByString(key) +} + +func (nb *plainMap__Builder) Remove(key string) (bool, error) { + if prevNode, err := nb.Transform( + datamodel.NewPath([]datamodel.PathSegment{datamodel.PathSegmentOfString(key)}), + func(_ datamodel.Node) (datamodel.NodeAmender, error) { + return nil, nil + }, + ); err != nil { + return false, err + } else { + // If there was a previous node, we just removed it. + return prevNode != nil, nil + } +} + +func (nb *plainMap__Builder) Keys() ([]string, error) { + keys := make([]string, 0, nb.Length()) + for itr := nb.w.MapIterator(); !itr.Done(); { + k, _, err := itr.Next() + if err != nil { + return nil, err + } + keyStr, err := k.AsString() + if err != nil { + return nil, err + } + keys = append(keys, keyStr) + } + return keys, nil +} + +func (nb *plainMap__Builder) Empty() bool { + return nb.Length() == 0 +} + +func (nb *plainMap__Builder) Length() int64 { + return nb.w.Length() +} + +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 +} + // -- NodeAssembler --> type plainMap__Assembler struct { diff --git a/traversal/walk.go b/traversal/walk.go index ff7805e4..812ee47f 100644 --- a/traversal/walk.go +++ b/traversal/walk.go @@ -7,7 +7,6 @@ import ( "github.com/ipld/go-ipld-prime/datamodel" "github.com/ipld/go-ipld-prime/linking" "github.com/ipld/go-ipld-prime/linking/preload" - "github.com/ipld/go-ipld-prime/node/basicnode" "github.com/ipld/go-ipld-prime/traversal/selector" ) @@ -496,12 +495,12 @@ func (prog Progress) walkTransforming(n datamodel.Node, s selector.Selector, fn nk := n.Kind() switch nk { case datamodel.Kind_List: - if _, castOk := n.Prototype().(datamodel.NodePrototypeSupportingAmend); castOk { + if _, castOk := n.Prototype().(datamodel.NodePrototypeSupportingListAmend); castOk { return prog.walk_transform_iterateAmendableList(n, s, fn, s.Interests()) } return prog.walk_transform_iterateList(n, s, fn, s.Interests()) case datamodel.Kind_Map: - if _, castOk := n.Prototype().(datamodel.NodePrototypeSupportingAmend); castOk { + if _, castOk := n.Prototype().(datamodel.NodePrototypeSupportingMapAmend); castOk { return prog.walk_transform_iterateAmendableMap(n, s, fn, s.Interests()) } return prog.walk_transform_iterateMap(n, s, fn, s.Interests()) @@ -582,20 +581,17 @@ func (prog Progress) walk_transform_iterateList(n datamodel.Node, s selector.Sel } func (prog Progress) walk_transform_iterateAmendableList(n datamodel.Node, s selector.Selector, fn TransformFn, attn []datamodel.PathSegment) (datamodel.Node, error) { - amender := n.Prototype().(datamodel.NodePrototypeSupportingAmend).AmendingBuilder(n) - - transform := func(ps datamodel.PathSegment, node datamodel.Node) error { - _, err := amender.Transform(datamodel.NewPath([]datamodel.PathSegment{ps}), func(_ datamodel.Node) (datamodel.NodeAmender, error) { - return basicnode.Prototype.Any.AmendingBuilder(node), nil - }) - return err - } + listAmender := n.Prototype().(datamodel.NodePrototypeSupportingListAmend).AmendingBuilder(n) for itr := selector.NewSegmentIterator(n); !itr.Done(); { ps, v, err := itr.Next() if err != nil { return nil, err } + idx, err := ps.Index() + if err != nil { + return nil, err + } if attn == nil || contains(attn, ps) { sNext, err := s.Explore(n, ps) if err != nil { @@ -627,21 +623,21 @@ func (prog Progress) walk_transform_iterateAmendableList(n datamodel.Node, s sel if err != nil { return nil, err } - if err := transform(ps, next); err != nil { + if err := listAmender.Set(idx, next); err != nil { return nil, err } } else { - if err := transform(ps, v); err != nil { + if err := listAmender.Set(idx, v); err != nil { return nil, err } } } else { - if err := transform(ps, v); err != nil { + if err := listAmender.Set(idx, v); err != nil { return nil, err } } } - return amender.Build(), nil + return listAmender.Build(), nil } func (prog Progress) walk_transform_iterateMap(n datamodel.Node, s selector.Selector, fn TransformFn, attn []datamodel.PathSegment) (datamodel.Node, error) { @@ -712,14 +708,7 @@ func (prog Progress) walk_transform_iterateMap(n datamodel.Node, s selector.Sele } func (prog Progress) walk_transform_iterateAmendableMap(n datamodel.Node, s selector.Selector, fn TransformFn, attn []datamodel.PathSegment) (datamodel.Node, error) { - amender := n.Prototype().(datamodel.NodePrototypeSupportingAmend).AmendingBuilder(n) - - transform := func(ps datamodel.PathSegment, node datamodel.Node) error { - _, err := amender.Transform(datamodel.NewPath([]datamodel.PathSegment{ps}), func(_ datamodel.Node) (datamodel.NodeAmender, error) { - return basicnode.Prototype.Any.AmendingBuilder(node), nil - }) - return err - } + mapAmender := n.Prototype().(datamodel.NodePrototypeSupportingMapAmend).AmendingBuilder(n) for itr := selector.NewSegmentIterator(n); !itr.Done(); { ps, v, err := itr.Next() @@ -758,19 +747,19 @@ func (prog Progress) walk_transform_iterateAmendableMap(n datamodel.Node, s sele if err != nil { return nil, err } - if err := transform(ps, next); err != nil { + if _, err := mapAmender.Put(ps.String(), next); err != nil { return nil, err } } else { - if err := transform(ps, v); err != nil { + if _, err := mapAmender.Put(ps.String(), v); err != nil { return nil, err } } } else { - if err := transform(ps, v); err != nil { + if _, err := mapAmender.Put(ps.String(), v); err != nil { return nil, err } } } - return amender.Build(), nil + return mapAmender.Build(), nil }