Skip to content

Commit 13a9e4a

Browse files
authored
Merge pull request #36 from howardjohn/v2/improvements
Various improvements to performance and stability
2 parents 62e0c90 + 6d5c3df commit 13a9e4a

File tree

6 files changed

+174
-128
lines changed

6 files changed

+174
-128
lines changed

.github/workflows/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
- name: Set up Go
1313
uses: actions/setup-go@v2
1414
with:
15-
go-version: 1.17
15+
go-version: '1.20'
1616

1717
- name: Build
1818
run: |

v2/bench_test.go

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package jsonpatch_test
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"gomodules.xyz/jsonpatch/v2"
8+
)
9+
10+
func BenchmarkCreatePatch(b *testing.B) {
11+
cases := []struct {
12+
name string
13+
a, b string
14+
}{
15+
{
16+
"complex",
17+
superComplexBase,
18+
superComplexA,
19+
},
20+
{
21+
"large array",
22+
largeArray(1000),
23+
largeArray(1000),
24+
},
25+
{
26+
"simple",
27+
simpleA,
28+
simpleB,
29+
},
30+
}
31+
32+
for _, tt := range cases {
33+
b.Run(tt.name, func(b *testing.B) {
34+
at := []byte(tt.a)
35+
bt := []byte(tt.b)
36+
for n := 0; n < b.N; n++ {
37+
_, _ = jsonpatch.CreatePatch(at, bt)
38+
}
39+
})
40+
}
41+
}
42+
43+
func largeArray(size int) string {
44+
type nested struct {
45+
A, B string
46+
}
47+
type example struct {
48+
Objects []nested
49+
}
50+
a := example{}
51+
for i := 0; i < size; i++ {
52+
a.Objects = append(a.Objects, nested{A: "a", B: "b"})
53+
}
54+
res, _ := json.Marshal(a)
55+
return string(res)
56+
}

v2/fuzz_test.go

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package jsonpatch_test
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
jp "github.com/evanphx/json-patch"
8+
"github.com/stretchr/testify/assert"
9+
"gomodules.xyz/jsonpatch/v2"
10+
)
11+
12+
func FuzzCreatePatch(f *testing.F) {
13+
add := func(a, b string) {
14+
f.Add([]byte(a), []byte(b))
15+
}
16+
add(simpleA, simpleB)
17+
add(superComplexBase, superComplexA)
18+
add(hyperComplexBase, hyperComplexA)
19+
add(arraySrc, arrayDst)
20+
add(empty, simpleA)
21+
add(point, lineString)
22+
f.Fuzz(func(t *testing.T, a, b []byte) {
23+
checkFuzz(t, a, b)
24+
})
25+
}
26+
27+
func checkFuzz(t *testing.T, src, dst []byte) {
28+
t.Logf("Test: %v -> %v", string(src), string(dst))
29+
patch, err := jsonpatch.CreatePatch(src, dst)
30+
if err != nil {
31+
// Ok to error, src or dst may be invalid
32+
t.Skip()
33+
}
34+
35+
// Applying library only works with arrays and structs, no primitives
36+
// We still do CreatePatch to make sure it doesn't panic
37+
if isPrimitive(src) || isPrimitive(dst) {
38+
return
39+
}
40+
41+
for _, p := range patch {
42+
if p.Path == "" {
43+
// json-patch doesn't handle this properly, but it is valid
44+
return
45+
}
46+
}
47+
48+
data, err := json.Marshal(patch)
49+
assert.Nil(t, err)
50+
51+
t.Logf("Applying patch %v", string(data))
52+
p2, err := jp.DecodePatch(data)
53+
assert.Nil(t, err)
54+
55+
d2, err := p2.Apply(src)
56+
assert.Nil(t, err)
57+
58+
assert.JSONEq(t, string(dst), string(d2))
59+
}
60+
61+
func isPrimitive(data []byte) bool {
62+
return data[0] != '{' && data[0] != '['
63+
}

v2/go.mod

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
module gomodules.xyz/jsonpatch/v2
22

3-
go 1.12
3+
go 1.20
44

55
require (
66
github.com/evanphx/json-patch v0.5.2
77
github.com/stretchr/testify v1.3.0
88
)
9+
10+
require (
11+
github.com/davecgh/go-spew v1.1.0 // indirect
12+
github.com/pkg/errors v0.9.1 // indirect
13+
github.com/pmezard/go-difflib v1.0.0 // indirect
14+
)

v2/jsonpatch.go

+34-126
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package jsonpatch
22

33
import (
4-
"bytes"
54
"encoding/json"
65
"fmt"
76
"reflect"
@@ -24,21 +23,28 @@ func (j *Operation) Json() string {
2423
}
2524

2625
func (j *Operation) MarshalJSON() ([]byte, error) {
27-
var b bytes.Buffer
28-
b.WriteString("{")
29-
b.WriteString(fmt.Sprintf(`"op":"%s"`, j.Operation))
30-
b.WriteString(fmt.Sprintf(`,"path":"%s"`, j.Path))
31-
// Consider omitting Value for non-nullable operations.
32-
if j.Value != nil || j.Operation == "replace" || j.Operation == "add" {
33-
v, err := json.Marshal(j.Value)
34-
if err != nil {
35-
return nil, err
36-
}
37-
b.WriteString(`,"value":`)
38-
b.Write(v)
39-
}
40-
b.WriteString("}")
41-
return b.Bytes(), nil
26+
// Ensure for add and replace we emit `value: null`
27+
if j.Value == nil && (j.Operation == "replace" || j.Operation == "add") {
28+
return json.Marshal(struct {
29+
Operation string `json:"op"`
30+
Path string `json:"path"`
31+
Value interface{} `json:"value"`
32+
}{
33+
Operation: j.Operation,
34+
Path: j.Path,
35+
})
36+
}
37+
// otherwise just marshal normally. We cannot literally do json.Marshal(j) as it would be recursively
38+
// calling this function.
39+
return json.Marshal(struct {
40+
Operation string `json:"op"`
41+
Path string `json:"path"`
42+
Value interface{} `json:"value,omitempty"`
43+
}{
44+
Operation: j.Operation,
45+
Path: j.Path,
46+
Value: j.Value,
47+
})
4248
}
4349

4450
type ByPath []Operation
@@ -149,9 +155,6 @@ func makePath(path string, newPart interface{}) string {
149155
if path == "" {
150156
return "/" + key
151157
}
152-
if strings.HasSuffix(path, "/") {
153-
return path + key
154-
}
155158
return path + "/" + key
156159
}
157160

@@ -211,22 +214,18 @@ func handleValues(av, bv interface{}, p string, patch []Operation) ([]Operation,
211214
}
212215
case []interface{}:
213216
bt := bv.([]interface{})
214-
if isSimpleArray(at) && isSimpleArray(bt) {
215-
patch = append(patch, compareEditDistance(at, bt, p)...)
216-
} else {
217-
n := min(len(at), len(bt))
218-
for i := len(at) - 1; i >= n; i-- {
219-
patch = append(patch, NewOperation("remove", makePath(p, i), nil))
220-
}
221-
for i := n; i < len(bt); i++ {
222-
patch = append(patch, NewOperation("add", makePath(p, i), bt[i]))
223-
}
224-
for i := 0; i < n; i++ {
225-
var err error
226-
patch, err = handleValues(at[i], bt[i], makePath(p, i), patch)
227-
if err != nil {
228-
return nil, err
229-
}
217+
n := min(len(at), len(bt))
218+
for i := len(at) - 1; i >= n; i-- {
219+
patch = append(patch, NewOperation("remove", makePath(p, i), nil))
220+
}
221+
for i := n; i < len(bt); i++ {
222+
patch = append(patch, NewOperation("add", makePath(p, i), bt[i]))
223+
}
224+
for i := 0; i < n; i++ {
225+
var err error
226+
patch, err = handleValues(at[i], bt[i], makePath(p, i), patch)
227+
if err != nil {
228+
return nil, err
230229
}
231230
}
232231
default:
@@ -235,100 +234,9 @@ func handleValues(av, bv interface{}, p string, patch []Operation) ([]Operation,
235234
return patch, nil
236235
}
237236

238-
func isBasicType(a interface{}) bool {
239-
switch a.(type) {
240-
case string, float64, bool:
241-
default:
242-
return false
243-
}
244-
return true
245-
}
246-
247-
func isSimpleArray(a []interface{}) bool {
248-
for i := range a {
249-
switch a[i].(type) {
250-
case string, float64, bool:
251-
default:
252-
val := reflect.ValueOf(a[i])
253-
if val.Kind() == reflect.Map {
254-
for _, k := range val.MapKeys() {
255-
av := val.MapIndex(k)
256-
if av.Kind() == reflect.Ptr || av.Kind() == reflect.Interface {
257-
if av.IsNil() {
258-
continue
259-
}
260-
av = av.Elem()
261-
}
262-
if av.Kind() != reflect.String && av.Kind() != reflect.Float64 && av.Kind() != reflect.Bool {
263-
return false
264-
}
265-
}
266-
return true
267-
}
268-
return false
269-
}
270-
}
271-
return true
272-
}
273-
274-
// https://en.wikipedia.org/wiki/Wagner%E2%80%93Fischer_algorithm
275-
// Adapted from https://github.com/texttheater/golang-levenshtein
276-
func compareEditDistance(s, t []interface{}, p string) []Operation {
277-
m := len(s)
278-
n := len(t)
279-
280-
d := make([][]int, m+1)
281-
for i := 0; i <= m; i++ {
282-
d[i] = make([]int, n+1)
283-
d[i][0] = i
284-
}
285-
for j := 0; j <= n; j++ {
286-
d[0][j] = j
287-
}
288-
289-
for j := 1; j <= n; j++ {
290-
for i := 1; i <= m; i++ {
291-
if reflect.DeepEqual(s[i-1], t[j-1]) {
292-
d[i][j] = d[i-1][j-1] // no op required
293-
} else {
294-
del := d[i-1][j] + 1
295-
add := d[i][j-1] + 1
296-
rep := d[i-1][j-1] + 1
297-
d[i][j] = min(rep, min(add, del))
298-
}
299-
}
300-
}
301-
302-
return backtrace(s, t, p, m, n, d)
303-
}
304-
305237
func min(x int, y int) int {
306238
if y < x {
307239
return y
308240
}
309241
return x
310242
}
311-
312-
func backtrace(s, t []interface{}, p string, i int, j int, matrix [][]int) []Operation {
313-
if i > 0 && matrix[i-1][j]+1 == matrix[i][j] {
314-
op := NewOperation("remove", makePath(p, i-1), nil)
315-
return append([]Operation{op}, backtrace(s, t, p, i-1, j, matrix)...)
316-
}
317-
if j > 0 && matrix[i][j-1]+1 == matrix[i][j] {
318-
op := NewOperation("add", makePath(p, i), t[j-1])
319-
return append([]Operation{op}, backtrace(s, t, p, i, j-1, matrix)...)
320-
}
321-
if i > 0 && j > 0 && matrix[i-1][j-1]+1 == matrix[i][j] {
322-
if isBasicType(s[0]) {
323-
op := NewOperation("replace", makePath(p, i-1), t[j-1])
324-
return append([]Operation{op}, backtrace(s, t, p, i-1, j-1, matrix)...)
325-
}
326-
327-
p2, _ := handleValues(s[i-1], t[j-1], makePath(p, i-1), []Operation{})
328-
return append(p2, backtrace(s, t, p, i-1, j-1, matrix)...)
329-
}
330-
if i > 0 && j > 0 && matrix[i-1][j-1] == matrix[i][j] {
331-
return backtrace(s, t, p, i-1, j-1, matrix)
332-
}
333-
return []Operation{}
334-
}

v2/jsonpatch_test.go

+13
Original file line numberDiff line numberDiff line change
@@ -836,6 +836,15 @@ var (
836836
}`
837837
)
838838

839+
var (
840+
emptyKeyA = `{"":[0]}`
841+
emptyKeyB = `{"":[]}`
842+
)
843+
844+
var (
845+
specialChars = string([]byte{123, 34, 92, 98, 34, 58, 91, 93, 125})
846+
)
847+
839848
func TestCreatePatch(t *testing.T) {
840849
cases := []struct {
841850
name string
@@ -881,6 +890,10 @@ func TestCreatePatch(t *testing.T) {
881890
{"Array at root", `[{"asdf":"qwerty"}]`, `[{"asdf":"bla"},{"asdf":"zzz"}]`},
882891
{"Empty array at root", `[]`, `[{"asdf":"bla"},{"asdf":"zzz"}]`},
883892
{"Null Key uses replace operation", nullKeyA, nullKeyB},
893+
// empty key
894+
{"Empty key", emptyKeyA, emptyKeyB},
895+
// special chars
896+
{"Special chars", empty, specialChars},
884897
}
885898

886899
for _, c := range cases {

0 commit comments

Comments
 (0)