Skip to content

Commit

Permalink
bug fixes and improvements for map/list-type nodes
Browse files Browse the repository at this point in the history
  • Loading branch information
smrz2001 committed Jun 9, 2022
1 parent ea2a176 commit c01bde4
Show file tree
Hide file tree
Showing 5 changed files with 818 additions and 329 deletions.
352 changes: 30 additions & 322 deletions traversal/amend/amender.go
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)
}
}
Loading

0 comments on commit c01bde4

Please sign in to comment.