You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
682 lines
11 KiB
682 lines
11 KiB
package jsonpatch |
|
|
|
import ( |
|
"bytes" |
|
"encoding/json" |
|
"fmt" |
|
"strconv" |
|
"strings" |
|
) |
|
|
|
const ( |
|
eRaw = iota |
|
eDoc |
|
eAry |
|
) |
|
|
|
var SupportNegativeIndices bool = true |
|
|
|
type lazyNode struct { |
|
raw *json.RawMessage |
|
doc partialDoc |
|
ary partialArray |
|
which int |
|
} |
|
|
|
type operation map[string]*json.RawMessage |
|
|
|
// Patch is an ordered collection of operations. |
|
type Patch []operation |
|
|
|
type partialDoc map[string]*lazyNode |
|
type partialArray []*lazyNode |
|
|
|
type container interface { |
|
get(key string) (*lazyNode, error) |
|
set(key string, val *lazyNode) error |
|
add(key string, val *lazyNode) error |
|
remove(key string) error |
|
} |
|
|
|
func newLazyNode(raw *json.RawMessage) *lazyNode { |
|
return &lazyNode{raw: raw, doc: nil, ary: nil, which: eRaw} |
|
} |
|
|
|
func (n *lazyNode) MarshalJSON() ([]byte, error) { |
|
switch n.which { |
|
case eRaw: |
|
return json.Marshal(n.raw) |
|
case eDoc: |
|
return json.Marshal(n.doc) |
|
case eAry: |
|
return json.Marshal(n.ary) |
|
default: |
|
return nil, fmt.Errorf("Unknown type") |
|
} |
|
} |
|
|
|
func (n *lazyNode) UnmarshalJSON(data []byte) error { |
|
dest := make(json.RawMessage, len(data)) |
|
copy(dest, data) |
|
n.raw = &dest |
|
n.which = eRaw |
|
return nil |
|
} |
|
|
|
func (n *lazyNode) intoDoc() (*partialDoc, error) { |
|
if n.which == eDoc { |
|
return &n.doc, nil |
|
} |
|
|
|
if n.raw == nil { |
|
return nil, fmt.Errorf("Unable to unmarshal nil pointer as partial document") |
|
} |
|
|
|
err := json.Unmarshal(*n.raw, &n.doc) |
|
|
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
n.which = eDoc |
|
return &n.doc, nil |
|
} |
|
|
|
func (n *lazyNode) intoAry() (*partialArray, error) { |
|
if n.which == eAry { |
|
return &n.ary, nil |
|
} |
|
|
|
if n.raw == nil { |
|
return nil, fmt.Errorf("Unable to unmarshal nil pointer as partial array") |
|
} |
|
|
|
err := json.Unmarshal(*n.raw, &n.ary) |
|
|
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
n.which = eAry |
|
return &n.ary, nil |
|
} |
|
|
|
func (n *lazyNode) compact() []byte { |
|
buf := &bytes.Buffer{} |
|
|
|
if n.raw == nil { |
|
return nil |
|
} |
|
|
|
err := json.Compact(buf, *n.raw) |
|
|
|
if err != nil { |
|
return *n.raw |
|
} |
|
|
|
return buf.Bytes() |
|
} |
|
|
|
func (n *lazyNode) tryDoc() bool { |
|
if n.raw == nil { |
|
return false |
|
} |
|
|
|
err := json.Unmarshal(*n.raw, &n.doc) |
|
|
|
if err != nil { |
|
return false |
|
} |
|
|
|
n.which = eDoc |
|
return true |
|
} |
|
|
|
func (n *lazyNode) tryAry() bool { |
|
if n.raw == nil { |
|
return false |
|
} |
|
|
|
err := json.Unmarshal(*n.raw, &n.ary) |
|
|
|
if err != nil { |
|
return false |
|
} |
|
|
|
n.which = eAry |
|
return true |
|
} |
|
|
|
func (n *lazyNode) equal(o *lazyNode) bool { |
|
if n.which == eRaw { |
|
if !n.tryDoc() && !n.tryAry() { |
|
if o.which != eRaw { |
|
return false |
|
} |
|
|
|
return bytes.Equal(n.compact(), o.compact()) |
|
} |
|
} |
|
|
|
if n.which == eDoc { |
|
if o.which == eRaw { |
|
if !o.tryDoc() { |
|
return false |
|
} |
|
} |
|
|
|
if o.which != eDoc { |
|
return false |
|
} |
|
|
|
for k, v := range n.doc { |
|
ov, ok := o.doc[k] |
|
|
|
if !ok { |
|
return false |
|
} |
|
|
|
if v == nil && ov == nil { |
|
continue |
|
} |
|
|
|
if !v.equal(ov) { |
|
return false |
|
} |
|
} |
|
|
|
return true |
|
} |
|
|
|
if o.which != eAry && !o.tryAry() { |
|
return false |
|
} |
|
|
|
if len(n.ary) != len(o.ary) { |
|
return false |
|
} |
|
|
|
for idx, val := range n.ary { |
|
if !val.equal(o.ary[idx]) { |
|
return false |
|
} |
|
} |
|
|
|
return true |
|
} |
|
|
|
func (o operation) kind() string { |
|
if obj, ok := o["op"]; ok && obj != nil { |
|
var op string |
|
|
|
err := json.Unmarshal(*obj, &op) |
|
|
|
if err != nil { |
|
return "unknown" |
|
} |
|
|
|
return op |
|
} |
|
|
|
return "unknown" |
|
} |
|
|
|
func (o operation) path() string { |
|
if obj, ok := o["path"]; ok && obj != nil { |
|
var op string |
|
|
|
err := json.Unmarshal(*obj, &op) |
|
|
|
if err != nil { |
|
return "unknown" |
|
} |
|
|
|
return op |
|
} |
|
|
|
return "unknown" |
|
} |
|
|
|
func (o operation) from() string { |
|
if obj, ok := o["from"]; ok && obj != nil { |
|
var op string |
|
|
|
err := json.Unmarshal(*obj, &op) |
|
|
|
if err != nil { |
|
return "unknown" |
|
} |
|
|
|
return op |
|
} |
|
|
|
return "unknown" |
|
} |
|
|
|
func (o operation) value() *lazyNode { |
|
if obj, ok := o["value"]; ok { |
|
return newLazyNode(obj) |
|
} |
|
|
|
return nil |
|
} |
|
|
|
func isArray(buf []byte) bool { |
|
Loop: |
|
for _, c := range buf { |
|
switch c { |
|
case ' ': |
|
case '\n': |
|
case '\t': |
|
continue |
|
case '[': |
|
return true |
|
default: |
|
break Loop |
|
} |
|
} |
|
|
|
return false |
|
} |
|
|
|
func findObject(pd *container, path string) (container, string) { |
|
doc := *pd |
|
|
|
split := strings.Split(path, "/") |
|
|
|
if len(split) < 2 { |
|
return nil, "" |
|
} |
|
|
|
parts := split[1 : len(split)-1] |
|
|
|
key := split[len(split)-1] |
|
|
|
var err error |
|
|
|
for _, part := range parts { |
|
|
|
next, ok := doc.get(decodePatchKey(part)) |
|
|
|
if next == nil || ok != nil { |
|
return nil, "" |
|
} |
|
|
|
if isArray(*next.raw) { |
|
doc, err = next.intoAry() |
|
|
|
if err != nil { |
|
return nil, "" |
|
} |
|
} else { |
|
doc, err = next.intoDoc() |
|
|
|
if err != nil { |
|
return nil, "" |
|
} |
|
} |
|
} |
|
|
|
return doc, decodePatchKey(key) |
|
} |
|
|
|
func (d *partialDoc) set(key string, val *lazyNode) error { |
|
(*d)[key] = val |
|
return nil |
|
} |
|
|
|
func (d *partialDoc) add(key string, val *lazyNode) error { |
|
(*d)[key] = val |
|
return nil |
|
} |
|
|
|
func (d *partialDoc) get(key string) (*lazyNode, error) { |
|
return (*d)[key], nil |
|
} |
|
|
|
func (d *partialDoc) remove(key string) error { |
|
_, ok := (*d)[key] |
|
if !ok { |
|
return fmt.Errorf("Unable to remove nonexistent key: %s", key) |
|
} |
|
|
|
delete(*d, key) |
|
return nil |
|
} |
|
|
|
func (d *partialArray) set(key string, val *lazyNode) error { |
|
if key == "-" { |
|
*d = append(*d, val) |
|
return nil |
|
} |
|
|
|
idx, err := strconv.Atoi(key) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
sz := len(*d) |
|
if idx+1 > sz { |
|
sz = idx + 1 |
|
} |
|
|
|
ary := make([]*lazyNode, sz) |
|
|
|
cur := *d |
|
|
|
copy(ary, cur) |
|
|
|
if idx >= len(ary) { |
|
return fmt.Errorf("Unable to access invalid index: %d", idx) |
|
} |
|
|
|
ary[idx] = val |
|
|
|
*d = ary |
|
return nil |
|
} |
|
|
|
func (d *partialArray) add(key string, val *lazyNode) error { |
|
if key == "-" { |
|
*d = append(*d, val) |
|
return nil |
|
} |
|
|
|
idx, err := strconv.Atoi(key) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
ary := make([]*lazyNode, len(*d)+1) |
|
|
|
cur := *d |
|
|
|
if idx >= len(ary) { |
|
return fmt.Errorf("Unable to access invalid index: %d", idx) |
|
} |
|
|
|
if SupportNegativeIndices { |
|
if idx < -len(ary) { |
|
return fmt.Errorf("Unable to access invalid index: %d", idx) |
|
} |
|
|
|
if idx < 0 { |
|
idx += len(ary) |
|
} |
|
} |
|
|
|
copy(ary[0:idx], cur[0:idx]) |
|
ary[idx] = val |
|
copy(ary[idx+1:], cur[idx:]) |
|
|
|
*d = ary |
|
return nil |
|
} |
|
|
|
func (d *partialArray) get(key string) (*lazyNode, error) { |
|
idx, err := strconv.Atoi(key) |
|
|
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
if idx >= len(*d) { |
|
return nil, fmt.Errorf("Unable to access invalid index: %d", idx) |
|
} |
|
|
|
return (*d)[idx], nil |
|
} |
|
|
|
func (d *partialArray) remove(key string) error { |
|
idx, err := strconv.Atoi(key) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
cur := *d |
|
|
|
if idx >= len(cur) { |
|
return fmt.Errorf("Unable to access invalid index: %d", idx) |
|
} |
|
|
|
if SupportNegativeIndices { |
|
if idx < -len(cur) { |
|
return fmt.Errorf("Unable to access invalid index: %d", idx) |
|
} |
|
|
|
if idx < 0 { |
|
idx += len(cur) |
|
} |
|
} |
|
|
|
ary := make([]*lazyNode, len(cur)-1) |
|
|
|
copy(ary[0:idx], cur[0:idx]) |
|
copy(ary[idx:], cur[idx+1:]) |
|
|
|
*d = ary |
|
return nil |
|
|
|
} |
|
|
|
func (p Patch) add(doc *container, op operation) error { |
|
path := op.path() |
|
|
|
con, key := findObject(doc, path) |
|
|
|
if con == nil { |
|
return fmt.Errorf("jsonpatch add operation does not apply: doc is missing path: \"%s\"", path) |
|
} |
|
|
|
return con.add(key, op.value()) |
|
} |
|
|
|
func (p Patch) remove(doc *container, op operation) error { |
|
path := op.path() |
|
|
|
con, key := findObject(doc, path) |
|
|
|
if con == nil { |
|
return fmt.Errorf("jsonpatch remove operation does not apply: doc is missing path: \"%s\"", path) |
|
} |
|
|
|
return con.remove(key) |
|
} |
|
|
|
func (p Patch) replace(doc *container, op operation) error { |
|
path := op.path() |
|
|
|
con, key := findObject(doc, path) |
|
|
|
if con == nil { |
|
return fmt.Errorf("jsonpatch replace operation does not apply: doc is missing path: %s", path) |
|
} |
|
|
|
_, ok := con.get(key) |
|
if ok != nil { |
|
return fmt.Errorf("jsonpatch replace operation does not apply: doc is missing key: %s", path) |
|
} |
|
|
|
return con.set(key, op.value()) |
|
} |
|
|
|
func (p Patch) move(doc *container, op operation) error { |
|
from := op.from() |
|
|
|
con, key := findObject(doc, from) |
|
|
|
if con == nil { |
|
return fmt.Errorf("jsonpatch move operation does not apply: doc is missing from path: %s", from) |
|
} |
|
|
|
val, err := con.get(key) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
err = con.remove(key) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
path := op.path() |
|
|
|
con, key = findObject(doc, path) |
|
|
|
if con == nil { |
|
return fmt.Errorf("jsonpatch move operation does not apply: doc is missing destination path: %s", path) |
|
} |
|
|
|
return con.set(key, val) |
|
} |
|
|
|
func (p Patch) test(doc *container, op operation) error { |
|
path := op.path() |
|
|
|
con, key := findObject(doc, path) |
|
|
|
if con == nil { |
|
return fmt.Errorf("jsonpatch test operation does not apply: is missing path: %s", path) |
|
} |
|
|
|
val, err := con.get(key) |
|
|
|
if err != nil { |
|
return err |
|
} |
|
|
|
if val == nil { |
|
if op.value().raw == nil { |
|
return nil |
|
} |
|
return fmt.Errorf("Testing value %s failed", path) |
|
} else if op.value() == nil { |
|
return fmt.Errorf("Testing value %s failed", path) |
|
} |
|
|
|
if val.equal(op.value()) { |
|
return nil |
|
} |
|
|
|
return fmt.Errorf("Testing value %s failed", path) |
|
} |
|
|
|
func (p Patch) copy(doc *container, op operation) error { |
|
from := op.from() |
|
|
|
con, key := findObject(doc, from) |
|
|
|
if con == nil { |
|
return fmt.Errorf("jsonpatch copy operation does not apply: doc is missing from path: %s", from) |
|
} |
|
|
|
val, err := con.get(key) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
path := op.path() |
|
|
|
con, key = findObject(doc, path) |
|
|
|
if con == nil { |
|
return fmt.Errorf("jsonpatch copy operation does not apply: doc is missing destination path: %s", path) |
|
} |
|
|
|
return con.set(key, val) |
|
} |
|
|
|
// Equal indicates if 2 JSON documents have the same structural equality. |
|
func Equal(a, b []byte) bool { |
|
ra := make(json.RawMessage, len(a)) |
|
copy(ra, a) |
|
la := newLazyNode(&ra) |
|
|
|
rb := make(json.RawMessage, len(b)) |
|
copy(rb, b) |
|
lb := newLazyNode(&rb) |
|
|
|
return la.equal(lb) |
|
} |
|
|
|
// DecodePatch decodes the passed JSON document as an RFC 6902 patch. |
|
func DecodePatch(buf []byte) (Patch, error) { |
|
var p Patch |
|
|
|
err := json.Unmarshal(buf, &p) |
|
|
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
return p, nil |
|
} |
|
|
|
// Apply mutates a JSON document according to the patch, and returns the new |
|
// document. |
|
func (p Patch) Apply(doc []byte) ([]byte, error) { |
|
return p.ApplyIndent(doc, "") |
|
} |
|
|
|
// ApplyIndent mutates a JSON document according to the patch, and returns the new |
|
// document indented. |
|
func (p Patch) ApplyIndent(doc []byte, indent string) ([]byte, error) { |
|
var pd container |
|
if doc[0] == '[' { |
|
pd = &partialArray{} |
|
} else { |
|
pd = &partialDoc{} |
|
} |
|
|
|
err := json.Unmarshal(doc, pd) |
|
|
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
err = nil |
|
|
|
for _, op := range p { |
|
switch op.kind() { |
|
case "add": |
|
err = p.add(&pd, op) |
|
case "remove": |
|
err = p.remove(&pd, op) |
|
case "replace": |
|
err = p.replace(&pd, op) |
|
case "move": |
|
err = p.move(&pd, op) |
|
case "test": |
|
err = p.test(&pd, op) |
|
case "copy": |
|
err = p.copy(&pd, op) |
|
default: |
|
err = fmt.Errorf("Unexpected kind: %s", op.kind()) |
|
} |
|
|
|
if err != nil { |
|
return nil, err |
|
} |
|
} |
|
|
|
if indent != "" { |
|
return json.MarshalIndent(pd, "", indent) |
|
} |
|
|
|
return json.Marshal(pd) |
|
} |
|
|
|
// From http://tools.ietf.org/html/rfc6901#section-4 : |
|
// |
|
// Evaluation of each reference token begins by decoding any escaped |
|
// character sequence. This is performed by first transforming any |
|
// occurrence of the sequence '~1' to '/', and then transforming any |
|
// occurrence of the sequence '~0' to '~'. |
|
|
|
var ( |
|
rfc6901Decoder = strings.NewReplacer("~1", "/", "~0", "~") |
|
) |
|
|
|
func decodePatchKey(k string) string { |
|
return rfc6901Decoder.Replace(k) |
|
}
|
|
|