diff --git a/traversal/amend/amender.go b/traversal/amend/amender.go new file mode 100644 index 00000000..2d4eb1dd --- /dev/null +++ b/traversal/amend/amender.go @@ -0,0 +1,148 @@ +package amend + +import ( + "fmt" + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/node/basicnode" +) + +type Amd string + +const ( + Amd_Add = "add" + Amd_Remove = "remove" + Amd_Replace = "replace" +) + +type Amendment struct { + Op Amd + Path datamodel.Path + Value datamodel.Node +} + +// -- Node --> + +var _ datamodel.Node = (AmenderNode)(nil) + +type AmenderNode = *_AmenderNode + +type _AmenderNode struct { + base datamodel.Node + addOps []Amendment + remOps map[string]Amendment +} + +func NewAmender(base datamodel.Node) AmenderNode { + return &_AmenderNode{base: base} +} + +func (*_AmenderNode) Kind() datamodel.Kind { + return datamodel.Kind_Map +} +func (*_AmenderNode) LookupByString(key string) (datamodel.Node, error) { + panic("misuse") +} +func (*_AmenderNode) LookupByNode(datamodel.Node) (datamodel.Node, error) { + panic("misuse") +} +func (*_AmenderNode) LookupByIndex(idx int64) (datamodel.Node, error) { + panic("misuse") +} +func (*_AmenderNode) LookupBySegment(seg datamodel.PathSegment) (datamodel.Node, error) { + panic("misuse") +} +func (a *_AmenderNode) MapIterator() datamodel.MapIterator { + if a.base.Kind() != datamodel.Kind_Map { + panic("misuse") + } + return &amender_Iterator{a, a.base.MapIterator(), 0} +} +func (*_AmenderNode) ListIterator() datamodel.ListIterator { + panic("misuse") +} +func (a *_AmenderNode) Length() int64 { + return a.base.Length() + int64(len(a.addOps)) - int64(len(a.remOps)) +} +func (*_AmenderNode) IsAbsent() bool { + panic("misuse") +} +func (*_AmenderNode) IsNull() bool { + panic("misuse") +} +func (*_AmenderNode) AsBool() (bool, error) { + panic("misuse") +} +func (*_AmenderNode) AsInt() (int64, error) { + panic("misuse") +} +func (*_AmenderNode) AsFloat() (float64, error) { + panic("misuse") +} +func (*_AmenderNode) AsString() (string, error) { + panic("misuse") +} +func (*_AmenderNode) AsBytes() ([]byte, error) { + panic("misuse") +} +func (*_AmenderNode) AsLink() (datamodel.Link, error) { + panic("misuse") +} +func (*_AmenderNode) Prototype() datamodel.NodePrototype { + panic("misuse") +} + +// -- Implementation --> + +type amender_Iterator struct { + a AmenderNode + b datamodel.MapIterator + idx int +} + +func (itr *amender_Iterator) Next() (k datamodel.Node, v datamodel.Node, _ error) { + if itr.Done() { + return nil, nil, datamodel.ErrIteratorOverread{} + } + if itr.b.Done() { + seg, _ := itr.a.addOps[itr.idx].Path.Shift() + key := seg.String() + k = basicnode.NewString(key) + v = itr.a.addOps[itr.idx].Value + itr.idx++ + return + } else { + for !itr.b.Done() { + key, value, err := itr.b.Next() + if err != nil { + return nil, nil, err + } + ks, _ := key.AsString() + if err != nil { + return nil, nil, err + } + if _, exists := itr.a.remOps[ks]; exists || value.IsAbsent() { + continue + } + return key, value, err + } + } + return nil, nil, datamodel.ErrIteratorOverread{} +} +func (itr *amender_Iterator) Done() bool { + return itr.b.Done() && (itr.idx >= len(itr.a.addOps)) +} + +func (a *_AmenderNode) Amend(op Amendment) error { + //seg, path := op.Path.Shift() + //key := seg.String() + + switch op.Op { + case Amd_Add: + { + a.addOps = append(a.addOps, op) + } + default: + return fmt.Errorf("misuse: invalid operation") + } + return nil +} diff --git a/traversal/amend/amender_test.go b/traversal/amend/amender_test.go new file mode 100644 index 00000000..280dc10b --- /dev/null +++ b/traversal/amend/amender_test.go @@ -0,0 +1,83 @@ +package amend + +import ( + "bytes" + "encoding/json" + "os" + "testing" + + qt "github.com/frankban/quicktest" + "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" + "github.com/warpfork/go-testmark" +) + +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() + // 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 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)) + }) + } +} diff --git a/traversal/amend/eval.go b/traversal/amend/eval.go new file mode 100644 index 00000000..f07ea091 --- /dev/null +++ b/traversal/amend/eval.go @@ -0,0 +1,33 @@ +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 + amender := &_AmenderNode{base: n} + for _, op := range ops { + err = EvalOne(amender, op) + if err != nil { + return nil, err + } + } + return amender, nil +} + +func EvalOne(a AmenderNode, op patch.Operation) error { + switch op.Op { + case patch.Op_Add: + return a.Amend(Amendment{ + Op: Amd_Add, + Path: op.Path, + Value: op.Value, + }) + default: + return fmt.Errorf("misuse: invalid operation") + } +}