forked from ipld/go-ipld-prime
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
bug fixes and improvements for map/list-type nodes
- Loading branch information
Showing
5 changed files
with
818 additions
and
329 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,339 +1,47 @@ | ||
package amend | ||
|
||
import ( | ||
"github.com/emirpasic/gods/maps/linkedhashmap" | ||
"github.com/ipld/go-ipld-prime/datamodel" | ||
"github.com/ipld/go-ipld-prime/node/basicnode" | ||
) | ||
import "github.com/ipld/go-ipld-prime/datamodel" | ||
|
||
// -- Node --> | ||
type Amender interface { | ||
// Get returns the node at the specified path. It will not create any intermediate nodes because this is just a | ||
// retrieval and not a modification operation. | ||
Get(path datamodel.Path) (datamodel.Node, error) | ||
|
||
var _ datamodel.Node = (AmenderNode)(nil) | ||
// Add will add the specified Node at the specified path. If `createParents = true`, any missing parents will be | ||
// created, otherwise this function will return an error. | ||
Add(path datamodel.Path, value datamodel.Node, createParents bool) error | ||
|
||
type AmenderNode = *amenderNode | ||
// Remove will remove the node at the specified path and return its value. This is useful for implementing a "move" | ||
// operation, where a node can be "removed" and then "added" at a different path. | ||
Remove(path datamodel.Path) (datamodel.Node, error) | ||
|
||
type amenderNode struct { | ||
base datamodel.Node | ||
mods *linkedhashmap.Map | ||
adds int | ||
rems int | ||
wrapper bool | ||
} | ||
|
||
func NewAmender(base datamodel.Node) AmenderNode { | ||
return &amenderNode{base, linkedhashmap.New(), 0, 0, true} | ||
} | ||
|
||
func (a *amenderNode) Kind() datamodel.Kind { | ||
if a.base != nil { | ||
// Same kind as the underlying `Node` | ||
return a.base.Kind() | ||
} else { | ||
// TODO: | ||
return datamodel.Kind_Map | ||
} | ||
} | ||
|
||
func (a *amenderNode) LookupByString(key string) (datamodel.Node, error) { | ||
if a.Kind() == datamodel.Kind_Map { | ||
// Added/removed nodes override the contents of the source node | ||
if mod, exists := a.mods.Get(key); exists { | ||
if mod != nil { | ||
return mod.(datamodel.Node), nil | ||
} else { | ||
return nil, datamodel.ErrNotExists{Segment: datamodel.PathSegmentOfString(key)} | ||
} | ||
} | ||
} | ||
if a.base != nil { | ||
// Now check the original node for the requested node | ||
return a.base.LookupByString(key) | ||
} | ||
return nil, datamodel.ErrNotExists{Segment: datamodel.PathSegmentOfString(key)} | ||
} | ||
|
||
func (a *amenderNode) LookupByNode(key datamodel.Node) (datamodel.Node, error) { | ||
if a.Kind() == datamodel.Kind_Map { | ||
ks, err := key.AsString() | ||
if err != nil { | ||
return nil, err | ||
} | ||
return a.LookupByString(ks) | ||
} | ||
return a.base.LookupByNode(key) | ||
} | ||
|
||
func (a *amenderNode) LookupByIndex(idx int64) (datamodel.Node, error) { | ||
if a.Kind() == datamodel.Kind_List { | ||
panic("TODO") | ||
} | ||
return a.base.LookupByIndex(idx) | ||
} | ||
|
||
func (a *amenderNode) LookupBySegment(seg datamodel.PathSegment) (datamodel.Node, error) { | ||
switch a.Kind() { | ||
case datamodel.Kind_Map: | ||
return a.LookupByString(seg.String()) | ||
case datamodel.Kind_List: | ||
idx, err := seg.Index() | ||
if err != nil { | ||
return nil, datamodel.ErrInvalidSegmentForList{TroubleSegment: seg, Reason: err} | ||
} | ||
return a.LookupByIndex(idx) | ||
default: | ||
return a.LookupBySegment(seg) | ||
} | ||
} | ||
|
||
func (a *amenderNode) MapIterator() datamodel.MapIterator { | ||
if a.Kind() == datamodel.Kind_Map { | ||
var baseItr datamodel.MapIterator = nil | ||
if a.base != nil { | ||
baseItr = a.base.MapIterator() | ||
} | ||
return &amender_MapIterator{a, baseItr, a.mods.Iterator(), 0} | ||
} | ||
panic("misuse") | ||
} | ||
|
||
func (a *amenderNode) ListIterator() datamodel.ListIterator { | ||
if a.Kind() == datamodel.Kind_List { | ||
panic("TODO") | ||
} | ||
panic("misuse") | ||
} | ||
|
||
func (a *amenderNode) Length() int64 { | ||
if a.base != nil { | ||
switch a.Kind() { | ||
case datamodel.Kind_Map: | ||
return int64(a.adds) - int64(a.rems) + a.base.Length() | ||
case datamodel.Kind_List: | ||
return int64(a.adds) - int64(a.rems) + a.base.Length() | ||
default: | ||
return a.base.Length() | ||
} | ||
} | ||
return int64(a.adds) - int64(a.rems) | ||
} | ||
|
||
func (a *amenderNode) IsAbsent() bool { | ||
return a.base.IsAbsent() | ||
} | ||
|
||
func (a *amenderNode) IsNull() bool { | ||
return a.base.IsNull() | ||
} | ||
|
||
func (a *amenderNode) AsBool() (bool, error) { | ||
return a.base.AsBool() | ||
} | ||
|
||
func (a *amenderNode) AsInt() (int64, error) { | ||
return a.base.AsInt() | ||
} | ||
|
||
func (a *amenderNode) AsFloat() (float64, error) { | ||
return a.base.AsFloat() | ||
} | ||
|
||
func (a *amenderNode) AsString() (string, error) { | ||
return a.base.AsString() | ||
} | ||
|
||
func (a *amenderNode) AsBytes() ([]byte, error) { | ||
return a.base.AsBytes() | ||
} | ||
|
||
func (a *amenderNode) AsLink() (datamodel.Link, error) { | ||
return a.base.AsLink() | ||
} | ||
|
||
func (a *amenderNode) Prototype() datamodel.NodePrototype { | ||
return a.base.Prototype() | ||
} | ||
|
||
// -- Implementation --> | ||
|
||
type amender_MapIterator struct { | ||
a AmenderNode | ||
b datamodel.MapIterator | ||
m linkedhashmap.Iterator | ||
idx int | ||
} | ||
|
||
func (itr *amender_MapIterator) Next() (k datamodel.Node, v datamodel.Node, _ error) { | ||
if itr.Done() { | ||
return nil, nil, datamodel.ErrIteratorOverread{} | ||
} | ||
// Iterate over the source node first, skipping removed nodes and pulling added nodes from `mods`. Existing child | ||
// nodes can be present as wrapped `AmenderNode` objects when they form part of a hierarchy with a newly added node | ||
// at the leaf. | ||
var err error | ||
if itr.b != nil { | ||
for !itr.b.Done() { | ||
k, v, err = itr.b.Next() | ||
if err != nil { | ||
return nil, nil, err | ||
} | ||
ks, _ := k.AsString() | ||
if err != nil { | ||
return nil, nil, err | ||
} | ||
if mod, exists := itr.a.mods.Get(ks); exists { | ||
// Since we're dipping into `mods` here, for each node found, increment the `mods` iterator index so | ||
// that we can exit early when we fall-through to processing the `mods`. | ||
itr.idx++ | ||
v = mod.(datamodel.Node) | ||
if v.IsNull() { // Skip removed nodes | ||
continue | ||
} | ||
// Fall-through and return added nodes | ||
} | ||
return | ||
} | ||
} | ||
// Iterate over mods, skipping removed nodes. | ||
for itr.m.Next() { | ||
itr.idx++ | ||
key := itr.m.Key() | ||
k = basicnode.NewString(key.(string)) | ||
v = itr.m.Value().(datamodel.Node) | ||
// Skip removed nodes and added "wrapper" nodes that represent existing sub-nodes in the hierarchy corresponding | ||
// to an added leaf node. | ||
if v.IsNull() || itr.m.Value().(*amenderNode).wrapper { | ||
continue | ||
} | ||
return | ||
} | ||
return nil, nil, datamodel.ErrIteratorOverread{} | ||
} | ||
|
||
func (itr *amender_MapIterator) Done() bool { | ||
// Iteration is complete when all source nodes have been processed (skipping removed nodes) and all mods have been | ||
// processed. | ||
// | ||
// There are two edge cases: | ||
// - All mods are removals: The removed nodes would have been skipped while iterating the source node, and do not | ||
// need to be reprocessed if there aren't any added nodes to consider. | ||
// - Adds are wrappers: These added nodes are only used to wrap existing `Node` objects that would have been | ||
// processed while iterating the source node, and so do not need to be reprocessed. | ||
return ((itr.b == nil) || itr.b.Done()) && (itr.idx >= itr.a.mods.Size()) | ||
} | ||
// Replace will do an in-place replacement of the node at the specified path and return its previous value. | ||
Replace(path datamodel.Path, value datamodel.Node) (datamodel.Node, error) | ||
|
||
func (a *amenderNode) Get(path datamodel.Path) (datamodel.Node, error) { | ||
// This operation is only supported on a recursive type. | ||
if (a.Kind() != datamodel.Kind_Map) && (a.Kind() != datamodel.Kind_List) { | ||
panic("misuse") | ||
} | ||
childSeg, leafPath := path.Shift() | ||
childKey := childSeg.String() | ||
child, err := a.LookupByString(childKey) | ||
// Look for the child node in the current amender state and throw an error if it does not exist | ||
if err != nil { | ||
return nil, err | ||
} | ||
// Use a child amender but don't store it in `mods` because we're only using the amender's logic and not performing | ||
// any modifications. | ||
childAmender := &amenderNode{child, linkedhashmap.New(), 0, 0, true} | ||
if leafPath.Len() == 0 { | ||
// If the hierarchy has been fully traversed, return the leaf amender. | ||
return childAmender, nil | ||
} else { | ||
// We haven't reached the leaf yet, keep going. | ||
return childAmender.Get(leafPath) | ||
} | ||
} | ||
// Build returns a traversable node that can be used with existing codec implementations. An `Amender` does not | ||
// *have* to be a `Node` although currently, all `Amender` implementations are also `Node`s. | ||
Build() datamodel.Node | ||
|
||
func (a *amenderNode) Add(path datamodel.Path, value datamodel.Node) error { | ||
// This operation is only supported on a recursive type. | ||
if (a.Kind() != datamodel.Kind_Map) && (a.Kind() != datamodel.Kind_List) { | ||
panic("misuse") | ||
} | ||
childSeg, leafPath := path.Shift() | ||
childKey := childSeg.String() | ||
child, err := a.LookupByString(childKey) | ||
// Look for the child node in the current amender state but don't throw an error if it does not exist, just create | ||
// the new hierarchy | ||
if err != nil { | ||
if _, notFoundErr := err.(datamodel.ErrNotExists); !notFoundErr { | ||
// Return any error other than "not exists". | ||
return err | ||
} | ||
} | ||
childExists := child != nil | ||
atLeaf := leafPath.Len() == 0 | ||
// The leaf must not already exist | ||
if atLeaf { | ||
if childExists { | ||
return datamodel.ErrRepeatedMapKey{Key: basicnode.NewString(childKey)} | ||
} | ||
// Store the specified value in the leaf amender. | ||
child = value | ||
} | ||
childAmender := &amenderNode{child, linkedhashmap.New(), 0, 0, childExists} | ||
a.mods.Put(childKey, childAmender) | ||
// While building our nested amender tree, only count nodes as "added" when they didn't exist and had to be created | ||
// to fill out the hierarchy. | ||
if !childExists { | ||
a.adds++ | ||
} | ||
if atLeaf { | ||
// If the hierarchy has been fully traversed, return the leaf amender. | ||
return nil | ||
} else { | ||
// We haven't reached the leaf yet, keep going. | ||
return childAmender.Add(leafPath, value) | ||
} | ||
// isCreated returns whether an amender "wraps" an existing node or represents a new node in the hierarchy. | ||
isCreated() bool | ||
} | ||
|
||
func (a *amenderNode) Remove(path datamodel.Path) (datamodel.Node, error) { | ||
// This operation is only supported on a recursive type. | ||
if (a.Kind() != datamodel.Kind_Map) && (a.Kind() != datamodel.Kind_List) { | ||
// NewAmender returns a new amender of the right "type" (i.e. map, list, any) using the specified base node. | ||
func NewAmender(base datamodel.Node) Amender { | ||
// Do not allow creating a new amender without a base node to refer to. Amendment assumes that there is something to | ||
// amend. | ||
if base == nil { | ||
panic("misuse") | ||
} | ||
childSeg, leafPath := path.Shift() | ||
childKey := childSeg.String() | ||
child, err := a.LookupByString(childKey) | ||
// Look for the child node in the current amender state and throw an error if it does not exist | ||
if err != nil { | ||
return nil, err | ||
} | ||
if leafPath.Len() == 0 { | ||
// If the hierarchy has been fully traversed, return the "removed" leaf node. | ||
a.mods.Put(childKey, datamodel.Null) | ||
a.rems++ | ||
return child, nil | ||
} else { | ||
// We haven't reached the leaf yet, keep going. | ||
childAmender := &amenderNode{child, linkedhashmap.New(), 0, 0, true} | ||
return childAmender.Remove(leafPath) | ||
} | ||
return newAmender(base, nil, base.Kind(), false) | ||
} | ||
|
||
func (a *amenderNode) Replace(path datamodel.Path, value datamodel.Node) error { | ||
// This operation is only supported on a recursive type. | ||
if (a.Kind() != datamodel.Kind_Map) && (a.Kind() != datamodel.Kind_List) { | ||
panic("misuse") | ||
} | ||
childSeg, leafPath := path.Shift() | ||
childKey := childSeg.String() | ||
child, err := a.LookupByString(childKey) | ||
// Look for the child node in the current amender state but don't throw an error if it does not exist, just create | ||
// the new hierarchy | ||
if err != nil { | ||
if _, notFoundErr := err.(datamodel.ErrNotExists); !notFoundErr { | ||
// Return any error other than "not exists". | ||
return err | ||
} | ||
} | ||
childAmender := &amenderNode{child, linkedhashmap.New(), 0, 0, true} | ||
a.mods.Put(childKey, childAmender) | ||
if leafPath.Len() == 0 { | ||
// If the hierarchy has been fully traversed, return the leaf amender. | ||
return nil | ||
func newAmender(base datamodel.Node, parent Amender, kind datamodel.Kind, create bool) Amender { | ||
if kind == datamodel.Kind_Map { | ||
return newMapAmender(base, parent, create) | ||
} else if kind == datamodel.Kind_List { | ||
return newListAmender(base, parent, create) | ||
} else { | ||
// We haven't reached the leaf yet, keep going. | ||
return childAmender.Replace(leafPath, value) | ||
return newAnyAmender(base, parent, create) | ||
} | ||
} |
Oops, something went wrong.