-
Notifications
You must be signed in to change notification settings - Fork 50
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #350 from ipld/patch-feature
patch: first draft.
- Loading branch information
Showing
7 changed files
with
396 additions
and
10 deletions.
There are no files selected for viewing
Submodule .ipld
updated
23 files
+9 −0 | .github/dependabot.yml | |
+43 −0 | .site/img/what-is-ipld.svg | |
+2 −1 | README.md | |
+14 −2 | docs/intro/hello-world.md | |
+ − | docs/intro/what-is-ipld.png | |
+3 −3 | index.njk | |
+173 −0 | notebook/exploration-reports/2022.03-ipld-url-scheme.md | |
+333 −99 | package-lock.json | |
+3 −3 | package.json | |
+3 −3 | specs/advanced-data-layouts/hamt/fixture/alice-words/index.md | |
+28 −4 | specs/codecs/dag-cbor/spec.md | |
+39 −0 | specs/codecs/dag-cosmos/basic_types.md | |
+133 −0 | specs/codecs/dag-cosmos/cosmos_state.md | |
+29 −0 | specs/codecs/dag-cosmos/crypto_types.md | |
+30 −0 | specs/codecs/dag-cosmos/index.md | |
+495 −0 | specs/codecs/dag-cosmos/tendermint_chain.md | |
+ − | specs/codecs/dag-cosmos/tendermint_dag.png | |
+74 −0 | specs/codecs/dag-cosmos/typed_protobuf.md | |
+78 −20 | specs/codecs/dag-json/spec.md | |
+325 −0 | specs/patch/fixtures/fixtures-1.md | |
+11 −0 | specs/patch/fixtures/index.md | |
+32 −0 | specs/patch/index.md | |
+34 −3 | tools/index.md |
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,145 @@ | ||
// Package patch provides an implementation of the IPLD Patch specification. | ||
// IPLD Patch is a system for declaratively specifying patches to a document, | ||
// which can then be applied to produce a new, modified document. | ||
// | ||
// | ||
// This package is EXPERIMENTAL; its behavior and API might change as it's still | ||
// in development. | ||
package patch | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/ipld/go-ipld-prime/datamodel" | ||
"github.com/ipld/go-ipld-prime/traversal" | ||
) | ||
|
||
type Op string | ||
|
||
const ( | ||
Op_Add = "add" | ||
Op_Remove = "remove" | ||
Op_Replace = "replace" | ||
Op_Move = "move" | ||
Op_Copy = "copy" | ||
Op_Test = "test" | ||
) | ||
|
||
type Operation struct { | ||
Op Op // Always required. | ||
Path datamodel.Path // Always required. | ||
Value datamodel.Node // Present on 'add', 'replace', 'test'. | ||
From datamodel.Path // Present on 'move', 'copy'. | ||
} | ||
|
||
func Eval(n datamodel.Node, ops []Operation) (datamodel.Node, error) { | ||
var err error | ||
for _, op := range ops { | ||
n, err = EvalOne(n, op) | ||
if err != nil { | ||
return nil, err | ||
} | ||
} | ||
return n, nil | ||
} | ||
|
||
func EvalOne(n datamodel.Node, op Operation) (datamodel.Node, error) { | ||
switch op.Op { | ||
case Op_Add: | ||
// The behavior of the 'add' op in jsonpatch varies based on if the parent of the target path is a list. | ||
// If the parent of the target path is a list, then 'add' is really more of an 'insert': it should slide the rest of the values down. | ||
// There's also a special case for "-", which means "append to the end of the list". | ||
// Otherwise, if the destination path exists, it's an error. (No upserting.) | ||
// Handling this requires looking at the parent of the destination node, so we split this into *two* traversal.FocusedTransform calls. | ||
return traversal.FocusedTransform(n, op.Path.Pop(), func(prog traversal.Progress, parent datamodel.Node) (datamodel.Node, error) { | ||
if parent.Kind() == datamodel.Kind_List { | ||
seg := op.Path.Last() | ||
var idx int64 | ||
if seg.String() == "-" { | ||
idx = -1 | ||
} | ||
var err error | ||
idx, err = seg.Index() | ||
if err != nil { | ||
return nil, fmt.Errorf("patch-invalid-path-through-list: at %q", op.Path) // TODO error structuralization and review the code | ||
} | ||
|
||
nb := parent.Prototype().NewBuilder() | ||
la, err := nb.BeginList(parent.Length() + 1) | ||
if err != nil { | ||
return nil, err | ||
} | ||
for itr := n.ListIterator(); !itr.Done(); { | ||
i, v, err := itr.Next() | ||
if err != nil { | ||
return nil, err | ||
} | ||
if idx == i { | ||
la.AssembleValue().AssignNode(op.Value) | ||
} | ||
if err := la.AssembleValue().AssignNode(v); err != nil { | ||
return nil, err | ||
} | ||
} | ||
// TODO: is one-past-the-end supposed to be supported or supposed to be ruled out? | ||
if idx == -1 { | ||
la.AssembleValue().AssignNode(op.Value) | ||
} | ||
if err := la.Finish(); err != nil { | ||
return nil, err | ||
} | ||
return nb.Build(), nil | ||
} | ||
return prog.FocusedTransform(parent, datamodel.NewPath([]datamodel.PathSegment{op.Path.Last()}), func(prog traversal.Progress, point datamodel.Node) (datamodel.Node, error) { | ||
if point != nil && !point.IsAbsent() { | ||
return nil, fmt.Errorf("patch-target-exists: at %q", op.Path) // TODO error structuralization and review the code | ||
} | ||
return op.Value, nil | ||
}, false) | ||
}, false) | ||
case "remove": | ||
return traversal.FocusedTransform(n, op.Path, func(_ traversal.Progress, point datamodel.Node) (datamodel.Node, error) { | ||
return nil, nil // Returning a nil value here means "remove what's here". | ||
}, false) | ||
case "replace": | ||
// TODO i think you need a check that it's not landing under itself here | ||
return traversal.FocusedTransform(n, op.Path, func(_ traversal.Progress, point datamodel.Node) (datamodel.Node, error) { | ||
return op.Value, nil // is this right? what does FocusedTransform do re upsert? | ||
}, false) | ||
case "move": | ||
// TODO i think you need a check that it's not landing under itself here | ||
source, err := traversal.Get(n, op.From) | ||
if err != nil { | ||
return nil, err | ||
} | ||
n, err := traversal.FocusedTransform(n, op.Path, func(_ traversal.Progress, point datamodel.Node) (datamodel.Node, error) { | ||
return source, nil // is this right? what does FocusedTransform do re upsert? | ||
}, false) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return traversal.FocusedTransform(n, op.From, func(_ traversal.Progress, point datamodel.Node) (datamodel.Node, error) { | ||
return nil, nil // Returning a nil value here means "remove what's here". | ||
}, false) | ||
case "copy": | ||
// TODO i think you need a check that it's not landing under itself here | ||
source, err := traversal.Get(n, op.From) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return traversal.FocusedTransform(n, op.Path, func(_ traversal.Progress, point datamodel.Node) (datamodel.Node, error) { | ||
return source, nil // is this right? what does FocusedTransform do re upsert? | ||
}, false) | ||
case "test": | ||
point, err := traversal.Get(n, op.Path) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if datamodel.DeepEqual(point, op.Value) { | ||
return n, nil | ||
} | ||
return n, fmt.Errorf("test failed") // TODO real error handling and a code | ||
default: | ||
return nil, fmt.Errorf("misuse: invalid operation: %s", op.Op) // TODO real error handling and a code | ||
} | ||
} |
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,63 @@ | ||
package patch | ||
|
||
import ( | ||
_ "embed" | ||
|
||
"bytes" | ||
"io" | ||
|
||
"github.com/ipld/go-ipld-prime" | ||
"github.com/ipld/go-ipld-prime/codec" | ||
"github.com/ipld/go-ipld-prime/node/bindnode" | ||
"github.com/ipld/go-ipld-prime/schema" | ||
|
||
"github.com/ipld/go-ipld-prime/codec/json" | ||
"github.com/ipld/go-ipld-prime/datamodel" | ||
) | ||
|
||
//go:embed patch.ipldsch | ||
var embedSchema []byte | ||
|
||
var ts = func() *schema.TypeSystem { | ||
ts, err := ipld.LoadSchemaBytes(embedSchema) | ||
if err != nil { | ||
panic(err) | ||
} | ||
return ts | ||
}() | ||
|
||
func ParseBytes(b []byte, dec codec.Decoder) ([]Operation, error) { | ||
return Parse(bytes.NewReader(b), dec) | ||
} | ||
|
||
func Parse(r io.Reader, dec codec.Decoder) ([]Operation, error) { | ||
npt := bindnode.Prototype((*[]operationRaw)(nil), ts.TypeByName("OperationSequence")) | ||
nb := npt.Representation().NewBuilder() | ||
if err := json.Decode(nb, r); err != nil { | ||
return nil, err | ||
} | ||
opsRaw := bindnode.Unwrap(nb.Build()).(*[]operationRaw) | ||
var ops []Operation | ||
for _, opRaw := range *opsRaw { | ||
// TODO check the Op string | ||
op := Operation{ | ||
Op: Op(opRaw.Op), | ||
Path: datamodel.ParsePath(opRaw.Path), | ||
Value: opRaw.Value, | ||
} | ||
if opRaw.From != nil { | ||
op.From = datamodel.ParsePath(*opRaw.From) | ||
} | ||
ops = append(ops, op) | ||
} | ||
return ops, nil | ||
} | ||
|
||
// operationRaw is roughly the same structure as Operation, but more amenable to serialization | ||
// (it doesn't use high level library types that don't have a data model equivalent). | ||
type operationRaw struct { | ||
Op string | ||
Path string | ||
Value datamodel.Node | ||
From *string | ||
} |
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,39 @@ | ||
# Op represents the kind of operation to perfrom | ||
# The current set is based on the JSON Patch specification | ||
# We may end up adding more operations in the future | ||
type Op enum { | ||
| add | ||
| remove | ||
| replace | ||
| move | ||
| copy | ||
| test | ||
} | ||
|
||
# Operation and OperationSequence are the types that describe operations (but not what to apply them on). | ||
# See the Instruction type for describing both operations and what to apply them on. | ||
type Operation struct { | ||
op Op | ||
path String | ||
value optional Any | ||
from optional String | ||
} | ||
|
||
type OperationSequence [Operation] | ||
|
||
type Instruction struct { | ||
startAt Link | ||
operations OperationSequence | ||
# future: optional field for adl signalling and/or other lenses | ||
} | ||
|
||
type InstructionResult union { | ||
| Error "error" | ||
| Link "result" | ||
} representation keyed | ||
|
||
type Error struct { | ||
code String # enum forthcoming | ||
message String | ||
details {String:String} | ||
} |
Oops, something went wrong.