From b62a81c7ca1f64d483a00275a24590196d35d8a9 Mon Sep 17 00:00:00 2001 From: Mengqi Yu Date: Mon, 5 Mar 2018 18:24:32 -0800 Subject: [PATCH] vendor github.com/evanphx/json-patch --- Gopkg.lock | 8 +- .../github.com/evanphx/json-patch/.travis.yml | 16 + vendor/github.com/evanphx/json-patch/LICENSE | 25 + .../github.com/evanphx/json-patch/README.md | 29 + .../json-patch/cmd/json-patch/file_flag.go | 39 ++ .../evanphx/json-patch/cmd/json-patch/main.go | 56 ++ vendor/github.com/evanphx/json-patch/merge.go | 305 +++++++++ .../evanphx/json-patch/merge_test.go | 471 +++++++++++++ vendor/github.com/evanphx/json-patch/patch.go | 643 ++++++++++++++++++ .../evanphx/json-patch/patch_test.go | 340 +++++++++ 10 files changed, 1931 insertions(+), 1 deletion(-) create mode 100644 vendor/github.com/evanphx/json-patch/.travis.yml create mode 100644 vendor/github.com/evanphx/json-patch/LICENSE create mode 100644 vendor/github.com/evanphx/json-patch/README.md create mode 100644 vendor/github.com/evanphx/json-patch/cmd/json-patch/file_flag.go create mode 100644 vendor/github.com/evanphx/json-patch/cmd/json-patch/main.go create mode 100644 vendor/github.com/evanphx/json-patch/merge.go create mode 100644 vendor/github.com/evanphx/json-patch/merge_test.go create mode 100644 vendor/github.com/evanphx/json-patch/patch.go create mode 100644 vendor/github.com/evanphx/json-patch/patch_test.go diff --git a/Gopkg.lock b/Gopkg.lock index 2f33900ea..fea3e18a6 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -25,6 +25,12 @@ revision = "5741799b275a3c4a5a9623a993576d7545cf7b5c" version = "v2.4.0" +[[projects]] + branch = "master" + name = "github.com/evanphx/json-patch" + packages = ["."] + revision = "944e07253867aacae43c04b2e6a239005443f33a" + [[projects]] name = "github.com/ghodss/yaml" packages = ["."] @@ -226,6 +232,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "a778052416a71e5aca31256e71794eda11fed3c896b231ca36a045770abad827" + inputs-digest = "b4cf8af8194603b9195b180d2d425c6eb0b9b2f2d82493383b14c4d02d9dffff" solver-name = "gps-cdcl" solver-version = 1 diff --git a/vendor/github.com/evanphx/json-patch/.travis.yml b/vendor/github.com/evanphx/json-patch/.travis.yml new file mode 100644 index 000000000..2092c72c4 --- /dev/null +++ b/vendor/github.com/evanphx/json-patch/.travis.yml @@ -0,0 +1,16 @@ +language: go + +go: + - 1.8 + - 1.7 + +install: + - if ! go get code.google.com/p/go.tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi + - go get github.com/jessevdk/go-flags + +script: + - go get + - go test -cover ./... + +notifications: + email: false diff --git a/vendor/github.com/evanphx/json-patch/LICENSE b/vendor/github.com/evanphx/json-patch/LICENSE new file mode 100644 index 000000000..0eb9b72d8 --- /dev/null +++ b/vendor/github.com/evanphx/json-patch/LICENSE @@ -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. diff --git a/vendor/github.com/evanphx/json-patch/README.md b/vendor/github.com/evanphx/json-patch/README.md new file mode 100644 index 000000000..d0d826bac --- /dev/null +++ b/vendor/github.com/evanphx/json-patch/README.md @@ -0,0 +1,29 @@ +## JSON-Patch + +Provides the ability to modify and test a JSON according to a +[RFC6902 JSON patch](http://tools.ietf.org/html/rfc6902) and [RFC7396 JSON Merge Patch](https://tools.ietf.org/html/rfc7396). + +*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=master)](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)` + diff --git a/vendor/github.com/evanphx/json-patch/cmd/json-patch/file_flag.go b/vendor/github.com/evanphx/json-patch/cmd/json-patch/file_flag.go new file mode 100644 index 000000000..fe69d6e2a --- /dev/null +++ b/vendor/github.com/evanphx/json-patch/cmd/json-patch/file_flag.go @@ -0,0 +1,39 @@ +package main + +// Borrowed from Concourse: https://github.com/concourse/atc/blob/master/atccmd/file_flag.go + +import ( + "fmt" + "os" + "path/filepath" +) + +// FileFlag is a flag for passing a path to a file on disk. The file is +// expected to be a file, not a directory, that actually exists. +type FileFlag string + +// UnmarshalFlag implements go-flag's Unmarshaler interface +func (f *FileFlag) UnmarshalFlag(value string) error { + stat, err := os.Stat(value) + if err != nil { + return err + } + + if stat.IsDir() { + return fmt.Errorf("path '%s' is a directory, not a file", value) + } + + abs, err := filepath.Abs(value) + if err != nil { + return err + } + + *f = FileFlag(abs) + + return nil +} + +// Path is the path to the file +func (f FileFlag) Path() string { + return string(f) +} diff --git a/vendor/github.com/evanphx/json-patch/cmd/json-patch/main.go b/vendor/github.com/evanphx/json-patch/cmd/json-patch/main.go new file mode 100644 index 000000000..2be182635 --- /dev/null +++ b/vendor/github.com/evanphx/json-patch/cmd/json-patch/main.go @@ -0,0 +1,56 @@ +package main + +import ( + "fmt" + "io/ioutil" + "log" + "os" + + jsonpatch "github.com/evanphx/json-patch" + flags "github.com/jessevdk/go-flags" +) + +type opts struct { + PatchFilePaths []FileFlag `long:"patch-file" short:"p" value-name:"PATH" description:"Path to file with one or more operations"` +} + +func main() { + var o opts + _, err := flags.Parse(&o) + if err != nil { + log.Fatalf("error: %s\n", err) + } + + patches := make([]jsonpatch.Patch, len(o.PatchFilePaths)) + + for i, patchFilePath := range o.PatchFilePaths { + var bs []byte + bs, err = ioutil.ReadFile(patchFilePath.Path()) + if err != nil { + log.Fatalf("error reading patch file: %s", err) + } + + var patch jsonpatch.Patch + patch, err = jsonpatch.DecodePatch(bs) + if err != nil { + log.Fatalf("error decoding patch file: %s", err) + } + + patches[i] = patch + } + + doc, err := ioutil.ReadAll(os.Stdin) + if err != nil { + log.Fatalf("error reading from stdin: %s", err) + } + + mdoc := doc + for _, patch := range patches { + mdoc, err = patch.Apply(mdoc) + if err != nil { + log.Fatalf("error applying patch: %s", err) + } + } + + fmt.Printf("%s", mdoc) +} diff --git a/vendor/github.com/evanphx/json-patch/merge.go b/vendor/github.com/evanphx/json-patch/merge.go new file mode 100644 index 000000000..b9af252fe --- /dev/null +++ b/vendor/github.com/evanphx/json-patch/merge.go @@ -0,0 +1,305 @@ +package jsonpatch + +import ( + "encoding/json" + "fmt" + "reflect" +) + +func merge(cur, patch *lazyNode, mergeMerge bool) *lazyNode { + curDoc, err := cur.intoDoc() + + if err != nil { + pruneNulls(patch) + return patch + } + + patchDoc, err := patch.intoDoc() + + if err != nil { + return patch + } + + mergeDocs(curDoc, patchDoc, mergeMerge) + + return cur +} + +func mergeDocs(doc, patch *partialDoc, mergeMerge bool) { + for k, v := range *patch { + if v == nil { + if mergeMerge { + (*doc)[k] = nil + } else { + delete(*doc, k) + } + } else { + cur, ok := (*doc)[k] + + if !ok || cur == nil { + pruneNulls(v) + (*doc)[k] = v + } else { + (*doc)[k] = merge(cur, v, mergeMerge) + } + } + } +} + +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 { + 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") + +// MergeMergePatches merges two merge patches together, such that +// applying this resulting merged merge patch to a document yields the same +// as merging each merge patch to the document in succession. +func MergeMergePatches(patch1Data, patch2Data []byte) ([]byte, error) { + return doMergePatch(patch1Data, patch2Data, true) +} + +// MergePatch merges the patchData into the docData. +func MergePatch(docData, patchData []byte) ([]byte, error) { + return doMergePatch(docData, patchData, false) +} + +func doMergePatch(docData, patchData []byte, mergeMerge bool) ([]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 { + if mergeMerge { + doc = patch + } else { + 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, mergeMerge) + } + + 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 nil: + // Both nil, fine. + 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 + case []interface{}: + bt := bv.([]interface{}) + return matchesArray(at, bt) + } + 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 +} diff --git a/vendor/github.com/evanphx/json-patch/merge_test.go b/vendor/github.com/evanphx/json-patch/merge_test.go new file mode 100644 index 000000000..5d3ecffe6 --- /dev/null +++ b/vendor/github.com/evanphx/json-patch/merge_test.go @@ -0,0 +1,471 @@ +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) + } +} + +func TestMergePatchReturnsEmptyArrayOnEmptyArray(t *testing.T) { + doc := `{ "array": ["one", "two"] }` + pat := `{ "array": [] }` + + exp := `{ "array": [] }` + + res, err := MergePatch([]byte(doc), []byte(pat)) + + if err != nil { + t.Errorf("Unexpected error: %s, %s", err, string(res)) + } + + if !compareJSON(exp, string(res)) { + t.Fatalf("Emtpy array did not return not return as empty array") + } +} + +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 TestCreateMergePatchNil(t *testing.T) { + doc := `{ "title": "hello", "nested": {"one": 1, "two": [{"one":null}, {"two":null}, {"three":null}]} }` + pat := doc + + exp := `{}` + + 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 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)) + } + */ +} + +func TestMergeObjectWithInnerArray(t *testing.T) { + stateString := `{ + "OuterArray": [ + { + "InnerArray": [ + { + "StringAttr": "abc123" + } + ], + "StringAttr": "def456" + } + ] + }` + + patch, err := CreateMergePatch([]byte(stateString), []byte(stateString)) + if err != nil { + t.Fatal(err) + } + + if string(patch) != "{}" { + t.Fatalf("Patch should have been {} but was: %v", string(patch)) + } +} + +func TestMergeReplaceKeyNotEscape(t *testing.T) { + doc := `{ "title": "hello", "nested": {"title/escaped": 1, "two": 2} }` + pat := `{ "title": "goodbye", "nested": {"title/escaped": 2, "two": 2} }` + + exp := `{ "title": "goodbye", "nested": {"title/escaped": 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.Log(string(res)) + t.Fatalf("Key was not replaced") + } +} + +func TestMergePatchReplaceKeyNotEscaping(t *testing.T) { + doc := `{ "obj": { "title/escaped": "hello" } }` + pat := `{ "obj": { "title/escaped": "goodbye" } }` + exp := `{ "obj": { "title/escaped": "goodbye" } }` + + res := mergePatch(doc, pat) + + if !compareJSON(exp, res) { + t.Fatalf("Key was not replaced") + } +} + +func TestMergeMergePatches(t *testing.T) { + cases := []struct { + demonstrates string + p1 string + p2 string + exp string + }{ + { + demonstrates: "simple patches are merged normally", + p1: `{"add1": 1}`, + p2: `{"add2": 2}`, + exp: `{"add1": 1, "add2": 2}`, + }, + { + demonstrates: "nulls are kept", + p1: `{"del1": null}`, + p2: `{"del2": null}`, + exp: `{"del1": null, "del2": null}`, + }, + { + demonstrates: "a key added then deleted is kept deleted", + p1: `{"add_then_delete": "atd"}`, + p2: `{"add_then_delete": null}`, + exp: `{"add_then_delete": null}`, + }, + { + demonstrates: "a key deleted then added is kept added", + p1: `{"delete_then_add": null}`, + p2: `{"delete_then_add": "dta"}`, + exp: `{"delete_then_add": "dta"}`, + }, + { + demonstrates: "object overrides array", + p1: `[]`, + p2: `{"del": null, "add": "a"}`, + exp: `{"del": null, "add": "a"}`, + }, + { + demonstrates: "array overrides object", + p1: `{"del": null, "add": "a"}`, + p2: `[]`, + exp: `[]`, + }, + } + + for _, c := range cases { + out, err := MergeMergePatches([]byte(c.p1), []byte(c.p2)) + + if err != nil { + panic(err) + } + + if !compareJSON(c.exp, string(out)) { + t.Logf("Error while trying to demonstrate: %v", c.demonstrates) + t.Logf("Got %v", string(out)) + t.Logf("Expected %v", c.exp) + t.Fatalf("Merged merge patch is incorrect") + } + } +} diff --git a/vendor/github.com/evanphx/json-patch/patch.go b/vendor/github.com/evanphx/json-patch/patch.go new file mode 100644 index 000000000..de70dbca7 --- /dev/null +++ b/vendor/github.com/evanphx/json-patch/patch.go @@ -0,0 +1,643 @@ +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 + 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 + } + + 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 *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 < 0 { + idx *= -1 + + if idx > len(ary) { + return fmt.Errorf("Unable to access invalid index: %d", idx) + } + idx = len(ary) - idx + } + + 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 remove invalid index: %d", idx) + } + + 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) + } + + val, ok := con.get(key) + if val == nil || 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) + } + + 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) +} diff --git a/vendor/github.com/evanphx/json-patch/patch_test.go b/vendor/github.com/evanphx/json-patch/patch_test.go new file mode 100644 index 000000000..40ff5af3f --- /dev/null +++ b/vendor/github.com/evanphx/json-patch/patch_test.go @@ -0,0 +1,340 @@ +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 objA, objB map[string]interface{} + json.Unmarshal([]byte(a), &objA) + json.Unmarshal([]byte(b), &objB) + + // fmt.Printf("Comparing %#v\nagainst %#v\n", objA, objB) + return reflect.DeepEqual(objA, objB) +} + +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" ] }`, + }, + { + `{ "foo": [ "bar", "baz" ] }`, + `[ + { "op": "add", "path": "/foo/-1", "value": "qux" } + ]`, + `{ "foo": [ "bar", "baz", "qux" ] }`, + }, + { + `{ "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 } }`, + }, + { + `{ "foo": "bar" }`, + `[ { "op": "add", "path": "/baz", "value": null } ]`, + `{ "baz": null, "foo": "bar" }`, + }, + { + `{ "foo": ["bar"]}`, + `[ { "op": "replace", "path": "/foo/0", "value": "baz"}]`, + `{ "foo": ["baz"]}`, + }, + { + `{ "foo": ["bar","baz"]}`, + `[ { "op": "replace", "path": "/foo/0", "value": "bum"}]`, + `{ "foo": ["bum","baz"]}`, + }, + { + `{ "foo": ["bar","qux","baz"]}`, + `[ { "op": "replace", "path": "/foo/1", "value": "bum"}]`, + `{ "foo": ["bar", "bum","baz"]}`, + }, + { + `[ {"foo": ["bar","qux","baz"]}]`, + `[ { "op": "replace", "path": "/0/foo/0", "value": "bum"}]`, + `[ {"foo": ["bum","qux","baz"]}]`, + }, + { + `[ {"foo": ["bar","qux","baz"], "bar": ["qux","baz"]}]`, + `[ { "op": "copy", "from": "/0/foo/0", "path": "/0/bar/0"}]`, + `[ {"foo": ["bar","qux","baz"], "bar": ["bar", "baz"]}]`, + }, + { + `[ {"foo": ["bar","qux","baz"], "bar": ["qux","baz"]}]`, + `[ { "op": "copy", "from": "/0/foo/0", "path": "/0/bar"}]`, + `[ {"foo": ["bar","qux","baz"], "bar": ["bar", "qux", "baz"]}]`, + }, + { + `[ { "foo": {"bar": ["qux","baz"]}, "baz": {"qux": "bum"}}]`, + `[ { "op": "copy", "from": "/0/foo/bar", "path": "/0/baz/bar"}]`, + `[ { "baz": {"bar": ["qux","baz"], "qux":"bum"}, "foo": {"bar": ["qux","baz"]}}]`, + }, +} + +type BadCase struct { + doc, patch string +} + +var MutationTestCases = []BadCase{ + { + `{ "foo": "bar", "qux": { "baz": 1, "bar": null } }`, + `[ { "op": "remove", "path": "/qux/bar" } ]`, + }, + { + `{ "foo": "bar", "qux": { "baz": 1, "bar": null } }`, + `[ { "op": "replace", "path": "/qux/baz", "value": null } ]`, + }, +} + +var BadCases = []BadCase{ + { + `{ "foo": "bar" }`, + `[ { "op": "add", "path": "/baz/bat", "value": "qux" } ]`, + }, + { + `{ "a": { "b": { "d": 1 } } }`, + `[ { "op": "remove", "path": "/a/b/c" } ]`, + }, + { + `{ "a": { "b": { "d": 1 } } }`, + `[ { "op": "move", "from": "/a/b/c", "path": "/a/b/e" } ]`, + }, + { + `{ "a": { "b": [1] } }`, + `[ { "op": "remove", "path": "/a/b/1" } ]`, + }, + { + `{ "a": { "b": [1] } }`, + `[ { "op": "move", "from": "/a/b/1", "path": "/a/b/2" } ]`, + }, + { + `{ "foo": "bar" }`, + `[ { "op": "add", "pathz": "/baz", "value": "qux" } ]`, + }, + { + `{ "foo": "bar" }`, + `[ { "op": "add", "path": "", "value": "qux" } ]`, + }, + { + `{ "foo": ["bar","baz"]}`, + `[ { "op": "replace", "path": "/foo/2", "value": "bum"}]`, + }, + { + `{ "foo": ["bar","baz"]}`, + `[ { "op": "add", "path": "/foo/-4", "value": "bum"}]`, + }, + + { + `{ "name":{ "foo": "bat", "qux": "bum"}}`, + `[ { "op": "replace", "path": "/foo/bar", "value":"baz"}]`, + }, +} + +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", + }, + { + `{ "baz": "qux" }`, + `[ { "op": "test", "path": "/foo", "value": 42 } ]`, + false, + "/foo", + }, + { + `{ "baz": "qux" }`, + `[ { "op": "test", "path": "/foo", "value": null } ]`, + true, + "", + }, + { + `{ "baz/foo": "qux" }`, + `[ { "op": "test", "path": "/baz~1foo", "value": "qux"} ]`, + true, + "", + }, +} + +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) + } + } + } +}