Skip to content
56 changes: 56 additions & 0 deletions datamodel/amender.go
Original file line number Diff line number Diff line change
@@ -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
}
14 changes: 13 additions & 1 deletion datamodel/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,14 +238,26 @@ 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.
// Might include a wide range of point `Transform`, etc, methods.
// 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.
Expand Down
110 changes: 79 additions & 31 deletions node/basicnode/any.go
Original file line number Diff line number Diff line change
@@ -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{}
)

Expand All @@ -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 -->
Expand All @@ -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() {
Expand All @@ -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 {
Expand All @@ -102,90 +123,117 @@ 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 {
if nb.kind != datamodel.Kind_Invalid {
panic("misuse")
}
nb.kind = datamodel.Kind_Int
nb.scalarNode = NewInt(v)
nb.baseNode = NewInt(v)
return nil
}
func (nb *anyBuilder) AssignFloat(v float64) error {
if nb.kind != datamodel.Kind_Invalid {
panic("misuse")
}
nb.kind = datamodel.Kind_Float
nb.scalarNode = NewFloat(v)
nb.baseNode = NewFloat(v)
return nil
}
func (nb *anyBuilder) AssignString(v string) error {
if nb.kind != datamodel.Kind_Invalid {
panic("misuse")
}
nb.kind = datamodel.Kind_String
nb.scalarNode = NewString(v)
nb.baseNode = NewString(v)
return nil
}
func (nb *anyBuilder) AssignBytes(v []byte) error {
if nb.kind != datamodel.Kind_Invalid {
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 {
if nb.kind != datamodel.Kind_Invalid {
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 {
if nb.kind != datamodel.Kind_Invalid {
panic("misuse")
}
nb.kind = 99
nb.scalarNode = v
nb.baseNode = v
return nil
}
func (anyBuilder) Prototype() datamodel.NodePrototype {
return Prototype.Any
}

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
Expand Down
Loading