From 437510bf419f63435fef30a3734e47d381d8d253 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 --- datamodel/amender.go | 12 +++ datamodel/node.go | 2 +- node/basicnode/any.go | 95 +++++++++++++------- node/basicnode/list.go | 173 ++++++++++++++++++++++++++++++++++--- node/basicnode/map.go | 119 +++++++++++++++++++++---- node/basicnode/map_test.go | 17 ---- 6 files changed, 340 insertions(+), 78 deletions(-) create mode 100644 datamodel/amender.go diff --git a/datamodel/amender.go b/datamodel/amender.go new file mode 100644 index 00000000..a9b7b8b5 --- /dev/null +++ b/datamodel/amender.go @@ -0,0 +1,12 @@ +package datamodel + +// NodeAmender layers onto NodeBuilder the ability to transform all or part of a Node under construction. This bypasses +// the +// +// To achieve this, NodeAmender exposes a Transform function that can take in the Node (or a child Node in case of +// recursive nodes) +type NodeAmender interface { + NodeBuilder + + Transform(path Path, transform func(Node) (NodeAmender, error)) (Node, error) +} diff --git a/datamodel/node.go b/datamodel/node.go index 625f472d..969f96ed 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. diff --git a/node/basicnode/any.go b/node/basicnode/any.go index cc248463..5eba14fb 100644 --- a/node/basicnode/any.go +++ b/node/basicnode/any.go @@ -7,8 +7,9 @@ import ( 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 +35,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 +74,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 +95,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 +120,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 +128,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 +136,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 +144,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 +152,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 +160,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 +168,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 +176,49 @@ 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 func(datamodel.Node) (datamodel.NodeAmender, error)) (datamodel.Node, error) { + // If `baseNode` is set and supports amendment, apply the transformation. If it doesn't, and the root is being + // replaced, replace it. If the transformation is for a nested node in a non-amendable recursive object, panic. + 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..8bb0783b 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.NodePrototypeSupportingAmend = 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,24 @@ 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) +} + +// -- NodePrototypeSupportingAmend --> + +func (p Prototype__List) AmendingBuilder(base datamodel.Node) datamodel.NodeAmender { + var w *plainList + if base != nil { + if baseList, castOk := base.(*plainList); !castOk { + panic("misuse") + } else { + w = baseList + } + } else { + w = &plainList{} + } + return &plainList__Builder{plainList__Assembler{w: w}} } // -- NodeBuilder --> @@ -113,8 +147,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 +157,119 @@ func (nb *plainList__Builder) Reset() { nb.w = &plainList{} } +// -- NodeAmender --> + +func (nb *plainList__Builder) Transform(path datamodel.Path, transform func(datamodel.Node) (datamodel.NodeAmender, error)) (datamodel.Node, error) { + // Can only transform the root of the node or an immediate child. + if path.Len() > 2 { + panic("misuse") + } else + // 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 `FocusedTransform`, which (quite reasonably) does an + // in-place replacement of list elements, and JSON Patch (https://datatracker.ietf.org/doc/html/rfc6902), which + // does not specify list element replacement. The only "compliant" way to do this today is to first "remove" the + // target node and then "add" its replacement at the same index, which seems 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 { + newX := make([]datamodel.NodeAmender, nb.w.Length()-1) + copy(newX, nb.w.x[:childIdx]) + copy(newX[:childIdx], nb.w.x[childIdx+1:]) + nb.w.x = newX + } else if err = nb.storeChildAmender(childIdx, newChildVal); err != nil { + return nil, err + } + return prevChildVal, nil +} + +func (nb *plainList__Builder) storeChildAmender(childIdx int64, a datamodel.NodeAmender) error { + var elems []datamodel.NodeAmender + n := a.Build() + 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 + // than one element at a particular index in the list. + // + // Rules: + // - If appending to the end of the main list, all elements from the transformed list should be considered + // "created" because they did not exist before. + // - If updating at a particular index in the main list, however, use the first element from the transformed + // list to replace the existing element at that index in the main list, then insert the rest of the + // transformed list elements after. + // + // A special case to consider is that of a list element genuinely being a list itself. If that is the case, the + // transformation MUST wrap the element in another list so that, once unwrapped, the element can be replaced or + // inserted without affecting its semantics. Otherwise, the sub-list's elements will get expanded onto that + // index in the main list. + 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() { + nb.w.x = append(nb.w.x, elems...) + } else { + numElems := int64(len(elems)) + newX := make([]datamodel.NodeAmender, nb.w.Length()+numElems-1) + copy(newX, nb.w.x[:childIdx]) + copy(newX[childIdx:], elems) + copy(newX[childIdx+numElems:], nb.w.x[childIdx+1:]) + nb.w.x = newX + } + return nil +} + // -- NodeAssembler --> type plainList__Assembler struct { @@ -155,7 +302,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 +438,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/map.go b/node/basicnode/map.go index 9a86fc52..b3c549c0 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.NodePrototypeSupportingAmend = 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,24 @@ 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) +} + +// -- NodePrototypeSupportingAmend --> + +func (p Prototype__Map) AmendingBuilder(base datamodel.Node) datamodel.NodeAmender { + var b *plainMap + if base != nil { + if baseMap, castOk := base.(*plainMap); !castOk { + panic("misuse") + } else { + b = baseMap + } + } else { + b = &plainMap{} + } + return &plainMap__Builder{plainMap__Assembler{w: b}} } // -- NodeBuilder --> @@ -123,8 +148,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 +158,67 @@ func (nb *plainMap__Builder) Reset() { nb.w = &plainMap{} } +// -- NodeAmender --> + +func (nb *plainMap__Builder) Transform(path datamodel.Path, transform func(datamodel.Node) (datamodel.NodeAmender, error)) (datamodel.Node, error) { + // Can only transform the root of the node or an immediate child. + if path.Len() > 2 { + panic("misuse") + } else + // 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) + 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 + } 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 + } +} + // -- NodeAssembler --> type plainMap__Assembler struct { @@ -168,7 +254,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 +490,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..404f5dfd 100644 --- a/node/basicnode/map_test.go +++ b/node/basicnode/map_test.go @@ -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)