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.
- Loading branch information
Showing
8 changed files
with
978 additions
and
0 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
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
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 |
---|---|---|
@@ -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) | ||
} | ||
} |
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 |
---|---|---|
@@ -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)) | ||
}) | ||
} | ||
} |
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 |
---|---|---|
@@ -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") | ||
} |
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 |
---|---|---|
@@ -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 | ||
} | ||
} |
Oops, something went wrong.