diff --git a/datamodel/amender.go b/datamodel/amender.go new file mode 100644 index 00000000..6d0bc123 --- /dev/null +++ b/datamodel/amender.go @@ -0,0 +1,56 @@ +package datamodel + +// AmendFn takes a Node and returns a NodeAmender that stores any applied transformations. The returned NodeAmender +// allows further transformations to be applied to the Node under construction. +type AmendFn func(Node) (NodeAmender, error) + +// NodeAmender adds to NodeBuilder the ability to transform all or part of a Node under construction. +type NodeAmender interface { + NodeBuilder + + // Transform takes in a Node (or a child Node of a recursive node) along with a transformation function that returns + // a new NodeAmender with the transformed results. + // + // 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) // returns a list Node with the values + + NodeAmender +} + +// MapAmender adds a map-like interface to NodeAmender +type MapAmender interface { + Put(key string, value Node) error + Get(key string) (Node, error) + Remove(key string) (bool, error) + Keys() (Node, error) // returns a list Node with the keys + + containerAmender +} + +// ListAmender adds a list-like interface to NodeAmender +type ListAmender interface { + Get(idx int64) (Node, error) + Remove(idx int64) error + // Append will add Node(s) to the end of the list. It can accept a list Node with multiple values to append. + Append(value Node) error + // Insert will add Node(s) at the specified index and shift subsequent elements to the right. It can accept a list + // Node with multiple values to insert. + // Passing an index equal to the length of the list will add Node(s) to the end of the list like Append. + Insert(idx int64, value Node) error + // Set will add Node(s) at the specified index and shift subsequent elements to the right. It can accept a list Node + // with multiple values to insert. + // Passing an index equal to the length of the list will add Node(s) to the end of the list like Append. + // Set is different from Insert in that it will start its insertion at the specified index, overwriting it in the + // process, while Insert will only add the Node(s). + Set(idx int64, value Node) error + + containerAmender +} diff --git a/datamodel/node.go b/datamodel/node.go index 625f472d..ee500d1d 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) NodeAmender // 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. @@ -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/any.go b/node/basicnode/any.go index cc248463..a1f33554 100644 --- a/node/basicnode/any.go +++ b/node/basicnode/any.go @@ -1,14 +1,18 @@ package basicnode import ( + "fmt" + "reflect" + "github.com/ipld/go-ipld-prime/datamodel" "github.com/ipld/go-ipld-prime/linking" ) var ( //_ datamodel.Node = &anyNode{} - _ datamodel.NodePrototype = Prototype__Any{} - _ datamodel.NodeBuilder = &anyBuilder{} + _ datamodel.NodePrototype = Prototype__Any{} + _ datamodel.NodePrototypeSupportingAmend = Prototype__Any{} + _ datamodel.NodeBuilder = &anyBuilder{} //_ datamodel.NodeAssembler = &anyAssembler{} ) @@ -34,8 +38,24 @@ func Chooser(_ datamodel.Link, _ linking.LinkContext) (datamodel.NodePrototype, type Prototype__Any struct{} -func (Prototype__Any) NewBuilder() datamodel.NodeBuilder { - return &anyBuilder{} +func (p Prototype__Any) NewBuilder() datamodel.NodeBuilder { + return p.AmendingBuilder(nil) +} + +// -- NodePrototypeSupportingAmend --> + +func (p Prototype__Any) AmendingBuilder(base datamodel.Node) datamodel.NodeAmender { + ab := &anyBuilder{} + if base != nil { + ab.kind = base.Kind() + if npa, castOk := base.Prototype().(datamodel.NodePrototypeSupportingAmend); castOk { + ab.amender = npa.AmendingBuilder(base) + } else { + // This node could be either scalar or recursive + ab.baseNode = base + } + } + return ab } // -- NodeBuilder --> @@ -57,17 +77,16 @@ type anyBuilder struct { kind datamodel.Kind // Only one of the following ends up being used... - // but we don't know in advance which one, so all are embeded here. + // but we don't know in advance which one, so both are embedded here. // This uses excessive space, but amortizes allocations, and all will be // freed as soon as the builder is done. - // Builders are only used for recursives; - // scalars are simple enough we just do them directly. - // 'scalarNode' may also hold another Node of unknown prototype (possibly not even from this package), + // An amender is only used for amendable nodes, while all non-amendable nodes (both recursives and scalars) are + // stored directly. + // 'baseNode' may also hold another Node of unknown prototype (possibly not even from this package), // in which case this is indicated by 'kind==99'. - mapBuilder plainMap__Builder - listBuilder plainList__Builder - scalarNode datamodel.Node + amender datamodel.NodeAmender + baseNode datamodel.Node } func (nb *anyBuilder) Reset() { @@ -79,16 +98,18 @@ func (nb *anyBuilder) BeginMap(sizeHint int64) (datamodel.MapAssembler, error) { panic("misuse") } nb.kind = datamodel.Kind_Map - nb.mapBuilder.w = &plainMap{} - return nb.mapBuilder.BeginMap(sizeHint) + mapBuilder := Prototype.Map.NewBuilder().(*plainMap__Builder) + nb.amender = mapBuilder + return mapBuilder.BeginMap(sizeHint) } func (nb *anyBuilder) BeginList(sizeHint int64) (datamodel.ListAssembler, error) { if nb.kind != datamodel.Kind_Invalid { panic("misuse") } nb.kind = datamodel.Kind_List - nb.listBuilder.w = &plainList{} - return nb.listBuilder.BeginList(sizeHint) + listBuilder := Prototype.List.NewBuilder().(*plainList__Builder) + nb.amender = listBuilder + return listBuilder.BeginList(sizeHint) } func (nb *anyBuilder) AssignNull() error { if nb.kind != datamodel.Kind_Invalid { @@ -102,7 +123,7 @@ func (nb *anyBuilder) AssignBool(v bool) error { panic("misuse") } nb.kind = datamodel.Kind_Bool - nb.scalarNode = NewBool(v) + nb.baseNode = NewBool(v) return nil } func (nb *anyBuilder) AssignInt(v int64) error { @@ -110,7 +131,7 @@ func (nb *anyBuilder) AssignInt(v int64) error { panic("misuse") } nb.kind = datamodel.Kind_Int - nb.scalarNode = NewInt(v) + nb.baseNode = NewInt(v) return nil } func (nb *anyBuilder) AssignFloat(v float64) error { @@ -118,7 +139,7 @@ func (nb *anyBuilder) AssignFloat(v float64) error { panic("misuse") } nb.kind = datamodel.Kind_Float - nb.scalarNode = NewFloat(v) + nb.baseNode = NewFloat(v) return nil } func (nb *anyBuilder) AssignString(v string) error { @@ -126,7 +147,7 @@ func (nb *anyBuilder) AssignString(v string) error { panic("misuse") } nb.kind = datamodel.Kind_String - nb.scalarNode = NewString(v) + nb.baseNode = NewString(v) return nil } func (nb *anyBuilder) AssignBytes(v []byte) error { @@ -134,7 +155,7 @@ func (nb *anyBuilder) AssignBytes(v []byte) error { panic("misuse") } nb.kind = datamodel.Kind_Bytes - nb.scalarNode = NewBytes(v) + nb.baseNode = NewBytes(v) return nil } func (nb *anyBuilder) AssignLink(v datamodel.Link) error { @@ -142,7 +163,7 @@ func (nb *anyBuilder) AssignLink(v datamodel.Link) error { panic("misuse") } nb.kind = datamodel.Kind_Link - nb.scalarNode = NewLink(v) + nb.baseNode = NewLink(v) return nil } func (nb *anyBuilder) AssignNode(v datamodel.Node) error { @@ -150,7 +171,7 @@ func (nb *anyBuilder) AssignNode(v datamodel.Node) error { panic("misuse") } nb.kind = 99 - nb.scalarNode = v + nb.baseNode = v return nil } func (anyBuilder) Prototype() datamodel.NodePrototype { @@ -158,34 +179,61 @@ func (anyBuilder) Prototype() datamodel.NodePrototype { } func (nb *anyBuilder) Build() datamodel.Node { + if nb.amender != nil { + return nb.amender.Build() + } switch nb.kind { case datamodel.Kind_Invalid: panic("misuse") case datamodel.Kind_Map: - return nb.mapBuilder.Build() + return nb.baseNode case datamodel.Kind_List: - return nb.listBuilder.Build() + return nb.baseNode case datamodel.Kind_Null: return datamodel.Null case datamodel.Kind_Bool: - return nb.scalarNode + return nb.baseNode case datamodel.Kind_Int: - return nb.scalarNode + return nb.baseNode case datamodel.Kind_Float: - return nb.scalarNode + return nb.baseNode case datamodel.Kind_String: - return nb.scalarNode + return nb.baseNode case datamodel.Kind_Bytes: - return nb.scalarNode + return nb.baseNode case datamodel.Kind_Link: - return nb.scalarNode + return nb.baseNode case 99: - return nb.scalarNode + return nb.baseNode default: panic("unreachable") } } +// -- NodeAmender --> + +func (nb *anyBuilder) Transform(path datamodel.Path, transform datamodel.AmendFn) (datamodel.Node, error) { + // If the root is being replaced, replace it. If the transformation is for a nested node in a non-amendable + // recursive object, panic. + if path.Len() == 0 { + prevNode := nb.Build() + if newNode, err := transform(prevNode); err != nil { + return nil, err + } else if newAb, castOk := newNode.(*anyBuilder); !castOk { + return nil, fmt.Errorf("transform: cannot transform root into incompatible type: %v", reflect.TypeOf(newAb)) + } else { + nb.amender = newAb.amender + nb.baseNode = newAb.baseNode + return prevNode, nil + } + } + if nb.amender != nil { + return nb.amender.Transform(path, transform) + } + // `Transform` should never be called for a non-amendable node + panic("misuse") +} + // -- NodeAssembler --> // ... oddly enough, we seem to be able to put off implementing this diff --git a/node/basicnode/list.go b/node/basicnode/list.go index 6f7582bb..632b7a4b 100644 --- a/node/basicnode/list.go +++ b/node/basicnode/list.go @@ -1,22 +1,26 @@ package basicnode import ( + "fmt" + "reflect" + "github.com/ipld/go-ipld-prime/datamodel" "github.com/ipld/go-ipld-prime/node/mixins" ) var ( - _ datamodel.Node = &plainList{} - _ datamodel.NodePrototype = 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. // It can contain any kind of value. // plainList is also embedded in the 'any' struct and usable from there. type plainList struct { - x []datamodel.Node + x []datamodel.NodeAmender } // -- Node interface methods --> @@ -31,17 +35,31 @@ func (plainList) LookupByNode(datamodel.Node) (datamodel.Node, error) { return mixins.List{TypeName: "list"}.LookupByNode(nil) } func (n *plainList) LookupByIndex(idx int64) (datamodel.Node, error) { + if v, err := n.lookupAmenderByIndex(idx); err != nil { + return nil, err + } else { + return v.Build(), nil + } +} +func (n *plainList) lookupAmenderByIndex(idx int64) (datamodel.NodeAmender, error) { if n.Length() <= idx { return nil, datamodel.ErrNotExists{Segment: datamodel.PathSegmentOfInt(idx)} } return n.x[idx], nil } func (n *plainList) LookupBySegment(seg datamodel.PathSegment) (datamodel.Node, error) { + if v, err := n.lookupAmenderBySegment(seg); err != nil { + return nil, err + } else { + return v.Build(), nil + } +} +func (n *plainList) lookupAmenderBySegment(seg datamodel.PathSegment) (datamodel.NodeAmender, error) { idx, err := seg.Index() if err != nil { return nil, datamodel.ErrInvalidSegmentForList{TroubleSegment: seg, Reason: err} } - return n.LookupByIndex(idx) + return n.lookupAmenderByIndex(idx) } func (plainList) MapIterator() datamodel.MapIterator { return nil @@ -89,7 +107,7 @@ func (itr *plainList_ListIterator) Next() (idx int64, v datamodel.Node, _ error) if itr.Done() { return -1, nil, datamodel.ErrIteratorOverread{} } - v = itr.n.x[itr.idx] + v = itr.n.x[itr.idx].Build() idx = int64(itr.idx) itr.idx++ return @@ -102,8 +120,23 @@ func (itr *plainList_ListIterator) Done() bool { type Prototype__List struct{} -func (Prototype__List) NewBuilder() datamodel.NodeBuilder { - return &plainList__Builder{plainList__Assembler{w: &plainList{}}} +func (p Prototype__List) NewBuilder() datamodel.NodeBuilder { + return p.AmendingBuilder(nil) +} + +// -- NodePrototypeSupportingListAmend --> + +func (p Prototype__List) AmendingBuilder(base datamodel.Node) datamodel.ListAmender { + nb := &plainList__Builder{plainList__Assembler{w: &plainList{}}} + if base != nil { + if baseList, castOk := base.(*plainList); !castOk { + panic("misuse") + } else { + // Make a deep copy of the base list + datamodel.Copy(baseList, nb) + } + } + return nb } // -- NodeBuilder --> @@ -113,8 +146,8 @@ type plainList__Builder struct { } func (nb *plainList__Builder) Build() datamodel.Node { - if nb.state != laState_finished { - panic("invalid state: assembler must be 'finished' before Build can be called!") + if (nb.state != laState_initial) && (nb.state != laState_finished) { + panic("invalid state: assembly in progress must be 'finished' before Build can be called!") } return nb.w } @@ -123,6 +156,210 @@ func (nb *plainList__Builder) Reset() { nb.w = &plainList{} } +// -- NodeAmender --> + +func (nb *plainList__Builder) Transform(path datamodel.Path, transform datamodel.AmendFn) (datamodel.Node, error) { + return nb.transform(path, transform, true) +} + +func (nb *plainList__Builder) transform(path datamodel.Path, transform datamodel.AmendFn, replace bool) (datamodel.Node, error) { + // Can only transform the root of the node or an immediate child. + if path.Len() > 1 { + panic("misuse") + } + // Allow the root of the node to be replaced. + if path.Len() == 0 { + prevNode := nb.Build() + if newNode, err := transform(prevNode); err != nil { + return nil, err + } else if newLb, castOk := newNode.(*plainList__Builder); !castOk { + return nil, fmt.Errorf("transform: cannot transform root into incompatible type: %v", reflect.TypeOf(newLb)) + } else { + *nb.w = *newLb.w + return prevNode, nil + } + } + childSeg, _ := path.Shift() + childIdx, err := childSeg.Index() + var childAmender datamodel.NodeAmender + if err != nil { + if childSeg.String() == "-" { + // "-" indicates appending a new element to the end of the list. + childIdx = nb.w.Length() + } 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 >= nb.w.Length() { + return nil, fmt.Errorf("transform: cannot navigate path segment %q at %q because it is beyond the list bounds", childSeg, 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. + childAmender, err = nb.w.lookupAmenderByIndex(childIdx) + if err != nil { + // Return any error other than "not exists" + if _, notFoundErr := err.(datamodel.ErrNotExists); !notFoundErr { + return nil, fmt.Errorf("transform: child at %q did not exist)", path) + } + } + } + // 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 `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. + var prevChildVal datamodel.Node = nil + if childAmender != nil { + prevChildVal = childAmender.Build() + } + if newChildVal, err := transform(prevChildVal); err != nil { + return nil, err + } else if newChildVal == nil { + idx := int(childIdx) + nb.w.x[idx] = nil + // Ref: https://pkg.go.dev/golang.org/x/exp/slices#Delete + nb.w.x = append(nb.w.x[:idx], nb.w.x[idx+1:]...) + } else if err = nb.storeChildAmender(childIdx, newChildVal, replace); err != nil { + return nil, err + } + return prevChildVal, nil +} + +func (nb *plainList__Builder) storeChildAmender(childIdx int64, a datamodel.NodeAmender, replace bool) error { + var elems []datamodel.NodeAmender + n := a.Build() + if n.Kind() == datamodel.Kind_List { + 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 `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 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 + // inserted without affecting its semantics. Otherwise, the sub-list's elements will get expanded onto that + // index in the main list. + for i := range elems { + elem, err := n.LookupByIndex(int64(i)) + if err != nil { + return err + } + elems[i] = Prototype.Any.AmendingBuilder(elem) + } + } else { + elems = []datamodel.NodeAmender{Prototype.Any.AmendingBuilder(n)} + } + if childIdx == nb.w.Length() { + // Operations at the end of the list are straightforward - just append, and we're done. + nb.w.x = append(nb.w.x, elems...) + return nil + } + numElems := len(elems) + if numElems > 0 { + if nb.w.x == nil { + // Allocate storage space + nb.w.x = make([]datamodel.NodeAmender, numElems) + } + copyStartIdx := 0 + if replace { + // Use the first passed element to replace the element currently at the specified index + nb.w.x[childIdx] = elems[0] + copyStartIdx++ + } + if !replace || (numElems > 1) { + // If more elements were specified, insert them after the specified index. + nb.w.x = Insert(nb.w.x, int(childIdx)+copyStartIdx, elems[copyStartIdx:]...) + } + } + return nil +} + +// Ref: https://pkg.go.dev/golang.org/x/exp/slices#Insert +func Insert(x []datamodel.NodeAmender, idx int, elems ...datamodel.NodeAmender) []datamodel.NodeAmender { + tot := len(x) + len(elems) + if tot <= cap(x) { + x2 := x[:tot] + copy(x2[idx+len(elems):], x[idx:]) + copy(x2[idx:], elems) + return x2 + } + x2 := make([]datamodel.NodeAmender, tot) + copy(x2, x[:idx]) + copy(x2[idx:], elems) + copy(x2[idx+len(elems):], x[idx:]) + return x2 +} + +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(value datamodel.Node) error { + return nb.Set(nb.Length(), value) +} + +func (nb *plainList__Builder) Insert(idx int64, value datamodel.Node) error { + return nb.addElements(idx, value, false) +} + +func (nb *plainList__Builder) Set(idx int64, value datamodel.Node) error { + return nb.addElements(idx, value, true) +} + +func (nb *plainList__Builder) addElements(idx int64, value datamodel.Node, replaced bool) 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) { + return Prototype.Any.AmendingBuilder(value), nil + }, + replaced, + ) + return err +} + +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) { + return nb.Build(), nil +} + // -- NodeAssembler --> type plainList__Assembler struct { @@ -155,7 +392,7 @@ func (na *plainList__Assembler) BeginList(sizeHint int64) (datamodel.ListAssembl sizeHint = 0 } // Allocate storage space. - na.w.x = make([]datamodel.Node, 0, sizeHint) + na.w.x = make([]datamodel.NodeAmender, 0, sizeHint) // That's it; return self as the ListAssembler. We already have all the right methods on this structure. return na, nil } @@ -291,7 +528,7 @@ func (lva *plainList__ValueAssembler) AssignLink(v datamodel.Link) error { return lva.AssignNode(&vb) } func (lva *plainList__ValueAssembler) AssignNode(v datamodel.Node) error { - lva.la.w.x = append(lva.la.w.x, v) + lva.la.w.x = append(lva.la.w.x, Prototype.Any.AmendingBuilder(v)) lva.la.state = laState_initial lva.la = nil // invalidate self to prevent further incorrect use. return nil diff --git a/node/basicnode/list_test.go b/node/basicnode/list_test.go index 68cb1778..9d8e3ce7 100644 --- a/node/basicnode/list_test.go +++ b/node/basicnode/list_test.go @@ -1,12 +1,402 @@ package basicnode_test import ( + "fmt" "testing" + qt "github.com/frankban/quicktest" + + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/fluent/qp" + "github.com/ipld/go-ipld-prime/must" "github.com/ipld/go-ipld-prime/node/basicnode" "github.com/ipld/go-ipld-prime/node/tests" + "github.com/ipld/go-ipld-prime/printer" ) func TestList(t *testing.T) { tests.SpecTestListString(t, basicnode.Prototype.List) } + +func TestListAmendingBuilderNewNode(t *testing.T) { + amender := basicnode.Prototype.List.AmendingBuilder(nil) + + err := amender.Append(basicnode.NewString("cat")) + if err != nil { + t.Fatal(err) + } + err = amender.Append(basicnode.NewString("dog")) + if err != nil { + t.Fatal(err) + } + err = amender.Append(basicnode.NewString("eel")) + if err != nil { + t.Fatal(err) + } + listNode := amender.Build() + expect := `list{ + 0: string{"cat"} + 1: string{"dog"} + 2: string{"eel"} +}` + actual := printer.Sprint(listNode) + qt.Assert(t, actual, qt.Equals, expect) + + // Update an element at the start + err = amender.Set(0, basicnode.NewString("cow")) + if err != nil { + t.Fatal(err) + } + expect = `list{ + 0: string{"cow"} + 1: string{"dog"} + 2: string{"eel"} +}` + actual = printer.Sprint(listNode) + qt.Assert(t, actual, qt.Equals, expect) + + // Update an element in the middle + err = amender.Set(1, basicnode.NewString("fox")) + if err != nil { + t.Fatal(err) + } + expect = `list{ + 0: string{"cow"} + 1: string{"fox"} + 2: string{"eel"} +}` + actual = printer.Sprint(listNode) + qt.Assert(t, actual, qt.Equals, expect) + + // Update an element at the end + err = amender.Set(amender.Length(), basicnode.NewString("dog")) + if err != nil { + t.Fatal(err) + } + expect = `list{ + 0: string{"cow"} + 1: string{"fox"} + 2: string{"eel"} + 3: string{"dog"} +}` + actual = printer.Sprint(listNode) + qt.Assert(t, actual, qt.Equals, expect) + + // Delete an element from the start + err = amender.Remove(0) + if err != nil { + t.Fatal(err) + } + expect = `list{ + 0: string{"fox"} + 1: string{"eel"} + 2: string{"dog"} +}` + actual = printer.Sprint(listNode) + qt.Assert(t, actual, qt.Equals, expect) + + // Delete an element from the middle + err = amender.Remove(1) + if err != nil { + t.Fatal(err) + } + expect = `list{ + 0: string{"fox"} + 1: string{"dog"} +}` + actual = printer.Sprint(listNode) + qt.Assert(t, actual, qt.Equals, expect) + + // Delete an element from the end + err = amender.Remove(1) + if err != nil { + t.Fatal(err) + } + expect = `list{ + 0: string{"fox"} +}` + actual = printer.Sprint(listNode) + qt.Assert(t, actual, qt.Equals, expect) + + // Insert an element at the start + err = amender.Insert(0, basicnode.NewString("cat")) + if err != nil { + t.Fatal(err) + } + expect = `list{ + 0: string{"cat"} + 1: string{"fox"} +}` + actual = printer.Sprint(listNode) + qt.Assert(t, actual, qt.Equals, expect) + + // Insert an element in the middle + err = amender.Insert(1, basicnode.NewString("dog")) + if err != nil { + t.Fatal(err) + } + expect = `list{ + 0: string{"cat"} + 1: string{"dog"} + 2: string{"fox"} +}` + actual = printer.Sprint(listNode) + qt.Assert(t, actual, qt.Equals, expect) + + // Insert an element at the end + err = amender.Insert(amender.Length(), basicnode.NewString("eel")) + if err != nil { + t.Fatal(err) + } + expect = `list{ + 0: string{"cat"} + 1: string{"dog"} + 2: string{"fox"} + 3: string{"eel"} +}` + actual = printer.Sprint(listNode) + qt.Assert(t, actual, qt.Equals, expect) + + // Access values of list, using index + r, err := listNode.LookupByIndex(0) + if err != nil { + t.Fatal(err) + } + qt.Check(t, "cat", qt.Equals, must.String(r)) + + // Access values of list, using PathSegment + r, err = listNode.LookupBySegment(datamodel.ParsePathSegment("2")) + if err != nil { + t.Fatal(err) + } + qt.Check(t, "fox", qt.Equals, must.String(r)) + + // Access updated value of list, using get + r, err = amender.Get(3) + if err != nil { + t.Fatal(err) + } + qt.Check(t, "eel", qt.Equals, must.String(r)) + + // Validate the node's prototype + np := listNode.Prototype() + qt.Check(t, fmt.Sprintf("%T", np), qt.Equals, "basicnode.Prototype__List") +} + +func TestListAmendingBuilderExistingNode(t *testing.T) { + listNode, err := qp.BuildList(basicnode.Prototype.List, -1, func(am datamodel.ListAssembler) { + qp.ListEntry(am, qp.String("fox")) + qp.ListEntry(am, qp.String("cow")) + qp.ListEntry(am, qp.String("deer")) + }) + if err != nil { + t.Fatal(err) + } + expect := `list{ + 0: string{"fox"} + 1: string{"cow"} + 2: string{"deer"} +}` + actual := printer.Sprint(listNode) + qt.Assert(t, actual, qt.Equals, expect) + + amender := basicnode.Prototype.List.AmendingBuilder(listNode) + err = amender.Append(basicnode.NewString("cat")) + if err != nil { + t.Fatal(err) + } + newListNode := amender.Build() + expect = `list{ + 0: string{"fox"} + 1: string{"cow"} + 2: string{"deer"} + 3: string{"cat"} +}` + actual = printer.Sprint(newListNode) + qt.Assert(t, actual, qt.Equals, expect) + + insertElems, err := qp.BuildList(basicnode.Prototype.List, -1, func(am datamodel.ListAssembler) { + qp.ListEntry(am, qp.String("eel")) + qp.ListEntry(am, qp.String("dog")) + }) + if err != nil { + t.Fatal(err) + } + err = amender.Insert(3, insertElems) + if err != nil { + t.Fatal(err) + } + expect = `list{ + 0: string{"fox"} + 1: string{"cow"} + 2: string{"deer"} + 3: string{"eel"} + 4: string{"dog"} + 5: string{"cat"} +}` + actual = printer.Sprint(newListNode) + qt.Assert(t, actual, qt.Equals, expect) + + err = amender.Append(basicnode.NewString("eel")) + if err != nil { + t.Fatal(err) + } + expect = `list{ + 0: string{"fox"} + 1: string{"cow"} + 2: string{"deer"} + 3: string{"eel"} + 4: string{"dog"} + 5: string{"cat"} + 6: string{"eel"} +}` + actual = printer.Sprint(newListNode) + qt.Assert(t, actual, qt.Equals, expect) + + // The original node should not have been updated + expect = `list{ + 0: string{"fox"} + 1: string{"cow"} + 2: string{"deer"} +}` + actual = printer.Sprint(listNode) + qt.Assert(t, actual, qt.Equals, expect) +} + +func TestListAmendingBuilderCopiedNode(t *testing.T) { + listNode, err := qp.BuildList(basicnode.Prototype.List, -1, func(am datamodel.ListAssembler) { + qp.ListEntry(am, qp.String("fox")) + qp.ListEntry(am, qp.String("cow")) + qp.ListEntry(am, qp.String("deer")) + }) + if err != nil { + t.Fatal(err) + } + amender := basicnode.Prototype.List.AmendingBuilder(listNode) + newListNode := amender.Build() + expect := `list{ + 0: string{"fox"} + 1: string{"cow"} + 2: string{"deer"} +}` + actual := printer.Sprint(newListNode) + qt.Assert(t, actual, qt.Equals, expect) + + setElems, err := qp.BuildList(basicnode.Prototype.List, -1, func(am datamodel.ListAssembler) { + qp.ListEntry(am, qp.String("cat")) + qp.ListEntry(am, qp.String("dog")) + }) + if err != nil { + t.Fatal(err) + } + err = amender.Set(0, setElems) + if err != nil { + t.Fatal(err) + } + expect = `list{ + 0: string{"cat"} + 1: string{"dog"} + 2: string{"cow"} + 3: string{"deer"} +}` + actual = printer.Sprint(newListNode) + qt.Assert(t, actual, qt.Equals, expect) + + insertElems, err := qp.BuildList(basicnode.Prototype.List, -1, func(am datamodel.ListAssembler) { + qp.ListEntry(am, qp.String("eel")) + qp.ListEntry(am, qp.String("fox")) + }) + if err != nil { + t.Fatal(err) + } + err = amender.Insert(1, insertElems) + if err != nil { + t.Fatal(err) + } + expect = `list{ + 0: string{"cat"} + 1: string{"eel"} + 2: string{"fox"} + 3: string{"dog"} + 4: string{"cow"} + 5: string{"deer"} +}` + actual = printer.Sprint(newListNode) + qt.Assert(t, actual, qt.Equals, expect) + + appendElems, err := qp.BuildList(basicnode.Prototype.List, -1, func(am datamodel.ListAssembler) { + qp.ListEntry(am, qp.String("rat")) + }) + if err != nil { + t.Fatal(err) + } + err = amender.Append(appendElems) + if err != nil { + t.Fatal(err) + } + expect = `list{ + 0: string{"cat"} + 1: string{"eel"} + 2: string{"fox"} + 3: string{"dog"} + 4: string{"cow"} + 5: string{"deer"} + 6: string{"rat"} +}` + actual = printer.Sprint(newListNode) + qt.Assert(t, actual, qt.Equals, expect) + + // Pass through an empty list. This should have no effect. + appendElems, err = qp.BuildList(basicnode.Prototype.List, -1, func(am datamodel.ListAssembler) {}) + if err != nil { + t.Fatal(err) + } + err = amender.Append(appendElems) + if err != nil { + t.Fatal(err) + } + actual = printer.Sprint(newListNode) + qt.Assert(t, actual, qt.Equals, expect) + + // Pass through a list containing another list + nestedList, err := qp.BuildList(basicnode.Prototype.List, 1, func(am datamodel.ListAssembler) { + qp.ListEntry(am, qp.String("bat")) + }) + if err != nil { + t.Fatal(err) + } + appendElems, err = qp.BuildList(basicnode.Prototype.List, 1, func(am datamodel.ListAssembler) { + qp.ListEntry(am, qp.Node(nestedList)) + }) + if err != nil { + t.Fatal(err) + } + err = amender.Append(appendElems) + if err != nil { + t.Fatal(err) + } + // The new node should have been updated to have a list node at the end + expect = `list{ + 0: string{"cat"} + 1: string{"eel"} + 2: string{"fox"} + 3: string{"dog"} + 4: string{"cow"} + 5: string{"deer"} + 6: string{"rat"} + 7: list{ + 0: string{"bat"} + } +}` + actual = printer.Sprint(newListNode) + qt.Assert(t, actual, qt.Equals, expect) + + // The original node should not have been updated + expect = `list{ + 0: string{"fox"} + 1: string{"cow"} + 2: string{"deer"} +}` + actual = printer.Sprint(listNode) + qt.Assert(t, actual, qt.Equals, expect) +} diff --git a/node/basicnode/map.go b/node/basicnode/map.go index 9a86fc52..de2b2045 100644 --- a/node/basicnode/map.go +++ b/node/basicnode/map.go @@ -2,29 +2,31 @@ package basicnode import ( "fmt" + "reflect" "github.com/ipld/go-ipld-prime/datamodel" "github.com/ipld/go-ipld-prime/node/mixins" ) var ( - _ datamodel.Node = &plainMap{} - _ datamodel.NodePrototype = Prototype__Map{} - _ datamodel.NodeBuilder = &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. // 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 map[string]datamodel.NodeAmender // 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. } type plainMap__Entry struct { - k plainString // address of this used when we return keys as nodes, such as in iterators. Need in one place to amortize shifts to heap when ptr'ing for iface. - v datamodel.Node // identical to map values. keeping them here simplifies iteration. (in codegen'd maps, this position is also part of amortization, but in this implementation, that's less useful.) + k plainString // address of this used when we return keys as nodes, such as in iterators. Need in one place to amortize shifts to heap when ptr'ing for iface. + v datamodel.NodeAmender // identical to map values. keeping them here simplifies iteration. (in codegen'd maps, this position is also part of amortization, but in this implementation, that's less useful.) // note on alternate implementations: 'v' could also use the 'any' type, and thus amortize value allocations. the memory size trade would be large however, so we don't, here. } @@ -34,6 +36,13 @@ func (plainMap) Kind() datamodel.Kind { return datamodel.Kind_Map } func (n *plainMap) LookupByString(key string) (datamodel.Node, error) { + if a, err := n.lookupAmenderByString(key); err != nil { + return nil, err + } else { + return a.Build(), nil + } +} +func (n *plainMap) lookupAmenderByString(key string) (datamodel.NodeAmender, error) { v, exists := n.m[key] if !exists { return nil, datamodel.ErrNotExists{Segment: datamodel.PathSegmentOfString(key)} @@ -100,7 +109,7 @@ func (itr *plainMap_MapIterator) Next() (k datamodel.Node, v datamodel.Node, _ e return nil, nil, datamodel.ErrIteratorOverread{} } k = &itr.n.t[itr.idx].k - v = itr.n.t[itr.idx].v + v = itr.n.t[itr.idx].v.Build() itr.idx++ return } @@ -112,8 +121,23 @@ func (itr *plainMap_MapIterator) Done() bool { type Prototype__Map struct{} -func (Prototype__Map) NewBuilder() datamodel.NodeBuilder { - return &plainMap__Builder{plainMap__Assembler{w: &plainMap{}}} +func (p Prototype__Map) NewBuilder() datamodel.NodeBuilder { + return p.AmendingBuilder(nil) +} + +// -- NodePrototypeSupportingMapAmend --> + +func (p Prototype__Map) AmendingBuilder(base datamodel.Node) datamodel.MapAmender { + nb := &plainMap__Builder{plainMap__Assembler{w: &plainMap{}}} + if base != nil { + if baseMap, castOk := base.(*plainMap); !castOk { + panic("misuse") + } else { + // Make a deep copy of the base map + datamodel.Copy(baseMap, nb) + } + } + return nb } // -- NodeBuilder --> @@ -123,8 +147,8 @@ type plainMap__Builder struct { } func (nb *plainMap__Builder) Build() datamodel.Node { - if nb.state != maState_finished { - panic("invalid state: assembler must be 'finished' before Build can be called!") + if (nb.state != maState_initial) && (nb.state != maState_finished) { + panic("invalid state: assembly in progress must be 'finished' before Build can be called!") } return nb.w } @@ -133,6 +157,141 @@ func (nb *plainMap__Builder) Reset() { nb.w = &plainMap{} } +// -- 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. + if path.Len() > 1 { + panic("misuse") + } + // Allow the root of the node to be replaced. + if path.Len() == 0 { + prevNode := nb.Build() + if newNb, err := transform(prevNode); err != nil { + return nil, err + } else if newMb, castOk := newNb.(*plainMap__Builder); !castOk { + return nil, fmt.Errorf("transform: cannot transform root into incompatible type: %v", reflect.TypeOf(newNb)) + } else { + *nb.w = *newMb.w + return prevNode, nil + } + } + childSeg, _ := path.Shift() + childKey := childSeg.String() + childAmender, err := nb.w.lookupAmenderByString(childKey) + if err != nil { + // Return any error other than "not exists" + if _, notFoundErr := err.(datamodel.ErrNotExists); !notFoundErr { + return nil, fmt.Errorf("transform: child at %q did not exist)", path) + } + } + // Allocate storage space + if nb.w.m == nil { + nb.w.t = make([]plainMap__Entry, 0, 1) + nb.w.m = make(map[string]datamodel.NodeAmender, 1) + } + var prevChildVal datamodel.Node = nil + if childAmender != nil { + prevChildVal = childAmender.Build() + } + if newChildAmender, err := transform(prevChildVal); err != nil { + return nil, err + } else { + for idx, v := range nb.w.t { + if string(v.k) == childKey { + if newChildAmender == nil { + delete(nb.w.m, childKey) + 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 + } + return prevChildVal, nil + } + } + nb.w.t = append(nb.w.t, plainMap__Entry{plainString(childKey), newChildAmender}) + nb.w.m[childKey] = newChildAmender + return prevChildVal, nil + } +} + +func (nb *plainMap__Builder) Put(key string, value datamodel.Node) error { + if _, 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 err + } else { + // If there was no previous node, we just added a new node. + return 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() (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 + } + for itr := nb.w.MapIterator(); !itr.Done(); { + k, v, err := itr.Next() + if err != nil { + return nil, err + } + var n datamodel.Node + if keysOrValues { + n = k + } else { + n = v + } + if err := lb.Append(n); err != nil { + return nil, err + } + } + return lb.Build(), 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) { + return nb.toList(false) +} + // -- NodeAssembler --> type plainMap__Assembler struct { @@ -168,7 +327,7 @@ func (na *plainMap__Assembler) BeginMap(sizeHint int64) (datamodel.MapAssembler, } // Allocate storage space. na.w.t = make([]plainMap__Entry, 0, sizeHint) - na.w.m = make(map[string]datamodel.Node, sizeHint) + na.w.m = make(map[string]datamodel.NodeAmender, sizeHint) // That's it; return self as the MapAssembler. We already have all the right methods on this structure. return na, nil } @@ -404,8 +563,9 @@ func (mva *plainMap__ValueAssembler) AssignLink(v datamodel.Link) error { } 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 + a := Prototype.Any.AmendingBuilder(v) + mva.ma.w.t[l].v = a + mva.ma.w.m[string(mva.ma.w.t[l].k)] = a mva.ma.state = maState_initial mva.ma = nil // invalidate self to prevent further incorrect use. return nil diff --git a/node/basicnode/map_test.go b/node/basicnode/map_test.go index 03e17d9d..415c2919 100644 --- a/node/basicnode/map_test.go +++ b/node/basicnode/map_test.go @@ -138,7 +138,7 @@ func TestMapBuilder(t *testing.T) { anotherNode := c.Build() actual = printer.Sprint(anotherNode) - qt.Assert(t, expect, qt.Equals, actual) + qt.Assert(t, actual, qt.Equals, expect) // access values of map, using string r, err := anotherNode.LookupByString("cat") @@ -258,14 +258,6 @@ func TestMapLookupError(t *testing.T) { } func TestMapNewBuilderUsageError(t *testing.T) { - qt.Assert(t, - func() { - b := basicnode.Prototype.Map.NewBuilder() - _ = b.Build() - }, - qt.PanicMatches, - `invalid state: assembler must be 'finished' before Build can be called!`) - // construct an empty map b := basicnode.Prototype.Map.NewBuilder() ma, err := b.BeginMap(0) @@ -282,15 +274,6 @@ func TestMapNewBuilderUsageError(t *testing.T) { expect := `map{}` qt.Check(t, expect, qt.Equals, actual) - // reset will return the state to 'initial', so Build will panic once again - b.Reset() - qt.Assert(t, - func() { - _ = b.Build() - }, - qt.PanicMatches, - `invalid state: assembler must be 'finished' before Build can be called!`) - // assembling a key without a value will cause Finish to panic b.Reset() ma, err = b.BeginMap(0) @@ -325,3 +308,353 @@ 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("cat", basicnode.NewString("meow")) + if err != nil { + t.Fatal(err) + } + // Retry adding the entry + err = amender.Put("cat", basicnode.NewString("meow")) + if err != nil { + t.Fatal(err) + } + err = amender.Put("dog", basicnode.NewString("bark")) + if err != nil { + t.Fatal(err) + } + err = amender.Put("eel", basicnode.NewString("zap")) + if err != nil { + t.Fatal(err) + } + + mapNode := amender.Build() + expect := `map{ + string{"cat"}: string{"meow"} + string{"dog"}: string{"bark"} + string{"eel"}: string{"zap"} +}` + actual := printer.Sprint(mapNode) + 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("cat", basicnode.NewString("purr")) + if err != nil { + t.Fatal(err) + } + + // Access updated value of map, using get + r, err = amender.Get("cat") + if err != nil { + t.Fatal(err) + } + qt.Check(t, "purr", qt.Equals, must.String(r)) + + // Remove an entry + removed, err := amender.Remove("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"} +}` + actual = printer.Sprint(mapNode) + qt.Assert(t, actual, qt.Equals, expect) + + // Should not find "cat" + _, err = amender.Get("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, actual, qt.Equals, expect) + + 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, actual, qt.Equals, expect) +} + +func TestMapAmendingBuilderExistingNode(t *testing.T) { + b := basicnode.Prototype.Map.NewBuilder() + + 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) + } + + mapNode := b.Build() + // Wrap in an amending build + amender := basicnode.Prototype.Map.AmendingBuilder(b.Build()) + newMapNode := amender.Build() + expect := `map{ + string{"cat"}: string{"meow"} + string{"dog"}: string{"bark"} + string{"eel"}: string{"zap"} +}` + actual := printer.Sprint(newMapNode) + qt.Check(t, expect, qt.Equals, actual) + + // Access values of map, using string + r, err := newMapNode.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 = newMapNode.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 = newMapNode.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 := newMapNode.Prototype() + qt.Check(t, fmt.Sprintf("%T", np), qt.Equals, "basicnode.Prototype__Map") + + // Amend the map + err = amender.Put("cat", basicnode.NewString("purr")) + if err != nil { + t.Fatal(err) + } + + // Access updated value of map, using get + r, err = amender.Get("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"} +}` + actual = printer.Sprint(newMapNode) + qt.Assert(t, actual, qt.Equals, expect) + + // Remove an entry + removed, err := amender.Remove("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"} +}` + actual = printer.Sprint(newMapNode) + qt.Assert(t, actual, qt.Equals, expect) + + // Should not find "cat" + _, err = amender.Get("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, actual, qt.Equals, expect) + + 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, actual, qt.Equals, expect) + + // 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(mapNode) + qt.Assert(t, actual, qt.Equals, expect) +} + +func TestMapAmendingBuilderCopiedNode(t *testing.T) { + b := basicnode.Prototype.Map.NewBuilder() + + 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) + } + + mapNode := b.Build() + amender := basicnode.Prototype.Map.AmendingBuilder(mapNode) + newMapNode := amender.Build() + + expect := `map{ + string{"cat"}: string{"meow"} + string{"dog"}: string{"bark"} + string{"eel"}: string{"zap"} +}` + actual := printer.Sprint(newMapNode) + qt.Check(t, expect, qt.Equals, actual) + + // Amend the copied map + err = amender.Put("cat", basicnode.NewString("purr")) + if err != nil { + t.Fatal(err) + } + expect = `map{ + string{"cat"}: string{"purr"} + string{"dog"}: string{"bark"} + string{"eel"}: string{"zap"} +}` + actual = printer.Sprint(newMapNode) + qt.Assert(t, actual, qt.Equals, expect) + + // Remove an entry + removed, err := amender.Remove("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"} +}` + actual = printer.Sprint(newMapNode) + qt.Assert(t, actual, qt.Equals, expect) + + _, err = amender.Get("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, actual, qt.Equals, expect) + + 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, actual, qt.Equals, expect) + + // 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(mapNode) + qt.Assert(t, actual, qt.Equals, expect) +} diff --git a/traversal/walk.go b/traversal/walk.go index 1bd4e6c2..402aec8d 100644 --- a/traversal/walk.go +++ b/traversal/walk.go @@ -3,7 +3,6 @@ package traversal import ( "errors" "fmt" - "github.com/ipld/go-ipld-prime/datamodel" "github.com/ipld/go-ipld-prime/linking" "github.com/ipld/go-ipld-prime/linking/preload" @@ -495,8 +494,14 @@ func (prog Progress) walkTransforming(n datamodel.Node, s selector.Selector, fn nk := n.Kind() switch nk { case datamodel.Kind_List: + if np, castOk := n.Prototype().(datamodel.NodePrototypeSupportingListAmend); castOk { + return prog.walk_transform_iterateAmendableList(np.AmendingBuilder(n), s, fn, s.Interests()) + } return prog.walk_transform_iterateList(n, s, fn, s.Interests()) case datamodel.Kind_Map: + if np, castOk := n.Prototype().(datamodel.NodePrototypeSupportingMapAmend); castOk { + return prog.walk_transform_iterateAmendableMap(np.AmendingBuilder(n), s, fn, s.Interests()) + } return prog.walk_transform_iterateMap(n, s, fn, s.Interests()) default: return n, nil @@ -574,6 +579,65 @@ func (prog Progress) walk_transform_iterateList(n datamodel.Node, s selector.Sel return bldr.Build(), nil } +func (prog Progress) walk_transform_iterateAmendableList(listAmender datamodel.ListAmender, s selector.Selector, fn TransformFn, attn []datamodel.PathSegment) (datamodel.Node, error) { + n := listAmender.Build() + 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 { + return nil, err + } + if sNext != nil { + progNext := prog + progNext.Path = prog.Path.AppendSegment(ps) + if v.Kind() == datamodel.Kind_Link { + lnk, _ := v.AsLink() + if prog.Cfg.LinkVisitOnlyOnce { + if _, seen := prog.SeenLinks[lnk]; seen { + continue + } + prog.SeenLinks[lnk] = struct{}{} + } + progNext.LastBlock.Path = progNext.Path + progNext.LastBlock.Link = lnk + v, err = progNext.loadLink(lnk, v, n) + if err != nil { + if _, ok := err.(SkipMe); ok { + continue + } + return nil, err + } + } + + next, err := progNext.WalkTransforming(v, sNext, fn) + if err != nil { + return nil, err + } + if err := listAmender.Set(idx, next); err != nil { + return nil, err + } + } else { + if err := listAmender.Set(idx, v); err != nil { + return nil, err + } + } + } else { + if err := listAmender.Set(idx, v); err != nil { + return nil, err + } + } + } + return listAmender.Build(), nil +} + func (prog Progress) walk_transform_iterateMap(n datamodel.Node, s selector.Selector, fn TransformFn, attn []datamodel.PathSegment) (datamodel.Node, error) { bldr := n.Prototype().NewBuilder() mapBldr, err := bldr.BeginMap(n.Length()) @@ -640,3 +704,59 @@ func (prog Progress) walk_transform_iterateMap(n datamodel.Node, s selector.Sele } return bldr.Build(), nil } + +func (prog Progress) walk_transform_iterateAmendableMap(mapAmender datamodel.MapAmender, s selector.Selector, fn TransformFn, attn []datamodel.PathSegment) (datamodel.Node, error) { + n := mapAmender.Build() + for itr := selector.NewSegmentIterator(n); !itr.Done(); { + ps, v, err := itr.Next() + if err != nil { + return nil, err + } + + if attn == nil || contains(attn, ps) { + sNext, err := s.Explore(n, ps) + if err != nil { + return nil, err + } + if sNext != nil { + progNext := prog + progNext.Path = prog.Path.AppendSegment(ps) + if v.Kind() == datamodel.Kind_Link { + lnk, _ := v.AsLink() + if prog.Cfg.LinkVisitOnlyOnce { + if _, seen := prog.SeenLinks[lnk]; seen { + continue + } + prog.SeenLinks[lnk] = struct{}{} + } + progNext.LastBlock.Path = progNext.Path + progNext.LastBlock.Link = lnk + v, err = progNext.loadLink(lnk, v, n) + if err != nil { + if _, ok := err.(SkipMe); ok { + continue + } + return nil, err + } + } + + next, err := progNext.WalkTransforming(v, sNext, fn) + if err != nil { + return nil, err + } + if err := mapAmender.Put(ps.String(), next); err != nil { + return nil, err + } + } else { + if err := mapAmender.Put(ps.String(), v); err != nil { + return nil, err + } + } + } else { + if err := mapAmender.Put(ps.String(), v); err != nil { + return nil, err + } + } + } + return mapAmender.Build(), nil +}