Skip to content

Commit

Permalink
feat: add map/list amender interfaces
Browse files Browse the repository at this point in the history
  • Loading branch information
smrz2001 committed Jul 6, 2023
1 parent 5c2edf3 commit 8984f18
Show file tree
Hide file tree
Showing 5 changed files with 231 additions and 49 deletions.
31 changes: 31 additions & 0 deletions datamodel/amender.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,34 @@ type NodeAmender interface {
// 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)

NodeAmender
}

// MapAmender adds a map-like interface to NodeAmender
type MapAmender interface {
Put(key string, value Node) (bool, error)
Get(key string) (Node, error)
Remove(key string) (bool, error)
Keys() ([]string, error)

containerAmender
}

// ListAmender adds a list-like interface to NodeAmender
type ListAmender interface {
Get(idx int64) (Node, error)
Remove(idx int64) error
Append(values ...Node) error
Insert(idx int64, values ...Node) error
Set(idx int64, value Node) error

containerAmender
}
12 changes: 12 additions & 0 deletions datamodel/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,18 @@ type NodePrototypeSupportingAmend interface {
// 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
106 changes: 92 additions & 14 deletions node/basicnode/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import (
)

var (
_ datamodel.Node = &plainList{}
_ datamodel.NodePrototype = Prototype__List{}
_ datamodel.NodePrototypeSupportingAmend = Prototype__List{}
_ datamodel.NodeBuilder = &plainList__Builder{}
_ datamodel.NodeAssembler = &plainList__Assembler{}
_ datamodel.Node = &plainList{}
_ datamodel.NodePrototype = Prototype__List{}
_ datamodel.NodePrototypeSupportingListAmend = Prototype__List{}
_ datamodel.NodeBuilder = &plainList__Builder{}
_ datamodel.NodeAssembler = &plainList__Assembler{}
)

// plainList is a concrete type that provides a list-kind datamodel.Node.
Expand Down Expand Up @@ -124,9 +124,9 @@ func (p Prototype__List) NewBuilder() datamodel.NodeBuilder {
return p.AmendingBuilder(nil)
}

// -- NodePrototypeSupportingAmend -->
// -- NodePrototypeSupportingListAmend -->

func (p Prototype__List) AmendingBuilder(base datamodel.Node) datamodel.NodeAmender {
func (p Prototype__List) AmendingBuilder(base datamodel.Node) datamodel.ListAmender {
var w *plainList
if base != nil {
if baseList, castOk := base.(*plainList); !castOk {
Expand Down Expand Up @@ -206,7 +206,7 @@ func (nb *plainList__Builder) Transform(path datamodel.Path, transform datamodel
// - 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
// with two conflicting sets of semantics - the current `focus` and `walk`, which (quite reasonably) do 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.
Expand All @@ -233,15 +233,15 @@ func (nb *plainList__Builder) storeChildAmender(childIdx int64, a datamodel.Node
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
// Patch) and replacements (needed by `focus` and `walk`), 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.
// - If appending to the end of the main list, all elements from the transformed list will be individually
// appended to the end of the list.
// - If updating at a particular index in the main list, 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
Expand Down Expand Up @@ -270,6 +270,84 @@ func (nb *plainList__Builder) storeChildAmender(childIdx int64, a datamodel.Node
return nil
}

func (nb *plainList__Builder) Get(idx int64) (datamodel.Node, error) {
return nb.w.LookupByIndex(idx)
}

func (nb *plainList__Builder) Remove(idx int64) error {
_, err := nb.Transform(
datamodel.NewPath([]datamodel.PathSegment{datamodel.PathSegmentOfInt(idx)}),
func(_ datamodel.Node) (datamodel.NodeAmender, error) {
return nil, nil
},
)
return err
}

func (nb *plainList__Builder) Append(values ...datamodel.Node) error {
// Passing an index equal to the length of the list will append the passed values to the end of the list
return nb.Insert(nb.Length(), values...)
}

func (nb *plainList__Builder) Insert(idx int64, values ...datamodel.Node) error {
var ps datamodel.PathSegment
if idx == nb.Length() {
ps = datamodel.PathSegmentOfString("-") // indicates appending to the end of the list
} else {
ps = datamodel.PathSegmentOfInt(idx)
}
_, err := nb.Transform(
datamodel.NewPath([]datamodel.PathSegment{ps}),
func(_ datamodel.Node) (datamodel.NodeAmender, error) {
// Put all the passed values into a new list. That will result in these values being expanded at the
// specified index.
na := nb.Prototype().NewBuilder()
la, err := na.BeginList(int64(len(values)))
if err != nil {
return nil, err
}
for _, v := range values {
if err := la.AssembleValue().AssignNode(v); err != nil {
return nil, err
}
}
if err := la.Finish(); err != nil {
return nil, err
}
return Prototype.Any.AmendingBuilder(na.Build()), nil
},
)
return err
}

func (nb *plainList__Builder) Set(idx int64, value datamodel.Node) error {
return nb.Insert(idx, value)
}

func (nb *plainList__Builder) Empty() bool {
return nb.Length() == 0
}

func (nb *plainList__Builder) Length() int64 {
return nb.w.Length()
}

func (nb *plainList__Builder) Clear() {
nb.Reset()
}

func (nb *plainList__Builder) Values() ([]datamodel.Node, error) {
values := make([]datamodel.Node, 0, nb.Length())
for itr := nb.w.ListIterator(); !itr.Done(); {
_, v, err := itr.Next()
if err != nil {
return nil, err
}
values = append(values, v)
}
return values, nil
}

// -- NodeAssembler -->

type plainList__Assembler struct {
Expand Down
88 changes: 80 additions & 8 deletions node/basicnode/map.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import (
)

var (
_ datamodel.Node = &plainMap{}
_ datamodel.NodePrototype = Prototype__Map{}
_ datamodel.NodePrototypeSupportingAmend = Prototype__Map{}
_ datamodel.NodeAmender = &plainMap__Builder{}
_ datamodel.NodeAssembler = &plainMap__Assembler{}
_ datamodel.Node = &plainMap{}
_ datamodel.NodePrototype = Prototype__Map{}
_ datamodel.NodePrototypeSupportingMapAmend = Prototype__Map{}
_ datamodel.NodeAmender = &plainMap__Builder{}
_ datamodel.NodeAssembler = &plainMap__Assembler{}
)

// plainMap is a concrete type that provides a map-kind datamodel.Node.
Expand Down Expand Up @@ -125,9 +125,9 @@ func (p Prototype__Map) NewBuilder() datamodel.NodeBuilder {
return p.AmendingBuilder(nil)
}

// -- NodePrototypeSupportingAmend -->
// -- NodePrototypeSupportingMapAmend -->

func (p Prototype__Map) AmendingBuilder(base datamodel.Node) datamodel.NodeAmender {
func (p Prototype__Map) AmendingBuilder(base datamodel.Node) datamodel.MapAmender {
var b *plainMap
if base != nil {
if baseMap, castOk := base.(*plainMap); !castOk {
Expand Down Expand Up @@ -158,7 +158,7 @@ func (nb *plainMap__Builder) Reset() {
nb.w = &plainMap{}
}

// -- NodeAmender -->
// -- MapAmender -->

func (nb *plainMap__Builder) Transform(path datamodel.Path, transform datamodel.AmendFn) (datamodel.Node, error) {
// Can only transform the root of the node or an immediate child.
Expand Down Expand Up @@ -219,6 +219,78 @@ func (nb *plainMap__Builder) Transform(path datamodel.Path, transform datamodel.
}
}

func (nb *plainMap__Builder) Put(key string, value datamodel.Node) (bool, error) {
if prevNode, err := nb.Transform(
datamodel.NewPath([]datamodel.PathSegment{datamodel.PathSegmentOfString(key)}),
func(_ datamodel.Node) (datamodel.NodeAmender, error) {
return Prototype.Any.AmendingBuilder(value), nil
},
); err != nil {
return false, err
} else {
// If there was no previous node, we just added a new node.
return prevNode == nil, nil
}
}

func (nb *plainMap__Builder) Get(key string) (datamodel.Node, error) {
return nb.w.LookupByString(key)
}

func (nb *plainMap__Builder) Remove(key string) (bool, error) {
if prevNode, err := nb.Transform(
datamodel.NewPath([]datamodel.PathSegment{datamodel.PathSegmentOfString(key)}),
func(_ datamodel.Node) (datamodel.NodeAmender, error) {
return nil, nil
},
); err != nil {
return false, err
} else {
// If there was a previous node, we just removed it.
return prevNode != nil, nil
}
}

func (nb *plainMap__Builder) Keys() ([]string, error) {
keys := make([]string, 0, nb.Length())
for itr := nb.w.MapIterator(); !itr.Done(); {
k, _, err := itr.Next()
if err != nil {
return nil, err
}
keyStr, err := k.AsString()
if err != nil {
return nil, err
}
keys = append(keys, keyStr)
}
return keys, nil
}

func (nb *plainMap__Builder) Empty() bool {
return nb.Length() == 0
}

func (nb *plainMap__Builder) Length() int64 {
return nb.w.Length()
}

func (nb *plainMap__Builder) Clear() {
nb.Reset()
}

func (nb *plainMap__Builder) Values() ([]datamodel.Node, error) {
values := make([]datamodel.Node, 0, nb.Length())
for itr := nb.w.MapIterator(); !itr.Done(); {
_, v, err := itr.Next()
if err != nil {
return nil, err
}
values = append(values, v)
}
return values, nil
}

// -- NodeAssembler -->

type plainMap__Assembler struct {
Expand Down
Loading

0 comments on commit 8984f18

Please sign in to comment.