Add basic JSON DPM package (ROUGH)
This commit is contained in:
parent
fd0ce5ea71
commit
8991586a0b
5 changed files with 1887 additions and 0 deletions
247
util/json/json.go
Executable file
247
util/json/json.go
Executable file
|
@ -0,0 +1,247 @@
|
||||||
|
// Copyright © 2016 Abcum Ltd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package json
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Operation struct {
|
||||||
|
Op string `json:"op"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Value interface{} `json:"value,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ByPath []Operation
|
||||||
|
|
||||||
|
func (a ByPath) Len() int { return len(a) }
|
||||||
|
func (a ByPath) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||||
|
func (a ByPath) Less(i, j int) bool { return a[i].Path < a[j].Path }
|
||||||
|
|
||||||
|
func NewPatch(op, path string, value interface{}) Operation {
|
||||||
|
return Operation{Op: op, Path: path, Value: value}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatePatch creates a patch as specified in http://jsonpatch.com/
|
||||||
|
//
|
||||||
|
// 'a' is original, 'b' is the modified document. Both are to be given as json encoded content.
|
||||||
|
// The function will return an array of Operations
|
||||||
|
//
|
||||||
|
// An error will be returned if any of the two documents are invalid.
|
||||||
|
func Diff(a, b interface{}) ([]Operation, error) {
|
||||||
|
|
||||||
|
va, oka := a.([]byte)
|
||||||
|
vb, okb := b.([]byte)
|
||||||
|
|
||||||
|
if oka && okb {
|
||||||
|
ia := map[string]interface{}{}
|
||||||
|
ib := map[string]interface{}{}
|
||||||
|
err := json.Unmarshal(va, &ia)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(vb, &ib)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return diff(ia, ib, "", []Operation{})
|
||||||
|
}
|
||||||
|
|
||||||
|
ma, oka := a.(map[string]interface{})
|
||||||
|
mb, okb := b.(map[string]interface{})
|
||||||
|
|
||||||
|
if oka && okb {
|
||||||
|
return diff(ma, mb, "", []Operation{})
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("Invalid input format")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true if the values matches (must be json types)
|
||||||
|
// The types of the values must match, otherwise it will always return false
|
||||||
|
// If two map[string]interface{} are given, all elements must match.
|
||||||
|
func matchesValue(av, bv interface{}) bool {
|
||||||
|
if reflect.TypeOf(av) != reflect.TypeOf(bv) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch at := av.(type) {
|
||||||
|
case string:
|
||||||
|
bt := bv.(string)
|
||||||
|
if bt == at {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
case float64:
|
||||||
|
bt := bv.(float64)
|
||||||
|
if bt == at {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
case bool:
|
||||||
|
bt := bv.(bool)
|
||||||
|
if bt == at {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
case map[string]interface{}:
|
||||||
|
bt := bv.(map[string]interface{})
|
||||||
|
for key := range at {
|
||||||
|
if !matchesValue(at[key], bt[key]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for key := range bt {
|
||||||
|
if !matchesValue(at[key], bt[key]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
case []interface{}:
|
||||||
|
bt := bv.([]interface{})
|
||||||
|
if len(bt) != len(at) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for key := range at {
|
||||||
|
if !matchesValue(at[key], bt[key]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for key := range bt {
|
||||||
|
if !matchesValue(at[key], bt[key]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func makePath(path string, newPart interface{}) string {
|
||||||
|
if path == "" {
|
||||||
|
return fmt.Sprintf("/%v", newPart)
|
||||||
|
} else {
|
||||||
|
if strings.HasSuffix(path, "/") {
|
||||||
|
path = path + fmt.Sprintf("%v", newPart)
|
||||||
|
} else {
|
||||||
|
path = path + fmt.Sprintf("/%v", newPart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
// diff returns the (recursive) difference between a and b as an array of Operations.
|
||||||
|
func diff(a, b map[string]interface{}, path string, patch []Operation) ([]Operation, error) {
|
||||||
|
for key, bv := range b {
|
||||||
|
p := makePath(path, key)
|
||||||
|
av, ok := a[key]
|
||||||
|
// value was added
|
||||||
|
if !ok {
|
||||||
|
patch = append(patch, NewPatch("add", p, bv))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// If types have changed, replace completely
|
||||||
|
if reflect.TypeOf(av) != reflect.TypeOf(bv) {
|
||||||
|
patch = append(patch, NewPatch("replace", p, bv))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Types are the same, compare values
|
||||||
|
var err error
|
||||||
|
patch, err = handleValues(av, bv, p, patch)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Now add all deleted values as nil
|
||||||
|
for key := range a {
|
||||||
|
_, found := b[key]
|
||||||
|
if !found {
|
||||||
|
p := makePath(path, key)
|
||||||
|
|
||||||
|
patch = append(patch, NewPatch("remove", p, nil))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return patch, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleValues(av, bv interface{}, p string, patch []Operation) ([]Operation, error) {
|
||||||
|
var err error
|
||||||
|
switch at := av.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
bt := bv.(map[string]interface{})
|
||||||
|
patch, err = diff(at, bt, p, patch)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case string, float64, bool:
|
||||||
|
if !matchesValue(av, bv) {
|
||||||
|
patch = append(patch, NewPatch("replace", p, bv))
|
||||||
|
}
|
||||||
|
case []interface{}:
|
||||||
|
bt := bv.([]interface{})
|
||||||
|
if len(at) != len(bt) {
|
||||||
|
// arrays are not the same
|
||||||
|
patch = append(patch, compareArray(at, bt, p)...)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
for i, _ := range bt {
|
||||||
|
patch, err = handleValues(at[i], bt[i], makePath(p, i), patch)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case nil:
|
||||||
|
switch bv.(type) {
|
||||||
|
case nil:
|
||||||
|
// Both nil, fine.
|
||||||
|
default:
|
||||||
|
patch = append(patch, NewPatch("add", p, bv))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("Unknown type:%T ", av))
|
||||||
|
}
|
||||||
|
return patch, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareArray(av, bv []interface{}, p string) []Operation {
|
||||||
|
retval := []Operation{}
|
||||||
|
// var err error
|
||||||
|
for i, v := range av {
|
||||||
|
found := false
|
||||||
|
for _, v2 := range bv {
|
||||||
|
if reflect.DeepEqual(v, v2) {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
retval = append(retval, NewPatch("remove", makePath(p, i), nil))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, v := range bv {
|
||||||
|
found := false
|
||||||
|
for _, v2 := range av {
|
||||||
|
if reflect.DeepEqual(v, v2) {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
retval = append(retval, NewPatch("add", makePath(p, i), v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return retval
|
||||||
|
}
|
321
util/json/merge.go
Executable file
321
util/json/merge.go
Executable file
|
@ -0,0 +1,321 @@
|
||||||
|
// Copyright © 2016 Abcum Ltd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package json
|
||||||
|
|
||||||
|
/*import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func merge(cur, patch *lazyNode) *lazyNode {
|
||||||
|
curDoc, err := cur.intoDoc()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
pruneNulls(patch)
|
||||||
|
return patch
|
||||||
|
}
|
||||||
|
|
||||||
|
patchDoc, err := patch.intoDoc()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return patch
|
||||||
|
}
|
||||||
|
|
||||||
|
mergeDocs(curDoc, patchDoc)
|
||||||
|
|
||||||
|
return cur
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeDocs(doc, patch *partialDoc) {
|
||||||
|
for k, v := range *patch {
|
||||||
|
k := decodePatchKey(k)
|
||||||
|
if v == nil {
|
||||||
|
delete(*doc, k)
|
||||||
|
} else {
|
||||||
|
cur, ok := (*doc)[k]
|
||||||
|
|
||||||
|
if !ok || cur == nil {
|
||||||
|
pruneNulls(v)
|
||||||
|
(*doc)[k] = v
|
||||||
|
} else {
|
||||||
|
(*doc)[k] = merge(cur, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pruneNulls(n *lazyNode) {
|
||||||
|
sub, err := n.intoDoc()
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
pruneDocNulls(sub)
|
||||||
|
} else {
|
||||||
|
ary, err := n.intoAry()
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
pruneAryNulls(ary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pruneDocNulls(doc *partialDoc) *partialDoc {
|
||||||
|
for k, v := range *doc {
|
||||||
|
if v == nil {
|
||||||
|
delete(*doc, k)
|
||||||
|
} else {
|
||||||
|
pruneNulls(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc
|
||||||
|
}
|
||||||
|
|
||||||
|
func pruneAryNulls(ary *partialArray) *partialArray {
|
||||||
|
newAry := []*lazyNode{}
|
||||||
|
|
||||||
|
for _, v := range *ary {
|
||||||
|
if v != nil {
|
||||||
|
pruneNulls(v)
|
||||||
|
newAry = append(newAry, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*ary = newAry
|
||||||
|
|
||||||
|
return ary
|
||||||
|
}
|
||||||
|
|
||||||
|
var errBadJSONDoc = fmt.Errorf("Invalid JSON Document")
|
||||||
|
var errBadJSONPatch = fmt.Errorf("Invalid JSON Patch")
|
||||||
|
|
||||||
|
// MergePatch merges the patchData into the docData.
|
||||||
|
func MergePatch(docData, patchData []byte) ([]byte, error) {
|
||||||
|
doc := &partialDoc{}
|
||||||
|
|
||||||
|
docErr := json.Unmarshal(docData, doc)
|
||||||
|
|
||||||
|
patch := &partialDoc{}
|
||||||
|
|
||||||
|
patchErr := json.Unmarshal(patchData, patch)
|
||||||
|
|
||||||
|
if _, ok := docErr.(*json.SyntaxError); ok {
|
||||||
|
return nil, errBadJSONDoc
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := patchErr.(*json.SyntaxError); ok {
|
||||||
|
return nil, errBadJSONPatch
|
||||||
|
}
|
||||||
|
|
||||||
|
if docErr == nil && *doc == nil {
|
||||||
|
return nil, errBadJSONDoc
|
||||||
|
}
|
||||||
|
|
||||||
|
if patchErr == nil && *patch == nil {
|
||||||
|
return nil, errBadJSONPatch
|
||||||
|
}
|
||||||
|
|
||||||
|
if docErr != nil || patchErr != nil {
|
||||||
|
// Not an error, just not a doc, so we turn straight into the patch
|
||||||
|
if patchErr == nil {
|
||||||
|
doc = pruneDocNulls(patch)
|
||||||
|
} else {
|
||||||
|
patchAry := &partialArray{}
|
||||||
|
patchErr = json.Unmarshal(patchData, patchAry)
|
||||||
|
|
||||||
|
if patchErr != nil {
|
||||||
|
return nil, errBadJSONPatch
|
||||||
|
}
|
||||||
|
|
||||||
|
pruneAryNulls(patchAry)
|
||||||
|
|
||||||
|
out, patchErr := json.Marshal(patchAry)
|
||||||
|
|
||||||
|
if patchErr != nil {
|
||||||
|
return nil, errBadJSONPatch
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mergeDocs(doc, patch)
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateMergePatch creates a merge patch as specified in http://tools.ietf.org/html/draft-ietf-appsawg-json-merge-patch-07
|
||||||
|
//
|
||||||
|
// 'a' is original, 'b' is the modified document. Both are to be given as json encoded content.
|
||||||
|
// The function will return a mergeable json document with differences from a to b.
|
||||||
|
//
|
||||||
|
// An error will be returned if any of the two documents are invalid.
|
||||||
|
func CreateMergePatch(a, b []byte) ([]byte, error) {
|
||||||
|
aI := map[string]interface{}{}
|
||||||
|
bI := map[string]interface{}{}
|
||||||
|
err := json.Unmarshal(a, &aI)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errBadJSONDoc
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(b, &bI)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errBadJSONDoc
|
||||||
|
}
|
||||||
|
dest, err := getDiff(aI, bI)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return json.Marshal(dest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true if the array matches (must be json types).
|
||||||
|
// As is idiomatic for go, an empty array is not the same as a nil array.
|
||||||
|
func matchesArray(a, b []interface{}) bool {
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (a == nil && b != nil) || (a != nil && b == nil) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := range a {
|
||||||
|
if !matchesValue(a[i], b[i]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true if the values matches (must be json types)
|
||||||
|
// The types of the values must match, otherwise it will always return false
|
||||||
|
// If two map[string]interface{} are given, all elements must match.
|
||||||
|
func matchesValue(av, bv interface{}) bool {
|
||||||
|
if reflect.TypeOf(av) != reflect.TypeOf(bv) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch at := av.(type) {
|
||||||
|
case string:
|
||||||
|
bt := bv.(string)
|
||||||
|
if bt == at {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
case float64:
|
||||||
|
bt := bv.(float64)
|
||||||
|
if bt == at {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
case bool:
|
||||||
|
bt := bv.(bool)
|
||||||
|
if bt == at {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
case map[string]interface{}:
|
||||||
|
bt := bv.(map[string]interface{})
|
||||||
|
for key := range at {
|
||||||
|
if !matchesValue(at[key], bt[key]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for key := range bt {
|
||||||
|
if !matchesValue(at[key], bt[key]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
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 {
|
||||||
|
escapedKey := encodePatchKey(key)
|
||||||
|
av, ok := a[key]
|
||||||
|
// value was added
|
||||||
|
if !ok {
|
||||||
|
into[escapedKey] = bv
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// If types have changed, replace completely
|
||||||
|
if reflect.TypeOf(av) != reflect.TypeOf(bv) {
|
||||||
|
into[escapedKey] = 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[escapedKey] = dst
|
||||||
|
}
|
||||||
|
case string, float64, bool:
|
||||||
|
if !matchesValue(av, bv) {
|
||||||
|
into[escapedKey] = bv
|
||||||
|
}
|
||||||
|
case []interface{}:
|
||||||
|
bt := bv.([]interface{})
|
||||||
|
if !matchesArray(at, bt) {
|
||||||
|
into[escapedKey] = bv
|
||||||
|
}
|
||||||
|
case nil:
|
||||||
|
switch bv.(type) {
|
||||||
|
case nil:
|
||||||
|
// Both nil, fine.
|
||||||
|
default:
|
||||||
|
into[escapedKey] = 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
rfc6901Encoder = strings.NewReplacer("~", "~0", "/", "~1")
|
||||||
|
rfc6901Decoder = strings.NewReplacer("~1", "/", "~0", "~")
|
||||||
|
)
|
||||||
|
|
||||||
|
func decodePatchKey(k string) string {
|
||||||
|
return rfc6901Decoder.Replace(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodePatchKey(k string) string {
|
||||||
|
return rfc6901Encoder.Replace(k)
|
||||||
|
}
|
||||||
|
*/
|
407
util/json/merge_test.go
Executable file
407
util/json/merge_test.go
Executable file
|
@ -0,0 +1,407 @@
|
||||||
|
// Copyright © 2016 Abcum Ltd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package json
|
||||||
|
|
||||||
|
// 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 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 TestMergeReplaceKeyRequiringEscape(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~1escaped": 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 TestMergePatchReplaceKeyRequiringEscaping(t *testing.T) {
|
||||||
|
// doc := `{ "obj": { "title/escaped": "hello" } }`
|
||||||
|
// pat := `{ "obj": { "title~1escaped": "goodbye" } }`
|
||||||
|
// exp := `{ "obj": { "title/escaped": "goodbye" } }`
|
||||||
|
|
||||||
|
// res := mergePatch(doc, pat)
|
||||||
|
|
||||||
|
// if !compareJSON(exp, res) {
|
||||||
|
// t.Fatalf("Key was not replaced")
|
||||||
|
// }
|
||||||
|
// }
|
601
util/json/patch.go
Executable file
601
util/json/patch.go
Executable file
|
@ -0,0 +1,601 @@
|
||||||
|
// Copyright © 2016 Abcum Ltd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package json
|
||||||
|
|
||||||
|
/*import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
eRaw = iota
|
||||||
|
eDoc
|
||||||
|
eAry
|
||||||
|
)
|
||||||
|
|
||||||
|
const LeftBrace byte = 91 // []byte("[")
|
||||||
|
|
||||||
|
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, "/")
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("Testing value %s failed", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if val.equal(op.value()) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("Testing value %s failed", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal indicates if 2 JSON documents have the same structural equality.
|
||||||
|
func Equal(a, b []byte) bool {
|
||||||
|
ra := make(json.RawMessage, len(a))
|
||||||
|
copy(ra, a)
|
||||||
|
la := newLazyNode(&ra)
|
||||||
|
|
||||||
|
rb := make(json.RawMessage, len(b))
|
||||||
|
copy(rb, b)
|
||||||
|
lb := newLazyNode(&rb)
|
||||||
|
|
||||||
|
return la.equal(lb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodePatch decodes the passed JSON document as an RFC 6902 patch.
|
||||||
|
func DecodePatch(buf []byte) (Patch, error) {
|
||||||
|
var p Patch
|
||||||
|
|
||||||
|
err := json.Unmarshal(buf, &p)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply mutates a JSON document according to the patch, and returns the new
|
||||||
|
// document.
|
||||||
|
func (p Patch) Apply(doc []byte) ([]byte, error) {
|
||||||
|
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] == LeftBrace {
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
*/
|
311
util/json/patch_test.go
Executable file
311
util/json/patch_test.go
Executable file
|
@ -0,0 +1,311 @@
|
||||||
|
// Copyright © 2016 Abcum Ltd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package json
|
||||||
|
|
||||||
|
// import (
|
||||||
|
// "bytes"
|
||||||
|
// "encoding/json"
|
||||||
|
// "fmt"
|
||||||
|
// "reflect"
|
||||||
|
// "testing"
|
||||||
|
// )
|
||||||
|
|
||||||
|
// func reformatJSON(j string) string {
|
||||||
|
// buf := new(bytes.Buffer)
|
||||||
|
|
||||||
|
// json.Indent(buf, []byte(j), "", " ")
|
||||||
|
|
||||||
|
// return buf.String()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func compareJSON(a, b string) bool {
|
||||||
|
// // return Equal([]byte(a), []byte(b))
|
||||||
|
|
||||||
|
// var obj_a, obj_b map[string]interface{}
|
||||||
|
// json.Unmarshal([]byte(a), &obj_a)
|
||||||
|
// json.Unmarshal([]byte(b), &obj_b)
|
||||||
|
|
||||||
|
// // fmt.Printf("Comparing %#v\nagainst %#v\n", obj_a, obj_b)
|
||||||
|
// return reflect.DeepEqual(obj_a, obj_b)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func applyPatch(doc, patch string) (string, error) {
|
||||||
|
// obj, err := DecodePatch([]byte(patch))
|
||||||
|
|
||||||
|
// if err != nil {
|
||||||
|
// panic(err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// out, err := obj.Apply([]byte(doc))
|
||||||
|
|
||||||
|
// if err != nil {
|
||||||
|
// return "", err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return string(out), nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// type Case struct {
|
||||||
|
// doc, patch, result string
|
||||||
|
// }
|
||||||
|
|
||||||
|
// var Cases = []Case{
|
||||||
|
// {
|
||||||
|
// `{ "foo": "bar"}`,
|
||||||
|
// `[
|
||||||
|
// { "op": "add", "path": "/baz", "value": "qux" }
|
||||||
|
// ]`,
|
||||||
|
// `{
|
||||||
|
// "baz": "qux",
|
||||||
|
// "foo": "bar"
|
||||||
|
// }`,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// `{ "foo": [ "bar", "baz" ] }`,
|
||||||
|
// `[
|
||||||
|
// { "op": "add", "path": "/foo/1", "value": "qux" }
|
||||||
|
// ]`,
|
||||||
|
// `{ "foo": [ "bar", "qux", "baz" ] }`,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// `{ "baz": "qux", "foo": "bar" }`,
|
||||||
|
// `[ { "op": "remove", "path": "/baz" } ]`,
|
||||||
|
// `{ "foo": "bar" }`,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// `{ "foo": [ "bar", "qux", "baz" ] }`,
|
||||||
|
// `[ { "op": "remove", "path": "/foo/1" } ]`,
|
||||||
|
// `{ "foo": [ "bar", "baz" ] }`,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// `{ "baz": "qux", "foo": "bar" }`,
|
||||||
|
// `[ { "op": "replace", "path": "/baz", "value": "boo" } ]`,
|
||||||
|
// `{ "baz": "boo", "foo": "bar" }`,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// `{
|
||||||
|
// "foo": {
|
||||||
|
// "bar": "baz",
|
||||||
|
// "waldo": "fred"
|
||||||
|
// },
|
||||||
|
// "qux": {
|
||||||
|
// "corge": "grault"
|
||||||
|
// }
|
||||||
|
// }`,
|
||||||
|
// `[ { "op": "move", "from": "/foo/waldo", "path": "/qux/thud" } ]`,
|
||||||
|
// `{
|
||||||
|
// "foo": {
|
||||||
|
// "bar": "baz"
|
||||||
|
// },
|
||||||
|
// "qux": {
|
||||||
|
// "corge": "grault",
|
||||||
|
// "thud": "fred"
|
||||||
|
// }
|
||||||
|
// }`,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// `{ "foo": [ "all", "grass", "cows", "eat" ] }`,
|
||||||
|
// `[ { "op": "move", "from": "/foo/1", "path": "/foo/3" } ]`,
|
||||||
|
// `{ "foo": [ "all", "cows", "eat", "grass" ] }`,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// `{ "foo": "bar" }`,
|
||||||
|
// `[ { "op": "add", "path": "/child", "value": { "grandchild": { } } } ]`,
|
||||||
|
// `{ "foo": "bar", "child": { "grandchild": { } } }`,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// `{ "foo": ["bar"] }`,
|
||||||
|
// `[ { "op": "add", "path": "/foo/-", "value": ["abc", "def"] } ]`,
|
||||||
|
// `{ "foo": ["bar", ["abc", "def"]] }`,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// `{ "foo": "bar", "qux": { "baz": 1, "bar": null } }`,
|
||||||
|
// `[ { "op": "remove", "path": "/qux/bar" } ]`,
|
||||||
|
// `{ "foo": "bar", "qux": { "baz": 1 } }`,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// `{ "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"]}]`,
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 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" } ]`,
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 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 a new issue