Skip to content

Commit

Permalink
feat: ipld amend
Browse files Browse the repository at this point in the history
  • Loading branch information
smrz2001 committed Jun 10, 2022
1 parent 120991f commit 6c654fa
Show file tree
Hide file tree
Showing 8 changed files with 978 additions and 15 deletions.
16 changes: 1 addition & 15 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,7 @@ require (
)

require (
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 // indirect
github.com/minio/sha256-simd v1.0.0 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/multiformats/go-base32 v0.0.3 // indirect
github.com/multiformats/go-base36 v0.1.0 // indirect
github.com/multiformats/go-multibase v0.0.3 // indirect
github.com/multiformats/go-varint v0.0.6 // indirect
github.com/rogpeppe/go-internal v1.6.1 // indirect
github.com/emirpasic/gods v1.18.1
github.com/smartystreets/goconvey v1.6.4 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/warpfork/go-wish v0.0.0-20200122115046-b9ea61034e4a // indirect
golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf // indirect
golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 // indirect
lukechampine.com/blake3 v1.1.6 // indirect
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
Expand Down
47 changes: 47 additions & 0 deletions traversal/amend/amender.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package amend

import "github.com/ipld/go-ipld-prime/datamodel"

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)

// 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

// 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)

// 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)

// 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

// isCreated returns whether an amender "wraps" an existing node or represents a new node in the hierarchy.
isCreated() bool
}

// 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")
}
return newAmender(base, nil, base.Kind(), false)
}

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 {
return newAnyAmender(base, parent, create)
}
}
92 changes: 92 additions & 0 deletions traversal/amend/amender_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package amend

import (
"bytes"
"encoding/json"
"os"
"strings"
"testing"

qt "github.com/frankban/quicktest"
"github.com/warpfork/go-testmark"

"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec"
"github.com/ipld/go-ipld-prime/codec/dagjson"
"github.com/ipld/go-ipld-prime/traversal/patch"
)

func TestSpecFixtures(t *testing.T) {
dir := "../../.ipld/specs/patch/fixtures/"
testOneSpecFixtureFile(t, dir+"fixtures-1.md")
}

func testOneSpecFixtureFile(t *testing.T, filename string) {
doc, err := testmark.ReadFile(filename)
if os.IsNotExist(err) {
t.Skipf("not running spec suite: %s (did you clone the submodule with the data?)", err)
}
if err != nil {
t.Fatalf("spec file parse failed?!: %s", err)
}

// Data hunk in this spec file are in "directories" of a test scenario each.
doc.BuildDirIndex()

for _, dir := range doc.DirEnt.ChildrenList {
t.Run(dir.Name, func(t *testing.T) {
// Grab all the data hunks.
// Each "directory" contains three piece of data:
// - `initial` -- this is the "block". It's arbitrary example data. They're all in json (or dag-json) format, for simplicity.
// - `patch` -- this is a list of patch ops. Again, as json.
// - `result` -- this is the expected result object. Again, as json.
initialBlob := dir.Children["initial"].Hunk.Body
patchBlob := dir.Children["patch"].Hunk.Body
resultBlob := dir.Children["result"].Hunk.Body

// Parse everything.
initial, err := ipld.Decode(initialBlob, dagjson.Decode)
if err != nil {
t.Fatalf("failed to parse fixture data: %s", err)
}
ops, err := patch.ParseBytes(patchBlob, dagjson.Decode)
if err != nil {
t.Fatalf("failed to parse fixture patch: %s", err)
}
// We don't actually keep the decoded result object. We're just gonna serialize the result and textually diff that instead.
_, err = ipld.Decode(resultBlob, dagjson.Decode)
if err != nil {
t.Fatalf("failed to parse fixture data: %s", err)
}

// Do the thing!
actualResult, err := Eval(initial, ops)
if strings.HasSuffix(dir.Name, "-fail") {
if err == nil {
t.Fatalf("patch was expected to fail")
} else {
return
}
} else {
if err != nil {
t.Fatalf("patch did not apply: %s", err)
}
}

// Serialize (and pretty print) result, so that we can diff it.
actualResultBlob, err := ipld.Encode(actualResult, dagjson.EncodeOptions{
EncodeLinks: true,
EncodeBytes: true,
MapSortMode: codec.MapSortMode_None,
}.Encode)
if err != nil {
t.Errorf("failed to reserialize result: %s", err)
}
var actualResultBlobPretty bytes.Buffer
json.Indent(&actualResultBlobPretty, actualResultBlob, "", "\t")

// Diff!
qt.Assert(t, actualResultBlobPretty.String()+"\n", qt.Equals, string(resultBlob))
})
}
}
129 changes: 129 additions & 0 deletions traversal/amend/any.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package amend

import (
"github.com/ipld/go-ipld-prime/datamodel"
)

var (
_ datamodel.Node = &anyAmender{}
_ Amender = &anyAmender{}
)

type anyAmender struct {
base datamodel.Node
parent Amender
created bool
}

func newAnyAmender(base datamodel.Node, parent Amender, create bool) Amender {
return &anyAmender{base, parent, create}
}

func (a *anyAmender) isCreated() bool {
return a.created
}

func (a *anyAmender) Build() datamodel.Node {
// `anyAmender` is also a `Node`.
return (datamodel.Node)(a)
}

func (a *anyAmender) Kind() datamodel.Kind {
return a.base.Kind()
}

func (a *anyAmender) LookupByString(key string) (datamodel.Node, error) {
return a.base.LookupByString(key)
}

func (a *anyAmender) LookupByNode(key datamodel.Node) (datamodel.Node, error) {
return a.base.LookupByNode(key)
}

func (a *anyAmender) LookupByIndex(idx int64) (datamodel.Node, error) {
return a.base.LookupByIndex(idx)
}

func (a *anyAmender) LookupBySegment(seg datamodel.PathSegment) (datamodel.Node, error) {
return a.base.LookupBySegment(seg)
}

func (a *anyAmender) MapIterator() datamodel.MapIterator {
return a.base.MapIterator()
}

func (a *anyAmender) ListIterator() datamodel.ListIterator {
return a.base.ListIterator()
}

func (a *anyAmender) Length() int64 {
return a.base.Length()
}

func (a *anyAmender) IsAbsent() bool {
return a.base.IsAbsent()
}

func (a *anyAmender) IsNull() bool {
return a.base.IsNull()
}

func (a *anyAmender) AsBool() (bool, error) {
return a.base.AsBool()
}

func (a *anyAmender) AsInt() (int64, error) {
return a.base.AsInt()
}

func (a *anyAmender) AsFloat() (float64, error) {
return a.base.AsFloat()
}

func (a *anyAmender) AsString() (string, error) {
return a.base.AsString()
}

func (a *anyAmender) AsBytes() ([]byte, error) {
return a.base.AsBytes()
}

func (a *anyAmender) AsLink() (datamodel.Link, error) {
return a.base.AsLink()
}

func (a *anyAmender) Prototype() datamodel.NodePrototype {
return a.base.Prototype()
}

func (a *anyAmender) Get(path datamodel.Path) (datamodel.Node, error) {
// If the base node is an amender, use it, otherwise panic.
if amd, castOk := a.base.(Amender); castOk {
return amd.Get(path)
}
panic("misuse")
}

func (a *anyAmender) Add(path datamodel.Path, value datamodel.Node, createParents bool) error {
// If the base node is an amender, use it, otherwise panic.
if amd, castOk := a.base.(Amender); castOk {
return amd.Add(path, value, createParents)
}
panic("misuse")
}

func (a *anyAmender) Remove(path datamodel.Path) (datamodel.Node, error) {
// If the base node is an amender, use it, otherwise panic.
if amd, castOk := a.base.(Amender); castOk {
return amd.Remove(path)
}
panic("misuse")
}

func (a *anyAmender) Replace(path datamodel.Path, value datamodel.Node) (datamodel.Node, error) {
// If the base node is an amender, use it, otherwise panic.
if amd, castOk := a.base.(Amender); castOk {
return amd.Replace(path, value)
}
panic("misuse")
}
57 changes: 57 additions & 0 deletions traversal/amend/eval.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package amend

import (
"fmt"

"github.com/ipld/go-ipld-prime/datamodel"
"github.com/ipld/go-ipld-prime/traversal/patch"
)

func Eval(n datamodel.Node, ops []patch.Operation) (datamodel.Node, error) {
var err error
a := NewAmender(n) // One Amender To Patch Them All
for _, op := range ops {
err = EvalOne(a, op)
if err != nil {
return nil, err
}
}
return a.Build(), nil
}

func EvalOne(a Amender, op patch.Operation) error {
switch op.Op {
case patch.Op_Add:
return a.Add(op.Path, op.Value, true)
case patch.Op_Remove:
_, err := a.Remove(op.Path)
return err
case patch.Op_Replace:
_, err := a.Replace(op.Path, op.Value)
return err
case patch.Op_Move:
source, err := a.Remove(op.From)
if err != nil {
return err
}
// Similar to `replace` with the difference that the destination path might not exist and need to be created.
return a.Add(op.Path, source, true)
case patch.Op_Copy:
source, err := a.Get(op.From)
if err != nil {
return err
}
return a.Add(op.Path, source, false)
case patch.Op_Test:
point, err := a.Get(op.Path)
if err != nil {
return err
}
if datamodel.DeepEqual(point, op.Value) {
return nil
}
return fmt.Errorf("test failed") // TODO real error handling and a code
default:
return fmt.Errorf("misuse: invalid operation") // TODO real error handling and a code
}
}
Loading

0 comments on commit 6c654fa

Please sign in to comment.