From 2bb8dd654533ad2df9146045938fa697841dcb59 Mon Sep 17 00:00:00 2001 From: Jeroen Soeters Date: Mon, 19 May 2025 13:06:44 -0500 Subject: [PATCH 01/12] Remove evahphx/jsonpatch dependency --- go.mod | 8 ++------ go.sum | 5 ----- jsonpatch_array_at_root_test.go | 20 +------------------- 3 files changed, 3 insertions(+), 30 deletions(-) diff --git a/go.mod b/go.mod index 5f84b39..2d556c8 100644 --- a/go.mod +++ b/go.mod @@ -2,14 +2,10 @@ module github.com/JeroenSoeters/jsonpatch go 1.23.4 -require ( - github.com/davecgh/go-spew v1.1.1 - github.com/evanphx/json-patch v0.5.2 - github.com/stretchr/testify v1.10.0 -) +require github.com/stretchr/testify v1.10.0 require ( - github.com/pkg/errors v0.9.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 0355b63..713a0b4 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,5 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= -github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= diff --git a/jsonpatch_array_at_root_test.go b/jsonpatch_array_at_root_test.go index fe855d8..b7a4be5 100644 --- a/jsonpatch_array_at_root_test.go +++ b/jsonpatch_array_at_root_test.go @@ -1,12 +1,8 @@ package jsonpatch import ( - "encoding/json" - "fmt" "testing" - "github.com/davecgh/go-spew/spew" - jsonpatch "github.com/evanphx/json-patch" "github.com/stretchr/testify/assert" ) @@ -39,22 +35,8 @@ func TestJSONPatchCreate(t *testing.T) { for name, tc := range cases { t.Run(name, func(t *testing.T) { - patch, err := CreatePatch([]byte(tc.a), []byte(tc.b), false) + _, err := CreatePatch([]byte(tc.a), []byte(tc.b), false) assert.NoError(t, err) - - patchBytes, err := json.Marshal(patch) - assert.NoError(t, err) - - fmt.Printf("%s\n", string(patchBytes)) - - p, err := jsonpatch.DecodePatch(patchBytes) - assert.NoError(t, err) - - res, err := p.Apply([]byte(tc.a)) - assert.NoError(t, err) - spew.Dump(res) - - assert.Equal(t, tc.b, string(res)) }) } } From 7d8b1a90b0c70c962738cb6f8d424c039a8840d4 Mon Sep 17 00:00:00 2001 From: Jeroen Soeters Date: Fri, 23 May 2025 14:08:20 -0500 Subject: [PATCH 02/12] First step --- jsonpatch.go | 158 +++++++++++++++-------- jsonpatch_simple_test.go | 12 +- jsonpatch_strategy_ensure_exists_test.go | 32 +++++ 3 files changed, 144 insertions(+), 58 deletions(-) create mode 100644 jsonpatch_strategy_ensure_exists_test.go diff --git a/jsonpatch.go b/jsonpatch.go index d5d4dd7..b9c2315 100644 --- a/jsonpatch.go +++ b/jsonpatch.go @@ -10,10 +10,18 @@ import ( var errBadJSONDoc = fmt.Errorf("Invalid JSON Document") +type MergeStrategy = string + +const ( + ExactMatch MergeStrategy = "exactMatch" + EnsureExists MergeStrategy = "ensureExists" + EnsureAbsent MergeStrategy = "ensureAbsent" +) + type JsonPatchOperation struct { - Operation string `json:"op"` - Path string `json:"path"` - Value interface{} `json:"value,omitempty"` + Operation string `json:"op"` + Path string `json:"path"` + Value any `json:"value,omitempty"` } func (j *JsonPatchOperation) Json() string { @@ -45,7 +53,7 @@ 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(operation, path string, value interface{}) JsonPatchOperation { +func NewPatch(operation, path string, value any) JsonPatchOperation { return JsonPatchOperation{Operation: operation, Path: path, Value: value} } @@ -57,8 +65,24 @@ func NewPatch(operation, path string, value interface{}) JsonPatchOperation { // // An error will be returned if any of the two documents are invalid. func CreatePatch(a, b []byte, ignoreArrayOrder bool) ([]JsonPatchOperation, error) { - var aI interface{} - var bI interface{} + var aI any + var bI any + + err := json.Unmarshal(a, &aI) + if err != nil { + return nil, errBadJSONDoc + } + err = json.Unmarshal(b, &bI) + if err != nil { + return nil, errBadJSONDoc + } + + return handleValues(aI, bI, "", []JsonPatchOperation{}, ExactMatch, ignoreArrayOrder) +} + +func CreatePatch_StrategyEnsureExists(a, b []byte) ([]JsonPatchOperation, error) { + var aI any + var bI any err := json.Unmarshal(a, &aI) if err != nil { @@ -69,14 +93,14 @@ func CreatePatch(a, b []byte, ignoreArrayOrder bool) ([]JsonPatchOperation, erro return nil, errBadJSONDoc } - return handleValues(aI, bI, "", []JsonPatchOperation{}, ignoreArrayOrder) + return handleValues(aI, bI, "", []JsonPatchOperation{}, EnsureExists, false) } // 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. +// If two map[string]any are given, all elements must match. // If ignoreArrayOrder is true and both values are arrays, they are compared as sets -func matchesValue(av, bv interface{}, ignoreArrayOrder bool) bool { +func matchesValue(av, bv any, ignoreArrayOrder bool) bool { if reflect.TypeOf(av) != reflect.TypeOf(bv) { return false } @@ -96,8 +120,8 @@ func matchesValue(av, bv interface{}, ignoreArrayOrder bool) bool { if bt == at { return true } - case map[string]interface{}: - bt := bv.(map[string]interface{}) + case map[string]any: + bt := bv.(map[string]any) for key := range at { if !matchesValue(at[key], bt[key], ignoreArrayOrder) { return false @@ -109,8 +133,8 @@ func matchesValue(av, bv interface{}, ignoreArrayOrder bool) bool { } } return true - case []interface{}: - bt := bv.([]interface{}) + case []any: + bt := bv.([]any) if len(bt) != len(at) { return false } @@ -178,7 +202,7 @@ func matchesValue(av, bv interface{}, ignoreArrayOrder bool) bool { var rfc6901Encoder = strings.NewReplacer("~", "~0", "/", "~1") -func makePath(path string, newPart interface{}) string { +func makePath(path string, newPart any) string { key := rfc6901Encoder.Replace(fmt.Sprintf("%v", newPart)) if path == "" { return "/" + key @@ -190,45 +214,75 @@ func makePath(path string, newPart interface{}) string { } // diff returns the (recursive) difference between a and b as an array of JsonPatchOperations. -func diff(a, b map[string]interface{}, path string, patch []JsonPatchOperation, ignoreArrayOrder bool) ([]JsonPatchOperation, 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 +func diff(a, b map[string]any, path string, patch []JsonPatchOperation, strategy MergeStrategy, ignoreArrayOrder bool) ([]JsonPatchOperation, error) { + switch strategy { + case ExactMatch: + for key, bv := range b { + p := makePath(path, key) + av, ok := a[key] + // If the key is not present in a, add it + 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, strategy, ignoreArrayOrder) + if err != nil { + return nil, err + } } - // Types are the same, compare values - var err error - patch, err = handleValues(av, bv, p, patch, ignoreArrayOrder) - 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)) + } } - } - // Now add all deleted values as nil - for key := range a { - _, found := b[key] - if !found { + return patch, nil + case EnsureExists: + for key, bv := range b { p := makePath(path, key) - - patch = append(patch, NewPatch("remove", p, nil)) + av, ok := a[key] + // If key is not present, add it + 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, strategy, ignoreArrayOrder) + if err != nil { + return nil, err + } } + // We don't generate remove operations in "ensure exists" mode + return patch, nil + case EnsureAbsent: + fmt.Println("EnsureAbsent strategy is not implemented yet") } - return patch, nil + + return nil, fmt.Errorf("Unknown strategy: %s", strategy) } -func handleValues(av, bv interface{}, p string, patch []JsonPatchOperation, ignoreArrayOrder bool) ([]JsonPatchOperation, error) { +func handleValues(av, bv any, p string, patch []JsonPatchOperation, strategy MergeStrategy, ignoreArrayOrder bool) ([]JsonPatchOperation, error) { var err error switch at := av.(type) { - case map[string]interface{}: - bt := bv.(map[string]interface{}) - patch, err = diff(at, bt, p, patch, ignoreArrayOrder) + case map[string]any: + bt := bv.(map[string]any) + patch, err = diff(at, bt, p, patch, strategy, ignoreArrayOrder) if err != nil { return nil, err } @@ -236,8 +290,8 @@ func handleValues(av, bv interface{}, p string, patch []JsonPatchOperation, igno if !matchesValue(av, bv, ignoreArrayOrder) { patch = append(patch, NewPatch("replace", p, bv)) } - case []interface{}: - bt, ok := bv.([]interface{}) + case []any: + bt, ok := bv.([]any) if !ok { // array replaced by non-array patch = append(patch, NewPatch("replace", p, bv)) @@ -249,7 +303,7 @@ func handleValues(av, bv interface{}, p string, patch []JsonPatchOperation, igno // No patch needed! } else { for i := range bt { - patch, err = handleValues(at[i], bt[i], makePath(p, i), patch, ignoreArrayOrder) + patch, err = handleValues(at[i], bt[i], makePath(p, i), patch, strategy, ignoreArrayOrder) if err != nil { return nil, err } @@ -258,7 +312,7 @@ func handleValues(av, bv interface{}, p string, patch []JsonPatchOperation, igno case nil: switch bv.(type) { case nil: - // Both nil, fine. + // Both nil, fine. default: patch = append(patch, NewPatch("add", p, bv)) } @@ -269,7 +323,7 @@ func handleValues(av, bv interface{}, p string, patch []JsonPatchOperation, igno } // compareArray generates remove and add operations for `av` and `bv`. -func compareArray(av, bv []interface{}, p string, ignoreArrayOrder bool) []JsonPatchOperation { +func compareArray(av, bv []any, p string, ignoreArrayOrder bool) []JsonPatchOperation { retval := []JsonPatchOperation{} // If arrays have same elements in different order and we're ignoring order, return empty patch @@ -278,7 +332,7 @@ func compareArray(av, bv []interface{}, p string, ignoreArrayOrder bool) []JsonP } // Find elements that need to be removed - processArray(av, bv, func(i int, value interface{}) { + processArray(av, bv, func(i int, value any) { retval = append(retval, NewPatch("remove", makePath(p, i), nil)) }, ignoreArrayOrder) @@ -290,7 +344,7 @@ func compareArray(av, bv []interface{}, p string, ignoreArrayOrder bool) []JsonP // Find elements that need to be added. // NOTE we pass in `bv` then `av` so that processArray can find the missing elements. - processArray(bv, av, func(i int, value interface{}) { + processArray(bv, av, func(i int, value any) { retval = append(retval, NewPatch("add", makePath(p, i), value)) }, ignoreArrayOrder) @@ -299,7 +353,7 @@ func compareArray(av, bv []interface{}, p string, ignoreArrayOrder bool) []JsonP // processArray processes `av` and `bv` calling `applyOp` whenever a value is absent. // It keeps track of which indexes have already had `applyOp` called for and automatically skips them so you can process duplicate objects correctly. -func processArray(av, bv []interface{}, applyOp func(i int, value interface{}), ignoreArrayOrder bool) { +func processArray(av, bv []any, applyOp func(i int, value any), ignoreArrayOrder bool) { foundIndexes := make(map[int]struct{}, len(av)) reverseFoundIndexes := make(map[int]struct{}, len(av)) diff --git a/jsonpatch_simple_test.go b/jsonpatch_simple_test.go index 275ea97..97017fe 100644 --- a/jsonpatch_simple_test.go +++ b/jsonpatch_simple_test.go @@ -92,9 +92,9 @@ func TestVsEmpty(t *testing.T) { } func BenchmarkBigArrays(b *testing.B) { - var a1, a2 []interface{} - a1 = make([]interface{}, 100) - a2 = make([]interface{}, 101) + var a1, a2 []any + a1 = make([]any, 100) + a2 = make([]any, 101) for i := 0; i < 100; i++ { a1[i] = i @@ -106,9 +106,9 @@ func BenchmarkBigArrays(b *testing.B) { } func BenchmarkBigArrays2(b *testing.B) { - var a1, a2 []interface{} - a1 = make([]interface{}, 100) - a2 = make([]interface{}, 101) + var a1, a2 []any + a1 = make([]any, 100) + a2 = make([]any, 101) for i := 0; i < 100; i++ { a1[i] = i diff --git a/jsonpatch_strategy_ensure_exists_test.go b/jsonpatch_strategy_ensure_exists_test.go new file mode 100644 index 0000000..f293039 --- /dev/null +++ b/jsonpatch_strategy_ensure_exists_test.go @@ -0,0 +1,32 @@ +package jsonpatch + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +var simpleObj = `{"a":100, "b":20}` +var simpleObjModifyProp = `{"b":250}` +var simpleObjAddProp = `{"c":"hello"}` + +func TestCreatePatch_ModifyProperty_GeneratesReplaceOperation(t *testing.T) { + patch, err := CreatePatch_StrategyEnsureExists([]byte(simpleObj), []byte(simpleObjModifyProp)) + assert.NoError(t, err) + assert.Equal(t, 1, len(patch), "they should be equal") + change := patch[0] + assert.Equal(t, "replace", change.Operation, "they should be equal") + assert.Equal(t, "/b", change.Path, "they should be equal") + var expected float64 = 250 + assert.Equal(t, expected, change.Value, "they should be equal") +} + +func TestCreatePatch_AddProperty_GeneratesAddOperation(t *testing.T) { + patch, err := CreatePatch_StrategyEnsureExists([]byte(simpleObj), []byte(simpleObjAddProp)) + assert.NoError(t, err) + assert.Equal(t, 1, len(patch), "they should be equal") + change := patch[0] + assert.Equal(t, "add", change.Operation, "they should be equal") + assert.Equal(t, "/c", change.Path, "they should be equal") + assert.Equal(t, "hello", change.Value, "they should be equal") +} From d2ad1134e777bc00e7b3dfe638876f041836167d Mon Sep 17 00:00:00 2001 From: Jeroen Soeters Date: Fri, 23 May 2025 21:38:57 -0500 Subject: [PATCH 03/12] Introduce strategies. Arrays with primitives and value objects implemented for EnsureExistsStrategy --- jsonpatch.go | 242 ++++++++++++++++------- jsonpatch_simple_test.go | 4 +- jsonpatch_strategy_ensure_exists_test.go | 109 ++++++++++ 3 files changed, 277 insertions(+), 78 deletions(-) diff --git a/jsonpatch.go b/jsonpatch.go index b9c2315..abf2d14 100644 --- a/jsonpatch.go +++ b/jsonpatch.go @@ -10,13 +10,34 @@ import ( var errBadJSONDoc = fmt.Errorf("Invalid JSON Document") -type MergeStrategy = string +type Strategy interface { + isStrategy() +} -const ( - ExactMatch MergeStrategy = "exactMatch" - EnsureExists MergeStrategy = "ensureExists" - EnsureAbsent MergeStrategy = "ensureAbsent" -) +type ExactMatchStrategy struct { + ignoreArrayOrder bool +} + +func (ExactMatchStrategy) isStrategy() {} + +func NewExactMatchStrategy(ignoreArrayOrder bool) ExactMatchStrategy { + return ExactMatchStrategy{ignoreArrayOrder: ignoreArrayOrder} +} + +type IdentitySet struct { + path string + key string +} + +type EnsureExistsStrategy struct { + identitySets []IdentitySet +} + +func (EnsureExistsStrategy) isStrategy() {} + +type EnsureAbsentStrategy struct{} + +func (EnsureAbsentStrategy) isStrategy() {} type JsonPatchOperation struct { Operation string `json:"op"` @@ -77,7 +98,7 @@ func CreatePatch(a, b []byte, ignoreArrayOrder bool) ([]JsonPatchOperation, erro return nil, errBadJSONDoc } - return handleValues(aI, bI, "", []JsonPatchOperation{}, ExactMatch, ignoreArrayOrder) + return handleValues(aI, bI, "", []JsonPatchOperation{}, NewExactMatchStrategy(ignoreArrayOrder)) } func CreatePatch_StrategyEnsureExists(a, b []byte) ([]JsonPatchOperation, error) { @@ -93,7 +114,7 @@ func CreatePatch_StrategyEnsureExists(a, b []byte) ([]JsonPatchOperation, error) return nil, errBadJSONDoc } - return handleValues(aI, bI, "", []JsonPatchOperation{}, EnsureExists, false) + return handleValues(aI, bI, "", []JsonPatchOperation{}, EnsureExistsStrategy{}) } // Returns true if the values matches (must be json types) @@ -214,9 +235,9 @@ func makePath(path string, newPart any) string { } // diff returns the (recursive) difference between a and b as an array of JsonPatchOperations. -func diff(a, b map[string]any, path string, patch []JsonPatchOperation, strategy MergeStrategy, ignoreArrayOrder bool) ([]JsonPatchOperation, error) { - switch strategy { - case ExactMatch: +func diff(a, b map[string]any, path string, patch []JsonPatchOperation, strategy Strategy) ([]JsonPatchOperation, error) { + switch strategy.(type) { + case ExactMatchStrategy: for key, bv := range b { p := makePath(path, key) av, ok := a[key] @@ -232,7 +253,7 @@ func diff(a, b map[string]any, path string, patch []JsonPatchOperation, strategy } // Types are the same, compare values var err error - patch, err = handleValues(av, bv, p, patch, strategy, ignoreArrayOrder) + patch, err = handleValues(av, bv, p, patch, strategy) if err != nil { return nil, err } @@ -247,7 +268,7 @@ func diff(a, b map[string]any, path string, patch []JsonPatchOperation, strategy } } return patch, nil - case EnsureExists: + case EnsureExistsStrategy: for key, bv := range b { p := makePath(path, key) av, ok := a[key] @@ -263,26 +284,30 @@ func diff(a, b map[string]any, path string, patch []JsonPatchOperation, strategy } // Types are the same, compare values var err error - patch, err = handleValues(av, bv, p, patch, strategy, ignoreArrayOrder) + patch, err = handleValues(av, bv, p, patch, strategy) if err != nil { return nil, err } } // We don't generate remove operations in "ensure exists" mode return patch, nil - case EnsureAbsent: + case EnsureAbsentStrategy: fmt.Println("EnsureAbsent strategy is not implemented yet") } return nil, fmt.Errorf("Unknown strategy: %s", strategy) } -func handleValues(av, bv any, p string, patch []JsonPatchOperation, strategy MergeStrategy, ignoreArrayOrder bool) ([]JsonPatchOperation, error) { +func handleValues(av, bv any, p string, patch []JsonPatchOperation, strategy Strategy) ([]JsonPatchOperation, error) { var err error + ignoreArrayOrder := false + if s, ok := strategy.(ExactMatchStrategy); ok { + ignoreArrayOrder = s.ignoreArrayOrder + } switch at := av.(type) { case map[string]any: bt := bv.(map[string]any) - patch, err = diff(at, bt, p, patch, strategy, ignoreArrayOrder) + patch, err = diff(at, bt, p, patch, strategy) if err != nil { return nil, err } @@ -291,23 +316,37 @@ func handleValues(av, bv any, p string, patch []JsonPatchOperation, strategy Mer patch = append(patch, NewPatch("replace", p, bv)) } case []any: - bt, ok := bv.([]any) - if !ok { - // array replaced by non-array - patch = append(patch, NewPatch("replace", p, bv)) - } else if len(at) != len(bt) { - // arrays are not the same length - patch = append(patch, compareArray(at, bt, p, ignoreArrayOrder)...) - } else if ignoreArrayOrder && matchesValue(at, bt, true) { - // Arrays have the same elements, just in different order, and we're ignoring order - // No patch needed! - } else { - for i := range bt { - patch, err = handleValues(at[i], bt[i], makePath(p, i), patch, strategy, ignoreArrayOrder) - if err != nil { - return nil, err + switch strategy.(type) { + case ExactMatchStrategy: + bt, ok := bv.([]any) + if !ok { + // array replaced by non-array + patch = append(patch, NewPatch("replace", p, bv)) + } else if len(at) != len(bt) { + // arrays are not the same length + patch = append(patch, compareArray(at, bt, p, strategy)...) + } else if ignoreArrayOrder && matchesValue(at, bt, true) { + // Arrays have the same elements, just in different order, and we're ignoring order + // No patch needed! + } else { + for i := range bt { + patch, err = handleValues(at[i], bt[i], makePath(p, i), patch, strategy) + if err != nil { + return nil, err + } } } + case EnsureExistsStrategy: + bt, ok := bv.([]any) + if !ok { + // array replaced by non-array + patch = append(patch, NewPatch("replace", p, bv)) + } else { + // compare arrays + patch = append(patch, compareArray(at, bt, p, strategy)...) + } + case EnsureAbsentStrategy: + return nil, fmt.Errorf("EnsureAbsent strategy is not implemented for arrays") } case nil: switch bv.(type) { @@ -323,41 +362,108 @@ func handleValues(av, bv any, p string, patch []JsonPatchOperation, strategy Mer } // compareArray generates remove and add operations for `av` and `bv`. -func compareArray(av, bv []any, p string, ignoreArrayOrder bool) []JsonPatchOperation { +func compareArray(av, bv []any, p string, strategy Strategy) []JsonPatchOperation { retval := []JsonPatchOperation{} - // If arrays have same elements in different order and we're ignoring order, return empty patch - if ignoreArrayOrder && len(av) == len(bv) && matchesValue(av, bv, true) { - return retval - } - - // Find elements that need to be removed - processArray(av, bv, func(i int, value any) { - retval = append(retval, NewPatch("remove", makePath(p, i), nil)) - }, ignoreArrayOrder) - - reversed := make([]JsonPatchOperation, len(retval)) - for i := 0; i < len(retval); i++ { - reversed[len(retval)-1-i] = retval[i] + switch s := strategy.(type) { + case ExactMatchStrategy: + // If arrays have same elements in different order and we're ignoring order, return empty patch + if s.ignoreArrayOrder && len(av) == len(bv) && matchesValue(av, bv, true) { + return retval + } + // Find elements that need to be removed + processArray(av, bv, func(i int, value any) { + retval = append(retval, NewPatch("remove", makePath(p, i), nil)) + }, strategy) + + reversed := make([]JsonPatchOperation, len(retval)) + for i := 0; i < len(retval); i++ { + reversed[len(retval)-1-i] = retval[i] + } + retval = reversed + + // Find elements that need to be added. + // NOTE we pass in `bv` then `av` so that processArray can find the missing elements. + processArray(bv, av, func(i int, value any) { + retval = append(retval, NewPatch("add", makePath(p, i), value)) + }, strategy) + case EnsureExistsStrategy: + processArray(bv, av, func(i int, value any) { + retval = append(retval, NewPatch("add", makePath(p, i), value)) + }, strategy) + case EnsureAbsentStrategy: + return nil } - retval = reversed - - // Find elements that need to be added. - // NOTE we pass in `bv` then `av` so that processArray can find the missing elements. - processArray(bv, av, func(i int, value any) { - retval = append(retval, NewPatch("add", makePath(p, i), value)) - }, ignoreArrayOrder) return retval } // processArray processes `av` and `bv` calling `applyOp` whenever a value is absent. // It keeps track of which indexes have already had `applyOp` called for and automatically skips them so you can process duplicate objects correctly. -func processArray(av, bv []any, applyOp func(i int, value any), ignoreArrayOrder bool) { +func processArray(av, bv []any, applyOp func(i int, value any), strategy Strategy) { foundIndexes := make(map[int]struct{}, len(av)) reverseFoundIndexes := make(map[int]struct{}, len(av)) - if ignoreArrayOrder { + switch s := strategy.(type) { + case ExactMatchStrategy: + if s.ignoreArrayOrder { + // Create a map of elements and their counts in bv + bvCounts := make(map[string]int) + bvSeen := make(map[string]int) // Track how many we've seen during processing + + for _, v := range bv { + jsonBytes, err := json.Marshal(v) + if err != nil { + continue // Skip if we can't marshal + } + jsonStr := string(jsonBytes) + bvCounts[jsonStr]++ + } + + // Check each element in av + for i, v := range av { + jsonBytes, err := json.Marshal(v) + if err != nil { + applyOp(i, v) // If we can't marshal, treat it as not found + continue + } + + jsonStr := string(jsonBytes) + // If element exists in bv and we haven't seen all of them yet + if bvCounts[jsonStr] > bvSeen[jsonStr] { + foundIndexes[i] = struct{}{} + bvSeen[jsonStr]++ + } + } + + // Apply op for all elements in av that weren't found + for i, v := range av { + if _, ok := foundIndexes[i]; !ok { + applyOp(i, v) + } + } + } else { + // Original implementation for when order matters + for i, v := range av { + for i2, v2 := range bv { + if _, ok := reverseFoundIndexes[i2]; ok { + // We already found this index. + continue + } + if reflect.DeepEqual(v, v2) { + // Mark this index as found since it matches exactly. + foundIndexes[i] = struct{}{} + reverseFoundIndexes[i2] = struct{}{} + break + } + } + if _, ok := foundIndexes[i]; !ok { + applyOp(i, v) + } + } + } + case EnsureExistsStrategy: + offset := len(bv) // Create a map of elements and their counts in bv bvCounts := make(map[string]int) bvSeen := make(map[string]int) // Track how many we've seen during processing @@ -375,7 +481,7 @@ func processArray(av, bv []any, applyOp func(i int, value any), ignoreArrayOrder for i, v := range av { jsonBytes, err := json.Marshal(v) if err != nil { - applyOp(i, v) // If we can't marshal, treat it as not found + applyOp(i+offset, v) // If we can't marshal, treat it as not found continue } @@ -390,27 +496,11 @@ func processArray(av, bv []any, applyOp func(i int, value any), ignoreArrayOrder // Apply op for all elements in av that weren't found for i, v := range av { if _, ok := foundIndexes[i]; !ok { - applyOp(i, v) - } - } - } else { - // Original implementation for when order matters - for i, v := range av { - for i2, v2 := range bv { - if _, ok := reverseFoundIndexes[i2]; ok { - // We already found this index. - continue - } - if reflect.DeepEqual(v, v2) { - // Mark this index as found since it matches exactly. - foundIndexes[i] = struct{}{} - reverseFoundIndexes[i2] = struct{}{} - break - } - } - if _, ok := foundIndexes[i]; !ok { - applyOp(i, v) + applyOp(i+offset, v) } } + return + case EnsureAbsentStrategy: + return } } diff --git a/jsonpatch_simple_test.go b/jsonpatch_simple_test.go index 97017fe..47e9cdd 100644 --- a/jsonpatch_simple_test.go +++ b/jsonpatch_simple_test.go @@ -101,7 +101,7 @@ func BenchmarkBigArrays(b *testing.B) { a2[i+1] = i } for i := 0; i < b.N; i++ { - compareArray(a1, a2, "/", false) + compareArray(a1, a2, "/", NewExactMatchStrategy(false)) } } @@ -115,6 +115,6 @@ func BenchmarkBigArrays2(b *testing.B) { a2[i] = i } for i := 0; i < b.N; i++ { - compareArray(a1, a2, "/", false) + compareArray(a1, a2, "/", NewExactMatchStrategy(false)) } } diff --git a/jsonpatch_strategy_ensure_exists_test.go b/jsonpatch_strategy_ensure_exists_test.go index f293039..2109e61 100644 --- a/jsonpatch_strategy_ensure_exists_test.go +++ b/jsonpatch_strategy_ensure_exists_test.go @@ -9,6 +9,22 @@ import ( var simpleObj = `{"a":100, "b":20}` var simpleObjModifyProp = `{"b":250}` var simpleObjAddProp = `{"c":"hello"}` +var simpleObjEmtpyPrmitiveArray = `{"a":100, "b":[]}` +var simpleObjSingletonPrimitiveArray = `{"a":100, "b":[1]}` +var simpleObjMultipleItemPrimitiveArray = `{"a":100, "b":[1,2]}` +var simpleObjAddPrimitiveArrayItem = `{"b":[3]}` +var simpleObjAddDuplicateArrayItem = `{"b":[2]}` +var simpleObjSingletonObjectArray = `{"a":100, "b":[{"c":1}]}` +var simpleObjAddObjectArrayItem = `{"b":[{"c":2}]}` +var simpleObjKeyValueArray = `{"a":100, "b":[{"c":1},{"d":2}]}` +var simpleObjAddKeyValueArrayItem = `{"b":[{"e":3}]}` +var simpleObjModifyKeyValueArrayItem = `{"b":[{"d":3}]}` + +var nestedObj = `{"a":100, "b":{"c":200}}` +var nestedObjModifyProp = `{"b":{"c":250}}` +var nestedObjAddProp = `{"b":{"d":"hello"}}` +var nestedObjPrimitiveArray = `{"a":100, "b":{"c":[200]}}` +var nestedObjAddPrimitiveArrayItem = `{"b":{"c":[250]}}` func TestCreatePatch_ModifyProperty_GeneratesReplaceOperation(t *testing.T) { patch, err := CreatePatch_StrategyEnsureExists([]byte(simpleObj), []byte(simpleObjModifyProp)) @@ -30,3 +46,96 @@ func TestCreatePatch_AddProperty_GeneratesAddOperation(t *testing.T) { assert.Equal(t, "/c", change.Path, "they should be equal") assert.Equal(t, "hello", change.Value, "they should be equal") } + +func TestCreatePatch_NestedObject_ModifyProperty_GeneratesReplaceOperation(t *testing.T) { + patch, err := CreatePatch_StrategyEnsureExists([]byte(nestedObj), []byte(nestedObjModifyProp)) + assert.NoError(t, err) + assert.Equal(t, 1, len(patch), "they should be equal") + change := patch[0] + assert.Equal(t, "replace", change.Operation, "they should be equal") + assert.Equal(t, "/b/c", change.Path, "they should be equal") + var expected float64 = 250 + assert.Equal(t, expected, change.Value, "they should be equal") +} + +func TestCreatePatch_NestedObject_AddProperty_GeneratesAddOperation(t *testing.T) { + patch, err := CreatePatch_StrategyEnsureExists([]byte(nestedObj), []byte(nestedObjAddProp)) + assert.NoError(t, err) + assert.Equal(t, 1, len(patch), "they should be equal") + change := patch[0] + assert.Equal(t, "add", change.Operation, "they should be equal") + assert.Equal(t, "/b/d", change.Path, "they should be equal") + assert.Equal(t, "hello", change.Value, "they should be equal") +} + +func TestCreatePatch_EmptyPrimitiveArray_AddItem_GeneratesAddOperation(t *testing.T) { + patch, err := CreatePatch_StrategyEnsureExists([]byte(simpleObjEmtpyPrmitiveArray), []byte(simpleObjAddPrimitiveArrayItem)) + assert.NoError(t, err) + assert.Equal(t, 1, len(patch), "they should be equal") + change := patch[0] + assert.Equal(t, "add", change.Operation, "they should be equal") + assert.Equal(t, "/b/0", change.Path, "they should be equal") + var expected float64 = 3 + assert.Equal(t, expected, change.Value, "they should be equal") +} + +func TestCreatePatch_SingletonPrimitiveArray_AddItem_GeneratesAddOperation(t *testing.T) { + patch, err := CreatePatch_StrategyEnsureExists([]byte(simpleObjSingletonPrimitiveArray), []byte(simpleObjAddPrimitiveArrayItem)) + assert.NoError(t, err) + assert.Equal(t, 1, len(patch), "they should be equal") + change := patch[0] + assert.Equal(t, "add", change.Operation, "they should be equal") + assert.Equal(t, "/b/1", change.Path, "they should be equal") + var expected float64 = 3 + assert.Equal(t, expected, change.Value, "they should be equal") +} + +func TestCreatePatch_MultipleItemPrimitiveArray_AddItem_GeneratesAddOperation(t *testing.T) { + patch, err := CreatePatch_StrategyEnsureExists([]byte(simpleObjMultipleItemPrimitiveArray), []byte(simpleObjAddPrimitiveArrayItem)) + assert.NoError(t, err) + assert.Equal(t, 1, len(patch), "they should be equal") + change := patch[0] + assert.Equal(t, "add", change.Operation, "they should be equal") + assert.Equal(t, "/b/2", change.Path, "they should be equal") + var expected float64 = 3 + assert.Equal(t, expected, change.Value, "they should be equal") +} + +func TestCreatePatch_SingletonPrimitiveArray_AddDuplicateItem_GeneratesNoOperations(t *testing.T) { + patch, err := CreatePatch_StrategyEnsureExists([]byte(simpleObjMultipleItemPrimitiveArray), []byte(simpleObjAddDuplicateArrayItem)) + assert.NoError(t, err) + assert.Equal(t, 0, len(patch), "they should be equal") +} + +func TestCreatePatch_NestedObject_PrimitiveArray_AddItem_GeneratesAddOperation(t *testing.T) { + patch, err := CreatePatch_StrategyEnsureExists([]byte(nestedObjPrimitiveArray), []byte(nestedObjAddPrimitiveArrayItem)) + assert.NoError(t, err) + assert.Equal(t, 1, len(patch), "they should be equal") + change := patch[0] + assert.Equal(t, "add", change.Operation, "they should be equal") + assert.Equal(t, "/b/c/1", change.Path, "they should be equal") + var expected float64 = 250 + assert.Equal(t, expected, change.Value, "they should be equal") +} + +func TestCreatePatch_SingletonObjectArray_AddItem_GeneratesAddOperation(t *testing.T) { + patch, err := CreatePatch_StrategyEnsureExists([]byte(simpleObjSingletonObjectArray), []byte(simpleObjAddObjectArrayItem)) + assert.NoError(t, err) + assert.Equal(t, 1, len(patch), "they should be equal") + change := patch[0] + assert.Equal(t, "add", change.Operation, "they should be equal") + assert.Equal(t, "/b/1", change.Path, "they should be equal") + var expected = map[string]any{"c": float64(2)} + assert.Equal(t, expected, change.Value, "they should be equal") +} + +func TestCreatePatch_KeyValueArray_AddItem_GeneratesAddOperation(t *testing.T) { + patch, err := CreatePatch_StrategyEnsureExists([]byte(simpleObjKeyValueArray), []byte(simpleObjAddKeyValueArrayItem)) + assert.NoError(t, err) + assert.Equal(t, 1, len(patch), "they should be equal") + change := patch[0] + assert.Equal(t, "add", change.Operation, "they should be equal") + assert.Equal(t, "/b/2", change.Path, "they should be equal") + var expected = map[string]any{"e": float64(3)} + assert.Equal(t, expected, change.Value, "they should be equal") +} From 68fa5c36b6a121d84700c57e61b49777d0be85be Mon Sep 17 00:00:00 2001 From: Jeroen Soeters Date: Fri, 23 May 2025 23:35:25 -0500 Subject: [PATCH 04/12] Support for modifying in identity sets --- jsonpatch.go | 133 +++++++++++++++++++++-- jsonpatch_strategy_ensure_exists_test.go | 28 ++++- 2 files changed, 145 insertions(+), 16 deletions(-) diff --git a/jsonpatch.go b/jsonpatch.go index abf2d14..e0efb85 100644 --- a/jsonpatch.go +++ b/jsonpatch.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "reflect" + "strconv" "strings" ) @@ -24,19 +25,61 @@ func NewExactMatchStrategy(ignoreArrayOrder bool) ExactMatchStrategy { return ExactMatchStrategy{ignoreArrayOrder: ignoreArrayOrder} } -type IdentitySet struct { - path string - key string +type Path string +type Key string +type SetIdentities map[Path]Key + +func (s SetIdentities) Add(path Path, key Key) { + if s == nil { + s = make(SetIdentities) + } + s[path] = key +} + +func (s SetIdentities) Get(path Path) (Key, bool) { + if s == nil { + return "", false + } + key, ok := s[path] + return key, ok +} + +func toJsonPath(path string) string { + if path == "" || path == "/" { + return "$" + } + + parts := strings.Split(path, "/") + var jsonPathParts []string + + for _, part := range parts { + if part == "" { + continue + } + + _, err := strconv.Atoi(part) + if err == nil { + jsonPathParts = append(jsonPathParts, "[*]") + } else { + jsonPathParts = append(jsonPathParts, "."+part) + } + } + + return "$" + strings.Join(jsonPathParts, "") } type EnsureExistsStrategy struct { - identitySets []IdentitySet + setKeys SetIdentities } func (EnsureExistsStrategy) isStrategy() {} type EnsureAbsentStrategy struct{} +func NewEnsureExistsStrategy(setKeys SetIdentities) EnsureExistsStrategy { + return EnsureExistsStrategy{setKeys: setKeys} +} + func (EnsureAbsentStrategy) isStrategy() {} type JsonPatchOperation struct { @@ -113,8 +156,12 @@ func CreatePatch_StrategyEnsureExists(a, b []byte) ([]JsonPatchOperation, error) if err != nil { return nil, errBadJSONDoc } + sets := SetIdentities{ + Path("$.t"): Key("k"), + Path("$.t[*]"): Key("nk"), + } - return handleValues(aI, bI, "", []JsonPatchOperation{}, EnsureExistsStrategy{}) + return handleValues(aI, bI, "", []JsonPatchOperation{}, NewEnsureExistsStrategy(sets)) } // Returns true if the values matches (must be json types) @@ -372,7 +419,7 @@ func compareArray(av, bv []any, p string, strategy Strategy) []JsonPatchOperatio return retval } // Find elements that need to be removed - processArray(av, bv, func(i int, value any) { + processArray(av, bv, p, func(i int, value any) { retval = append(retval, NewPatch("remove", makePath(p, i), nil)) }, strategy) @@ -384,13 +431,21 @@ func compareArray(av, bv []any, p string, strategy Strategy) []JsonPatchOperatio // Find elements that need to be added. // NOTE we pass in `bv` then `av` so that processArray can find the missing elements. - processArray(bv, av, func(i int, value any) { + processArray(bv, av, p, func(i int, value any) { retval = append(retval, NewPatch("add", makePath(p, i), value)) }, strategy) case EnsureExistsStrategy: - processArray(bv, av, func(i int, value any) { - retval = append(retval, NewPatch("add", makePath(p, i), value)) - }, strategy) + if _, ok := s.setKeys.Get(Path(toJsonPath(p))); ok { + processIdentitySet(bv, av, p, func(i int, value any) { + retval = append(retval, NewPatch("add", makePath(p, i), value)) + }, func(ops []JsonPatchOperation) { + retval = append(retval, ops...) + }, strategy) + } else { + processArray(bv, av, p, func(i int, value any) { + retval = append(retval, NewPatch("add", makePath(p, i), value)) + }, strategy) + } case EnsureAbsentStrategy: return nil } @@ -398,9 +453,59 @@ func compareArray(av, bv []any, p string, strategy Strategy) []JsonPatchOperatio return retval } +func processIdentitySet(av, bv []any, path string, applyOp func(i int, value any), replaceOps func(ops []JsonPatchOperation), strategy Strategy) { + foundIndexes := make(map[int]struct{}, len(av)) + bvCounts := make(map[string]int) + bvSeen := make(map[string]int) // Track how many we've seen during processing + offset := len(bv) + + for _, v := range bv { + jsonBytes, err := json.Marshal(v) + if key, ok := strategy.(EnsureExistsStrategy).setKeys.Get(Path(toJsonPath(path))); ok { + jsonBytes, err = json.Marshal(v.(map[string]any)[string(key)]) + } + if err != nil { + continue // Skip if we can't marshal + } + jsonStr := string(jsonBytes) + bvCounts[jsonStr]++ + } + + // Check each element in av + for i, v := range av { + jsonBytes, err := json.Marshal(v) + if key, ok := strategy.(EnsureExistsStrategy).setKeys.Get(Path(toJsonPath(path))); ok { + jsonBytes, err = json.Marshal(v.(map[string]any)[string(key)]) + } + if err != nil { + applyOp(i+offset, v) // If we can't marshal, treat it as not found + continue + } + + jsonStr := string(jsonBytes) + // If element exists in bv and we haven't seen all of them yet + if bvCounts[jsonStr] > bvSeen[jsonStr] { + foundIndexes[i] = struct{}{} + bvSeen[jsonStr]++ + updateOps, err := handleValues(bv[bvSeen[jsonStr]], v, fmt.Sprintf("%s/%d", path, bvSeen[jsonStr]), []JsonPatchOperation{}, strategy) + if err != nil { + return + } + replaceOps(updateOps) + } + } + + // Apply op for all elements in av that weren't found + for i, v := range av { + if _, ok := foundIndexes[i]; !ok { + applyOp(i+offset, v) + } + } +} + // processArray processes `av` and `bv` calling `applyOp` whenever a value is absent. // It keeps track of which indexes have already had `applyOp` called for and automatically skips them so you can process duplicate objects correctly. -func processArray(av, bv []any, applyOp func(i int, value any), strategy Strategy) { +func processArray(av, bv []any, path string, applyOp func(i int, value any), strategy Strategy) { foundIndexes := make(map[int]struct{}, len(av)) reverseFoundIndexes := make(map[int]struct{}, len(av)) @@ -470,6 +575,9 @@ func processArray(av, bv []any, applyOp func(i int, value any), strategy Strateg for _, v := range bv { jsonBytes, err := json.Marshal(v) + if key, ok := s.setKeys.Get(Path(toJsonPath(path))); ok { + jsonBytes, err = json.Marshal(v.(map[string]any)[string(key)]) + } if err != nil { continue // Skip if we can't marshal } @@ -480,6 +588,9 @@ func processArray(av, bv []any, applyOp func(i int, value any), strategy Strateg // Check each element in av for i, v := range av { jsonBytes, err := json.Marshal(v) + if key, ok := s.setKeys.Get(Path(toJsonPath(path))); ok { + jsonBytes, err = json.Marshal(v.(map[string]any)[string(key)]) + } if err != nil { applyOp(i+offset, v) // If we can't marshal, treat it as not found continue diff --git a/jsonpatch_strategy_ensure_exists_test.go b/jsonpatch_strategy_ensure_exists_test.go index 2109e61..874ed3d 100644 --- a/jsonpatch_strategy_ensure_exists_test.go +++ b/jsonpatch_strategy_ensure_exists_test.go @@ -16,9 +16,10 @@ var simpleObjAddPrimitiveArrayItem = `{"b":[3]}` var simpleObjAddDuplicateArrayItem = `{"b":[2]}` var simpleObjSingletonObjectArray = `{"a":100, "b":[{"c":1}]}` var simpleObjAddObjectArrayItem = `{"b":[{"c":2}]}` -var simpleObjKeyValueArray = `{"a":100, "b":[{"c":1},{"d":2}]}` -var simpleObjAddKeyValueArrayItem = `{"b":[{"e":3}]}` -var simpleObjModifyKeyValueArrayItem = `{"b":[{"d":3}]}` +var simpleObjAddDuplicateObjectArrayItem = `{"b":[{"c":1}]}` +var simpleObjKeyValueArray = `{"a":100, "t":[{"k":1, "v":1},{"k":2, "v":2}]}` +var simpleObjAddKeyValueArrayItem = `{"t":[{"k":3, "v":3}]}` +var simpleObjModifyKeyValueArrayItem = `{"t":[{"k":2, "v":3}]}` var nestedObj = `{"a":100, "b":{"c":200}}` var nestedObjModifyProp = `{"b":{"c":250}}` @@ -135,7 +136,24 @@ func TestCreatePatch_KeyValueArray_AddItem_GeneratesAddOperation(t *testing.T) { assert.Equal(t, 1, len(patch), "they should be equal") change := patch[0] assert.Equal(t, "add", change.Operation, "they should be equal") - assert.Equal(t, "/b/2", change.Path, "they should be equal") - var expected = map[string]any{"e": float64(3)} + assert.Equal(t, "/t/2", change.Path, "they should be equal") + var expected = map[string]any{"k": float64(3), "v": float64(3)} + assert.Equal(t, expected, change.Value, "they should be equal") +} + +func TestCreatePatch_SingletonObjectArray_AddDuplicateItem_GeneratesNoOperations(t *testing.T) { + patch, err := CreatePatch_StrategyEnsureExists([]byte(simpleObjSingletonObjectArray), []byte(simpleObjAddDuplicateObjectArrayItem)) + assert.NoError(t, err) + assert.Equal(t, 0, len(patch), "they should be equal") +} + +func TestCreatePatch_KeyValueArray_ModifyItem_GeneratesReplaceOperation(t *testing.T) { + patch, err := CreatePatch_StrategyEnsureExists([]byte(simpleObjKeyValueArray), []byte(simpleObjModifyKeyValueArrayItem)) + assert.NoError(t, err) + assert.Equal(t, 1, len(patch), "they should be equal") + change := patch[0] + assert.Equal(t, "replace", change.Operation, "they should be equal") + assert.Equal(t, "/t/1/v", change.Path, "they should be equal") + var expected float64 = 3 assert.Equal(t, expected, change.Value, "they should be equal") } From 05b456f8110b4bbc193346eb6e9871e84031ab17 Mon Sep 17 00:00:00 2001 From: Jeroen Soeters Date: Sun, 25 May 2025 23:05:04 -0500 Subject: [PATCH 05/12] Nested identity set now working --- jsonpatch.go | 44 +++++++++++++++------ jsonpatch_strategy_ensure_exists_test.go | 49 ++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 12 deletions(-) diff --git a/jsonpatch.go b/jsonpatch.go index e0efb85..59ee850 100644 --- a/jsonpatch.go +++ b/jsonpatch.go @@ -29,6 +29,30 @@ type Path string type Key string type SetIdentities map[Path]Key +// TODO: write test for this +func NewPath(path string) Path { + if path == "" || path == "/" { + return Path("$") + } + + parts := strings.Split(path, "/") + var jsonPathParts []string + + for _, part := range parts { + if part == "" { + continue + } + + _, err := strconv.Atoi(part) + if err == nil { + jsonPathParts = append(jsonPathParts, "[*]") + } else { + jsonPathParts = append(jsonPathParts, "."+part) + } + } + return Path("$" + strings.Join(jsonPathParts, "")) +} + func (s SetIdentities) Add(path Path, key Key) { if s == nil { s = make(SetIdentities) @@ -127,7 +151,7 @@ func NewPatch(operation, path string, value any) JsonPatchOperation { // The function will return an array of JsonPatchOperations // If ignoreArrayOrder is true, arrays with the same elements but in different order will be considered equal // -// An error will be returned if any of the two documents are invalid. +// An e rror will be returned if any of the two documents are invalid. func CreatePatch(a, b []byte, ignoreArrayOrder bool) ([]JsonPatchOperation, error) { var aI any var bI any @@ -157,8 +181,8 @@ func CreatePatch_StrategyEnsureExists(a, b []byte) ([]JsonPatchOperation, error) return nil, errBadJSONDoc } sets := SetIdentities{ - Path("$.t"): Key("k"), - Path("$.t[*]"): Key("nk"), + Path("$.t"): Key("k"), + Path("$.t[*].v"): Key("nk"), } return handleValues(aI, bI, "", []JsonPatchOperation{}, NewEnsureExistsStrategy(sets)) @@ -456,10 +480,11 @@ func compareArray(av, bv []any, p string, strategy Strategy) []JsonPatchOperatio func processIdentitySet(av, bv []any, path string, applyOp func(i int, value any), replaceOps func(ops []JsonPatchOperation), strategy Strategy) { foundIndexes := make(map[int]struct{}, len(av)) bvCounts := make(map[string]int) + lookup := make(map[string]int) bvSeen := make(map[string]int) // Track how many we've seen during processing offset := len(bv) - for _, v := range bv { + for i, v := range bv { jsonBytes, err := json.Marshal(v) if key, ok := strategy.(EnsureExistsStrategy).setKeys.Get(Path(toJsonPath(path))); ok { jsonBytes, err = json.Marshal(v.(map[string]any)[string(key)]) @@ -468,6 +493,7 @@ func processIdentitySet(av, bv []any, path string, applyOp func(i int, value any continue // Skip if we can't marshal } jsonStr := string(jsonBytes) + lookup[jsonStr] = i bvCounts[jsonStr]++ } @@ -484,10 +510,10 @@ func processIdentitySet(av, bv []any, path string, applyOp func(i int, value any jsonStr := string(jsonBytes) // If element exists in bv and we haven't seen all of them yet - if bvCounts[jsonStr] > bvSeen[jsonStr] { + if index, ok := lookup[jsonStr]; ok { foundIndexes[i] = struct{}{} bvSeen[jsonStr]++ - updateOps, err := handleValues(bv[bvSeen[jsonStr]], v, fmt.Sprintf("%s/%d", path, bvSeen[jsonStr]), []JsonPatchOperation{}, strategy) + updateOps, err := handleValues(bv[index], v, fmt.Sprintf("%s/%d", path, lookup[jsonStr]), []JsonPatchOperation{}, strategy) if err != nil { return } @@ -575,9 +601,6 @@ func processArray(av, bv []any, path string, applyOp func(i int, value any), str for _, v := range bv { jsonBytes, err := json.Marshal(v) - if key, ok := s.setKeys.Get(Path(toJsonPath(path))); ok { - jsonBytes, err = json.Marshal(v.(map[string]any)[string(key)]) - } if err != nil { continue // Skip if we can't marshal } @@ -588,9 +611,6 @@ func processArray(av, bv []any, path string, applyOp func(i int, value any), str // Check each element in av for i, v := range av { jsonBytes, err := json.Marshal(v) - if key, ok := s.setKeys.Get(Path(toJsonPath(path))); ok { - jsonBytes, err = json.Marshal(v.(map[string]any)[string(key)]) - } if err != nil { applyOp(i+offset, v) // If we can't marshal, treat it as not found continue diff --git a/jsonpatch_strategy_ensure_exists_test.go b/jsonpatch_strategy_ensure_exists_test.go index 874ed3d..f5e4c87 100644 --- a/jsonpatch_strategy_ensure_exists_test.go +++ b/jsonpatch_strategy_ensure_exists_test.go @@ -20,6 +20,30 @@ var simpleObjAddDuplicateObjectArrayItem = `{"b":[{"c":1}]}` var simpleObjKeyValueArray = `{"a":100, "t":[{"k":1, "v":1},{"k":2, "v":2}]}` var simpleObjAddKeyValueArrayItem = `{"t":[{"k":3, "v":3}]}` var simpleObjModifyKeyValueArrayItem = `{"t":[{"k":2, "v":3}]}` +var simpleObjAddDuplicateKeyValueArrayItem = `{"t":[{"k":2, "v":2}]}` +var complexNextedKeyValueArray = `{ + "a":100, + "t":[ + {"k":1, + "v":[ + {"nk":11, "c":"x", "d":[1,2]}, + {"nk":22, "c":"y", "d":[3,4]} + ] + }, + {"k":2, + "v":[ + {"nk":33, "c":"z", "d":[5,6]} + ] + } + ]}` +var complexNextedKeyValueArrayModifyItem = `{ + "t":[ + {"k":2, + "v":[ + {"nk":33, "c":"zz", "d":[7,8]} + ] + } + ]}` var nestedObj = `{"a":100, "b":{"c":200}}` var nestedObjModifyProp = `{"b":{"c":250}}` @@ -157,3 +181,28 @@ func TestCreatePatch_KeyValueArray_ModifyItem_GeneratesReplaceOperation(t *testi var expected float64 = 3 assert.Equal(t, expected, change.Value, "they should be equal") } + +func TestCreatePatch_KeyValueArray_AddDuplicateItem_GeneratesNoOperations(t *testing.T) { + patch, err := CreatePatch_StrategyEnsureExists([]byte(simpleObjKeyValueArray), []byte(simpleObjAddDuplicateKeyValueArrayItem)) + assert.NoError(t, err) + assert.Equal(t, 0, len(patch), "they should be equal") +} + +func TestCreatePatch_ComplexNestedKeyValueArray_ModifyItem_GeneratesReplaceOperation(t *testing.T) { + patch, err := CreatePatch_StrategyEnsureExists([]byte(complexNextedKeyValueArray), []byte(complexNextedKeyValueArrayModifyItem)) + assert.NoError(t, err) + assert.Equal(t, 3, len(patch), "they should be equal") + change := patch[0] + assert.Equal(t, "replace", change.Operation, "they should be equal") + assert.Equal(t, "/t/1/v/0/c", change.Path, "they should be equal") + assert.Equal(t, "zz", change.Value, "they should be equal") + change = patch[1] + assert.Equal(t, "add", change.Operation, "they should be equal") + assert.Equal(t, "/t/1/v/0/d/2", change.Path, "they should be equal") + assert.Equal(t, float64(7), change.Value, "they should be equal") + change = patch[2] + assert.Equal(t, "add", change.Operation, "they should be equal") + assert.Equal(t, "add", change.Operation, "they should be equal") + assert.Equal(t, "/t/1/v/0/d/3", change.Path, "they should be equal") + assert.Equal(t, float64(8), change.Value, "they should be equal") +} From ca77bee9262b27504b18ecf97de53cf4eaf2a78d Mon Sep 17 00:00:00 2001 From: Jeroen Soeters Date: Mon, 26 May 2025 11:20:13 -0500 Subject: [PATCH 06/12] Mid-refactor. Tests green --- jsonpatch.go | 212 +++-- jsonpatch_array_at_root_test.go | 26 +- jsonpatch_array_test.go | 12 +- jsonpatch_complex_test.go | 16 +- jsonpatch_geojson_test.go | 7 +- jsonpatch_hypercomplex_test.go | 8 +- jsonpatch_simple_test.go | 18 +- jsonpatch_strategy_ensure_exists_test.go | 72 +- jsonpatch_supercomplex_test.go | 971 ++++++++++++----------- 9 files changed, 682 insertions(+), 660 deletions(-) diff --git a/jsonpatch.go b/jsonpatch.go index 59ee850..7700c57 100644 --- a/jsonpatch.go +++ b/jsonpatch.go @@ -27,40 +27,37 @@ func NewExactMatchStrategy(ignoreArrayOrder bool) ExactMatchStrategy { type Path string type Key string -type SetIdentities map[Path]Key +type EntitySets map[Path]Key -// TODO: write test for this -func NewPath(path string) Path { - if path == "" || path == "/" { - return Path("$") - } - - parts := strings.Split(path, "/") - var jsonPathParts []string - - for _, part := range parts { - if part == "" { - continue - } +type Collections struct { + entitySets EntitySets + arrays []string +} - _, err := strconv.Atoi(part) - if err == nil { - jsonPathParts = append(jsonPathParts, "[*]") - } else { - jsonPathParts = append(jsonPathParts, "."+part) +func (c *Collections) isArray(path string) bool { + jsonPath := toJsonPath(path) + for _, array := range c.arrays { + if jsonPath == array { + return true } } - return Path("$" + strings.Join(jsonPathParts, "")) + return false } -func (s SetIdentities) Add(path Path, key Key) { +func (c *Collections) isEntitySet(path string) bool { + jsonPath := toJsonPath(path) + _, ok := c.entitySets[Path(jsonPath)] + return ok +} + +func (s EntitySets) Add(path Path, key Key) { if s == nil { - s = make(SetIdentities) + s = make(EntitySets) } s[path] = key } -func (s SetIdentities) Get(path Path) (Key, bool) { +func (s EntitySets) Get(path Path) (Key, bool) { if s == nil { return "", false } @@ -92,19 +89,13 @@ func toJsonPath(path string) string { return "$" + strings.Join(jsonPathParts, "") } -type EnsureExistsStrategy struct { - setKeys SetIdentities -} - -func (EnsureExistsStrategy) isStrategy() {} - -type EnsureAbsentStrategy struct{} +type PatchStrategy string -func NewEnsureExistsStrategy(setKeys SetIdentities) EnsureExistsStrategy { - return EnsureExistsStrategy{setKeys: setKeys} -} - -func (EnsureAbsentStrategy) isStrategy() {} +const ( + PatchStrategyExactMatch PatchStrategy = "exact-match" + PatchStrategyEnsureExists PatchStrategy = "ensure-exists" + PatchStrategyEnsureAbsent PatchStrategy = "ensure-absent" +) type JsonPatchOperation struct { Operation string `json:"op"` @@ -152,7 +143,7 @@ func NewPatch(operation, path string, value any) JsonPatchOperation { // If ignoreArrayOrder is true, arrays with the same elements but in different order will be considered equal // // An e rror will be returned if any of the two documents are invalid. -func CreatePatch(a, b []byte, ignoreArrayOrder bool) ([]JsonPatchOperation, error) { +func CreatePatch(a, b []byte, collections Collections, strategy PatchStrategy) ([]JsonPatchOperation, error) { var aI any var bI any @@ -165,7 +156,7 @@ func CreatePatch(a, b []byte, ignoreArrayOrder bool) ([]JsonPatchOperation, erro return nil, errBadJSONDoc } - return handleValues(aI, bI, "", []JsonPatchOperation{}, NewExactMatchStrategy(ignoreArrayOrder)) + return handleValues(aI, bI, "", []JsonPatchOperation{}, strategy, collections) } func CreatePatch_StrategyEnsureExists(a, b []byte) ([]JsonPatchOperation, error) { @@ -180,12 +171,17 @@ func CreatePatch_StrategyEnsureExists(a, b []byte) ([]JsonPatchOperation, error) if err != nil { return nil, errBadJSONDoc } - sets := SetIdentities{ + + entitySets := EntitySets{ Path("$.t"): Key("k"), Path("$.t[*].v"): Key("nk"), } + collections := Collections{ + entitySets: entitySets, + arrays: []string{}, + } - return handleValues(aI, bI, "", []JsonPatchOperation{}, NewEnsureExistsStrategy(sets)) + return handleValues(aI, bI, "", []JsonPatchOperation{}, PatchStrategyEnsureExists, collections) } // Returns true if the values matches (must be json types) @@ -306,9 +302,9 @@ func makePath(path string, newPart any) string { } // diff returns the (recursive) difference between a and b as an array of JsonPatchOperations. -func diff(a, b map[string]any, path string, patch []JsonPatchOperation, strategy Strategy) ([]JsonPatchOperation, error) { - switch strategy.(type) { - case ExactMatchStrategy: +func diff(a, b map[string]any, path string, patch []JsonPatchOperation, strategy PatchStrategy, collections Collections) ([]JsonPatchOperation, error) { + switch strategy { + case PatchStrategyExactMatch: for key, bv := range b { p := makePath(path, key) av, ok := a[key] @@ -324,7 +320,7 @@ func diff(a, b map[string]any, path string, patch []JsonPatchOperation, strategy } // Types are the same, compare values var err error - patch, err = handleValues(av, bv, p, patch, strategy) + patch, err = handleValues(av, bv, p, patch, strategy, collections) if err != nil { return nil, err } @@ -339,7 +335,7 @@ func diff(a, b map[string]any, path string, patch []JsonPatchOperation, strategy } } return patch, nil - case EnsureExistsStrategy: + case PatchStrategyEnsureExists: for key, bv := range b { p := makePath(path, key) av, ok := a[key] @@ -355,69 +351,56 @@ func diff(a, b map[string]any, path string, patch []JsonPatchOperation, strategy } // Types are the same, compare values var err error - patch, err = handleValues(av, bv, p, patch, strategy) + patch, err = handleValues(av, bv, p, patch, strategy, collections) if err != nil { return nil, err } } // We don't generate remove operations in "ensure exists" mode return patch, nil - case EnsureAbsentStrategy: + case PatchStrategyEnsureAbsent: fmt.Println("EnsureAbsent strategy is not implemented yet") } return nil, fmt.Errorf("Unknown strategy: %s", strategy) } -func handleValues(av, bv any, p string, patch []JsonPatchOperation, strategy Strategy) ([]JsonPatchOperation, error) { +func handleValues(av, bv any, p string, patch []JsonPatchOperation, strategy PatchStrategy, collections Collections) ([]JsonPatchOperation, error) { var err error - ignoreArrayOrder := false - if s, ok := strategy.(ExactMatchStrategy); ok { - ignoreArrayOrder = s.ignoreArrayOrder - } + ignoreArrayOrder := !collections.isArray(p) switch at := av.(type) { case map[string]any: bt := bv.(map[string]any) - patch, err = diff(at, bt, p, patch, strategy) + patch, err = diff(at, bt, p, patch, strategy, collections) if err != nil { return nil, err } + return patch, nil case string, float64, bool: if !matchesValue(av, bv, ignoreArrayOrder) { patch = append(patch, NewPatch("replace", p, bv)) } + return patch, nil case []any: - switch strategy.(type) { - case ExactMatchStrategy: - bt, ok := bv.([]any) - if !ok { - // array replaced by non-array - patch = append(patch, NewPatch("replace", p, bv)) - } else if len(at) != len(bt) { - // arrays are not the same length - patch = append(patch, compareArray(at, bt, p, strategy)...) - } else if ignoreArrayOrder && matchesValue(at, bt, true) { - // Arrays have the same elements, just in different order, and we're ignoring order - // No patch needed! - } else { - for i := range bt { - patch, err = handleValues(at[i], bt[i], makePath(p, i), patch, strategy) - if err != nil { - return nil, err - } + bt, replaceWithOtherCollection := bv.([]any) + switch { + case !replaceWithOtherCollection: + patch = append(patch, NewPatch("replace", p, bv)) + case collections.isArray(p) && len(at) != len(bt): + patch = append(patch, compareArray(at, bt, p, strategy, collections)...) + case collections.isArray(p) && len(at) == len(bt): + // If arrays have the same length, we can compare them element by element + for i := range bt { + patch, err = handleValues(at[i], bt[i], makePath(p, i), patch, strategy, collections) + if err != nil { + return nil, err } } - case EnsureExistsStrategy: - bt, ok := bv.([]any) - if !ok { - // array replaced by non-array - patch = append(patch, NewPatch("replace", p, bv)) - } else { - // compare arrays - patch = append(patch, compareArray(at, bt, p, strategy)...) + default: + // If this is not an array, we treat it as a set of values. + if !matchesValue(at, bt, true) { + patch = append(patch, compareArray(at, bt, p, strategy, collections)...) } - case EnsureAbsentStrategy: - return nil, fmt.Errorf("EnsureAbsent strategy is not implemented for arrays") } case nil: switch bv.(type) { @@ -433,19 +416,20 @@ func handleValues(av, bv any, p string, patch []JsonPatchOperation, strategy Str } // compareArray generates remove and add operations for `av` and `bv`. -func compareArray(av, bv []any, p string, strategy Strategy) []JsonPatchOperation { +func compareArray(av, bv []any, p string, strategy PatchStrategy, collections Collections) []JsonPatchOperation { retval := []JsonPatchOperation{} + ignoreArrayOrder := !collections.isArray(p) - switch s := strategy.(type) { - case ExactMatchStrategy: + switch strategy { + case PatchStrategyExactMatch: // If arrays have same elements in different order and we're ignoring order, return empty patch - if s.ignoreArrayOrder && len(av) == len(bv) && matchesValue(av, bv, true) { + if len(av) == len(bv) && matchesValue(av, bv, true) { return retval } // Find elements that need to be removed - processArray(av, bv, p, func(i int, value any) { + processArray(av, bv, func(i int, value any) { retval = append(retval, NewPatch("remove", makePath(p, i), nil)) - }, strategy) + }, strategy, ignoreArrayOrder) reversed := make([]JsonPatchOperation, len(retval)) for i := 0; i < len(retval); i++ { @@ -455,29 +439,29 @@ func compareArray(av, bv []any, p string, strategy Strategy) []JsonPatchOperatio // Find elements that need to be added. // NOTE we pass in `bv` then `av` so that processArray can find the missing elements. - processArray(bv, av, p, func(i int, value any) { + processArray(bv, av, func(i int, value any) { retval = append(retval, NewPatch("add", makePath(p, i), value)) - }, strategy) - case EnsureExistsStrategy: - if _, ok := s.setKeys.Get(Path(toJsonPath(p))); ok { + }, strategy, ignoreArrayOrder) + case PatchStrategyEnsureExists: + if collections.isEntitySet(p) { processIdentitySet(bv, av, p, func(i int, value any) { retval = append(retval, NewPatch("add", makePath(p, i), value)) }, func(ops []JsonPatchOperation) { retval = append(retval, ops...) - }, strategy) + }, strategy, collections) } else { - processArray(bv, av, p, func(i int, value any) { + processArray(bv, av, func(i int, value any) { retval = append(retval, NewPatch("add", makePath(p, i), value)) - }, strategy) + }, strategy, ignoreArrayOrder) } - case EnsureAbsentStrategy: + case PatchStrategyEnsureAbsent: return nil } return retval } -func processIdentitySet(av, bv []any, path string, applyOp func(i int, value any), replaceOps func(ops []JsonPatchOperation), strategy Strategy) { +func processIdentitySet(av, bv []any, path string, applyOp func(i int, value any), replaceOps func(ops []JsonPatchOperation), strategy PatchStrategy, collections Collections) { foundIndexes := make(map[int]struct{}, len(av)) bvCounts := make(map[string]int) lookup := make(map[string]int) @@ -486,7 +470,7 @@ func processIdentitySet(av, bv []any, path string, applyOp func(i int, value any for i, v := range bv { jsonBytes, err := json.Marshal(v) - if key, ok := strategy.(EnsureExistsStrategy).setKeys.Get(Path(toJsonPath(path))); ok { + if key, ok := collections.entitySets.Get(Path(toJsonPath(path))); ok { jsonBytes, err = json.Marshal(v.(map[string]any)[string(key)]) } if err != nil { @@ -500,7 +484,7 @@ func processIdentitySet(av, bv []any, path string, applyOp func(i int, value any // Check each element in av for i, v := range av { jsonBytes, err := json.Marshal(v) - if key, ok := strategy.(EnsureExistsStrategy).setKeys.Get(Path(toJsonPath(path))); ok { + if key, ok := collections.entitySets.Get(Path(toJsonPath(path))); ok { jsonBytes, err = json.Marshal(v.(map[string]any)[string(key)]) } if err != nil { @@ -513,7 +497,7 @@ func processIdentitySet(av, bv []any, path string, applyOp func(i int, value any if index, ok := lookup[jsonStr]; ok { foundIndexes[i] = struct{}{} bvSeen[jsonStr]++ - updateOps, err := handleValues(bv[index], v, fmt.Sprintf("%s/%d", path, lookup[jsonStr]), []JsonPatchOperation{}, strategy) + updateOps, err := handleValues(bv[index], v, fmt.Sprintf("%s/%d", path, lookup[jsonStr]), []JsonPatchOperation{}, strategy, collections) if err != nil { return } @@ -531,27 +515,23 @@ func processIdentitySet(av, bv []any, path string, applyOp func(i int, value any // processArray processes `av` and `bv` calling `applyOp` whenever a value is absent. // It keeps track of which indexes have already had `applyOp` called for and automatically skips them so you can process duplicate objects correctly. -func processArray(av, bv []any, path string, applyOp func(i int, value any), strategy Strategy) { +func processArray(av, bv []any, applyOp func(i int, value any), strategy PatchStrategy, ignoreArrayOrder bool) { foundIndexes := make(map[int]struct{}, len(av)) - reverseFoundIndexes := make(map[int]struct{}, len(av)) - - switch s := strategy.(type) { - case ExactMatchStrategy: - if s.ignoreArrayOrder { - // Create a map of elements and their counts in bv - bvCounts := make(map[string]int) - bvSeen := make(map[string]int) // Track how many we've seen during processing + bvElements := make(map[string]struct{}) + reverseFoundIndexes := make(map[int]struct{}, len(bv)) + switch strategy { + case PatchStrategyExactMatch: + if ignoreArrayOrder { for _, v := range bv { jsonBytes, err := json.Marshal(v) if err != nil { continue // Skip if we can't marshal } jsonStr := string(jsonBytes) - bvCounts[jsonStr]++ + bvElements[jsonStr] = struct{}{} } - // Check each element in av for i, v := range av { jsonBytes, err := json.Marshal(v) if err != nil { @@ -561,15 +541,7 @@ func processArray(av, bv []any, path string, applyOp func(i int, value any), str jsonStr := string(jsonBytes) // If element exists in bv and we haven't seen all of them yet - if bvCounts[jsonStr] > bvSeen[jsonStr] { - foundIndexes[i] = struct{}{} - bvSeen[jsonStr]++ - } - } - - // Apply op for all elements in av that weren't found - for i, v := range av { - if _, ok := foundIndexes[i]; !ok { + if _, ok := bvElements[jsonStr]; !ok { applyOp(i, v) } } @@ -593,7 +565,7 @@ func processArray(av, bv []any, path string, applyOp func(i int, value any), str } } } - case EnsureExistsStrategy: + case PatchStrategyEnsureExists: offset := len(bv) // Create a map of elements and their counts in bv bvCounts := make(map[string]int) @@ -631,7 +603,7 @@ func processArray(av, bv []any, path string, applyOp func(i int, value any), str } } return - case EnsureAbsentStrategy: + case PatchStrategyEnsureAbsent: return } } diff --git a/jsonpatch_array_at_root_test.go b/jsonpatch_array_at_root_test.go index b7a4be5..5c88a75 100644 --- a/jsonpatch_array_at_root_test.go +++ b/jsonpatch_array_at_root_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestJSONPatchCreate(t *testing.T) { +func TestJSONPatchCreate_ObjectRoot(t *testing.T) { cases := map[string]struct { a string b string @@ -19,6 +19,24 @@ func TestJSONPatchCreate(t *testing.T) { `{"items":[{"asdf":"qwerty"}]}`, `{"items":[{"asdf":"bla"},{"asdf":"zzz"}]}`, }, + } + + collections := Collections{ + arrays: []string{"$.items"}, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + _, err := CreatePatch([]byte(tc.a), []byte(tc.b), collections, PatchStrategyExactMatch) + assert.NoError(t, err) + }) + } +} +func TestJSONPatchCreate_ArrayRoot(t *testing.T) { + cases := map[string]struct { + a string + b string + }{ "array": { `[{"asdf":"qwerty"}]`, `[{"asdf":"bla"},{"asdf":"zzz"}]`, @@ -33,9 +51,13 @@ func TestJSONPatchCreate(t *testing.T) { }, } + collections := Collections{ + arrays: []string{"$"}, + } + for name, tc := range cases { t.Run(name, func(t *testing.T) { - _, err := CreatePatch([]byte(tc.a), []byte(tc.b), false) + _, err := CreatePatch([]byte(tc.a), []byte(tc.b), collections, PatchStrategyExactMatch) assert.NoError(t, err) }) } diff --git a/jsonpatch_array_test.go b/jsonpatch_array_test.go index e6d7c69..3057524 100644 --- a/jsonpatch_array_test.go +++ b/jsonpatch_array_test.go @@ -15,10 +15,14 @@ var ( arrayUpdated = `{ "persons": [{"name":"Ed"},{},{}] }` + + arrayTestCollections = Collections{ + arrays: []string{"$.persons"}, + } ) func TestArrayAddMultipleEmptyObjects(t *testing.T) { - patch, e := CreatePatch([]byte(arrayBase), []byte(arrayUpdated), false) + patch, e := CreatePatch([]byte(arrayBase), []byte(arrayUpdated), arrayTestCollections, PatchStrategyExactMatch) assert.NoError(t, e) t.Log("Patch:", patch) assert.Equal(t, 1, len(patch), "they should be equal") @@ -31,7 +35,7 @@ func TestArrayAddMultipleEmptyObjects(t *testing.T) { } func TestArrayRemoveMultipleEmptyObjects(t *testing.T) { - patch, e := CreatePatch([]byte(arrayUpdated), []byte(arrayBase), false) + patch, e := CreatePatch([]byte(arrayUpdated), []byte(arrayBase), arrayTestCollections, PatchStrategyExactMatch) assert.NoError(t, e) t.Log("Patch:", patch) assert.Equal(t, 1, len(patch), "they should be equal") @@ -56,7 +60,7 @@ var ( // TestArrayRemoveSpaceInbetween tests removing one blank item from a group blanks which is in between non blank items which also end with a blank item. This tests that the correct index is removed func TestArrayRemoveSpaceInbetween(t *testing.T) { t.Skip("This test fails. TODO change compareArray algorithm to match by index instead of by object equality") - patch, e := CreatePatch([]byte(arrayWithSpacesBase), []byte(arrayWithSpacesUpdated), false) + patch, e := CreatePatch([]byte(arrayWithSpacesBase), []byte(arrayWithSpacesUpdated), arrayTestCollections, PatchStrategyExactMatch) assert.NoError(t, e) t.Log("Patch:", patch) assert.Equal(t, 1, len(patch), "they should be equal") @@ -80,7 +84,7 @@ var ( // TestArrayRemoveMulti tests removing multi groups. This tests that the correct index is removed func TestArrayRemoveMulti(t *testing.T) { - patch, e := CreatePatch([]byte(arrayRemoveMultiBase), []byte(arrayRemoveMultisUpdated), false) + patch, e := CreatePatch([]byte(arrayRemoveMultiBase), []byte(arrayRemoveMultisUpdated), arrayTestCollections, PatchStrategyExactMatch) assert.NoError(t, e) t.Log("Patch:", patch) assert.Equal(t, 3, len(patch), "they should be equal") diff --git a/jsonpatch_complex_test.go b/jsonpatch_complex_test.go index b2f0585..501945e 100644 --- a/jsonpatch_complex_test.go +++ b/jsonpatch_complex_test.go @@ -14,13 +14,17 @@ var complexC = `{"a":100, "b":[{"c1":"hello", "d1":"foo"},{"c2":"hello2", "d2":" var complexD = `{"a":100, "b":[{"c1":"hello", "d1":"foo"},{"c2":"hello2", "d2":"foo2"}, {"c3":"hello3", "d3":"foo3"} ], "e":{"f":200, "g":"h", "i":"j"}}` var complexE = `{"a":100, "b":[{"c1":"hello", "d1":"foo"},{"c2":"hello2", "d2":"foo2"} ], "e":{"f":200, "g":"h", "i":"j"}}` +var complex_test_collections = Collections{ + arrays: []string{"$.b"}, +} + func TestComplexSame(t *testing.T) { - patch, e := CreatePatch([]byte(complexBase), []byte(complexBase), false) + patch, e := CreatePatch([]byte(complexBase), []byte(complexBase), complex_test_collections, PatchStrategyExactMatch) assert.NoError(t, e) assert.Equal(t, 0, len(patch), "they should be equal") } func TestComplexOneStringReplaceInArray(t *testing.T) { - patch, e := CreatePatch([]byte(complexBase), []byte(complexA), false) + patch, e := CreatePatch([]byte(complexBase), []byte(complexA), complex_test_collections, PatchStrategyExactMatch) assert.NoError(t, e) assert.Equal(t, 1, len(patch), "they should be equal") change := patch[0] @@ -30,7 +34,7 @@ func TestComplexOneStringReplaceInArray(t *testing.T) { } func TestComplexOneIntReplace(t *testing.T) { - patch, e := CreatePatch([]byte(complexBase), []byte(complexB), false) + patch, e := CreatePatch([]byte(complexBase), []byte(complexB), complex_test_collections, PatchStrategyExactMatch) assert.NoError(t, e) assert.Equal(t, 1, len(patch), "they should be equal") change := patch[0] @@ -41,7 +45,7 @@ func TestComplexOneIntReplace(t *testing.T) { } func TestComplexOneAdd(t *testing.T) { - patch, e := CreatePatch([]byte(complexBase), []byte(complexC), false) + patch, e := CreatePatch([]byte(complexBase), []byte(complexC), complex_test_collections, PatchStrategyExactMatch) assert.NoError(t, e) assert.Equal(t, 1, len(patch), "they should be equal") change := patch[0] @@ -56,7 +60,7 @@ func TestComplexOneAdd(t *testing.T) { } func TestComplexOneAddToArray(t *testing.T) { - patch, e := CreatePatch([]byte(complexBase), []byte(complexC), false) + patch, e := CreatePatch([]byte(complexBase), []byte(complexC), complex_test_collections, PatchStrategyExactMatch) assert.NoError(t, e) assert.Equal(t, 1, len(patch), "they should be equal") change := patch[0] @@ -71,7 +75,7 @@ func TestComplexOneAddToArray(t *testing.T) { } func TestComplexVsEmpty(t *testing.T) { - patch, e := CreatePatch([]byte(complexBase), []byte(empty), false) + patch, e := CreatePatch([]byte(complexBase), []byte(empty), complex_test_collections, PatchStrategyExactMatch) assert.NoError(t, e) assert.Equal(t, 3, len(patch), "they should be equal") sort.Sort(ByPath(patch)) diff --git a/jsonpatch_geojson_test.go b/jsonpatch_geojson_test.go index 196ddfb..50ab49e 100644 --- a/jsonpatch_geojson_test.go +++ b/jsonpatch_geojson_test.go @@ -9,9 +9,12 @@ import ( var point = `{"type":"Point", "coordinates":[0.0, 1.0]}` var lineString = `{"type":"LineString", "coordinates":[[0.0, 1.0], [2.0, 3.0]]}` +var geotestCollections = Collections{ + arrays: []string{"$.coordinates"}, +} func TestPointLineStringReplace(t *testing.T) { - patch, e := CreatePatch([]byte(point), []byte(lineString), false) + patch, e := CreatePatch([]byte(point), []byte(lineString), geotestCollections, PatchStrategyExactMatch) assert.NoError(t, e) assert.Equal(t, len(patch), 3, "they should be equal") sort.Sort(ByPath(patch)) @@ -30,7 +33,7 @@ func TestPointLineStringReplace(t *testing.T) { } func TestLineStringPointReplace(t *testing.T) { - patch, e := CreatePatch([]byte(lineString), []byte(point), false) + patch, e := CreatePatch([]byte(lineString), []byte(point), geotestCollections, PatchStrategyExactMatch) assert.NoError(t, e) assert.Equal(t, len(patch), 3, "they should be equal") sort.Sort(ByPath(patch)) diff --git a/jsonpatch_hypercomplex_test.go b/jsonpatch_hypercomplex_test.go index bfc521c..526b8a0 100644 --- a/jsonpatch_hypercomplex_test.go +++ b/jsonpatch_hypercomplex_test.go @@ -155,14 +155,18 @@ var hyperComplexA = ` ] }` +var hyperComplexTestCollections = Collections{ + arrays: []string{"$.goods", "$.goods[*].batters.batter", "$.goods[*].topping"}, +} + func TestHyperComplexSame(t *testing.T) { - patch, e := CreatePatch([]byte(hyperComplexBase), []byte(hyperComplexBase), false) + patch, e := CreatePatch([]byte(hyperComplexBase), []byte(hyperComplexBase), hyperComplexTestCollections, PatchStrategyExactMatch) assert.NoError(t, e) assert.Equal(t, len(patch), 0, "they should be equal") } func TestHyperComplexBoolReplace(t *testing.T) { - patch, e := CreatePatch([]byte(hyperComplexBase), []byte(hyperComplexA), false) + patch, e := CreatePatch([]byte(hyperComplexBase), []byte(hyperComplexA), hyperComplexTestCollections, PatchStrategyExactMatch) assert.NoError(t, e) assert.Equal(t, 3, len(patch), "they should be equal") sort.Sort(ByPath(patch)) diff --git a/jsonpatch_simple_test.go b/jsonpatch_simple_test.go index 47e9cdd..a896cc0 100644 --- a/jsonpatch_simple_test.go +++ b/jsonpatch_simple_test.go @@ -17,7 +17,7 @@ var simpleG = `{"a":100, "b":null, "d":"foo"}` var empty = `{}` func TestOneNullReplace(t *testing.T) { - patch, e := CreatePatch([]byte(simplef), []byte(simpleG), false) + patch, e := CreatePatch([]byte(simplef), []byte(simpleG), Collections{}, PatchStrategyExactMatch) assert.NoError(t, e) assert.Equal(t, len(patch), 1, "they should be equal") change := patch[0] @@ -27,13 +27,13 @@ func TestOneNullReplace(t *testing.T) { } func TestSame(t *testing.T) { - patch, e := CreatePatch([]byte(simpleA), []byte(simpleA), false) + patch, e := CreatePatch([]byte(simpleA), []byte(simpleA), Collections{}, PatchStrategyExactMatch) assert.NoError(t, e) assert.Equal(t, len(patch), 0, "they should be equal") } func TestOneStringReplace(t *testing.T) { - patch, e := CreatePatch([]byte(simpleA), []byte(simpleB), false) + patch, e := CreatePatch([]byte(simpleA), []byte(simpleB), Collections{}, PatchStrategyExactMatch) assert.NoError(t, e) assert.Equal(t, len(patch), 1, "they should be equal") change := patch[0] @@ -43,7 +43,7 @@ func TestOneStringReplace(t *testing.T) { } func TestOneIntReplace(t *testing.T) { - patch, e := CreatePatch([]byte(simpleA), []byte(simpleC), false) + patch, e := CreatePatch([]byte(simpleA), []byte(simpleC), Collections{}, PatchStrategyExactMatch) assert.NoError(t, e) assert.Equal(t, len(patch), 1, "they should be equal") change := patch[0] @@ -54,7 +54,7 @@ func TestOneIntReplace(t *testing.T) { } func TestOneAdd(t *testing.T) { - patch, e := CreatePatch([]byte(simpleA), []byte(simpleD), false) + patch, e := CreatePatch([]byte(simpleA), []byte(simpleD), Collections{}, PatchStrategyExactMatch) assert.NoError(t, e) assert.Equal(t, len(patch), 1, "they should be equal") change := patch[0] @@ -64,7 +64,7 @@ func TestOneAdd(t *testing.T) { } func TestOneRemove(t *testing.T) { - patch, e := CreatePatch([]byte(simpleA), []byte(simpleE), false) + patch, e := CreatePatch([]byte(simpleA), []byte(simpleE), Collections{}, PatchStrategyExactMatch) assert.NoError(t, e) assert.Equal(t, len(patch), 1, "they should be equal") change := patch[0] @@ -74,7 +74,7 @@ func TestOneRemove(t *testing.T) { } func TestVsEmpty(t *testing.T) { - patch, e := CreatePatch([]byte(simpleA), []byte(empty), false) + patch, e := CreatePatch([]byte(simpleA), []byte(empty), Collections{}, PatchStrategyExactMatch) assert.NoError(t, e) assert.Equal(t, len(patch), 3, "they should be equal") sort.Sort(ByPath(patch)) @@ -101,7 +101,7 @@ func BenchmarkBigArrays(b *testing.B) { a2[i+1] = i } for i := 0; i < b.N; i++ { - compareArray(a1, a2, "/", NewExactMatchStrategy(false)) + compareArray(a1, a2, "/", PatchStrategyExactMatch, Collections{}) } } @@ -115,6 +115,6 @@ func BenchmarkBigArrays2(b *testing.B) { a2[i] = i } for i := 0; i < b.N; i++ { - compareArray(a1, a2, "/", NewExactMatchStrategy(false)) + compareArray(a1, a2, "/", PatchStrategyExactMatch, Collections{}) } } diff --git a/jsonpatch_strategy_ensure_exists_test.go b/jsonpatch_strategy_ensure_exists_test.go index f5e4c87..ab1e4cf 100644 --- a/jsonpatch_strategy_ensure_exists_test.go +++ b/jsonpatch_strategy_ensure_exists_test.go @@ -22,27 +22,27 @@ var simpleObjAddKeyValueArrayItem = `{"t":[{"k":3, "v":3}]}` var simpleObjModifyKeyValueArrayItem = `{"t":[{"k":2, "v":3}]}` var simpleObjAddDuplicateKeyValueArrayItem = `{"t":[{"k":2, "v":2}]}` var complexNextedKeyValueArray = `{ - "a":100, + "a":100, "t":[ - {"k":1, - "v":[ - {"nk":11, "c":"x", "d":[1,2]}, - {"nk":22, "c":"y", "d":[3,4]} - ] - }, - {"k":2, - "v":[ - {"nk":33, "c":"z", "d":[5,6]} - ] - } + {"k":1, + "v":[ + {"nk":11, "c":"x", "d":[1,2]}, + {"nk":22, "c":"y", "d":[3,4]} + ] + }, + {"k":2, + "v":[ + {"nk":33, "c":"z", "d":[5,6]} + ] + } ]}` var complexNextedKeyValueArrayModifyItem = `{ "t":[ - {"k":2, - "v":[ - {"nk":33, "c":"zz", "d":[7,8]} - ] - } + {"k":2, + "v":[ + {"nk":33, "c":"zz", "d":[7,8]} + ] + } ]}` var nestedObj = `{"a":100, "b":{"c":200}}` @@ -51,8 +51,16 @@ var nestedObjAddProp = `{"b":{"d":"hello"}}` var nestedObjPrimitiveArray = `{"a":100, "b":{"c":[200]}}` var nestedObjAddPrimitiveArrayItem = `{"b":{"c":[250]}}` +var ensureExistsStrategyTestCollections = Collections{ + entitySets: EntitySets{ + Path("$.t"): Key("k"), + Path("$.t[*].v"): Key("nk"), + }, + arrays: []string{}, // No arrays in this test, only sets +} + func TestCreatePatch_ModifyProperty_GeneratesReplaceOperation(t *testing.T) { - patch, err := CreatePatch_StrategyEnsureExists([]byte(simpleObj), []byte(simpleObjModifyProp)) + patch, err := CreatePatch([]byte(simpleObj), []byte(simpleObjModifyProp), ensureExistsStrategyTestCollections, PatchStrategyEnsureExists) assert.NoError(t, err) assert.Equal(t, 1, len(patch), "they should be equal") change := patch[0] @@ -63,7 +71,7 @@ func TestCreatePatch_ModifyProperty_GeneratesReplaceOperation(t *testing.T) { } func TestCreatePatch_AddProperty_GeneratesAddOperation(t *testing.T) { - patch, err := CreatePatch_StrategyEnsureExists([]byte(simpleObj), []byte(simpleObjAddProp)) + patch, err := CreatePatch([]byte(simpleObj), []byte(simpleObjAddProp), ensureExistsStrategyTestCollections, PatchStrategyEnsureExists) assert.NoError(t, err) assert.Equal(t, 1, len(patch), "they should be equal") change := patch[0] @@ -73,7 +81,7 @@ func TestCreatePatch_AddProperty_GeneratesAddOperation(t *testing.T) { } func TestCreatePatch_NestedObject_ModifyProperty_GeneratesReplaceOperation(t *testing.T) { - patch, err := CreatePatch_StrategyEnsureExists([]byte(nestedObj), []byte(nestedObjModifyProp)) + patch, err := CreatePatch([]byte(nestedObj), []byte(nestedObjModifyProp), ensureExistsStrategyTestCollections, PatchStrategyEnsureExists) assert.NoError(t, err) assert.Equal(t, 1, len(patch), "they should be equal") change := patch[0] @@ -84,7 +92,7 @@ func TestCreatePatch_NestedObject_ModifyProperty_GeneratesReplaceOperation(t *te } func TestCreatePatch_NestedObject_AddProperty_GeneratesAddOperation(t *testing.T) { - patch, err := CreatePatch_StrategyEnsureExists([]byte(nestedObj), []byte(nestedObjAddProp)) + patch, err := CreatePatch([]byte(nestedObj), []byte(nestedObjAddProp), ensureExistsStrategyTestCollections, PatchStrategyEnsureExists) assert.NoError(t, err) assert.Equal(t, 1, len(patch), "they should be equal") change := patch[0] @@ -94,7 +102,7 @@ func TestCreatePatch_NestedObject_AddProperty_GeneratesAddOperation(t *testing.T } func TestCreatePatch_EmptyPrimitiveArray_AddItem_GeneratesAddOperation(t *testing.T) { - patch, err := CreatePatch_StrategyEnsureExists([]byte(simpleObjEmtpyPrmitiveArray), []byte(simpleObjAddPrimitiveArrayItem)) + patch, err := CreatePatch([]byte(simpleObjEmtpyPrmitiveArray), []byte(simpleObjAddPrimitiveArrayItem), ensureExistsStrategyTestCollections, PatchStrategyEnsureExists) assert.NoError(t, err) assert.Equal(t, 1, len(patch), "they should be equal") change := patch[0] @@ -105,7 +113,7 @@ func TestCreatePatch_EmptyPrimitiveArray_AddItem_GeneratesAddOperation(t *testin } func TestCreatePatch_SingletonPrimitiveArray_AddItem_GeneratesAddOperation(t *testing.T) { - patch, err := CreatePatch_StrategyEnsureExists([]byte(simpleObjSingletonPrimitiveArray), []byte(simpleObjAddPrimitiveArrayItem)) + patch, err := CreatePatch([]byte(simpleObjSingletonPrimitiveArray), []byte(simpleObjAddPrimitiveArrayItem), ensureExistsStrategyTestCollections, PatchStrategyEnsureExists) assert.NoError(t, err) assert.Equal(t, 1, len(patch), "they should be equal") change := patch[0] @@ -116,7 +124,7 @@ func TestCreatePatch_SingletonPrimitiveArray_AddItem_GeneratesAddOperation(t *te } func TestCreatePatch_MultipleItemPrimitiveArray_AddItem_GeneratesAddOperation(t *testing.T) { - patch, err := CreatePatch_StrategyEnsureExists([]byte(simpleObjMultipleItemPrimitiveArray), []byte(simpleObjAddPrimitiveArrayItem)) + patch, err := CreatePatch([]byte(simpleObjMultipleItemPrimitiveArray), []byte(simpleObjAddPrimitiveArrayItem), ensureExistsStrategyTestCollections, PatchStrategyEnsureExists) assert.NoError(t, err) assert.Equal(t, 1, len(patch), "they should be equal") change := patch[0] @@ -127,13 +135,13 @@ func TestCreatePatch_MultipleItemPrimitiveArray_AddItem_GeneratesAddOperation(t } func TestCreatePatch_SingletonPrimitiveArray_AddDuplicateItem_GeneratesNoOperations(t *testing.T) { - patch, err := CreatePatch_StrategyEnsureExists([]byte(simpleObjMultipleItemPrimitiveArray), []byte(simpleObjAddDuplicateArrayItem)) + patch, err := CreatePatch([]byte(simpleObjMultipleItemPrimitiveArray), []byte(simpleObjAddDuplicateArrayItem), ensureExistsStrategyTestCollections, PatchStrategyEnsureExists) assert.NoError(t, err) assert.Equal(t, 0, len(patch), "they should be equal") } func TestCreatePatch_NestedObject_PrimitiveArray_AddItem_GeneratesAddOperation(t *testing.T) { - patch, err := CreatePatch_StrategyEnsureExists([]byte(nestedObjPrimitiveArray), []byte(nestedObjAddPrimitiveArrayItem)) + patch, err := CreatePatch([]byte(nestedObjPrimitiveArray), []byte(nestedObjAddPrimitiveArrayItem), ensureExistsStrategyTestCollections, PatchStrategyEnsureExists) assert.NoError(t, err) assert.Equal(t, 1, len(patch), "they should be equal") change := patch[0] @@ -144,7 +152,7 @@ func TestCreatePatch_NestedObject_PrimitiveArray_AddItem_GeneratesAddOperation(t } func TestCreatePatch_SingletonObjectArray_AddItem_GeneratesAddOperation(t *testing.T) { - patch, err := CreatePatch_StrategyEnsureExists([]byte(simpleObjSingletonObjectArray), []byte(simpleObjAddObjectArrayItem)) + patch, err := CreatePatch([]byte(simpleObjSingletonObjectArray), []byte(simpleObjAddObjectArrayItem), ensureExistsStrategyTestCollections, PatchStrategyEnsureExists) assert.NoError(t, err) assert.Equal(t, 1, len(patch), "they should be equal") change := patch[0] @@ -155,7 +163,7 @@ func TestCreatePatch_SingletonObjectArray_AddItem_GeneratesAddOperation(t *testi } func TestCreatePatch_KeyValueArray_AddItem_GeneratesAddOperation(t *testing.T) { - patch, err := CreatePatch_StrategyEnsureExists([]byte(simpleObjKeyValueArray), []byte(simpleObjAddKeyValueArrayItem)) + patch, err := CreatePatch([]byte(simpleObjKeyValueArray), []byte(simpleObjAddKeyValueArrayItem), ensureExistsStrategyTestCollections, PatchStrategyEnsureExists) assert.NoError(t, err) assert.Equal(t, 1, len(patch), "they should be equal") change := patch[0] @@ -166,13 +174,13 @@ func TestCreatePatch_KeyValueArray_AddItem_GeneratesAddOperation(t *testing.T) { } func TestCreatePatch_SingletonObjectArray_AddDuplicateItem_GeneratesNoOperations(t *testing.T) { - patch, err := CreatePatch_StrategyEnsureExists([]byte(simpleObjSingletonObjectArray), []byte(simpleObjAddDuplicateObjectArrayItem)) + patch, err := CreatePatch([]byte(simpleObjSingletonObjectArray), []byte(simpleObjAddDuplicateObjectArrayItem), ensureExistsStrategyTestCollections, PatchStrategyEnsureExists) assert.NoError(t, err) assert.Equal(t, 0, len(patch), "they should be equal") } func TestCreatePatch_KeyValueArray_ModifyItem_GeneratesReplaceOperation(t *testing.T) { - patch, err := CreatePatch_StrategyEnsureExists([]byte(simpleObjKeyValueArray), []byte(simpleObjModifyKeyValueArrayItem)) + patch, err := CreatePatch([]byte(simpleObjKeyValueArray), []byte(simpleObjModifyKeyValueArrayItem), ensureExistsStrategyTestCollections, PatchStrategyEnsureExists) assert.NoError(t, err) assert.Equal(t, 1, len(patch), "they should be equal") change := patch[0] @@ -183,13 +191,13 @@ func TestCreatePatch_KeyValueArray_ModifyItem_GeneratesReplaceOperation(t *testi } func TestCreatePatch_KeyValueArray_AddDuplicateItem_GeneratesNoOperations(t *testing.T) { - patch, err := CreatePatch_StrategyEnsureExists([]byte(simpleObjKeyValueArray), []byte(simpleObjAddDuplicateKeyValueArrayItem)) + patch, err := CreatePatch([]byte(simpleObjKeyValueArray), []byte(simpleObjAddDuplicateKeyValueArrayItem), ensureExistsStrategyTestCollections, PatchStrategyEnsureExists) assert.NoError(t, err) assert.Equal(t, 0, len(patch), "they should be equal") } func TestCreatePatch_ComplexNestedKeyValueArray_ModifyItem_GeneratesReplaceOperation(t *testing.T) { - patch, err := CreatePatch_StrategyEnsureExists([]byte(complexNextedKeyValueArray), []byte(complexNextedKeyValueArrayModifyItem)) + patch, err := CreatePatch([]byte(complexNextedKeyValueArray), []byte(complexNextedKeyValueArrayModifyItem), ensureExistsStrategyTestCollections, PatchStrategyEnsureExists) assert.NoError(t, err) assert.Equal(t, 3, len(patch), "they should be equal") change := patch[0] diff --git a/jsonpatch_supercomplex_test.go b/jsonpatch_supercomplex_test.go index f222a5e..5c80a22 100644 --- a/jsonpatch_supercomplex_test.go +++ b/jsonpatch_supercomplex_test.go @@ -7,501 +7,506 @@ import ( ) var superComplexBase = ` -{ - "annotations": { - "annotation": [ - { - "name": "version", - "value": "8" - }, - { - "name": "versionTag", - "value": "Published on May 13, 2015 at 8:48pm (MST)" - } - ] - }, - "attributes": { - "attribute-key": [ - { - "id": "3b05c943-d81a-436f-b242-8b519e7a6f30", - "properties": { - "visible": true - } - }, - { - "id": "d794c7ee-2a4b-4da4-bba7-e8b973d50c4b", - "properties": { - "visible": true - } - }, - { - "id": "a0259458-517c-480f-9f04-9b54b1b2af1f", - "properties": { - "visible": true - } - }, - { - "id": "9415f39d-c396-4458-9019-fc076c847964", - "properties": { - "visible": true - } - }, - { - "id": "0a2e49a9-8989-42fb-97da-cc66334f828b", - "properties": { - "visible": true - } - }, - { - "id": "27f5f14a-ea97-4feb-b22a-6ff754a31212", - "properties": { - "visible": true - } - }, - { - "id": "6f810508-4615-4fd0-9e87-80f9c94f9ad8", - "properties": { - "visible": true - } - }, - { - "id": "3451b1b2-7365-455c-8bb1-0b464d4d3ba1", - "properties": { - "visible": true - } - }, - { - "id": "a82ec957-8c26-41ea-8af6-6dd75c384801", - "properties": { - "visible": true - } - }, - { - "id": "736c5496-9a6e-4a82-aa00-456725796432", - "properties": { - "visible": true - } - }, - { - "id": "2d428b3c-9d3b-4ec1-bf98-e00673599d60", - "properties": { - "visible": true - } - }, - { - "id": "68566ebb-811d-4337-aba9-a8a8baf90e4b", - "properties": { - "visible": true - } - }, - { - "id": "ca88bab1-a1ea-40cc-8f96-96d1e9f1217d", - "properties": { - "visible": true - } - }, - { - "id": "c63a12c8-542d-47f3-bee1-30b5fe2b0690", - "properties": { - "visible": true - } - }, - { - "id": "cbd9e3bc-6a49-432a-a906-b1674c1de24c", - "properties": { - "visible": true - } - }, - { - "id": "03262f07-8a15-416d-a3f5-e2bf561c78f9", - "properties": { - "visible": true - } - }, - { - "id": "e5c93b87-83fc-45b6-b4d5-bf1e3f523075", - "properties": { - "visible": true - } - }, - { - "id": "72260ac5-3d51-49d7-bb31-f794dd129f1c", - "properties": { - "visible": true - } - }, - { - "id": "d856bde1-1b42-4935-9bee-c37e886c9ecf", - "properties": { - "visible": true - } - }, - { - "id": "62380509-bedf-4134-95c3-77ff377a4a6a", - "properties": { - "visible": true - } - }, - { - "id": "f4ed5ac9-b386-49a6-a0a0-6f3341ce9021", - "properties": { - "visible": true - } - }, - { - "id": "528d2bd2-87fe-4a49-954a-c93a03256929", - "properties": { - "visible": true - } - }, - { - "id": "ff8951f1-61a7-416b-9223-fac4bb6dac50", - "properties": { - "visible": true - } - }, - { - "id": "95c2b011-d782-4042-8a07-6aa4a5765c2e", - "properties": { - "visible": true - } - }, - { - "id": "dbe5837b-0624-4a05-91f3-67b5bd9b812a", - "properties": { - "visible": true - } - }, - { - "id": "13f198ed-82ab-4e51-8144-bfaa5bf77fd5", - "properties": { - "visible": true - } - }, - { - "id": "025312eb-12b6-47e6-9750-0fb31ddc2111", - "properties": { - "visible": true - } - }, - { - "id": "24292d58-db66-4ef3-8f4f-005d7b719433", - "properties": { - "visible": true - } - }, - { - "id": "22e5b5c4-821c-413a-a5b1-ab866d9a03bb", - "properties": { - "visible": true - } - }, - { - "id": "2fde0aac-df89-403d-998e-854b949c7b57", - "properties": { - "visible": true - } - }, - { - "id": "8b576876-5c16-4178-805e-24984c24fac3", - "properties": { - "visible": true - } - }, - { - "id": "415b7d2a-b362-4f1e-b83a-927802328ecb", - "properties": { - "visible": true - } - }, - { - "id": "8ef24fc2-ab25-4f22-9d9f-61902b49dc01", - "properties": { - "visible": true - } - }, - { - "id": "2299b09e-9f8e-4b79-a55c-a7edacde2c85", - "properties": { - "visible": true - } - }, - { - "id": "bf506538-f438-425c-be85-5aa2f9b075b8", - "properties": { - "visible": true - } - }, - { - "id": "2b501dc6-799d-4675-9144-fac77c50c57c", - "properties": { - "visible": true - } - }, - { - "id": "c0446da1-e069-417e-bd5a-34edcd028edc", - "properties": { - "visible": true - } - } - ] - } -}` + { + "annotations": { + "annotation": [ + { + "name": "version", + "value": "8" + }, + { + "name": "versionTag", + "value": "Published on May 13, 2015 at 8:48pm (MST)" + } + ] + }, + "attributes": { + "attribute-key": [ + { + "id": "3b05c943-d81a-436f-b242-8b519e7a6f30", + "properties": { + "visible": true + } + }, + { + "id": "d794c7ee-2a4b-4da4-bba7-e8b973d50c4b", + "properties": { + "visible": true + } + }, + { + "id": "a0259458-517c-480f-9f04-9b54b1b2af1f", + "properties": { + "visible": true + } + }, + { + "id": "9415f39d-c396-4458-9019-fc076c847964", + "properties": { + "visible": true + } + }, + { + "id": "0a2e49a9-8989-42fb-97da-cc66334f828b", + "properties": { + "visible": true + } + }, + { + "id": "27f5f14a-ea97-4feb-b22a-6ff754a31212", + "properties": { + "visible": true + } + }, + { + "id": "6f810508-4615-4fd0-9e87-80f9c94f9ad8", + "properties": { + "visible": true + } + }, + { + "id": "3451b1b2-7365-455c-8bb1-0b464d4d3ba1", + "properties": { + "visible": true + } + }, + { + "id": "a82ec957-8c26-41ea-8af6-6dd75c384801", + "properties": { + "visible": true + } + }, + { + "id": "736c5496-9a6e-4a82-aa00-456725796432", + "properties": { + "visible": true + } + }, + { + "id": "2d428b3c-9d3b-4ec1-bf98-e00673599d60", + "properties": { + "visible": true + } + }, + { + "id": "68566ebb-811d-4337-aba9-a8a8baf90e4b", + "properties": { + "visible": true + } + }, + { + "id": "ca88bab1-a1ea-40cc-8f96-96d1e9f1217d", + "properties": { + "visible": true + } + }, + { + "id": "c63a12c8-542d-47f3-bee1-30b5fe2b0690", + "properties": { + "visible": true + } + }, + { + "id": "cbd9e3bc-6a49-432a-a906-b1674c1de24c", + "properties": { + "visible": true + } + }, + { + "id": "03262f07-8a15-416d-a3f5-e2bf561c78f9", + "properties": { + "visible": true + } + }, + { + "id": "e5c93b87-83fc-45b6-b4d5-bf1e3f523075", + "properties": { + "visible": true + } + }, + { + "id": "72260ac5-3d51-49d7-bb31-f794dd129f1c", + "properties": { + "visible": true + } + }, + { + "id": "d856bde1-1b42-4935-9bee-c37e886c9ecf", + "properties": { + "visible": true + } + }, + { + "id": "62380509-bedf-4134-95c3-77ff377a4a6a", + "properties": { + "visible": true + } + }, + { + "id": "f4ed5ac9-b386-49a6-a0a0-6f3341ce9021", + "properties": { + "visible": true + } + }, + { + "id": "528d2bd2-87fe-4a49-954a-c93a03256929", + "properties": { + "visible": true + } + }, + { + "id": "ff8951f1-61a7-416b-9223-fac4bb6dac50", + "properties": { + "visible": true + } + }, + { + "id": "95c2b011-d782-4042-8a07-6aa4a5765c2e", + "properties": { + "visible": true + } + }, + { + "id": "dbe5837b-0624-4a05-91f3-67b5bd9b812a", + "properties": { + "visible": true + } + }, + { + "id": "13f198ed-82ab-4e51-8144-bfaa5bf77fd5", + "properties": { + "visible": true + } + }, + { + "id": "025312eb-12b6-47e6-9750-0fb31ddc2111", + "properties": { + "visible": true + } + }, + { + "id": "24292d58-db66-4ef3-8f4f-005d7b719433", + "properties": { + "visible": true + } + }, + { + "id": "22e5b5c4-821c-413a-a5b1-ab866d9a03bb", + "properties": { + "visible": true + } + }, + { + "id": "2fde0aac-df89-403d-998e-854b949c7b57", + "properties": { + "visible": true + } + }, + { + "id": "8b576876-5c16-4178-805e-24984c24fac3", + "properties": { + "visible": true + } + }, + { + "id": "415b7d2a-b362-4f1e-b83a-927802328ecb", + "properties": { + "visible": true + } + }, + { + "id": "8ef24fc2-ab25-4f22-9d9f-61902b49dc01", + "properties": { + "visible": true + } + }, + { + "id": "2299b09e-9f8e-4b79-a55c-a7edacde2c85", + "properties": { + "visible": true + } + }, + { + "id": "bf506538-f438-425c-be85-5aa2f9b075b8", + "properties": { + "visible": true + } + }, + { + "id": "2b501dc6-799d-4675-9144-fac77c50c57c", + "properties": { + "visible": true + } + }, + { + "id": "c0446da1-e069-417e-bd5a-34edcd028edc", + "properties": { + "visible": true + } + } + ] + } + }` var superComplexA = ` -{ - "annotations": { - "annotation": [ - { - "name": "version", - "value": "8" - }, - { - "name": "versionTag", - "value": "Published on May 13, 2015 at 8:48pm (MST)" - } - ] - }, - "attributes": { - "attribute-key": [ - { - "id": "3b05c943-d81a-436f-b242-8b519e7a6f30", - "properties": { - "visible": true - } - }, - { - "id": "d794c7ee-2a4b-4da4-bba7-e8b973d50c4b", - "properties": { - "visible": true - } - }, - { - "id": "a0259458-517c-480f-9f04-9b54b1b2af1f", - "properties": { - "visible": true - } - }, - { - "id": "9415f39d-c396-4458-9019-fc076c847964", - "properties": { - "visible": true - } - }, - { - "id": "0a2e49a9-8989-42fb-97da-cc66334f828b", - "properties": { - "visible": true - } - }, - { - "id": "27f5f14a-ea97-4feb-b22a-6ff754a31212", - "properties": { - "visible": true - } - }, - { - "id": "6f810508-4615-4fd0-9e87-80f9c94f9ad8", - "properties": { - "visible": true - } - }, - { - "id": "3451b1b2-7365-455c-8bb1-0b464d4d3ba1", - "properties": { - "visible": true - } - }, - { - "id": "a82ec957-8c26-41ea-8af6-6dd75c384801", - "properties": { - "visible": true - } - }, - { - "id": "736c5496-9a6e-4a82-aa00-456725796432", - "properties": { - "visible": true - } - }, - { - "id": "2d428b3c-9d3b-4ec1-bf98-e00673599d60", - "properties": { - "visible": true - } - }, - { - "id": "68566ebb-811d-4337-aba9-a8a8baf90e4b", - "properties": { - "visible": true - } - }, - { - "id": "ca88bab1-a1ea-40cc-8f96-96d1e9f1217d", - "properties": { - "visible": true - } - }, - { - "id": "c63a12c8-542d-47f3-bee1-30b5fe2b0690", - "properties": { - "visible": true - } - }, - { - "id": "cbd9e3bc-6a49-432a-a906-b1674c1de24c", - "properties": { - "visible": true - } - }, - { - "id": "03262f07-8a15-416d-a3f5-e2bf561c78f9", - "properties": { - "visible": true - } - }, - { - "id": "e5c93b87-83fc-45b6-b4d5-bf1e3f523075", - "properties": { - "visible": true - } - }, - { - "id": "72260ac5-3d51-49d7-bb31-f794dd129f1c", - "properties": { - "visible": true - } - }, - { - "id": "d856bde1-1b42-4935-9bee-c37e886c9ecf", - "properties": { - "visible": true - } - }, - { - "id": "62380509-bedf-4134-95c3-77ff377a4a6a", - "properties": { - "visible": true - } - }, - { - "id": "f4ed5ac9-b386-49a6-a0a0-6f3341ce9021", - "properties": { - "visible": true - } - }, - { - "id": "528d2bd2-87fe-4a49-954a-c93a03256929", - "properties": { - "visible": true - } - }, - { - "id": "ff8951f1-61a7-416b-9223-fac4bb6dac50", - "properties": { - "visible": true - } - }, - { - "id": "95c2b011-d782-4042-8a07-6aa4a5765c2e", - "properties": { - "visible": true - } - }, - { - "id": "dbe5837b-0624-4a05-91f3-67b5bd9b812a", - "properties": { - "visible": true - } - }, - { - "id": "13f198ed-82ab-4e51-8144-bfaa5bf77fd5", - "properties": { - "visible": true - } - }, - { - "id": "025312eb-12b6-47e6-9750-0fb31ddc2111", - "properties": { - "visible": true - } - }, - { - "id": "24292d58-db66-4ef3-8f4f-005d7b719433", - "properties": { - "visible": true - } - }, - { - "id": "22e5b5c4-821c-413a-a5b1-ab866d9a03bb", - "properties": { - "visible": true - } - }, - { - "id": "2fde0aac-df89-403d-998e-854b949c7b57", - "properties": { - "visible": true - } - }, - { - "id": "8b576876-5c16-4178-805e-24984c24fac3", - "properties": { - "visible": true - } - }, - { - "id": "415b7d2a-b362-4f1e-b83a-927802328ecb", - "properties": { - "visible": true - } - }, - { - "id": "8ef24fc2-ab25-4f22-9d9f-61902b49dc01", - "properties": { - "visible": true - } - }, - { - "id": "2299b09e-9f8e-4b79-a55c-a7edacde2c85", - "properties": { - "visible": true - } - }, - { - "id": "bf506538-f438-425c-be85-5aa2f9b075b8", - "properties": { - "visible": true - } - }, - { - "id": "2b501dc6-799d-4675-9144-fac77c50c57c", - "properties": { - "visible": true - } - }, - { - "id": "c0446da1-e069-417e-bd5a-34edcd028edc", - "properties": { - "visible": false - } - } - ] - } -}` + { + "annotations": { + "annotation": [ + { + "name": "version", + "value": "8" + }, + { + "name": "versionTag", + "value": "Published on May 13, 2015 at 8:48pm (MST)" + } + ] + }, + "attributes": { + "attribute-key": [ + { + "id": "3b05c943-d81a-436f-b242-8b519e7a6f30", + "properties": { + "visible": true + } + }, + { + "id": "d794c7ee-2a4b-4da4-bba7-e8b973d50c4b", + "properties": { + "visible": true + } + }, + { + "id": "a0259458-517c-480f-9f04-9b54b1b2af1f", + "properties": { + "visible": true + } + }, + { + "id": "9415f39d-c396-4458-9019-fc076c847964", + "properties": { + "visible": true + } + }, + { + "id": "0a2e49a9-8989-42fb-97da-cc66334f828b", + "properties": { + "visible": true + } + }, + { + "id": "27f5f14a-ea97-4feb-b22a-6ff754a31212", + "properties": { + "visible": true + } + }, + { + "id": "6f810508-4615-4fd0-9e87-80f9c94f9ad8", + "properties": { + "visible": true + } + }, + { + "id": "3451b1b2-7365-455c-8bb1-0b464d4d3ba1", + "properties": { + "visible": true + } + }, + { + "id": "a82ec957-8c26-41ea-8af6-6dd75c384801", + "properties": { + "visible": true + } + }, + { + "id": "736c5496-9a6e-4a82-aa00-456725796432", + "properties": { + "visible": true + } + }, + { + "id": "2d428b3c-9d3b-4ec1-bf98-e00673599d60", + "properties": { + "visible": true + } + }, + { + "id": "68566ebb-811d-4337-aba9-a8a8baf90e4b", + "properties": { + "visible": true + } + }, + { + "id": "ca88bab1-a1ea-40cc-8f96-96d1e9f1217d", + "properties": { + "visible": true + } + }, + { + "id": "c63a12c8-542d-47f3-bee1-30b5fe2b0690", + "properties": { + "visible": true + } + }, + { + "id": "cbd9e3bc-6a49-432a-a906-b1674c1de24c", + "properties": { + "visible": true + } + }, + { + "id": "03262f07-8a15-416d-a3f5-e2bf561c78f9", + "properties": { + "visible": true + } + }, + { + "id": "e5c93b87-83fc-45b6-b4d5-bf1e3f523075", + "properties": { + "visible": true + } + }, + { + "id": "72260ac5-3d51-49d7-bb31-f794dd129f1c", + "properties": { + "visible": true + } + }, + { + "id": "d856bde1-1b42-4935-9bee-c37e886c9ecf", + "properties": { + "visible": true + } + }, + { + "id": "62380509-bedf-4134-95c3-77ff377a4a6a", + "properties": { + "visible": true + } + }, + { + "id": "f4ed5ac9-b386-49a6-a0a0-6f3341ce9021", + "properties": { + "visible": true + } + }, + { + "id": "528d2bd2-87fe-4a49-954a-c93a03256929", + "properties": { + "visible": true + } + }, + { + "id": "ff8951f1-61a7-416b-9223-fac4bb6dac50", + "properties": { + "visible": true + } + }, + { + "id": "95c2b011-d782-4042-8a07-6aa4a5765c2e", + "properties": { + "visible": true + } + }, + { + "id": "dbe5837b-0624-4a05-91f3-67b5bd9b812a", + "properties": { + "visible": true + } + }, + { + "id": "13f198ed-82ab-4e51-8144-bfaa5bf77fd5", + "properties": { + "visible": true + } + }, + { + "id": "025312eb-12b6-47e6-9750-0fb31ddc2111", + "properties": { + "visible": true + } + }, + { + "id": "24292d58-db66-4ef3-8f4f-005d7b719433", + "properties": { + "visible": true + } + }, + { + "id": "22e5b5c4-821c-413a-a5b1-ab866d9a03bb", + "properties": { + "visible": true + } + }, + { + "id": "2fde0aac-df89-403d-998e-854b949c7b57", + "properties": { + "visible": true + } + }, + { + "id": "8b576876-5c16-4178-805e-24984c24fac3", + "properties": { + "visible": true + } + }, + { + "id": "415b7d2a-b362-4f1e-b83a-927802328ecb", + "properties": { + "visible": true + } + }, + { + "id": "8ef24fc2-ab25-4f22-9d9f-61902b49dc01", + "properties": { + "visible": true + } + }, + { + "id": "2299b09e-9f8e-4b79-a55c-a7edacde2c85", + "properties": { + "visible": true + } + }, + { + "id": "bf506538-f438-425c-be85-5aa2f9b075b8", + "properties": { + "visible": true + } + }, + { + "id": "2b501dc6-799d-4675-9144-fac77c50c57c", + "properties": { + "visible": true + } + }, + { + "id": "c0446da1-e069-417e-bd5a-34edcd028edc", + "properties": { + "visible": false + } + } + ] + } + }` + +var superComplexTestCollections = Collections{ + entitySets: EntitySets{}, + arrays: []string{"$.annotations.annotation", "$.attributes.attribute-key"}, +} func TestSuperComplexSame(t *testing.T) { - patch, e := CreatePatch([]byte(superComplexBase), []byte(superComplexBase), false) + patch, e := CreatePatch([]byte(superComplexBase), []byte(superComplexBase), superComplexTestCollections, PatchStrategyExactMatch) assert.NoError(t, e) assert.Equal(t, 0, len(patch), "they should be equal") } func TestSuperComplexBoolReplace(t *testing.T) { - patch, e := CreatePatch([]byte(superComplexBase), []byte(superComplexA), false) + patch, e := CreatePatch([]byte(superComplexBase), []byte(superComplexA), superComplexTestCollections, PatchStrategyExactMatch) assert.NoError(t, e) assert.Equal(t, 1, len(patch), "they should be equal") change := patch[0] assert.Equal(t, "replace", change.Operation, "they should be equal") assert.Equal(t, "/attributes/attribute-key/36/properties/visible", change.Path, "they should be equal") - assert.Equal(t, false, change.Value, "they should be equal") + assert.Equal(t, change.Value, false, "they should be equal") } From 6f7080cd73b7b8448fadd65610471b68e6445585 Mon Sep 17 00:00:00 2001 From: Jeroen Soeters Date: Mon, 26 May 2025 19:20:00 -0500 Subject: [PATCH 07/12] Still refactoring --- jsonpatch.go | 163 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 104 insertions(+), 59 deletions(-) diff --git a/jsonpatch.go b/jsonpatch.go index 7700c57..b9eaf02 100644 --- a/jsonpatch.go +++ b/jsonpatch.go @@ -159,31 +159,6 @@ func CreatePatch(a, b []byte, collections Collections, strategy PatchStrategy) ( return handleValues(aI, bI, "", []JsonPatchOperation{}, strategy, collections) } -func CreatePatch_StrategyEnsureExists(a, b []byte) ([]JsonPatchOperation, error) { - var aI any - var bI any - - err := json.Unmarshal(a, &aI) - if err != nil { - return nil, errBadJSONDoc - } - err = json.Unmarshal(b, &bI) - if err != nil { - return nil, errBadJSONDoc - } - - entitySets := EntitySets{ - Path("$.t"): Key("k"), - Path("$.t[*].v"): Key("nk"), - } - collections := Collections{ - entitySets: entitySets, - arrays: []string{}, - } - - return handleValues(aI, bI, "", []JsonPatchOperation{}, PatchStrategyEnsureExists, collections) -} - // 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]any are given, all elements must match. @@ -385,6 +360,7 @@ func handleValues(av, bv any, p string, patch []JsonPatchOperation, strategy Pat bt, replaceWithOtherCollection := bv.([]any) switch { case !replaceWithOtherCollection: + // If the types are different, we replace the whole array patch = append(patch, NewPatch("replace", p, bv)) case collections.isArray(p) && len(at) != len(bt): patch = append(patch, compareArray(at, bt, p, strategy, collections)...) @@ -418,67 +394,137 @@ func handleValues(av, bv any, p string, patch []JsonPatchOperation, strategy Pat // compareArray generates remove and add operations for `av` and `bv`. func compareArray(av, bv []any, p string, strategy PatchStrategy, collections Collections) []JsonPatchOperation { retval := []JsonPatchOperation{} - ignoreArrayOrder := !collections.isArray(p) - - switch strategy { - case PatchStrategyExactMatch: - // If arrays have same elements in different order and we're ignoring order, return empty patch - if len(av) == len(bv) && matchesValue(av, bv, true) { - return retval - } - // Find elements that need to be removed - processArray(av, bv, func(i int, value any) { - retval = append(retval, NewPatch("remove", makePath(p, i), nil)) - }, strategy, ignoreArrayOrder) - reversed := make([]JsonPatchOperation, len(retval)) - for i := 0; i < len(retval); i++ { - reversed[len(retval)-1-i] = retval[i] + switch { + case collections.isArray(p): + if strategy == PatchStrategyExactMatch { + // Find elements that need to be removed + processArray(av, bv, func(i int, value any) { + retval = append(retval, NewPatch("remove", makePath(p, i), nil)) + }, strategy) + reversed := make([]JsonPatchOperation, len(retval)) + for i := 0; i < len(retval); i++ { + reversed[len(retval)-1-i] = retval[i] + } + retval = reversed } - retval = reversed // Find elements that need to be added. // NOTE we pass in `bv` then `av` so that processArray can find the missing elements. processArray(bv, av, func(i int, value any) { retval = append(retval, NewPatch("add", makePath(p, i), value)) - }, strategy, ignoreArrayOrder) - case PatchStrategyEnsureExists: - if collections.isEntitySet(p) { - processIdentitySet(bv, av, p, func(i int, value any) { - retval = append(retval, NewPatch("add", makePath(p, i), value)) + }, strategy) + case collections.isEntitySet(p): + if len(av) == len(bv) && matchesValue(av, bv, true) { + return retval + } + // TODO: removing is not tested yest! + if strategy == PatchStrategyExactMatch { + // Find elements that need to be removed + processIdentitySet(av, bv, p, func(i int, value any) { + retval = append(retval, NewPatch("remove", makePath(p, i), nil)) }, func(ops []JsonPatchOperation) { retval = append(retval, ops...) }, strategy, collections) - } else { - processArray(bv, av, func(i int, value any) { - retval = append(retval, NewPatch("add", makePath(p, i), value)) - }, strategy, ignoreArrayOrder) + reversed := make([]JsonPatchOperation, len(retval)) + for i := 0; i < len(retval); i++ { + reversed[len(retval)-1-i] = retval[i] + } + retval = reversed } - case PatchStrategyEnsureAbsent: - return nil + processIdentitySet(bv, av, p, func(i int, value any) { + retval = append(retval, NewPatch("add", makePath(p, i), value)) + }, func(ops []JsonPatchOperation) { + retval = append(retval, ops...) + }, strategy, collections) + default: // default to set + if len(av) == len(bv) && matchesValue(av, bv, true) { + return retval + } + // TODO: removing is not tested yest! + // also we need to check for PatchStrategyEnsureAbsent + if strategy == PatchStrategyExactMatch { + // Find elements that need to be removed + processSet(av, bv, p, func(i int, value any) { + retval = append(retval, NewPatch("remove", makePath(p, i), nil)) + }, func(ops []JsonPatchOperation) { + retval = append(retval, ops...) + }, strategy, collections) + reversed := make([]JsonPatchOperation, len(retval)) + for i := 0; i < len(retval); i++ { + reversed[len(retval)-1-i] = retval[i] + } + retval = reversed + } + + processSet(bv, av, p, func(i int, value any) { + retval = append(retval, NewPatch("add", makePath(p, i), value)) + }, func(ops []JsonPatchOperation) { + retval = append(retval, ops...) + }, strategy, collections) } return retval } -func processIdentitySet(av, bv []any, path string, applyOp func(i int, value any), replaceOps func(ops []JsonPatchOperation), strategy PatchStrategy, collections Collections) { +func processSet(av, bv []any, path string, applyOp func(i int, value any), replaceOps func(ops []JsonPatchOperation), strategy PatchStrategy, collections Collections) { foundIndexes := make(map[int]struct{}, len(av)) - bvCounts := make(map[string]int) lookup := make(map[string]int) - bvSeen := make(map[string]int) // Track how many we've seen during processing offset := len(bv) for i, v := range bv { jsonBytes, err := json.Marshal(v) - if key, ok := collections.entitySets.Get(Path(toJsonPath(path))); ok { - jsonBytes, err = json.Marshal(v.(map[string]any)[string(key)]) + if err != nil { + continue // Skip if we can't marshal + } + jsonStr := string(jsonBytes) + lookup[jsonStr] = i + } + + // Check each element in av + for i, v := range av { + jsonBytes, err := json.Marshal(v) + if err != nil { + applyOp(i+offset, v) // If we can't marshal, treat it as not found + continue + } + + jsonStr := string(jsonBytes) + // If element exists in bv and we haven't seen all of them yet + if index, ok := lookup[jsonStr]; ok { + foundIndexes[i] = struct{}{} + updateOps, err := handleValues(bv[index], v, fmt.Sprintf("%s/%d", path, lookup[jsonStr]), []JsonPatchOperation{}, strategy, collections) + if err != nil { + return + } + replaceOps(updateOps) + } + } + + // Apply op for all elements in av that weren't found + for i, v := range av { + if _, ok := foundIndexes[i]; !ok { + applyOp(i+offset, v) + } + } +} + +func processIdentitySet(av, bv []any, path string, applyOp func(i int, value any), replaceOps func(ops []JsonPatchOperation), strategy PatchStrategy, collections Collections) { + foundIndexes := make(map[int]struct{}, len(av)) + lookup := make(map[string]int) + offset := len(bv) + + for i, v := range bv { + var key = "" + if _, ok := collections.entitySets.Get(Path(toJsonPath(path))); !ok { + continue // If we don't have a key for this path, skip } + jsonBytes, err := json.Marshal(v.(map[string]any)[string(key)]) if err != nil { continue // Skip if we can't marshal } jsonStr := string(jsonBytes) lookup[jsonStr] = i - bvCounts[jsonStr]++ } // Check each element in av @@ -496,7 +542,6 @@ func processIdentitySet(av, bv []any, path string, applyOp func(i int, value any // If element exists in bv and we haven't seen all of them yet if index, ok := lookup[jsonStr]; ok { foundIndexes[i] = struct{}{} - bvSeen[jsonStr]++ updateOps, err := handleValues(bv[index], v, fmt.Sprintf("%s/%d", path, lookup[jsonStr]), []JsonPatchOperation{}, strategy, collections) if err != nil { return From 986c9cf8f039669701367627b3f414b8a3fc316f Mon Sep 17 00:00:00 2001 From: Jeroen Soeters Date: Tue, 27 May 2025 13:21:21 -0500 Subject: [PATCH 08/12] Cleanup --- jsonpatch.go | 205 ++++++++++++++------------------------------------- 1 file changed, 56 insertions(+), 149 deletions(-) diff --git a/jsonpatch.go b/jsonpatch.go index b9eaf02..dcfc5a4 100644 --- a/jsonpatch.go +++ b/jsonpatch.go @@ -5,26 +5,13 @@ import ( "encoding/json" "fmt" "reflect" + "slices" "strconv" "strings" ) var errBadJSONDoc = fmt.Errorf("Invalid JSON Document") -type Strategy interface { - isStrategy() -} - -type ExactMatchStrategy struct { - ignoreArrayOrder bool -} - -func (ExactMatchStrategy) isStrategy() {} - -func NewExactMatchStrategy(ignoreArrayOrder bool) ExactMatchStrategy { - return ExactMatchStrategy{ignoreArrayOrder: ignoreArrayOrder} -} - type Path string type Key string type EntitySets map[Path]Key @@ -36,12 +23,7 @@ type Collections struct { func (c *Collections) isArray(path string) bool { jsonPath := toJsonPath(path) - for _, array := range c.arrays { - if jsonPath == array { - return true - } - } - return false + return slices.Contains(c.arrays, jsonPath) } func (c *Collections) isEntitySet(path string) bool { @@ -241,16 +223,17 @@ func matchesValue(av, bv any, ignoreArrayOrder bool) bool { } return true - } else { - // Order matters, check each element in order - for key := range at { - if !matchesValue(at[key], bt[key], ignoreArrayOrder) { - return false - } + } + // Order matters, check each element in order + for key := range at { + if !matchesValue(at[key], bt[key], ignoreArrayOrder) { + return false } - return true } + + return true } + return false } @@ -278,28 +261,28 @@ func makePath(path string, newPart any) string { // diff returns the (recursive) difference between a and b as an array of JsonPatchOperations. func diff(a, b map[string]any, path string, patch []JsonPatchOperation, strategy PatchStrategy, collections Collections) ([]JsonPatchOperation, error) { - switch strategy { - case PatchStrategyExactMatch: - for key, bv := range b { - p := makePath(path, key) - av, ok := a[key] - // If the key is not present in a, add it - 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, strategy, collections) - if err != nil { - return nil, err - } + //TODO: handle EnsureAbsent strategy + for key, bv := range b { + p := makePath(path, key) + av, ok := a[key] + // If the key is not present in a, add it + 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, strategy, collections) + if err != nil { + return nil, err } + } + if strategy == PatchStrategyExactMatch { // Now add all deleted values as nil for key := range a { _, found := b[key] @@ -309,35 +292,8 @@ func diff(a, b map[string]any, path string, patch []JsonPatchOperation, strategy patch = append(patch, NewPatch("remove", p, nil)) } } - return patch, nil - case PatchStrategyEnsureExists: - for key, bv := range b { - p := makePath(path, key) - av, ok := a[key] - // If key is not present, add it - 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, strategy, collections) - if err != nil { - return nil, err - } - } - // We don't generate remove operations in "ensure exists" mode - return patch, nil - case PatchStrategyEnsureAbsent: - fmt.Println("EnsureAbsent strategy is not implemented yet") } - - return nil, fmt.Errorf("Unknown strategy: %s", strategy) + return patch, nil } func handleValues(av, bv any, p string, patch []JsonPatchOperation, strategy PatchStrategy, collections Collections) ([]JsonPatchOperation, error) { @@ -403,7 +359,7 @@ func compareArray(av, bv []any, p string, strategy PatchStrategy, collections Co retval = append(retval, NewPatch("remove", makePath(p, i), nil)) }, strategy) reversed := make([]JsonPatchOperation, len(retval)) - for i := 0; i < len(retval); i++ { + for i := range retval { reversed[len(retval)-1-i] = retval[i] } retval = reversed @@ -427,7 +383,7 @@ func compareArray(av, bv []any, p string, strategy PatchStrategy, collections Co retval = append(retval, ops...) }, strategy, collections) reversed := make([]JsonPatchOperation, len(retval)) - for i := 0; i < len(retval); i++ { + for i := range retval { reversed[len(retval)-1-i] = retval[i] } retval = reversed @@ -445,29 +401,21 @@ func compareArray(av, bv []any, p string, strategy PatchStrategy, collections Co // also we need to check for PatchStrategyEnsureAbsent if strategy == PatchStrategyExactMatch { // Find elements that need to be removed - processSet(av, bv, p, func(i int, value any) { - retval = append(retval, NewPatch("remove", makePath(p, i), nil)) - }, func(ops []JsonPatchOperation) { - retval = append(retval, ops...) - }, strategy, collections) + processSet(av, bv, func(i int, value any) { retval = append(retval, NewPatch("remove", makePath(p, i), nil)) }) reversed := make([]JsonPatchOperation, len(retval)) - for i := 0; i < len(retval); i++ { + for i := range retval { reversed[len(retval)-1-i] = retval[i] } retval = reversed } - processSet(bv, av, p, func(i int, value any) { - retval = append(retval, NewPatch("add", makePath(p, i), value)) - }, func(ops []JsonPatchOperation) { - retval = append(retval, ops...) - }, strategy, collections) + processSet(bv, av, func(i int, value any) { retval = append(retval, NewPatch("add", makePath(p, i), value)) }) } return retval } -func processSet(av, bv []any, path string, applyOp func(i int, value any), replaceOps func(ops []JsonPatchOperation), strategy PatchStrategy, collections Collections) { +func processSet(av, bv []any, applyOp func(i int, value any)) { foundIndexes := make(map[int]struct{}, len(av)) lookup := make(map[string]int) offset := len(bv) @@ -491,13 +439,8 @@ func processSet(av, bv []any, path string, applyOp func(i int, value any), repla jsonStr := string(jsonBytes) // If element exists in bv and we haven't seen all of them yet - if index, ok := lookup[jsonStr]; ok { + if _, ok := lookup[jsonStr]; ok { foundIndexes[i] = struct{}{} - updateOps, err := handleValues(bv[index], v, fmt.Sprintf("%s/%d", path, lookup[jsonStr]), []JsonPatchOperation{}, strategy, collections) - if err != nil { - return - } - replaceOps(updateOps) } } @@ -515,8 +458,8 @@ func processIdentitySet(av, bv []any, path string, applyOp func(i int, value any offset := len(bv) for i, v := range bv { - var key = "" - if _, ok := collections.entitySets.Get(Path(toJsonPath(path))); !ok { + key, ok := collections.entitySets.Get(Path(toJsonPath(path))) + if !ok { continue // If we don't have a key for this path, skip } jsonBytes, err := json.Marshal(v.(map[string]any)[string(key)]) @@ -527,19 +470,18 @@ func processIdentitySet(av, bv []any, path string, applyOp func(i int, value any lookup[jsonStr] = i } - // Check each element in av for i, v := range av { - jsonBytes, err := json.Marshal(v) - if key, ok := collections.entitySets.Get(Path(toJsonPath(path))); ok { - jsonBytes, err = json.Marshal(v.(map[string]any)[string(key)]) + key, ok := collections.entitySets.Get(Path(toJsonPath(path))) + if !ok { + continue // If we don't have a key for this path, skip } + jsonBytes, err := json.Marshal(v.(map[string]any)[string(key)]) if err != nil { applyOp(i+offset, v) // If we can't marshal, treat it as not found continue } jsonStr := string(jsonBytes) - // If element exists in bv and we haven't seen all of them yet if index, ok := lookup[jsonStr]; ok { foundIndexes[i] = struct{}{} updateOps, err := handleValues(bv[index], v, fmt.Sprintf("%s/%d", path, lookup[jsonStr]), []JsonPatchOperation{}, strategy, collections) @@ -550,7 +492,6 @@ func processIdentitySet(av, bv []any, path string, applyOp func(i int, value any } } - // Apply op for all elements in av that weren't found for i, v := range av { if _, ok := foundIndexes[i]; !ok { applyOp(i+offset, v) @@ -560,59 +501,28 @@ func processIdentitySet(av, bv []any, path string, applyOp func(i int, value any // processArray processes `av` and `bv` calling `applyOp` whenever a value is absent. // It keeps track of which indexes have already had `applyOp` called for and automatically skips them so you can process duplicate objects correctly. -func processArray(av, bv []any, applyOp func(i int, value any), strategy PatchStrategy, ignoreArrayOrder bool) { +func processArray(av, bv []any, applyOp func(i int, value any), strategy PatchStrategy) { foundIndexes := make(map[int]struct{}, len(av)) - bvElements := make(map[string]struct{}) - reverseFoundIndexes := make(map[int]struct{}, len(bv)) - switch strategy { case PatchStrategyExactMatch: - if ignoreArrayOrder { - for _, v := range bv { - jsonBytes, err := json.Marshal(v) - if err != nil { - continue // Skip if we can't marshal - } - jsonStr := string(jsonBytes) - bvElements[jsonStr] = struct{}{} - } - - for i, v := range av { - jsonBytes, err := json.Marshal(v) - if err != nil { - applyOp(i, v) // If we can't marshal, treat it as not found + reverseFoundIndexes := make(map[int]struct{}, len(bv)) + for i, v := range av { + for i2, v2 := range bv { + if _, ok := reverseFoundIndexes[i2]; ok { continue } - - jsonStr := string(jsonBytes) - // If element exists in bv and we haven't seen all of them yet - if _, ok := bvElements[jsonStr]; !ok { - applyOp(i, v) + if reflect.DeepEqual(v, v2) { + foundIndexes[i] = struct{}{} + reverseFoundIndexes[i2] = struct{}{} + break } } - } else { - // Original implementation for when order matters - for i, v := range av { - for i2, v2 := range bv { - if _, ok := reverseFoundIndexes[i2]; ok { - // We already found this index. - continue - } - if reflect.DeepEqual(v, v2) { - // Mark this index as found since it matches exactly. - foundIndexes[i] = struct{}{} - reverseFoundIndexes[i2] = struct{}{} - break - } - } - if _, ok := foundIndexes[i]; !ok { - applyOp(i, v) - } + if _, ok := foundIndexes[i]; !ok { + applyOp(i, v) } } case PatchStrategyEnsureExists: offset := len(bv) - // Create a map of elements and their counts in bv bvCounts := make(map[string]int) bvSeen := make(map[string]int) // Track how many we've seen during processing @@ -625,7 +535,6 @@ func processArray(av, bv []any, applyOp func(i int, value any), strategy PatchSt bvCounts[jsonStr]++ } - // Check each element in av for i, v := range av { jsonBytes, err := json.Marshal(v) if err != nil { @@ -634,14 +543,12 @@ func processArray(av, bv []any, applyOp func(i int, value any), strategy PatchSt } jsonStr := string(jsonBytes) - // If element exists in bv and we haven't seen all of them yet if bvCounts[jsonStr] > bvSeen[jsonStr] { foundIndexes[i] = struct{}{} bvSeen[jsonStr]++ } } - // Apply op for all elements in av that weren't found for i, v := range av { if _, ok := foundIndexes[i]; !ok { applyOp(i+offset, v) From dce396f60e723f3a5c7ffa29d509dec08f0db08f Mon Sep 17 00:00:00 2001 From: Jeroen Soeters Date: Tue, 27 May 2025 13:43:20 -0500 Subject: [PATCH 09/12] Make collections fields public --- jsonpatch_strategy_ensure_exists_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jsonpatch_strategy_ensure_exists_test.go b/jsonpatch_strategy_ensure_exists_test.go index ab1e4cf..3a25b5a 100644 --- a/jsonpatch_strategy_ensure_exists_test.go +++ b/jsonpatch_strategy_ensure_exists_test.go @@ -52,11 +52,11 @@ var nestedObjPrimitiveArray = `{"a":100, "b":{"c":[200]}}` var nestedObjAddPrimitiveArrayItem = `{"b":{"c":[250]}}` var ensureExistsStrategyTestCollections = Collections{ - entitySets: EntitySets{ + EntitySets: EntitySets{ Path("$.t"): Key("k"), Path("$.t[*].v"): Key("nk"), }, - arrays: []string{}, // No arrays in this test, only sets + Arrays: []string{}, // No arrays in this test, only sets } func TestCreatePatch_ModifyProperty_GeneratesReplaceOperation(t *testing.T) { From 52b8df0e0ef212b3d6e41a242c1e53a0d460e192 Mon Sep 17 00:00:00 2001 From: Jeroen Soeters Date: Tue, 27 May 2025 15:43:40 -0500 Subject: [PATCH 10/12] Cleanup --- jsonpatch.go | 12 ++++++------ jsonpatch_array_at_root_test.go | 4 ++-- jsonpatch_array_test.go | 4 ++-- jsonpatch_complex_test.go | 14 +++++++------- jsonpatch_geojson_test.go | 6 +++--- jsonpatch_hypercomplex_test.go | 4 ++-- ...sts_test.go => jsonpatch_set_operations_test.go | 0 jsonpatch_simple_test.go | 4 ++-- jsonpatch_supercomplex_test.go | 4 ++-- 9 files changed, 26 insertions(+), 26 deletions(-) rename jsonpatch_strategy_ensure_exists_test.go => jsonpatch_set_operations_test.go (100%) diff --git a/jsonpatch.go b/jsonpatch.go index dcfc5a4..046b34b 100644 --- a/jsonpatch.go +++ b/jsonpatch.go @@ -17,18 +17,18 @@ type Key string type EntitySets map[Path]Key type Collections struct { - entitySets EntitySets - arrays []string + EntitySets EntitySets + Arrays []string } func (c *Collections) isArray(path string) bool { jsonPath := toJsonPath(path) - return slices.Contains(c.arrays, jsonPath) + return slices.Contains(c.Arrays, jsonPath) } func (c *Collections) isEntitySet(path string) bool { jsonPath := toJsonPath(path) - _, ok := c.entitySets[Path(jsonPath)] + _, ok := c.EntitySets[Path(jsonPath)] return ok } @@ -458,7 +458,7 @@ func processIdentitySet(av, bv []any, path string, applyOp func(i int, value any offset := len(bv) for i, v := range bv { - key, ok := collections.entitySets.Get(Path(toJsonPath(path))) + key, ok := collections.EntitySets.Get(Path(toJsonPath(path))) if !ok { continue // If we don't have a key for this path, skip } @@ -471,7 +471,7 @@ func processIdentitySet(av, bv []any, path string, applyOp func(i int, value any } for i, v := range av { - key, ok := collections.entitySets.Get(Path(toJsonPath(path))) + key, ok := collections.EntitySets.Get(Path(toJsonPath(path))) if !ok { continue // If we don't have a key for this path, skip } diff --git a/jsonpatch_array_at_root_test.go b/jsonpatch_array_at_root_test.go index 5c88a75..f6bfef2 100644 --- a/jsonpatch_array_at_root_test.go +++ b/jsonpatch_array_at_root_test.go @@ -22,7 +22,7 @@ func TestJSONPatchCreate_ObjectRoot(t *testing.T) { } collections := Collections{ - arrays: []string{"$.items"}, + Arrays: []string{"$.items"}, } for name, tc := range cases { @@ -52,7 +52,7 @@ func TestJSONPatchCreate_ArrayRoot(t *testing.T) { } collections := Collections{ - arrays: []string{"$"}, + Arrays: []string{"$"}, } for name, tc := range cases { diff --git a/jsonpatch_array_test.go b/jsonpatch_array_test.go index 3057524..7c89e7b 100644 --- a/jsonpatch_array_test.go +++ b/jsonpatch_array_test.go @@ -17,7 +17,7 @@ var ( }` arrayTestCollections = Collections{ - arrays: []string{"$.persons"}, + Arrays: []string{"$.persons"}, } ) @@ -31,7 +31,7 @@ func TestArrayAddMultipleEmptyObjects(t *testing.T) { change := patch[0] assert.Equal(t, "add", change.Operation, "they should be equal") assert.Equal(t, "/persons/2", change.Path, "they should be equal") - assert.Equal(t, map[string]interface{}{}, change.Value, "they should be equal") + assert.Equal(t, map[string]any{}, change.Value, "they should be equal") } func TestArrayRemoveMultipleEmptyObjects(t *testing.T) { diff --git a/jsonpatch_complex_test.go b/jsonpatch_complex_test.go index 501945e..0178378 100644 --- a/jsonpatch_complex_test.go +++ b/jsonpatch_complex_test.go @@ -15,7 +15,7 @@ var complexD = `{"a":100, "b":[{"c1":"hello", "d1":"foo"},{"c2":"hello2", "d2":" var complexE = `{"a":100, "b":[{"c1":"hello", "d1":"foo"},{"c2":"hello2", "d2":"foo2"} ], "e":{"f":200, "g":"h", "i":"j"}}` var complex_test_collections = Collections{ - arrays: []string{"$.b"}, + Arrays: []string{"$.b"}, } func TestComplexSame(t *testing.T) { @@ -51,11 +51,11 @@ func TestComplexOneAdd(t *testing.T) { change := patch[0] assert.Equal(t, "add", change.Operation, "they should be equal") assert.Equal(t, "/k", change.Path, "they should be equal") - a := make(map[string]interface{}) - b := make(map[string]interface{}) + a := make(map[string]any) + b := make(map[string]any) a["l"] = "m" b["l"] = "o" - expected := []interface{}{a, b} + expected := []any{a, b} assert.Equal(t, expected, change.Value, "they should be equal") } @@ -66,11 +66,11 @@ func TestComplexOneAddToArray(t *testing.T) { change := patch[0] assert.Equal(t, "add", change.Operation, "they should be equal") assert.Equal(t, "/k", change.Path, "they should be equal") - a := make(map[string]interface{}) - b := make(map[string]interface{}) + a := make(map[string]any) + b := make(map[string]any) a["l"] = "m" b["l"] = "o" - expected := []interface{}{a, b} + expected := []any{a, b} assert.Equal(t, expected, change.Value, "they should be equal") } diff --git a/jsonpatch_geojson_test.go b/jsonpatch_geojson_test.go index 50ab49e..3bec3ef 100644 --- a/jsonpatch_geojson_test.go +++ b/jsonpatch_geojson_test.go @@ -10,7 +10,7 @@ import ( var point = `{"type":"Point", "coordinates":[0.0, 1.0]}` var lineString = `{"type":"LineString", "coordinates":[[0.0, 1.0], [2.0, 3.0]]}` var geotestCollections = Collections{ - arrays: []string{"$.coordinates"}, + Arrays: []string{"$.coordinates"}, } func TestPointLineStringReplace(t *testing.T) { @@ -21,11 +21,11 @@ func TestPointLineStringReplace(t *testing.T) { change := patch[0] assert.Equal(t, change.Operation, "replace", "they should be equal") assert.Equal(t, change.Path, "/coordinates/0", "they should be equal") - assert.Equal(t, change.Value, []interface{}{0.0, 1.0}, "they should be equal") + assert.Equal(t, change.Value, []any{0.0, 1.0}, "they should be equal") change = patch[1] assert.Equal(t, change.Operation, "replace", "they should be equal") assert.Equal(t, change.Path, "/coordinates/1", "they should be equal") - assert.Equal(t, change.Value, []interface{}{2.0, 3.0}, "they should be equal") + assert.Equal(t, change.Value, []any{2.0, 3.0}, "they should be equal") change = patch[2] assert.Equal(t, change.Operation, "replace", "they should be equal") assert.Equal(t, change.Path, "/type", "they should be equal") diff --git a/jsonpatch_hypercomplex_test.go b/jsonpatch_hypercomplex_test.go index 526b8a0..23abaff 100644 --- a/jsonpatch_hypercomplex_test.go +++ b/jsonpatch_hypercomplex_test.go @@ -156,7 +156,7 @@ var hyperComplexA = ` }` var hyperComplexTestCollections = Collections{ - arrays: []string{"$.goods", "$.goods[*].batters.batter", "$.goods[*].topping"}, + Arrays: []string{"$.goods", "$.goods[*].batters.batter", "$.goods[*].topping"}, } func TestHyperComplexSame(t *testing.T) { @@ -178,7 +178,7 @@ func TestHyperComplexBoolReplace(t *testing.T) { change = patch[1] assert.Equal(t, "add", change.Operation, "they should be equal") assert.Equal(t, "/goods/2/batters/batter/2", change.Path, "they should be equal") - assert.Equal(t, map[string]interface{}{"id": "1003", "type": "Vanilla"}, change.Value, "they should be equal") + assert.Equal(t, map[string]any{"id": "1003", "type": "Vanilla"}, change.Value, "they should be equal") change = patch[2] assert.Equal(t, change.Operation, "remove", "they should be equal") assert.Equal(t, change.Path, "/goods/2/topping/2", "they should be equal") diff --git a/jsonpatch_strategy_ensure_exists_test.go b/jsonpatch_set_operations_test.go similarity index 100% rename from jsonpatch_strategy_ensure_exists_test.go rename to jsonpatch_set_operations_test.go diff --git a/jsonpatch_simple_test.go b/jsonpatch_simple_test.go index a896cc0..db07998 100644 --- a/jsonpatch_simple_test.go +++ b/jsonpatch_simple_test.go @@ -96,7 +96,7 @@ func BenchmarkBigArrays(b *testing.B) { a1 = make([]any, 100) a2 = make([]any, 101) - for i := 0; i < 100; i++ { + for i := range 100 { a1[i] = i a2[i+1] = i } @@ -110,7 +110,7 @@ func BenchmarkBigArrays2(b *testing.B) { a1 = make([]any, 100) a2 = make([]any, 101) - for i := 0; i < 100; i++ { + for i := range 100 { a1[i] = i a2[i] = i } diff --git a/jsonpatch_supercomplex_test.go b/jsonpatch_supercomplex_test.go index 5c80a22..1912acb 100644 --- a/jsonpatch_supercomplex_test.go +++ b/jsonpatch_supercomplex_test.go @@ -491,8 +491,8 @@ var superComplexA = ` }` var superComplexTestCollections = Collections{ - entitySets: EntitySets{}, - arrays: []string{"$.annotations.annotation", "$.attributes.attribute-key"}, + EntitySets: EntitySets{}, + Arrays: []string{"$.annotations.annotation", "$.attributes.attribute-key"}, } func TestSuperComplexSame(t *testing.T) { From bc86c873022be198d222b152d1ad7c4d6cd8e562 Mon Sep 17 00:00:00 2001 From: Jeroen Soeters Date: Wed, 28 May 2025 17:41:09 -0500 Subject: [PATCH 11/12] Fix ExactMatch mode for both set types. Object properties are never removed now. --- jsonpatch.go | 27 ++-- jsonpatch_array_test.go | 4 +- jsonpatch_complex_test.go | 28 ++-- jsonpatch_entity_set_test.go | 107 +++++++++++++++ jsonpatch_object_test.go | 53 ++++++++ jsonpatch_set_operations_test.go | 216 ------------------------------ jsonpatch_set_test.go | 218 +++++++++++++++++++++++++++++++ jsonpatch_simple_test.go | 39 +++--- 8 files changed, 430 insertions(+), 262 deletions(-) create mode 100644 jsonpatch_entity_set_test.go create mode 100644 jsonpatch_object_test.go delete mode 100644 jsonpatch_set_operations_test.go create mode 100644 jsonpatch_set_test.go diff --git a/jsonpatch.go b/jsonpatch.go index 046b34b..8fa47ae 100644 --- a/jsonpatch.go +++ b/jsonpatch.go @@ -287,9 +287,9 @@ func diff(a, b map[string]any, path string, patch []JsonPatchOperation, strategy for key := range a { _, found := b[key] if !found { - p := makePath(path, key) + _ = makePath(path, key) - patch = append(patch, NewPatch("remove", p, nil)) + // patch = append(patch, NewPatch("remove", p, nil)) } } } @@ -375,21 +375,25 @@ func compareArray(av, bv []any, p string, strategy PatchStrategy, collections Co return retval } // TODO: removing is not tested yest! + removals := 0 if strategy == PatchStrategyExactMatch { // Find elements that need to be removed + elementsBeforeRemove := len(retval) processIdentitySet(av, bv, p, func(i int, value any) { retval = append(retval, NewPatch("remove", makePath(p, i), nil)) }, func(ops []JsonPatchOperation) { retval = append(retval, ops...) }, strategy, collections) + removals = len(retval) - elementsBeforeRemove reversed := make([]JsonPatchOperation, len(retval)) for i := range retval { reversed[len(retval)-1-i] = retval[i] } retval = reversed } + offset := len(av) - removals processIdentitySet(bv, av, p, func(i int, value any) { - retval = append(retval, NewPatch("add", makePath(p, i), value)) + retval = append(retval, NewPatch("add", makePath(p, i+offset), value)) }, func(ops []JsonPatchOperation) { retval = append(retval, ops...) }, strategy, collections) @@ -399,17 +403,20 @@ func compareArray(av, bv []any, p string, strategy PatchStrategy, collections Co } // TODO: removing is not tested yest! // also we need to check for PatchStrategyEnsureAbsent + removals := 0 if strategy == PatchStrategyExactMatch { // Find elements that need to be removed + elementsBeforeRemove := len(retval) processSet(av, bv, func(i int, value any) { retval = append(retval, NewPatch("remove", makePath(p, i), nil)) }) + removals = len(retval) - elementsBeforeRemove reversed := make([]JsonPatchOperation, len(retval)) for i := range retval { reversed[len(retval)-1-i] = retval[i] } retval = reversed } - - processSet(bv, av, func(i int, value any) { retval = append(retval, NewPatch("add", makePath(p, i), value)) }) + offset := len(av) - removals + processSet(bv, av, func(i int, value any) { retval = append(retval, NewPatch("add", makePath(p, i+offset), value)) }) } return retval @@ -418,7 +425,6 @@ func compareArray(av, bv []any, p string, strategy PatchStrategy, collections Co func processSet(av, bv []any, applyOp func(i int, value any)) { foundIndexes := make(map[int]struct{}, len(av)) lookup := make(map[string]int) - offset := len(bv) for i, v := range bv { jsonBytes, err := json.Marshal(v) @@ -433,7 +439,7 @@ func processSet(av, bv []any, applyOp func(i int, value any)) { for i, v := range av { jsonBytes, err := json.Marshal(v) if err != nil { - applyOp(i+offset, v) // If we can't marshal, treat it as not found + applyOp(i, v) // If we can't marshal, treat it as not found continue } @@ -447,7 +453,7 @@ func processSet(av, bv []any, applyOp func(i int, value any)) { // Apply op for all elements in av that weren't found for i, v := range av { if _, ok := foundIndexes[i]; !ok { - applyOp(i+offset, v) + applyOp(i, v) } } } @@ -455,7 +461,6 @@ func processSet(av, bv []any, applyOp func(i int, value any)) { func processIdentitySet(av, bv []any, path string, applyOp func(i int, value any), replaceOps func(ops []JsonPatchOperation), strategy PatchStrategy, collections Collections) { foundIndexes := make(map[int]struct{}, len(av)) lookup := make(map[string]int) - offset := len(bv) for i, v := range bv { key, ok := collections.EntitySets.Get(Path(toJsonPath(path))) @@ -477,7 +482,7 @@ func processIdentitySet(av, bv []any, path string, applyOp func(i int, value any } jsonBytes, err := json.Marshal(v.(map[string]any)[string(key)]) if err != nil { - applyOp(i+offset, v) // If we can't marshal, treat it as not found + applyOp(i, v) // If we can't marshal, treat it as not found continue } @@ -494,7 +499,7 @@ func processIdentitySet(av, bv []any, path string, applyOp func(i int, value any for i, v := range av { if _, ok := foundIndexes[i]; !ok { - applyOp(i+offset, v) + applyOp(i, v) } } } diff --git a/jsonpatch_array_test.go b/jsonpatch_array_test.go index 7c89e7b..65fc933 100644 --- a/jsonpatch_array_test.go +++ b/jsonpatch_array_test.go @@ -21,7 +21,7 @@ var ( } ) -func TestArrayAddMultipleEmptyObjects(t *testing.T) { +func TestArrayAddMultipleEmptyObjectsExactMatch(t *testing.T) { patch, e := CreatePatch([]byte(arrayBase), []byte(arrayUpdated), arrayTestCollections, PatchStrategyExactMatch) assert.NoError(t, e) t.Log("Patch:", patch) @@ -34,7 +34,7 @@ func TestArrayAddMultipleEmptyObjects(t *testing.T) { assert.Equal(t, map[string]any{}, change.Value, "they should be equal") } -func TestArrayRemoveMultipleEmptyObjects(t *testing.T) { +func TestArrayRemoveMultipleEmptyObjectsExactMatch(t *testing.T) { patch, e := CreatePatch([]byte(arrayUpdated), []byte(arrayBase), arrayTestCollections, PatchStrategyExactMatch) assert.NoError(t, e) t.Log("Patch:", patch) diff --git a/jsonpatch_complex_test.go b/jsonpatch_complex_test.go index 0178378..47b2955 100644 --- a/jsonpatch_complex_test.go +++ b/jsonpatch_complex_test.go @@ -1,7 +1,6 @@ package jsonpatch import ( - "sort" "testing" "github.com/stretchr/testify/assert" @@ -74,20 +73,21 @@ func TestComplexOneAddToArray(t *testing.T) { assert.Equal(t, expected, change.Value, "they should be equal") } +// We never remove keys from objects func TestComplexVsEmpty(t *testing.T) { patch, e := CreatePatch([]byte(complexBase), []byte(empty), complex_test_collections, PatchStrategyExactMatch) assert.NoError(t, e) - assert.Equal(t, 3, len(patch), "they should be equal") - sort.Sort(ByPath(patch)) - change := patch[0] - assert.Equal(t, "remove", change.Operation, "they should be equal") - assert.Equal(t, "/a", change.Path, "they should be equal") - - change = patch[1] - assert.Equal(t, "remove", change.Operation, "they should be equal") - assert.Equal(t, "/b", change.Path, "they should be equal") - - change = patch[2] - assert.Equal(t, "remove", change.Operation, "they should be equal") - assert.Equal(t, "/e", change.Path, "they should be equal") + assert.Equal(t, 0, len(patch), "they should be equal") + // sort.Sort(ByPath(patch)) + // change := patch[0] + // assert.Equal(t, "remove", change.Operation, "they should be equal") + // assert.Equal(t, "/a", change.Path, "they should be equal") + // + // change = patch[1] + // assert.Equal(t, "remove", change.Operation, "they should be equal") + // assert.Equal(t, "/b", change.Path, "they should be equal") + // + // change = patch[2] + // assert.Equal(t, "remove", change.Operation, "they should be equal") + // assert.Equal(t, "/e", change.Path, "they should be equal") } diff --git a/jsonpatch_entity_set_test.go b/jsonpatch_entity_set_test.go new file mode 100644 index 0000000..e8fe7ca --- /dev/null +++ b/jsonpatch_entity_set_test.go @@ -0,0 +1,107 @@ +package jsonpatch + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +var simpleObjEntitySet = `{"a":100, "t":[{"k":1, "v":1},{"k":2, "v":2}]}` +var simpleObjAddEntitySetItem = `{"t":[{"k":3, "v":3}]}` +var simpleObjModifyEntitySetItem = `{"t":[{"k":2, "v":3}]}` +var simpleObjAddDuplicateEntitySetItem = `{"t":[{"k":2, "v":2}]}` +var complexNextedEntitySet = `{ + "a":100, + "t":[ + {"k":1, + "v":[ + {"nk":11, "c":"x", "d":[1,2]}, + {"nk":22, "c":"y", "d":[3,4]} + ] + }, + {"k":2, + "v":[ + {"nk":33, "c":"z", "d":[5,6]} + ] + } + ]}` +var complexNextedEntitySetModifyItem = `{ + "t":[ + {"k":2, + "v":[ + {"nk":33, "c":"zz", "d":[7,8]} + ] + } + ]}` + +var entitySetTestCollections = Collections{ + EntitySets: EntitySets{ + Path("$.t"): Key("k"), + Path("$.t[*].v"): Key("nk"), + }, + Arrays: []string{}, // No arrays in this test, only sets +} + +func TestCreatePatch_AddItemToEntitySet_InEnsureExistsMode_GeneratesAddOperation(t *testing.T) { + patch, err := CreatePatch([]byte(simpleObjEntitySet), []byte(simpleObjAddEntitySetItem), entitySetTestCollections, PatchStrategyEnsureExists) + assert.NoError(t, err) + assert.Equal(t, 1, len(patch), "they should be equal") + change := patch[0] + assert.Equal(t, "add", change.Operation, "they should be equal") + assert.Equal(t, "/t/2", change.Path, "they should be equal") + var expected = map[string]any{"k": float64(3), "v": float64(3)} + assert.Equal(t, expected, change.Value, "they should be equal") +} + +func TestCreatePatch_AddItemToEntitySet_InExactMatchMode_GeneratesAddOperation(t *testing.T) { + patch, err := CreatePatch([]byte(simpleObjEntitySet), []byte(simpleObjAddEntitySetItem), entitySetTestCollections, PatchStrategyExactMatch) + assert.NoError(t, err) + assert.Equal(t, 3, len(patch), "they should be equal") + change := patch[0] + assert.Equal(t, "remove", change.Operation, "they should be equal") + assert.Equal(t, "/t/1", change.Path, "they should be equal") + change = patch[1] + assert.Equal(t, "remove", change.Operation, "they should be equal") + assert.Equal(t, "/t/0", change.Path, "they should be equal") + change = patch[2] + assert.Equal(t, "add", change.Operation, "they should be equal") + assert.Equal(t, "/t/0", change.Path, "they should be equal") + var expected = map[string]any{"k": float64(3), "v": float64(3)} + assert.Equal(t, expected, change.Value, "they should be equal") +} + +func TestCreatePatch_EntitySet_ModifyItem_GeneratesReplaceOperation(t *testing.T) { + patch, err := CreatePatch([]byte(simpleObjEntitySet), []byte(simpleObjModifyEntitySetItem), entitySetTestCollections, PatchStrategyEnsureExists) + assert.NoError(t, err) + assert.Equal(t, 1, len(patch), "they should be equal") + change := patch[0] + assert.Equal(t, "replace", change.Operation, "they should be equal") + assert.Equal(t, "/t/1/v", change.Path, "they should be equal") + var expected float64 = 3 + assert.Equal(t, expected, change.Value, "they should be equal") +} + +func TestCreatePatch_EntitySet_AddDuplicateItem_GeneratesNoOperations(t *testing.T) { + patch, err := CreatePatch([]byte(simpleObjEntitySet), []byte(simpleObjAddDuplicateEntitySetItem), entitySetTestCollections, PatchStrategyEnsureExists) + assert.NoError(t, err) + assert.Equal(t, 0, len(patch), "they should be equal") +} + +func TestCreatePatch_ComplexNestedEntitySet_ModifyItem_GeneratesReplaceOperation(t *testing.T) { + patch, err := CreatePatch([]byte(complexNextedEntitySet), []byte(complexNextedEntitySetModifyItem), entitySetTestCollections, PatchStrategyEnsureExists) + assert.NoError(t, err) + assert.Equal(t, 3, len(patch), "they should be equal") + change := patch[0] + assert.Equal(t, "replace", change.Operation, "they should be equal") + assert.Equal(t, "/t/1/v/0/c", change.Path, "they should be equal") + assert.Equal(t, "zz", change.Value, "they should be equal") + change = patch[1] + assert.Equal(t, "add", change.Operation, "they should be equal") + assert.Equal(t, "/t/1/v/0/d/2", change.Path, "they should be equal") + assert.Equal(t, float64(7), change.Value, "they should be equal") + change = patch[2] + assert.Equal(t, "add", change.Operation, "they should be equal") + assert.Equal(t, "add", change.Operation, "they should be equal") + assert.Equal(t, "/t/1/v/0/d/3", change.Path, "they should be equal") + assert.Equal(t, float64(8), change.Value, "they should be equal") +} diff --git a/jsonpatch_object_test.go b/jsonpatch_object_test.go new file mode 100644 index 0000000..14f9760 --- /dev/null +++ b/jsonpatch_object_test.go @@ -0,0 +1,53 @@ +package jsonpatch + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +var simpleObj = `{"a":100, "b":20}` +var simpleObjModifyProp = `{"b":250}` +var simpleObjAddProp = `{"c":"hello"}` + +func TestCreatePatch_ModifyProperty_GeneratesReplaceOperation(t *testing.T) { + patch, err := CreatePatch([]byte(simpleObj), []byte(simpleObjModifyProp), setTestCollections, PatchStrategyEnsureExists) + assert.NoError(t, err) + assert.Equal(t, 1, len(patch), "they should be equal") + change := patch[0] + assert.Equal(t, "replace", change.Operation, "they should be equal") + assert.Equal(t, "/b", change.Path, "they should be equal") + var expected float64 = 250 + assert.Equal(t, expected, change.Value, "they should be equal") +} + +func TestCreatePatch_AddProperty_GeneratesAddOperation(t *testing.T) { + patch, err := CreatePatch([]byte(simpleObj), []byte(simpleObjAddProp), setTestCollections, PatchStrategyEnsureExists) + assert.NoError(t, err) + assert.Equal(t, 1, len(patch), "they should be equal") + change := patch[0] + assert.Equal(t, "add", change.Operation, "they should be equal") + assert.Equal(t, "/c", change.Path, "they should be equal") + assert.Equal(t, "hello", change.Value, "they should be equal") +} + +func TestCreatePatch_NestedObject_ModifyProperty_GeneratesReplaceOperation(t *testing.T) { + patch, err := CreatePatch([]byte(nestedObj), []byte(nestedObjModifyProp), setTestCollections, PatchStrategyEnsureExists) + assert.NoError(t, err) + assert.Equal(t, 1, len(patch), "they should be equal") + change := patch[0] + assert.Equal(t, "replace", change.Operation, "they should be equal") + assert.Equal(t, "/b/c", change.Path, "they should be equal") + var expected float64 = 250 + assert.Equal(t, expected, change.Value, "they should be equal") +} + +func TestCreatePatch_NestedObject_AddProperty_GeneratesAddOperation(t *testing.T) { + patch, err := CreatePatch([]byte(nestedObj), []byte(nestedObjAddProp), setTestCollections, PatchStrategyEnsureExists) + assert.NoError(t, err) + assert.Equal(t, 1, len(patch), "they should be equal") + change := patch[0] + assert.Equal(t, "add", change.Operation, "they should be equal") + assert.Equal(t, "/b/d", change.Path, "they should be equal") + assert.Equal(t, "hello", change.Value, "they should be equal") +} diff --git a/jsonpatch_set_operations_test.go b/jsonpatch_set_operations_test.go deleted file mode 100644 index 3a25b5a..0000000 --- a/jsonpatch_set_operations_test.go +++ /dev/null @@ -1,216 +0,0 @@ -package jsonpatch - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -var simpleObj = `{"a":100, "b":20}` -var simpleObjModifyProp = `{"b":250}` -var simpleObjAddProp = `{"c":"hello"}` -var simpleObjEmtpyPrmitiveArray = `{"a":100, "b":[]}` -var simpleObjSingletonPrimitiveArray = `{"a":100, "b":[1]}` -var simpleObjMultipleItemPrimitiveArray = `{"a":100, "b":[1,2]}` -var simpleObjAddPrimitiveArrayItem = `{"b":[3]}` -var simpleObjAddDuplicateArrayItem = `{"b":[2]}` -var simpleObjSingletonObjectArray = `{"a":100, "b":[{"c":1}]}` -var simpleObjAddObjectArrayItem = `{"b":[{"c":2}]}` -var simpleObjAddDuplicateObjectArrayItem = `{"b":[{"c":1}]}` -var simpleObjKeyValueArray = `{"a":100, "t":[{"k":1, "v":1},{"k":2, "v":2}]}` -var simpleObjAddKeyValueArrayItem = `{"t":[{"k":3, "v":3}]}` -var simpleObjModifyKeyValueArrayItem = `{"t":[{"k":2, "v":3}]}` -var simpleObjAddDuplicateKeyValueArrayItem = `{"t":[{"k":2, "v":2}]}` -var complexNextedKeyValueArray = `{ - "a":100, - "t":[ - {"k":1, - "v":[ - {"nk":11, "c":"x", "d":[1,2]}, - {"nk":22, "c":"y", "d":[3,4]} - ] - }, - {"k":2, - "v":[ - {"nk":33, "c":"z", "d":[5,6]} - ] - } - ]}` -var complexNextedKeyValueArrayModifyItem = `{ - "t":[ - {"k":2, - "v":[ - {"nk":33, "c":"zz", "d":[7,8]} - ] - } - ]}` - -var nestedObj = `{"a":100, "b":{"c":200}}` -var nestedObjModifyProp = `{"b":{"c":250}}` -var nestedObjAddProp = `{"b":{"d":"hello"}}` -var nestedObjPrimitiveArray = `{"a":100, "b":{"c":[200]}}` -var nestedObjAddPrimitiveArrayItem = `{"b":{"c":[250]}}` - -var ensureExistsStrategyTestCollections = Collections{ - EntitySets: EntitySets{ - Path("$.t"): Key("k"), - Path("$.t[*].v"): Key("nk"), - }, - Arrays: []string{}, // No arrays in this test, only sets -} - -func TestCreatePatch_ModifyProperty_GeneratesReplaceOperation(t *testing.T) { - patch, err := CreatePatch([]byte(simpleObj), []byte(simpleObjModifyProp), ensureExistsStrategyTestCollections, PatchStrategyEnsureExists) - assert.NoError(t, err) - assert.Equal(t, 1, len(patch), "they should be equal") - change := patch[0] - assert.Equal(t, "replace", change.Operation, "they should be equal") - assert.Equal(t, "/b", change.Path, "they should be equal") - var expected float64 = 250 - assert.Equal(t, expected, change.Value, "they should be equal") -} - -func TestCreatePatch_AddProperty_GeneratesAddOperation(t *testing.T) { - patch, err := CreatePatch([]byte(simpleObj), []byte(simpleObjAddProp), ensureExistsStrategyTestCollections, PatchStrategyEnsureExists) - assert.NoError(t, err) - assert.Equal(t, 1, len(patch), "they should be equal") - change := patch[0] - assert.Equal(t, "add", change.Operation, "they should be equal") - assert.Equal(t, "/c", change.Path, "they should be equal") - assert.Equal(t, "hello", change.Value, "they should be equal") -} - -func TestCreatePatch_NestedObject_ModifyProperty_GeneratesReplaceOperation(t *testing.T) { - patch, err := CreatePatch([]byte(nestedObj), []byte(nestedObjModifyProp), ensureExistsStrategyTestCollections, PatchStrategyEnsureExists) - assert.NoError(t, err) - assert.Equal(t, 1, len(patch), "they should be equal") - change := patch[0] - assert.Equal(t, "replace", change.Operation, "they should be equal") - assert.Equal(t, "/b/c", change.Path, "they should be equal") - var expected float64 = 250 - assert.Equal(t, expected, change.Value, "they should be equal") -} - -func TestCreatePatch_NestedObject_AddProperty_GeneratesAddOperation(t *testing.T) { - patch, err := CreatePatch([]byte(nestedObj), []byte(nestedObjAddProp), ensureExistsStrategyTestCollections, PatchStrategyEnsureExists) - assert.NoError(t, err) - assert.Equal(t, 1, len(patch), "they should be equal") - change := patch[0] - assert.Equal(t, "add", change.Operation, "they should be equal") - assert.Equal(t, "/b/d", change.Path, "they should be equal") - assert.Equal(t, "hello", change.Value, "they should be equal") -} - -func TestCreatePatch_EmptyPrimitiveArray_AddItem_GeneratesAddOperation(t *testing.T) { - patch, err := CreatePatch([]byte(simpleObjEmtpyPrmitiveArray), []byte(simpleObjAddPrimitiveArrayItem), ensureExistsStrategyTestCollections, PatchStrategyEnsureExists) - assert.NoError(t, err) - assert.Equal(t, 1, len(patch), "they should be equal") - change := patch[0] - assert.Equal(t, "add", change.Operation, "they should be equal") - assert.Equal(t, "/b/0", change.Path, "they should be equal") - var expected float64 = 3 - assert.Equal(t, expected, change.Value, "they should be equal") -} - -func TestCreatePatch_SingletonPrimitiveArray_AddItem_GeneratesAddOperation(t *testing.T) { - patch, err := CreatePatch([]byte(simpleObjSingletonPrimitiveArray), []byte(simpleObjAddPrimitiveArrayItem), ensureExistsStrategyTestCollections, PatchStrategyEnsureExists) - assert.NoError(t, err) - assert.Equal(t, 1, len(patch), "they should be equal") - change := patch[0] - assert.Equal(t, "add", change.Operation, "they should be equal") - assert.Equal(t, "/b/1", change.Path, "they should be equal") - var expected float64 = 3 - assert.Equal(t, expected, change.Value, "they should be equal") -} - -func TestCreatePatch_MultipleItemPrimitiveArray_AddItem_GeneratesAddOperation(t *testing.T) { - patch, err := CreatePatch([]byte(simpleObjMultipleItemPrimitiveArray), []byte(simpleObjAddPrimitiveArrayItem), ensureExistsStrategyTestCollections, PatchStrategyEnsureExists) - assert.NoError(t, err) - assert.Equal(t, 1, len(patch), "they should be equal") - change := patch[0] - assert.Equal(t, "add", change.Operation, "they should be equal") - assert.Equal(t, "/b/2", change.Path, "they should be equal") - var expected float64 = 3 - assert.Equal(t, expected, change.Value, "they should be equal") -} - -func TestCreatePatch_SingletonPrimitiveArray_AddDuplicateItem_GeneratesNoOperations(t *testing.T) { - patch, err := CreatePatch([]byte(simpleObjMultipleItemPrimitiveArray), []byte(simpleObjAddDuplicateArrayItem), ensureExistsStrategyTestCollections, PatchStrategyEnsureExists) - assert.NoError(t, err) - assert.Equal(t, 0, len(patch), "they should be equal") -} - -func TestCreatePatch_NestedObject_PrimitiveArray_AddItem_GeneratesAddOperation(t *testing.T) { - patch, err := CreatePatch([]byte(nestedObjPrimitiveArray), []byte(nestedObjAddPrimitiveArrayItem), ensureExistsStrategyTestCollections, PatchStrategyEnsureExists) - assert.NoError(t, err) - assert.Equal(t, 1, len(patch), "they should be equal") - change := patch[0] - assert.Equal(t, "add", change.Operation, "they should be equal") - assert.Equal(t, "/b/c/1", change.Path, "they should be equal") - var expected float64 = 250 - assert.Equal(t, expected, change.Value, "they should be equal") -} - -func TestCreatePatch_SingletonObjectArray_AddItem_GeneratesAddOperation(t *testing.T) { - patch, err := CreatePatch([]byte(simpleObjSingletonObjectArray), []byte(simpleObjAddObjectArrayItem), ensureExistsStrategyTestCollections, PatchStrategyEnsureExists) - assert.NoError(t, err) - assert.Equal(t, 1, len(patch), "they should be equal") - change := patch[0] - assert.Equal(t, "add", change.Operation, "they should be equal") - assert.Equal(t, "/b/1", change.Path, "they should be equal") - var expected = map[string]any{"c": float64(2)} - assert.Equal(t, expected, change.Value, "they should be equal") -} - -func TestCreatePatch_KeyValueArray_AddItem_GeneratesAddOperation(t *testing.T) { - patch, err := CreatePatch([]byte(simpleObjKeyValueArray), []byte(simpleObjAddKeyValueArrayItem), ensureExistsStrategyTestCollections, PatchStrategyEnsureExists) - assert.NoError(t, err) - assert.Equal(t, 1, len(patch), "they should be equal") - change := patch[0] - assert.Equal(t, "add", change.Operation, "they should be equal") - assert.Equal(t, "/t/2", change.Path, "they should be equal") - var expected = map[string]any{"k": float64(3), "v": float64(3)} - assert.Equal(t, expected, change.Value, "they should be equal") -} - -func TestCreatePatch_SingletonObjectArray_AddDuplicateItem_GeneratesNoOperations(t *testing.T) { - patch, err := CreatePatch([]byte(simpleObjSingletonObjectArray), []byte(simpleObjAddDuplicateObjectArrayItem), ensureExistsStrategyTestCollections, PatchStrategyEnsureExists) - assert.NoError(t, err) - assert.Equal(t, 0, len(patch), "they should be equal") -} - -func TestCreatePatch_KeyValueArray_ModifyItem_GeneratesReplaceOperation(t *testing.T) { - patch, err := CreatePatch([]byte(simpleObjKeyValueArray), []byte(simpleObjModifyKeyValueArrayItem), ensureExistsStrategyTestCollections, PatchStrategyEnsureExists) - assert.NoError(t, err) - assert.Equal(t, 1, len(patch), "they should be equal") - change := patch[0] - assert.Equal(t, "replace", change.Operation, "they should be equal") - assert.Equal(t, "/t/1/v", change.Path, "they should be equal") - var expected float64 = 3 - assert.Equal(t, expected, change.Value, "they should be equal") -} - -func TestCreatePatch_KeyValueArray_AddDuplicateItem_GeneratesNoOperations(t *testing.T) { - patch, err := CreatePatch([]byte(simpleObjKeyValueArray), []byte(simpleObjAddDuplicateKeyValueArrayItem), ensureExistsStrategyTestCollections, PatchStrategyEnsureExists) - assert.NoError(t, err) - assert.Equal(t, 0, len(patch), "they should be equal") -} - -func TestCreatePatch_ComplexNestedKeyValueArray_ModifyItem_GeneratesReplaceOperation(t *testing.T) { - patch, err := CreatePatch([]byte(complexNextedKeyValueArray), []byte(complexNextedKeyValueArrayModifyItem), ensureExistsStrategyTestCollections, PatchStrategyEnsureExists) - assert.NoError(t, err) - assert.Equal(t, 3, len(patch), "they should be equal") - change := patch[0] - assert.Equal(t, "replace", change.Operation, "they should be equal") - assert.Equal(t, "/t/1/v/0/c", change.Path, "they should be equal") - assert.Equal(t, "zz", change.Value, "they should be equal") - change = patch[1] - assert.Equal(t, "add", change.Operation, "they should be equal") - assert.Equal(t, "/t/1/v/0/d/2", change.Path, "they should be equal") - assert.Equal(t, float64(7), change.Value, "they should be equal") - change = patch[2] - assert.Equal(t, "add", change.Operation, "they should be equal") - assert.Equal(t, "add", change.Operation, "they should be equal") - assert.Equal(t, "/t/1/v/0/d/3", change.Path, "they should be equal") - assert.Equal(t, float64(8), change.Value, "they should be equal") -} diff --git a/jsonpatch_set_test.go b/jsonpatch_set_test.go new file mode 100644 index 0000000..634cffd --- /dev/null +++ b/jsonpatch_set_test.go @@ -0,0 +1,218 @@ +package jsonpatch + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +var simpleObjEmtpyPrmitiveSet = `{"a":100, "b":[]}` +var simpleObjPrimitiveSetWithOneItem = `{"a":100, "b":[1]}` +var simpleObjPrimitiveSetWithMultipleItems = `{"a":100, "b":[1,2]}` +var simpleObjAddSingleItemToPrimitiveSet = `{"b":[3]}` +var simpleObjAddMultipleItemsToPrimitiveSet = `{"b":[3,4]}` +var simpleObjAddDuplicateItemToPrimitiveSet = `{"b":[2]}` +var simpleObjSingletonObjectSet = `{"a":100, "b":[{"c":1}]}` +var simpleObjAddObjectSetItem = `{"b":[{"c":2}]}` +var simpleObjAddDuplicateObjectSetItem = `{"b":[{"c":1}]}` + +var nestedObj = `{"a":100, "b":{"c":200}}` +var nestedObjModifyProp = `{"b":{"c":250}}` +var nestedObjAddProp = `{"b":{"d":"hello"}}` +var nestedObjPrimitiveSet = `{"a":100, "b":{"c":[200]}}` +var nestedObjAddPrimitiveSetItem = `{"b":{"c":[250]}}` + +var setTestCollections = Collections{ + EntitySets: EntitySets{}, + Arrays: []string{}, // No arrays in this test, only sets +} + +func TestCreatePatch_AddItemToEmptyPrimitiveSetInEnsureExistsMode_GeneratesAddOperation(t *testing.T) { + patch, err := CreatePatch([]byte(simpleObjEmtpyPrmitiveSet), []byte(simpleObjAddSingleItemToPrimitiveSet), setTestCollections, PatchStrategyEnsureExists) + assert.NoError(t, err) + assert.Equal(t, 1, len(patch), "they should be equal") + change := patch[0] + assert.Equal(t, "add", change.Operation, "they should be equal") + assert.Equal(t, "/b/0", change.Path, "they should be equal") + var expected float64 = 3 + assert.Equal(t, expected, change.Value, "they should be equal") +} + +func TestCreatePatch_AddItemToEmptyPrimitiveSetInEnsureExactMatchMode_GeneratesAddOperation(t *testing.T) { + patch, err := CreatePatch([]byte(simpleObjEmtpyPrmitiveSet), []byte(simpleObjAddSingleItemToPrimitiveSet), setTestCollections, PatchStrategyExactMatch) + assert.NoError(t, err) + assert.Equal(t, 1, len(patch), "they should be equal") + change := patch[0] + assert.Equal(t, "add", change.Operation, "they should be equal") + assert.Equal(t, "/b/0", change.Path, "they should be equal") + var expected float64 = 3 + assert.Equal(t, expected, change.Value, "they should be equal") +} + +func TestCreatePatch_AddItemToPrimitiveSetWithOneItemInEnsureExistsMode_GeneratesAddOperation(t *testing.T) { + patch, err := CreatePatch([]byte(simpleObjPrimitiveSetWithOneItem), []byte(simpleObjAddSingleItemToPrimitiveSet), setTestCollections, PatchStrategyEnsureExists) + assert.NoError(t, err) + assert.Equal(t, 1, len(patch), "they should be equal") + change := patch[0] + assert.Equal(t, "add", change.Operation, "they should be equal") + assert.Equal(t, "/b/1", change.Path, "they should be equal") + var expected float64 = 3 + assert.Equal(t, expected, change.Value, "they should be equal") +} + +func TestCreatePatch_AddItemToPrimitiveSetWithOneItemInExactMatchMode_GeneratesARemoveAndAnAddOperation(t *testing.T) { + patch, err := CreatePatch([]byte(simpleObjPrimitiveSetWithOneItem), []byte(simpleObjAddSingleItemToPrimitiveSet), setTestCollections, PatchStrategyExactMatch) + assert.NoError(t, err) + assert.Equal(t, 2, len(patch), "they should be equal") + change := patch[0] + assert.Equal(t, "remove", change.Operation, "they should be equal") + assert.Equal(t, "/b/0", change.Path, "they should be equal") + change = patch[1] + assert.Equal(t, "add", change.Operation, "they should be equal") + assert.Equal(t, "/b/0", change.Path, "they should be equal") + var expected float64 = 3 + assert.Equal(t, expected, change.Value, "they should be equal") +} + +func TestCreatePatch_AddItemToPrimitiveSetWithMultipleItems_InEnsureExistsMode_GeneratesAddOperation(t *testing.T) { + patch, err := CreatePatch([]byte(simpleObjPrimitiveSetWithMultipleItems), []byte(simpleObjAddSingleItemToPrimitiveSet), setTestCollections, PatchStrategyEnsureExists) + assert.NoError(t, err) + assert.Equal(t, 1, len(patch), "they should be equal") + change := patch[0] + assert.Equal(t, "add", change.Operation, "they should be equal") + assert.Equal(t, "/b/2", change.Path, "they should be equal") + var expected float64 = 3 + assert.Equal(t, expected, change.Value, "they should be equal") +} + +func TestCreatePatch_AddItemToPrimitiveSetWithMultipleItems_InExactMatchMode_GeneratesTwoRemovesAndOneAddOperation(t *testing.T) { + patch, err := CreatePatch([]byte(simpleObjPrimitiveSetWithMultipleItems), []byte(simpleObjAddSingleItemToPrimitiveSet), setTestCollections, PatchStrategyExactMatch) + assert.NoError(t, err) + assert.Equal(t, 3, len(patch), "they should be equal") + change := patch[0] + assert.Equal(t, "remove", change.Operation, "they should be equal") + assert.Equal(t, "/b/1", change.Path, "they should be equal") + change = patch[1] + assert.Equal(t, "remove", change.Operation, "they should be equal") + assert.Equal(t, "/b/0", change.Path, "they should be equal") + change = patch[2] + assert.Equal(t, "add", change.Operation, "they should be equal") + assert.Equal(t, "/b/0", change.Path, "they should be equal") + var expected float64 = 3 + assert.Equal(t, expected, change.Value, "they should be equal") +} + +func TestCreatePatch_AddMultipleItemsToPrimitiveSetWithMultipleItems_InEnsureExistsMode_GeneratesTwoAddOperations(t *testing.T) { + patch, err := CreatePatch([]byte(simpleObjPrimitiveSetWithMultipleItems), []byte(simpleObjAddMultipleItemsToPrimitiveSet), setTestCollections, PatchStrategyEnsureExists) + assert.NoError(t, err) + assert.Equal(t, 2, len(patch), "they should be equal") + change := patch[0] + assert.Equal(t, "add", change.Operation, "they should be equal") + assert.Equal(t, "/b/2", change.Path, "they should be equal") + var expected float64 = 3 + assert.Equal(t, expected, change.Value, "they should be equal") + change = patch[1] + assert.Equal(t, "add", change.Operation, "they should be equal") + assert.Equal(t, "/b/3", change.Path, "they should be equal") + expected = 4 + assert.Equal(t, expected, change.Value, "they should be equal") +} + +func TestCreatePatch_AddMultipleItemsToPrimitiveSetWithMultipleItems_InExactMatchMode_GeneratesTwoRemovesAndTwoAddOperations(t *testing.T) { + patch, err := CreatePatch([]byte(simpleObjPrimitiveSetWithMultipleItems), []byte(simpleObjAddMultipleItemsToPrimitiveSet), setTestCollections, PatchStrategyExactMatch) + assert.NoError(t, err) + assert.Equal(t, 4, len(patch), "they should be equal") + change := patch[0] + assert.Equal(t, "remove", change.Operation, "they should be equal") + assert.Equal(t, "/b/1", change.Path, "they should be equal") + change = patch[1] + assert.Equal(t, "remove", change.Operation, "they should be equal") + assert.Equal(t, "/b/0", change.Path, "they should be equal") + change = patch[2] + assert.Equal(t, "add", change.Operation, "they should be equal") + assert.Equal(t, "/b/0", change.Path, "they should be equal") + var expected float64 = 3 + assert.Equal(t, expected, change.Value, "they should be equal") + change = patch[3] + assert.Equal(t, "add", change.Operation, "they should be equal") + assert.Equal(t, "/b/1", change.Path, "they should be equal") + expected = 4 + assert.Equal(t, expected, change.Value, "they should be equal") +} + +func TestCreatePatch_AddDuplicateItemToPrimitiveSetWithOneMultipleItems_InEnsureExistsMode_GeneratesNoOperations(t *testing.T) { + patch, err := CreatePatch([]byte(simpleObjPrimitiveSetWithMultipleItems), []byte(simpleObjAddDuplicateItemToPrimitiveSet), setTestCollections, PatchStrategyEnsureExists) + assert.NoError(t, err) + assert.Equal(t, 0, len(patch), "they should be equal") +} + +func TestCreatePatch_AddDuplicateItemToPrimitiveSetWithOneMultipleItems_InExactMatchMode_GeneratesARemoveOperation(t *testing.T) { + patch, err := CreatePatch([]byte(simpleObjPrimitiveSetWithMultipleItems), []byte(simpleObjAddDuplicateItemToPrimitiveSet), setTestCollections, PatchStrategyExactMatch) + assert.NoError(t, err) + assert.Equal(t, 1, len(patch), "they should be equal") + change := patch[0] + assert.Equal(t, "remove", change.Operation, "they should be equal") + assert.Equal(t, "/b/0", change.Path, "they should be equal") +} + +func TestCreatePatch_AddItemToPrimitiveSetInNestedObject_InEnsureExistsMode_GeneratesAddOperation(t *testing.T) { + patch, err := CreatePatch([]byte(nestedObjPrimitiveSet), []byte(nestedObjAddPrimitiveSetItem), setTestCollections, PatchStrategyEnsureExists) + assert.NoError(t, err) + assert.Equal(t, 1, len(patch), "they should be equal") + change := patch[0] + assert.Equal(t, "add", change.Operation, "they should be equal") + assert.Equal(t, "/b/c/1", change.Path, "they should be equal") + var expected float64 = 250 + assert.Equal(t, expected, change.Value, "they should be equal") +} + +func TestCreatePatch_AddItemToPrimitiveSetInNestedObject_InExactMatchMode_GeneratesAddOperation(t *testing.T) { + patch, err := CreatePatch([]byte(nestedObjPrimitiveSet), []byte(nestedObjAddPrimitiveSetItem), setTestCollections, PatchStrategyExactMatch) + assert.NoError(t, err) + assert.Equal(t, 2, len(patch), "they should be equal") + change := patch[0] + assert.Equal(t, "remove", change.Operation, "they should be equal") + assert.Equal(t, "/b/c/0", change.Path, "they should be equal") + change = patch[1] + assert.Equal(t, "add", change.Operation, "they should be equal") + assert.Equal(t, "/b/c/0", change.Path, "they should be equal") + var expected float64 = 250 + assert.Equal(t, expected, change.Value, "they should be equal") +} + +func TestCreatePatch_AddItemToObjectSetWithOneItem_InEnsureExistsMode_GeneratesAddOperation(t *testing.T) { + patch, err := CreatePatch([]byte(simpleObjSingletonObjectSet), []byte(simpleObjAddObjectSetItem), setTestCollections, PatchStrategyEnsureExists) + assert.NoError(t, err) + assert.Equal(t, 1, len(patch), "they should be equal") + change := patch[0] + assert.Equal(t, "add", change.Operation, "they should be equal") + assert.Equal(t, "/b/1", change.Path, "they should be equal") + var expected = map[string]any{"c": float64(2)} + assert.Equal(t, expected, change.Value, "they should be equal") +} + +func TestCreatePatch_AddItemToObjectSetWithOneItem_InExactMatchMode_GeneratesAddOperation(t *testing.T) { + patch, err := CreatePatch([]byte(simpleObjSingletonObjectSet), []byte(simpleObjAddObjectSetItem), setTestCollections, PatchStrategyExactMatch) + assert.NoError(t, err) + assert.Equal(t, 2, len(patch), "they should be equal") + change := patch[0] + assert.Equal(t, "remove", change.Operation, "they should be equal") + assert.Equal(t, "/b/0", change.Path, "they should be equal") + change = patch[1] + assert.Equal(t, "add", change.Operation, "they should be equal") + assert.Equal(t, "/b/0", change.Path, "they should be equal") + var expected = map[string]any{"c": float64(2)} + assert.Equal(t, expected, change.Value, "they should be equal") +} + +func TestCreatePatch_AddDuplicateItemToObjectSetWithOneItem_InEnsureExistsMode_GeneratesNoOperations(t *testing.T) { + patch, err := CreatePatch([]byte(simpleObjSingletonObjectSet), []byte(simpleObjAddDuplicateObjectSetItem), setTestCollections, PatchStrategyEnsureExists) + assert.NoError(t, err) + assert.Equal(t, 0, len(patch), "they should be equal") +} + +func TestCreatePatch_AddDuplicateItemToObjectSetWithOneItem_InExactMatchMode_GeneratesNoOperations(t *testing.T) { + patch, err := CreatePatch([]byte(simpleObjSingletonObjectSet), []byte(simpleObjAddDuplicateObjectSetItem), setTestCollections, PatchStrategyExactMatch) + assert.NoError(t, err) + assert.Equal(t, 0, len(patch), "they should be equal") +} diff --git a/jsonpatch_simple_test.go b/jsonpatch_simple_test.go index db07998..3b037fe 100644 --- a/jsonpatch_simple_test.go +++ b/jsonpatch_simple_test.go @@ -1,7 +1,6 @@ package jsonpatch import ( - "sort" "testing" "github.com/stretchr/testify/assert" @@ -63,32 +62,34 @@ func TestOneAdd(t *testing.T) { assert.Equal(t, change.Value, "foo", "they should be equal") } +// We never remove properties from objects func TestOneRemove(t *testing.T) { patch, e := CreatePatch([]byte(simpleA), []byte(simpleE), Collections{}, PatchStrategyExactMatch) assert.NoError(t, e) - assert.Equal(t, len(patch), 1, "they should be equal") - change := patch[0] - assert.Equal(t, change.Operation, "remove", "they should be equal") - assert.Equal(t, change.Path, "/c", "they should be equal") - assert.Equal(t, change.Value, nil, "they should be equal") + assert.Equal(t, len(patch), 0, "they should be equal") + // change := patch[0] + // assert.Equal(t, change.Operation, "remove", "they should be equal") + // assert.Equal(t, change.Path, "/c", "they should be equal") + // assert.Equal(t, change.Value, nil, "they should be equal") } +// We never remove properties from objects func TestVsEmpty(t *testing.T) { patch, e := CreatePatch([]byte(simpleA), []byte(empty), Collections{}, PatchStrategyExactMatch) assert.NoError(t, e) - assert.Equal(t, len(patch), 3, "they should be equal") - sort.Sort(ByPath(patch)) - change := patch[0] - assert.Equal(t, change.Operation, "remove", "they should be equal") - assert.Equal(t, change.Path, "/a", "they should be equal") - - change = patch[1] - assert.Equal(t, change.Operation, "remove", "they should be equal") - assert.Equal(t, change.Path, "/b", "they should be equal") - - change = patch[2] - assert.Equal(t, change.Operation, "remove", "they should be equal") - assert.Equal(t, change.Path, "/c", "they should be equal") + assert.Equal(t, len(patch), 0, "they should be equal") + // sort.Sort(ByPath(patch)) + // change := patch[0] + // assert.Equal(t, change.Operation, "remove", "they should be equal") + // assert.Equal(t, change.Path, "/a", "they should be equal") + // + // change = patch[1] + // assert.Equal(t, change.Operation, "remove", "they should be equal") + // assert.Equal(t, change.Path, "/b", "they should be equal") + // + // change = patch[2] + // assert.Equal(t, change.Operation, "remove", "they should be equal") + // assert.Equal(t, change.Path, "/c", "they should be equal") } func BenchmarkBigArrays(b *testing.B) { From a1272ce4e1081ad25ab1dae703e71878e1dee379 Mon Sep 17 00:00:00 2001 From: Jeroen Soeters Date: Wed, 28 May 2025 17:46:10 -0500 Subject: [PATCH 12/12] Improve comment --- jsonpatch.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/jsonpatch.go b/jsonpatch.go index 8fa47ae..bfa867d 100644 --- a/jsonpatch.go +++ b/jsonpatch.go @@ -282,17 +282,17 @@ func diff(a, b map[string]any, path string, patch []JsonPatchOperation, strategy return nil, err } } - if strategy == PatchStrategyExactMatch { - // Now add all deleted values as nil - for key := range a { - _, found := b[key] - if !found { - _ = makePath(path, key) - - // patch = append(patch, NewPatch("remove", p, nil)) - } - } - } + // Leaving this here for now, but the current thinking is that we never remove properties from objects. + // if strategy == PatchStrategyExactMatch { + // // 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 }