Skip to content

Commit 213ca8c

Browse files
committedApr 4, 2022
refactor: export Differ type to allow reuse of resources
The Differ type is now exported, and provide a Reset method, which allows to reset the underlying storage to reduce allocations on reuse.
1 parent c04fcf3 commit 213ca8c

9 files changed

+138
-85
lines changed
 

‎bench_test.go

+13-12
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@ import (
66
"testing"
77
)
88

9-
func BenchmarkCompare(b *testing.B) {
10-
beforeBytes, err := ioutil.ReadFile("testdata/benchs/before.json")
9+
func BenchmarkMedium(b *testing.B) {
10+
beforeBytes, err := ioutil.ReadFile("testdata/benchs/medium/before.json")
1111
if err != nil {
1212
b.Fatal(err)
1313
}
14-
afterBytesOrdered, err := ioutil.ReadFile("testdata/benchs/after-ordered.json")
14+
afterBytesOrdered, err := ioutil.ReadFile("testdata/benchs/medium/after-ordered.json")
1515
if err != nil {
1616
b.Fatal(err)
1717
}
18-
afterBytesUnordered, err := ioutil.ReadFile("testdata/benchs/after-unordered.json")
18+
afterBytesUnordered, err := ioutil.ReadFile("testdata/benchs/medium/after-unordered.json")
1919
if err != nil {
2020
b.Fatal(err)
2121
}
@@ -64,15 +64,16 @@ func BenchmarkCompare(b *testing.B) {
6464
_ = patch
6565
}
6666
})
67-
b.Run("differ_diff/"+bb.name, func(b *testing.B) {
67+
b.Run("DifferCompare/"+bb.name, func(b *testing.B) {
68+
d := Differ{
69+
targetBytes: bb.afterBytes,
70+
}
71+
for _, opt := range bb.opts {
72+
opt(&d)
73+
}
6874
for i := 0; i < b.N; i++ {
69-
d := differ{
70-
targetBytes: bb.afterBytes,
71-
}
72-
for _, opt := range bb.opts {
73-
opt(&d)
74-
}
75-
d.diff(before, after)
75+
d.Compare(before, after)
76+
d.Reset()
7677
}
7778
})
7879
}

‎compare.go

+11-42
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,18 @@ package jsondiff
22

33
import "encoding/json"
44

5-
// An Option overrides the default diff behavior of
6-
// the CompareOpts and CompareJSONOpts function.
7-
type Option func(*differ)
8-
95
// Compare compares the JSON representations of the
106
// given values and returns the differences relative
117
// to the former as a list of JSON Patch operations.
128
func Compare(source, target interface{}) (Patch, error) {
13-
var d differ
9+
var d Differ
1410
return compare(&d, source, target)
1511
}
1612

1713
// CompareOpts is similar to Compare, but also accepts
18-
// a list of options to configure the diff behavior.
14+
// a list of options to configure the behavior.
1915
func CompareOpts(source, target interface{}, opts ...Option) (Patch, error) {
20-
var d differ
16+
var d Differ
2117
d.applyOpts(opts...)
2218

2319
return compare(&d, source, target)
@@ -27,47 +23,20 @@ func CompareOpts(source, target interface{}, opts ...Option) (Patch, error) {
2723
// returns the differences relative to the former as
2824
// a list of JSON Patch operations.
2925
func CompareJSON(source, target []byte) (Patch, error) {
30-
var d differ
26+
var d Differ
3127
return compareJSON(&d, source, target)
3228
}
3329

3430
// CompareJSONOpts is similar to CompareJSON, but also
35-
// accepts a list of options to configure the diff behavior.
31+
// accepts a list of options to configure the behavior.
3632
func CompareJSONOpts(source, target []byte, opts ...Option) (Patch, error) {
37-
var d differ
33+
var d Differ
3834
d.applyOpts(opts...)
3935

4036
return compareJSON(&d, source, target)
4137
}
4238

43-
// Factorize enables factorization of operations.
44-
func Factorize() Option {
45-
return func(o *differ) { o.opts.factorize = true }
46-
}
47-
48-
// Rationalize enables rationalization of operations.
49-
func Rationalize() Option {
50-
return func(o *differ) { o.opts.rationalize = true }
51-
}
52-
53-
// Equivalent disables the generation of operations for
54-
// arrays of equal length and content that are not ordered.
55-
func Equivalent() Option {
56-
return func(o *differ) { o.opts.equivalent = true }
57-
}
58-
59-
// Invertible enables the generation of an invertible
60-
// patch, by preceding each remove and replace operation
61-
// by a test operation that verifies the value at the
62-
// path that is being removed/replaced.
63-
// Note that copy operations are not invertible, and as
64-
// such, using this option disable the usage of copy
65-
// operation in favor of add operations.
66-
func Invertible() Option {
67-
return func(o *differ) { o.opts.invertible = true }
68-
}
69-
70-
func compare(d *differ, src, tgt interface{}) (Patch, error) {
39+
func compare(d *Differ, src, tgt interface{}) (Patch, error) {
7140
si, _, err := marshalUnmarshal(src)
7241
if err != nil {
7342
return nil, err
@@ -77,12 +46,12 @@ func compare(d *differ, src, tgt interface{}) (Patch, error) {
7746
return nil, err
7847
}
7948
d.targetBytes = tb
80-
d.diff(si, ti)
49+
d.Compare(si, ti)
8150

8251
return d.patch, nil
8352
}
8453

85-
func compareJSON(d *differ, src, tgt []byte) (Patch, error) {
54+
func compareJSON(d *Differ, src, tgt []byte) (Patch, error) {
8655
var si, ti interface{}
8756
if err := json.Unmarshal(src, &si); err != nil {
8857
return nil, err
@@ -91,13 +60,13 @@ func compareJSON(d *differ, src, tgt []byte) (Patch, error) {
9160
return nil, err
9261
}
9362
d.targetBytes = tgt
94-
d.diff(si, ti)
63+
d.Compare(si, ti)
9564

9665
return d.patch, nil
9766
}
9867

9968
// marshalUnmarshal returns the result of unmarshaling
100-
// the JSON representation of the given value.
69+
// the JSON representation of the given interface value.
10170
func marshalUnmarshal(i interface{}) (interface{}, []byte, error) {
10271
b, err := json.Marshal(i)
10372
if err != nil {

‎differ.go

+77-26
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import (
55
"strings"
66
)
77

8-
type differ struct {
8+
// A Differ is a JSON Patch generator.
9+
// The zero value is an empty generator ready to use.
10+
type Differ struct {
911
patch Patch
1012
hasher hasher
1113
hashmap map[uint64]jsonNode
@@ -20,14 +22,43 @@ type options struct {
2022
equivalent bool
2123
}
2224

23-
func (d *differ) diff(src, tgt interface{}) {
25+
// Patch returns the list of JSON patch operations
26+
// generated by the Differ. The patch is valid for usage
27+
// until the next comparison or reset.
28+
func (d *Differ) Patch() Patch {
29+
return d.patch
30+
}
31+
32+
// WithOpts applies the given options to the Differ
33+
// and returns it to allow chained calls.
34+
func (d *Differ) WithOpts(opts ...Option) *Differ {
35+
for _, o := range opts {
36+
o(d)
37+
}
38+
return d
39+
}
40+
41+
// Reset resets the Differ to be empty, but it retains the
42+
// underlying storage for use by future comparisons.
43+
func (d *Differ) Reset() {
44+
d.patch = d.patch[:0]
45+
46+
// Optimized map clear.
47+
for k := range d.hashmap {
48+
delete(d.hashmap, k)
49+
}
50+
}
51+
52+
// Compare computes the differences between src and tgt as
53+
// a series of JSON Patch operations.
54+
func (d *Differ) Compare(src, tgt interface{}) {
2455
if d.opts.factorize {
2556
d.prepare(emptyPtr, src, tgt)
2657
}
27-
d.compare(emptyPtr, src, tgt)
58+
d.diff(emptyPtr, src, tgt)
2859
}
2960

30-
func (d *differ) compare(ptr pointer, src, tgt interface{}) {
61+
func (d *Differ) diff(ptr pointer, src, tgt interface{}) {
3162
if src == nil && tgt == nil {
3263
return
3364
}
@@ -70,7 +101,7 @@ func (d *differ) compare(ptr pointer, src, tgt interface{}) {
70101
}
71102
}
72103

73-
func (d *differ) prepare(ptr pointer, src, tgt interface{}) {
104+
func (d *Differ) prepare(ptr pointer, src, tgt interface{}) {
74105
if src == nil && tgt == nil {
75106
return
76107
}
@@ -110,7 +141,7 @@ func (d *differ) prepare(ptr pointer, src, tgt interface{}) {
110141
}
111142
}
112143

113-
func (d *differ) rationalizeLastOps(ptr pointer, src, tgt interface{}, lastOpIdx int) {
144+
func (d *Differ) rationalizeLastOps(ptr pointer, src, tgt interface{}, lastOpIdx int) {
114145
newOps := make(Patch, 0, 2)
115146

116147
if d.opts.invertible {
@@ -140,23 +171,30 @@ func (d *differ) rationalizeLastOps(ptr pointer, src, tgt interface{}, lastOpIdx
140171

141172
// compareObjects generates the patch operations that
142173
// represents the differences between two JSON objects.
143-
func (d *differ) compareObjects(ptr pointer, src, tgt map[string]interface{}) {
144-
cmpSet := make(map[string]uint8)
174+
func (d *Differ) compareObjects(ptr pointer, src, tgt map[string]interface{}) {
175+
cmpSet := map[string]uint8{}
145176

146177
for k := range src {
147178
cmpSet[k] |= 1 << 0
148179
}
149180
for k := range tgt {
150181
cmpSet[k] |= 1 << 1
151182
}
152-
for _, k := range sortedObjectKeys(cmpSet) {
183+
keys := make([]string, 0, len(cmpSet))
184+
185+
for k := range cmpSet {
186+
keys = append(keys, k)
187+
}
188+
sortStrings(keys)
189+
190+
for _, k := range keys {
153191
v := cmpSet[k]
154192
inOld := v&(1<<0) != 0
155193
inNew := v&(1<<1) != 0
156194

157195
switch {
158196
case inOld && inNew:
159-
d.compare(ptr.appendKey(k), src[k], tgt[k])
197+
d.diff(ptr.appendKey(k), src[k], tgt[k])
160198
case inOld && !inNew:
161199
d.remove(ptr.appendKey(k), src[k])
162200
case !inOld && inNew:
@@ -167,7 +205,7 @@ func (d *differ) compareObjects(ptr pointer, src, tgt map[string]interface{}) {
167205

168206
// compareArrays generates the patch operations that
169207
// represents the differences between two JSON arrays.
170-
func (d *differ) compareArrays(ptr pointer, src, tgt []interface{}) {
208+
func (d *Differ) compareArrays(ptr pointer, src, tgt []interface{}) {
171209
size := min(len(src), len(tgt))
172210

173211
// When the source array contains more elements
@@ -183,7 +221,7 @@ func (d *differ) compareArrays(ptr pointer, src, tgt []interface{}) {
183221
// Compare the elements at each index present in
184222
// both the source and destination arrays.
185223
for i := 0; i < size; i++ {
186-
d.compare(ptr.appendIndex(i), src[i], tgt[i])
224+
d.diff(ptr.appendIndex(i), src[i], tgt[i])
187225
}
188226
next:
189227
// When the target array contains more elements
@@ -194,7 +232,7 @@ next:
194232
}
195233
}
196234

197-
func (d *differ) unorderedDeepEqualSlice(src, tgt []interface{}) bool {
235+
func (d *Differ) unorderedDeepEqualSlice(src, tgt []interface{}) bool {
198236
if len(src) != len(tgt) {
199237
return false
200238
}
@@ -206,7 +244,7 @@ func (d *differ) unorderedDeepEqualSlice(src, tgt []interface{}) bool {
206244
}
207245
for _, v := range tgt {
208246
k := d.hasher.digest(v)
209-
// If the digest hash if not in the diff,
247+
// If the digest hash if not in the Compare,
210248
// return early.
211249
if _, ok := diff[k]; !ok {
212250
return false
@@ -219,7 +257,7 @@ func (d *differ) unorderedDeepEqualSlice(src, tgt []interface{}) bool {
219257
return len(diff) == 0
220258
}
221259

222-
func (d *differ) add(ptr pointer, v interface{}) {
260+
func (d *Differ) add(ptr pointer, v interface{}) {
223261
if !d.opts.factorize {
224262
d.patch = d.patch.append(OperationAdd, emptyPtr, ptr, nil, v)
225263
return
@@ -246,21 +284,21 @@ func (d *differ) add(ptr pointer, v interface{}) {
246284
}
247285
}
248286

249-
func (d *differ) replace(ptr pointer, src, tgt interface{}) {
287+
func (d *Differ) replace(ptr pointer, src, tgt interface{}) {
250288
if d.opts.invertible {
251289
d.patch = d.patch.append(OperationTest, emptyPtr, ptr, nil, src)
252290
}
253291
d.patch = d.patch.append(OperationReplace, emptyPtr, ptr, src, tgt)
254292
}
255293

256-
func (d *differ) remove(ptr pointer, v interface{}) {
294+
func (d *Differ) remove(ptr pointer, v interface{}) {
257295
if d.opts.invertible {
258296
d.patch = d.patch.append(OperationTest, emptyPtr, ptr, nil, v)
259297
}
260298
d.patch = d.patch.append(OperationRemove, emptyPtr, ptr, v, nil)
261299
}
262300

263-
func (d *differ) findUnchanged(v interface{}) pointer {
301+
func (d *Differ) findUnchanged(v interface{}) pointer {
264302
if d.hashmap != nil {
265303
k := d.hasher.digest(v)
266304
node, ok := d.hashmap[k]
@@ -271,7 +309,7 @@ func (d *differ) findUnchanged(v interface{}) pointer {
271309
return emptyPtr
272310
}
273311

274-
func (d *differ) findRemoved(v interface{}) int {
312+
func (d *Differ) findRemoved(v interface{}) int {
275313
for i := 0; i < len(d.patch); i++ {
276314
op := d.patch[i]
277315
if op.Type == OperationRemove && deepEqual(op.OldValue, v) {
@@ -281,21 +319,34 @@ func (d *differ) findRemoved(v interface{}) int {
281319
return -1
282320
}
283321

284-
func (d *differ) applyOpts(opts ...Option) {
322+
func (d *Differ) applyOpts(opts ...Option) {
285323
for _, opt := range opts {
286324
if opt != nil {
287325
opt(d)
288326
}
289327
}
290328
}
291329

292-
func sortedObjectKeys(m map[string]uint8) []string {
293-
keys := make([]string, 0, len(m))
294-
for k := range m {
295-
keys = append(keys, k)
330+
func sortStrings(v []string) {
331+
if len(v) < 20 {
332+
insertionSort(v)
333+
} else {
334+
sort.Strings(v)
335+
}
336+
}
337+
338+
func insertionSort(v []string) {
339+
for j := 1; j < len(v); j++ {
340+
// Invariant: v[:j] contains the same elements as
341+
// the original slice v[:j], but in sorted order.
342+
key := v[j]
343+
i := j - 1
344+
for i >= 0 && v[i] > key {
345+
v[i+1] = v[i]
346+
i--
347+
}
348+
v[i+1] = key
296349
}
297-
sort.Strings(keys)
298-
return keys
299350
}
300351

301352
func min(i, j int) int {

‎differ_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,11 @@ func runTestCases(t *testing.T, cases []testcase, opts ...Option) {
6767
if err != nil {
6868
t.Error(err)
6969
}
70-
d := differ{
70+
d := Differ{
7171
targetBytes: beforeBytes,
7272
}
7373
d.applyOpts(opts...)
74-
d.diff(tc.Before, tc.After)
74+
d.Compare(tc.Before, tc.After)
7575

7676
if d.patch != nil {
7777
t.Logf("\n%s", d.patch)

‎hash.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"encoding/binary"
55
"hash/maphash"
66
"math"
7-
"sort"
87
"strconv"
98
)
109

@@ -15,6 +14,7 @@ type hasher struct {
1514
func (h *hasher) digest(val interface{}) uint64 {
1615
h.mh.Reset()
1716
h.hash(val)
17+
1818
return h.mh.Sum64()
1919
}
2020

@@ -36,13 +36,14 @@ func (h *hasher) hash(i interface{}) {
3636
h.hash(e)
3737
}
3838
case map[string]interface{}:
39+
keys := make([]string, 0, len(v))
40+
3941
// Extract keys first, and sort them
4042
// in lexicographical order.
41-
keys := make([]string, 0, len(v))
4243
for k := range v {
4344
keys = append(keys, k)
4445
}
45-
sort.Strings(keys)
46+
sortStrings(keys)
4647

4748
for _, k := range keys {
4849
_, _ = h.mh.WriteString(k)

‎option.go

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package jsondiff
2+
3+
// An Option changes the default behavior of a Differ.
4+
type Option func(*Differ)
5+
6+
// Factorize enables factorization of operations.
7+
func Factorize() Option {
8+
return func(o *Differ) { o.opts.factorize = true }
9+
}
10+
11+
// Rationalize enables rationalization of operations.
12+
func Rationalize() Option {
13+
return func(o *Differ) { o.opts.rationalize = true }
14+
}
15+
16+
// Equivalent disables the generation of operations for
17+
// arrays of equal length and content that are not ordered.
18+
func Equivalent() Option {
19+
return func(o *Differ) { o.opts.equivalent = true }
20+
}
21+
22+
// Invertible enables the generation of an invertible
23+
// patch, by preceding each remove and replace operation
24+
// by a test operation that verifies the value at the
25+
// path that is being removed/replaced.
26+
// Note that copy operations are not invertible, and as
27+
// such, using this option disable the usage of copy
28+
// operation in favor of add operations.
29+
func Invertible() Option {
30+
return func(o *Differ) { o.opts.invertible = true }
31+
}
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)
Please sign in to comment.