Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ipld amend #445

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from
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