vendor github.com/evanphx/json-patch
This commit is contained in:
		
							parent
							
								
									ce6b8f6927
								
							
						
					
					
						commit
						b62a81c7ca
					
				|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -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. | ||||
|  | @ -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** | ||||
| 
 | ||||
| [](http://godoc.org/github.com/evanphx/json-patch) | ||||
| 
 | ||||
| [](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)` | ||||
| 
 | ||||
|  | @ -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) | ||||
| } | ||||
|  | @ -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) | ||||
| } | ||||
|  | @ -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 | ||||
| } | ||||
|  | @ -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") | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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) | ||||
| } | ||||
|  | @ -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) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
		Loading…
	
		Reference in New Issue