godeps: add evanphx/json-patch

pull/6/head
Mike Danese 2015-02-23 11:15:54 -08:00
parent 5269e06aa1
commit a22a133ca1
8 changed files with 1408 additions and 4 deletions

12
Godeps/Godeps.json generated
View File

@ -116,6 +116,10 @@
"Comment": "v1.1.2-50-g692a500",
"Rev": "692a50017a7049b26cf7ea4ccfc0d8c77369a793"
},
{
"ImportPath": "github.com/evanphx/json-patch",
"Rev": "7dd4489c2eb6073e5a9d7746c3274c5b5f0387df"
},
{
"ImportPath": "github.com/fsouza/go-dockerclient",
"Rev": "d19717788084716e4adff0515be6289aa04bec46"
@ -155,15 +159,15 @@
"Comment": "0.1.3-8-g6633656",
"Rev": "6633656539c1639d9d78127b7d47c622b5d7b6dc"
},
{
"ImportPath": "github.com/matttproud/golang_protobuf_extensions/ext",
"Rev": "7a864a042e844af638df17ebbabf8183dace556a"
},
{
"ImportPath": "github.com/kr/pty",
"Comment": "release.r56-25-g05017fc",
"Rev": "05017fcccf23c823bfdea560dcc958a136e54fb7"
},
{
"ImportPath": "github.com/matttproud/golang_protobuf_extensions/ext",
"Rev": "7a864a042e844af638df17ebbabf8183dace556a"
},
{
"ImportPath": "github.com/miekg/dns",
"Rev": "3f504e8dabd5d562e997d19ce0200aa41973e1b2"

View File

@ -0,0 +1,14 @@
language: go
go:
- 1.4
- 1.3
install:
- if ! go get code.google.com/p/go.tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi
script:
- go test -cover ./...
notifications:
email: false

View File

@ -0,0 +1,25 @@
Copyright (c) 2014, Evan Phoenix
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the Evan Phoenix nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,29 @@
## JSON-Patch
Provides the abiilty to modify and test a JSON according to a
[RFC6902 JSON patch](http://tools.ietf.org/html/rfc6902) and [RFC7386 JSON Merge Patch](https://tools.ietf.org/html/rfc7386).
*Version*: **1.0**
[![GoDoc](https://godoc.org/github.com/evanphx/json-patch?status.svg)](http://godoc.org/github.com/evanphx/json-patch)
[![Build Status](https://travis-ci.org/evanphx/json-patch.svg?branch=RFC7386)](https://travis-ci.org/evanphx/json-patch)
### API Usage
* Given a `[]byte`, obtain a Patch object
`obj, err := jsonpatch.DecodePatch(patch)`
* Apply the patch and get a new document back
`out, err := obj.Apply(doc)`
* Create a JSON Merge Patch document based on two json documents (a to b):
`mergeDoc, err := jsonpatch.CreateMergePatch(a, b)`
* Bonus API: compare documents for structural equality
`jsonpatch.Equal(doca, docb)`

View File

@ -0,0 +1,280 @@
package jsonpatch
import (
"encoding/json"
"fmt"
"reflect"
)
func merge(cur, patch *lazyNode) *lazyNode {
curDoc, err := cur.intoDoc()
if err != nil {
pruneNulls(patch)
return patch
}
patchDoc, err := patch.intoDoc()
if err != nil {
return patch
}
mergeDocs(curDoc, patchDoc)
return cur
}
func mergeDocs(doc, patch *partialDoc) {
for k, v := range *patch {
if v == nil {
delete(*doc, k)
} else {
cur, ok := (*doc)[k]
if !ok || cur == nil {
pruneNulls(v)
(*doc)[k] = v
} else {
(*doc)[k] = merge(cur, v)
}
}
}
}
func pruneNulls(n *lazyNode) {
sub, err := n.intoDoc()
if err == nil {
pruneDocNulls(sub)
} else {
ary, err := n.intoAry()
if err == nil {
pruneAryNulls(ary)
}
}
}
func pruneDocNulls(doc *partialDoc) *partialDoc {
for k, v := range *doc {
if v == nil {
delete(*doc, k)
} else {
pruneNulls(v)
}
}
return doc
}
func pruneAryNulls(ary *partialArray) *partialArray {
var newAry []*lazyNode
for _, v := range *ary {
if v != nil {
pruneNulls(v)
newAry = append(newAry, v)
}
}
*ary = newAry
return ary
}
var errBadJSONDoc = fmt.Errorf("Invalid JSON Document")
var errBadJSONPatch = fmt.Errorf("Invalid JSON Patch")
// MergePatch merges the patchData into the docData.
func MergePatch(docData, patchData []byte) ([]byte, error) {
doc := &partialDoc{}
docErr := json.Unmarshal(docData, doc)
patch := &partialDoc{}
patchErr := json.Unmarshal(patchData, patch)
if _, ok := docErr.(*json.SyntaxError); ok {
return nil, errBadJSONDoc
}
if _, ok := patchErr.(*json.SyntaxError); ok {
return nil, errBadJSONPatch
}
if docErr == nil && *doc == nil {
return nil, errBadJSONDoc
}
if patchErr == nil && *patch == nil {
return nil, errBadJSONPatch
}
if docErr != nil || patchErr != nil {
// Not an error, just not a doc, so we turn straight into the patch
if patchErr == nil {
doc = pruneDocNulls(patch)
} else {
patchAry := &partialArray{}
patchErr = json.Unmarshal(patchData, patchAry)
if patchErr != nil {
return nil, errBadJSONPatch
}
pruneAryNulls(patchAry)
out, patchErr := json.Marshal(patchAry)
if patchErr != nil {
return nil, errBadJSONPatch
}
return out, nil
}
} else {
mergeDocs(doc, patch)
}
return json.Marshal(doc)
}
// CreateMergePatch creates a merge patch as specified in http://tools.ietf.org/html/draft-ietf-appsawg-json-merge-patch-07
//
// 'a' is original, 'b' is the modified document. Both are to be given as json encoded content.
// The function will return a mergeable json document with differences from a to b.
//
// An error will be returned if any of the two documents are invalid.
func CreateMergePatch(a, b []byte) ([]byte, error) {
aI := map[string]interface{}{}
bI := map[string]interface{}{}
err := json.Unmarshal(a, &aI)
if err != nil {
return nil, errBadJSONDoc
}
err = json.Unmarshal(b, &bI)
if err != nil {
return nil, errBadJSONDoc
}
dest, err := getDiff(aI, bI)
if err != nil {
return nil, err
}
return json.Marshal(dest)
}
// Returns true if the array matches (must be json types).
// As is idiomatic for go, an empty array is not the same as a nil array.
func matchesArray(a, b []interface{}) bool {
if len(a) != len(b) {
return false
}
if (a == nil && b != nil) || (a != nil && b == nil) {
return false
}
for i := range a {
if !matchesValue(a[i], b[i]) {
return false
}
}
return true
}
// Returns true if the values matches (must be json types)
// The types of the values must match, otherwise it will always return false
// If two map[string]interface{} are given, all elements must match.
func matchesValue(av, bv interface{}) bool {
if reflect.TypeOf(av) != reflect.TypeOf(bv) {
return false
}
switch at := av.(type) {
case string:
bt := bv.(string)
if bt == at {
return true
}
case float64:
bt := bv.(float64)
if bt == at {
return true
}
case bool:
bt := bv.(bool)
if bt == at {
return true
}
case map[string]interface{}:
bt := bv.(map[string]interface{})
for key := range at {
if !matchesValue(at[key], bt[key]) {
return false
}
}
for key := range bt {
if !matchesValue(at[key], bt[key]) {
return false
}
}
return true
}
return false
}
// getDiff returns the (recursive) difference between a and b as a map[string]interface{}.
func getDiff(a, b map[string]interface{}) (map[string]interface{}, error) {
into := map[string]interface{}{}
for key, bv := range b {
av, ok := a[key]
// value was added
if !ok {
into[key] = bv
continue
}
// If types have changed, replace completely
if reflect.TypeOf(av) != reflect.TypeOf(bv) {
into[key] = bv
continue
}
// Types are the same, compare values
switch at := av.(type) {
case map[string]interface{}:
bt := bv.(map[string]interface{})
dst := make(map[string]interface{}, len(bt))
dst, err := getDiff(at, bt)
if err != nil {
return nil, err
}
if len(dst) > 0 {
into[key] = dst
}
case string, float64, bool:
if !matchesValue(av, bv) {
into[key] = bv
}
case []interface{}:
bt := bv.([]interface{})
if !matchesArray(at, bt) {
into[key] = bv
}
case nil:
switch bv.(type) {
case nil:
// Both nil, fine.
default:
into[key] = bv
}
default:
panic(fmt.Sprintf("Unknown type:%T in key %s", av, key))
}
}
// Now add all deleted values as nil
for key := range a {
_, found := b[key]
if !found {
into[key] = nil
}
}
return into, nil
}

View File

@ -0,0 +1,322 @@
package jsonpatch
import (
"strings"
"testing"
)
func mergePatch(doc, patch string) string {
out, err := MergePatch([]byte(doc), []byte(patch))
if err != nil {
panic(err)
}
return string(out)
}
func TestMergePatchReplaceKey(t *testing.T) {
doc := `{ "title": "hello" }`
pat := `{ "title": "goodbye" }`
res := mergePatch(doc, pat)
if !compareJSON(pat, res) {
t.Fatalf("Key was not replaced")
}
}
func TestMergePatchIgnoresOtherValues(t *testing.T) {
doc := `{ "title": "hello", "age": 18 }`
pat := `{ "title": "goodbye" }`
res := mergePatch(doc, pat)
exp := `{ "title": "goodbye", "age": 18 }`
if !compareJSON(exp, res) {
t.Fatalf("Key was not replaced")
}
}
func TestMergePatchNilDoc(t *testing.T) {
doc := `{ "title": null }`
pat := `{ "title": {"foo": "bar"} }`
res := mergePatch(doc, pat)
exp := `{ "title": {"foo": "bar"} }`
if !compareJSON(exp, res) {
t.Fatalf("Key was not replaced")
}
}
func TestMergePatchRecursesIntoObjects(t *testing.T) {
doc := `{ "person": { "title": "hello", "age": 18 } }`
pat := `{ "person": { "title": "goodbye" } }`
res := mergePatch(doc, pat)
exp := `{ "person": { "title": "goodbye", "age": 18 } }`
if !compareJSON(exp, res) {
t.Fatalf("Key was not replaced")
}
}
type nonObjectCases struct {
doc, pat, res string
}
func TestMergePatchReplacesNonObjectsWholesale(t *testing.T) {
a1 := `[1]`
a2 := `[2]`
o1 := `{ "a": 1 }`
o2 := `{ "a": 2 }`
o3 := `{ "a": 1, "b": 1 }`
o4 := `{ "a": 2, "b": 1 }`
cases := []nonObjectCases{
{a1, a2, a2},
{o1, a2, a2},
{a1, o1, o1},
{o3, o2, o4},
}
for _, c := range cases {
act := mergePatch(c.doc, c.pat)
if !compareJSON(c.res, act) {
t.Errorf("whole object replacement failed")
}
}
}
func TestMergePatchReturnsErrorOnBadJSON(t *testing.T) {
_, err := MergePatch([]byte(`[[[[`), []byte(`1`))
if err == nil {
t.Errorf("Did not return an error for bad json: %s", err)
}
_, err = MergePatch([]byte(`1`), []byte(`[[[[`))
if err == nil {
t.Errorf("Did not return an error for bad json: %s", err)
}
}
var rfcTests = []struct {
target string
patch string
expected string
}{
// test cases from https://tools.ietf.org/html/rfc7386#appendix-A
{target: `{"a":"b"}`, patch: `{"a":"c"}`, expected: `{"a":"c"}`},
{target: `{"a":"b"}`, patch: `{"b":"c"}`, expected: `{"a":"b","b":"c"}`},
{target: `{"a":"b"}`, patch: `{"a":null}`, expected: `{}`},
{target: `{"a":"b","b":"c"}`, patch: `{"a":null}`, expected: `{"b":"c"}`},
{target: `{"a":["b"]}`, patch: `{"a":"c"}`, expected: `{"a":"c"}`},
{target: `{"a":"c"}`, patch: `{"a":["b"]}`, expected: `{"a":["b"]}`},
{target: `{"a":{"b": "c"}}`, patch: `{"a": {"b": "d","c": null}}`, expected: `{"a":{"b":"d"}}`},
{target: `{"a":[{"b":"c"}]}`, patch: `{"a":[1]}`, expected: `{"a":[1]}`},
{target: `["a","b"]`, patch: `["c","d"]`, expected: `["c","d"]`},
{target: `{"a":"b"}`, patch: `["c"]`, expected: `["c"]`},
// {target: `{"a":"foo"}`, patch: `null`, expected: `null`},
// {target: `{"a":"foo"}`, patch: `"bar"`, expected: `"bar"`},
{target: `{"e":null}`, patch: `{"a":1}`, expected: `{"a":1,"e":null}`},
{target: `[1,2]`, patch: `{"a":"b","c":null}`, expected: `{"a":"b"}`},
{target: `{}`, patch: `{"a":{"bb":{"ccc":null}}}`, expected: `{"a":{"bb":{}}}`},
}
func TestMergePatchRFCCases(t *testing.T) {
for i, c := range rfcTests {
out := mergePatch(c.target, c.patch)
if !compareJSON(out, c.expected) {
t.Errorf("case[%d], patch '%s' did not apply properly to '%s'. expected:\n'%s'\ngot:\n'%s'", i, c.patch, c.target, c.expected, out)
}
}
}
var rfcFailTests = `
{"a":"foo"} | null
{"a":"foo"} | "bar"
`
func TestMergePatchFailRFCCases(t *testing.T) {
tests := strings.Split(rfcFailTests, "\n")
for _, c := range tests {
if strings.TrimSpace(c) == "" {
continue
}
parts := strings.SplitN(c, "|", 2)
doc := strings.TrimSpace(parts[0])
pat := strings.TrimSpace(parts[1])
out, err := MergePatch([]byte(doc), []byte(pat))
if err != errBadJSONPatch {
t.Errorf("error not returned properly: %s, %s", err, string(out))
}
}
}
func TestMergeReplaceKey(t *testing.T) {
doc := `{ "title": "hello", "nested": {"one": 1, "two": 2} }`
pat := `{ "title": "goodbye", "nested": {"one": 2, "two": 2} }`
exp := `{ "title": "goodbye", "nested": {"one": 2} }`
res, err := CreateMergePatch([]byte(doc), []byte(pat))
if err != nil {
t.Errorf("Unexpected error: %s, %s", err, string(res))
}
if !compareJSON(exp, string(res)) {
t.Fatalf("Key was not replaced")
}
}
func TestMergeGetArray(t *testing.T) {
doc := `{ "title": "hello", "array": ["one", "two"], "notmatch": [1, 2, 3] }`
pat := `{ "title": "hello", "array": ["one", "two", "three"], "notmatch": [1, 2, 3] }`
exp := `{ "array": ["one", "two", "three"] }`
res, err := CreateMergePatch([]byte(doc), []byte(pat))
if err != nil {
t.Errorf("Unexpected error: %s, %s", err, string(res))
}
if !compareJSON(exp, string(res)) {
t.Fatalf("Array was not added")
}
}
func TestMergeGetObjArray(t *testing.T) {
doc := `{ "title": "hello", "array": [{"banana": true}, {"evil": false}], "notmatch": [{"one":1}, {"two":2}, {"three":3}] }`
pat := `{ "title": "hello", "array": [{"banana": false}, {"evil": true}], "notmatch": [{"one":1}, {"two":2}, {"three":3}] }`
exp := `{ "array": [{"banana": false}, {"evil": true}] }`
res, err := CreateMergePatch([]byte(doc), []byte(pat))
if err != nil {
t.Errorf("Unexpected error: %s, %s", err, string(res))
}
if !compareJSON(exp, string(res)) {
t.Fatalf("Object array was not added")
}
}
func TestMergeDeleteKey(t *testing.T) {
doc := `{ "title": "hello", "nested": {"one": 1, "two": 2} }`
pat := `{ "title": "hello", "nested": {"one": 1} }`
exp := `{"nested":{"two":null}}`
res, err := CreateMergePatch([]byte(doc), []byte(pat))
if err != nil {
t.Errorf("Unexpected error: %s, %s", err, string(res))
}
// We cannot use "compareJSON", since Equals does not report a difference if the value is null
if exp != string(res) {
t.Fatalf("Key was not removed")
}
}
func TestMergeEmptyArray(t *testing.T) {
doc := `{ "array": null }`
pat := `{ "array": [] }`
exp := `{"array":[]}`
res, err := CreateMergePatch([]byte(doc), []byte(pat))
if err != nil {
t.Errorf("Unexpected error: %s, %s", err, string(res))
}
// We cannot use "compareJSON", since Equals does not report a difference if the value is null
if exp != string(res) {
t.Fatalf("Key was not removed")
}
}
func TestMergeObjArray(t *testing.T) {
doc := `{ "array": [ {"a": {"b": 2}}, {"a": {"b": 3}} ]}`
exp := `{}`
res, err := CreateMergePatch([]byte(doc), []byte(doc))
if err != nil {
t.Errorf("Unexpected error: %s, %s", err, string(res))
}
// We cannot use "compareJSON", since Equals does not report a difference if the value is null
if exp != string(res) {
t.Fatalf("Array was not empty, was " + string(res))
}
}
func TestMergeComplexMatch(t *testing.T) {
doc := `{"hello": "world","t": true ,"f": false, "n": null,"i": 123,"pi": 3.1416,"a": [1, 2, 3, 4], "nested": {"hello": "world","t": true ,"f": false, "n": null,"i": 123,"pi": 3.1416,"a": [1, 2, 3, 4]} }`
empty := `{}`
res, err := CreateMergePatch([]byte(doc), []byte(doc))
if err != nil {
t.Errorf("Unexpected error: %s, %s", err, string(res))
}
// We cannot use "compareJSON", since Equals does not report a difference if the value is null
if empty != string(res) {
t.Fatalf("Did not get empty result, was:%s", string(res))
}
}
func TestMergeComplexAddAll(t *testing.T) {
doc := `{"hello": "world","t": true ,"f": false, "n": null,"i": 123,"pi": 3.1416,"a": [1, 2, 3, 4], "nested": {"hello": "world","t": true ,"f": false, "n": null,"i": 123,"pi": 3.1416,"a": [1, 2, 3, 4]} }`
empty := `{}`
res, err := CreateMergePatch([]byte(empty), []byte(doc))
if err != nil {
t.Errorf("Unexpected error: %s, %s", err, string(res))
}
if !compareJSON(doc, string(res)) {
t.Fatalf("Did not get everything as, it was:\n%s", string(res))
}
}
func TestMergeComplexRemoveAll(t *testing.T) {
doc := `{"hello": "world","t": true ,"f": false, "n": null,"i": 123,"pi": 3.1416,"a": [1, 2, 3, 4], "nested": {"hello": "world","t": true ,"f": false, "n": null,"i": 123,"pi": 3.1416,"a": [1, 2, 3, 4]} }`
exp := `{"a":null,"f":null,"hello":null,"i":null,"n":null,"nested":null,"pi":null,"t":null}`
empty := `{}`
res, err := CreateMergePatch([]byte(doc), []byte(empty))
if err != nil {
t.Errorf("Unexpected error: %s, %s", err, string(res))
}
if exp != string(res) {
t.Fatalf("Did not get result, was:%s", string(res))
}
// FIXME: Crashes if using compareJSON like this:
/*
if !compareJSON(doc, string(res)) {
t.Fatalf("Did not get everything as, it was:\n%s", string(res))
}
*/
}

View File

@ -0,0 +1,496 @@
package jsonpatch
import (
"bytes"
"encoding/json"
"fmt"
"strconv"
"strings"
)
const (
eRaw = iota
eDoc
eAry
)
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
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 *n.raw, nil
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
}
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
}
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{}
err := json.Compact(buf, *n.raw)
if err != nil {
return *n.raw
}
return buf.Bytes()
}
func (n *lazyNode) tryDoc() bool {
err := json.Unmarshal(*n.raw, &n.doc)
if err != nil {
return false
}
n.which = eDoc
return true
}
func (n *lazyNode) tryAry() bool {
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 {
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 {
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 {
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 *partialDoc, path string) (container, string) {
doc := container(pd)
split := strings.Split(path, "/")
parts := split[1 : len(split)-1]
key := split[len(split)-1]
var err error
for _, part := range parts {
next, ok := doc.get(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, key
}
func (d *partialDoc) set(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 {
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
}
ary := make([]*lazyNode, len(*d)+1)
cur := *d
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
}
return (*d)[idx], nil
}
func (d *partialArray) remove(key string) error {
idx, err := strconv.Atoi(key)
if err != nil {
return err
}
cur := *d
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 *partialDoc, op operation) error {
path := op.path()
con, key := findObject(doc, path)
if con == nil {
return fmt.Errorf("Missing container: %s", path)
}
con.set(key, op.value())
return nil
}
func (p Patch) remove(doc *partialDoc, op operation) error {
path := op.path()
con, key := findObject(doc, path)
return con.remove(key)
}
func (p Patch) replace(doc *partialDoc, op operation) error {
path := op.path()
con, key := findObject(doc, path)
con.set(key, op.value())
return nil
}
func (p Patch) move(doc *partialDoc, op operation) error {
from := op.from()
con, key := findObject(doc, from)
val, err := con.get(key)
if err != nil {
return err
}
con.remove(key)
path := op.path()
con, key = findObject(doc, path)
con.set(key, val)
return nil
}
func (p Patch) test(doc *partialDoc, op operation) error {
path := op.path()
con, key := findObject(doc, path)
val, err := con.get(key)
if err != nil {
return err
}
if val.equal(op.value()) {
return nil
}
return fmt.Errorf("Testing value %s failed", path)
}
// 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) {
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)
default:
err = fmt.Errorf("Unexpected kind: %s", op.kind())
}
if err != nil {
return nil, err
}
}
return json.Marshal(pd)
}

View File

@ -0,0 +1,234 @@
package jsonpatch
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"testing"
)
func reformatJSON(j string) string {
buf := new(bytes.Buffer)
json.Indent(buf, []byte(j), "", " ")
return buf.String()
}
func compareJSON(a, b string) bool {
// return Equal([]byte(a), []byte(b))
var obj_a, obj_b map[string]interface{}
json.Unmarshal([]byte(a), &obj_a)
json.Unmarshal([]byte(b), &obj_b)
// fmt.Printf("Comparing %#v\nagainst %#v\n", obj_a, obj_b)
return reflect.DeepEqual(obj_a, obj_b)
}
func applyPatch(doc, patch string) (string, error) {
obj, err := DecodePatch([]byte(patch))
if err != nil {
panic(err)
}
out, err := obj.Apply([]byte(doc))
if err != nil {
return "", err
}
return string(out), nil
}
type Case struct {
doc, patch, result string
}
var Cases = []Case{
{
`{ "foo": "bar"}`,
`[
{ "op": "add", "path": "/baz", "value": "qux" }
]`,
`{
"baz": "qux",
"foo": "bar"
}`,
},
{
`{ "foo": [ "bar", "baz" ] }`,
`[
{ "op": "add", "path": "/foo/1", "value": "qux" }
]`,
`{ "foo": [ "bar", "qux", "baz" ] }`,
},
{
`{ "baz": "qux", "foo": "bar" }`,
`[ { "op": "remove", "path": "/baz" } ]`,
`{ "foo": "bar" }`,
},
{
`{ "foo": [ "bar", "qux", "baz" ] }`,
`[ { "op": "remove", "path": "/foo/1" } ]`,
`{ "foo": [ "bar", "baz" ] }`,
},
{
`{ "baz": "qux", "foo": "bar" }`,
`[ { "op": "replace", "path": "/baz", "value": "boo" } ]`,
`{ "baz": "boo", "foo": "bar" }`,
},
{
`{
"foo": {
"bar": "baz",
"waldo": "fred"
},
"qux": {
"corge": "grault"
}
}`,
`[ { "op": "move", "from": "/foo/waldo", "path": "/qux/thud" } ]`,
`{
"foo": {
"bar": "baz"
},
"qux": {
"corge": "grault",
"thud": "fred"
}
}`,
},
{
`{ "foo": [ "all", "grass", "cows", "eat" ] }`,
`[ { "op": "move", "from": "/foo/1", "path": "/foo/3" } ]`,
`{ "foo": [ "all", "cows", "eat", "grass" ] }`,
},
{
`{ "foo": "bar" }`,
`[ { "op": "add", "path": "/child", "value": { "grandchild": { } } } ]`,
`{ "foo": "bar", "child": { "grandchild": { } } }`,
},
{
`{ "foo": ["bar"] }`,
`[ { "op": "add", "path": "/foo/-", "value": ["abc", "def"] } ]`,
`{ "foo": ["bar", ["abc", "def"]] }`,
},
{
`{ "foo": "bar", "qux": { "baz": 1, "bar": null } }`,
`[ { "op": "remove", "path": "/qux/bar" } ]`,
`{ "foo": "bar", "qux": { "baz": 1 } }`,
},
}
type BadCase struct {
doc, patch string
}
var MutationTestCases = []BadCase{
{
`{ "foo": "bar", "qux": { "baz": 1, "bar": null } }`,
`[ { "op": "remove", "path": "/qux/bar" } ]`,
},
}
var BadCases = []BadCase{
{
`{ "foo": "bar" }`,
`[ { "op": "add", "path": "/baz/bat", "value": "qux" } ]`,
},
}
func TestAllCases(t *testing.T) {
for _, c := range Cases {
out, err := applyPatch(c.doc, c.patch)
if err != nil {
t.Errorf("Unable to apply patch: %s", err)
}
if !compareJSON(out, c.result) {
t.Errorf("Patch did not apply. Expected:\n%s\n\nActual:\n%s",
reformatJSON(c.result), reformatJSON(out))
}
}
for _, c := range MutationTestCases {
out, err := applyPatch(c.doc, c.patch)
if err != nil {
t.Errorf("Unable to apply patch: %s", err)
}
if compareJSON(out, c.doc) {
t.Errorf("Patch did not apply. Original:\n%s\n\nPatched:\n%s",
reformatJSON(c.doc), reformatJSON(out))
}
}
for _, c := range BadCases {
_, err := applyPatch(c.doc, c.patch)
if err == nil {
t.Errorf("Patch should have failed to apply but it did not")
}
}
}
type TestCase struct {
doc, patch string
result bool
failedPath string
}
var TestCases = []TestCase{
{
`{
"baz": "qux",
"foo": [ "a", 2, "c" ]
}`,
`[
{ "op": "test", "path": "/baz", "value": "qux" },
{ "op": "test", "path": "/foo/1", "value": 2 }
]`,
true,
"",
},
{
`{ "baz": "qux" }`,
`[ { "op": "test", "path": "/baz", "value": "bar" } ]`,
false,
"/baz",
},
{
`{
"baz": "qux",
"foo": ["a", 2, "c"]
}`,
`[
{ "op": "test", "path": "/baz", "value": "qux" },
{ "op": "test", "path": "/foo/1", "value": "c" }
]`,
false,
"/foo/1",
},
}
func TestAllTest(t *testing.T) {
for _, c := range TestCases {
_, err := applyPatch(c.doc, c.patch)
if c.result && err != nil {
t.Errorf("Testing failed when it should have passed: %s", err)
} else if !c.result && err == nil {
t.Errorf("Testing passed when it should have faild: %s", err)
} else if !c.result {
expected := fmt.Sprintf("Testing value %s failed", c.failedPath)
if err.Error() != expected {
t.Errorf("Testing failed as expected but invalid message: expected [%s], got [%s]", expected, err)
}
}
}
}