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 24, 2022
1 parent b636575 commit 24469a6
Show file tree
Hide file tree
Showing 9 changed files with 1,447 additions and 0 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/ipld/go-ipld-prime
go 1.17

require (
github.com/emirpasic/gods v1.18.1
github.com/frankban/quicktest v1.14.3
github.com/google/go-cmp v0.5.8
github.com/ipfs/go-cid v0.2.0
Expand Down
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
55 changes: 55 additions & 0 deletions traversal/amend/amender.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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
}

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

func isCreated(a Amender) bool {
if ma, castOk := a.(*mapAmender); castOk {
return ma.created
} else if la, castOk := a.(*listAmender); castOk {
return la.created
} else if aa, castOk := a.(*anyAmender); castOk {
return aa.created
}
panic("misuse")
}
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))
})
}
}
125 changes: 125 additions & 0 deletions traversal/amend/any.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
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) 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")
}
Loading

0 comments on commit 24469a6

Please sign in to comment.