├── .atom-build.json ├── .gitignore ├── LICENSE ├── README.md ├── go.mod ├── jsondiff.go └── jsondiff_test.go /.atom-build.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "go", 3 | "name": "go install", 4 | "sh": false, 5 | "args": ["install"], 6 | "cwd": "{PROJECT_PATH}", 7 | "errorMatch": [ 8 | "(?[\\/0-9a-zA-Z\\._]+):(?\\d+)" 9 | ], 10 | "targets": { 11 | "go test": { 12 | "cmd": "go", 13 | "sh": false, 14 | "args": ["test"], 15 | "cwd": "{PROJECT_PATH}" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /webdemo/webdemo.js 2 | /webdemo/webdemo.js.map 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2019 nsf 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JsonDiff library 2 | 3 | The main purpose of the library is integration into tests which use json and providing human-readable output of test results. 4 | 5 | The lib can compare two json items and return a detailed report of the comparison. 6 | 7 | At the moment it can detect a couple of types of differences: 8 | 9 | - FullMatch - means items are identical. 10 | - SupersetMatch - means first item is a superset of a second item. 11 | - NoMatch - means objects are different. 12 | 13 | Being a superset means that every object and array which don't match completely in a second item must be a subset of a first item. For example: 14 | 15 | ```json 16 | {"a": 1, "b": 2, "c": 3} 17 | ``` 18 | 19 | Is a superset of (or second item is a subset of a first one): 20 | 21 | ```json 22 | {"a": 1, "c": 3} 23 | ``` 24 | 25 | Library API documentation can be found on godoc.org: https://godoc.org/github.com/nsf/jsondiff 26 | 27 | You can try **LIVE** version here (compiled to wasm): https://nosmileface.dev/jsondiff 28 | 29 | The library is inspired by http://tlrobinson.net/projects/javascript-fun/jsondiff/ 30 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nsf/jsondiff 2 | 3 | go 1.16 4 | -------------------------------------------------------------------------------- /jsondiff.go: -------------------------------------------------------------------------------- 1 | package jsondiff 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "reflect" 8 | "sort" 9 | "strconv" 10 | ) 11 | 12 | type Difference int 13 | 14 | const ( 15 | FullMatch Difference = iota 16 | SupersetMatch 17 | NoMatch 18 | FirstArgIsInvalidJson 19 | SecondArgIsInvalidJson 20 | BothArgsAreInvalidJson 21 | ) 22 | 23 | func (d Difference) String() string { 24 | switch d { 25 | case FullMatch: 26 | return "FullMatch" 27 | case SupersetMatch: 28 | return "SupersetMatch" 29 | case NoMatch: 30 | return "NoMatch" 31 | case FirstArgIsInvalidJson: 32 | return "FirstArgIsInvalidJson" 33 | case SecondArgIsInvalidJson: 34 | return "SecondArgIsInvalidJson" 35 | case BothArgsAreInvalidJson: 36 | return "BothArgsAreInvalidJson" 37 | } 38 | return "Invalid" 39 | } 40 | 41 | type Tag struct { 42 | Begin string 43 | End string 44 | } 45 | 46 | type Options struct { 47 | Normal Tag 48 | Added Tag 49 | Removed Tag 50 | Changed Tag 51 | Skipped Tag 52 | SkippedArrayElement func(n int) string 53 | SkippedObjectProperty func(n int) string 54 | Prefix string 55 | Indent string 56 | PrintTypes bool 57 | ChangedSeparator string 58 | // When provided, this function will be used to compare two numbers. By default numbers are compared using their 59 | // literal representation byte by byte. 60 | CompareNumbers func(a, b json.Number) bool 61 | // When true, only differences will be printed. By default, it will print the full json. 62 | SkipMatches bool 63 | } 64 | 65 | func SkippedArrayElement(n int) string { 66 | if n == 1 { 67 | return "...skipped 1 array element..." 68 | } else { 69 | ns := strconv.FormatInt(int64(n), 10) 70 | return "...skipped " + ns + " array elements..." 71 | } 72 | } 73 | 74 | func SkippedObjectProperty(n int) string { 75 | if n == 1 { 76 | return "...skipped 1 object property..." 77 | } else { 78 | ns := strconv.FormatInt(int64(n), 10) 79 | return "...skipped " + ns + " object properties..." 80 | } 81 | } 82 | 83 | // Provides a set of options in JSON format that are fully parseable. 84 | func DefaultJSONOptions() Options { 85 | return Options{ 86 | Added: Tag{Begin: "\"prop-added\":{", End: "}"}, 87 | Removed: Tag{Begin: "\"prop-removed\":{", End: "}"}, 88 | Changed: Tag{Begin: "{\"changed\":[", End: "]}"}, 89 | ChangedSeparator: ", ", 90 | Indent: " ", 91 | } 92 | } 93 | 94 | // Provides a set of options that are well suited for console output. Options 95 | // use ANSI foreground color escape sequences to highlight changes. 96 | func DefaultConsoleOptions() Options { 97 | return Options{ 98 | Added: Tag{Begin: "\033[0;32m", End: "\033[0m"}, 99 | Removed: Tag{Begin: "\033[0;31m", End: "\033[0m"}, 100 | Changed: Tag{Begin: "\033[0;33m", End: "\033[0m"}, 101 | Skipped: Tag{Begin: "\033[0;90m", End: "\033[0m"}, 102 | SkippedArrayElement: SkippedArrayElement, 103 | SkippedObjectProperty: SkippedObjectProperty, 104 | ChangedSeparator: " => ", 105 | Indent: " ", 106 | } 107 | } 108 | 109 | // Provides a set of options that are well suited for HTML output. Works best 110 | // inside
 tag.
111 | func DefaultHTMLOptions() Options {
112 | 	return Options{
113 | 		Added:                 Tag{Begin: ``, End: ``},
114 | 		Removed:               Tag{Begin: ``, End: ``},
115 | 		Changed:               Tag{Begin: ``, End: ``},
116 | 		Skipped:               Tag{Begin: ``, End: ``},
117 | 		SkippedArrayElement:   SkippedArrayElement,
118 | 		SkippedObjectProperty: SkippedObjectProperty,
119 | 		ChangedSeparator:      " => ",
120 | 		Indent:                "    ",
121 | 	}
122 | }
123 | 
124 | type context struct {
125 | 	opts    *Options
126 | 	level   int
127 | 	lastTag *Tag
128 | 	diff    Difference
129 | }
130 | 
131 | func (ctx *context) compareNumbers(a, b json.Number) bool {
132 | 	if ctx.opts.CompareNumbers != nil {
133 | 		return ctx.opts.CompareNumbers(a, b)
134 | 	} else {
135 | 		return a == b
136 | 	}
137 | }
138 | 
139 | func (ctx *context) terminateTag(buf *bytes.Buffer) {
140 | 	if ctx.lastTag != nil {
141 | 		buf.WriteString(ctx.lastTag.End)
142 | 		ctx.lastTag = nil
143 | 	}
144 | }
145 | 
146 | func (ctx *context) newline(buf *bytes.Buffer, s string) {
147 | 	buf.WriteString(s)
148 | 	if ctx.lastTag != nil {
149 | 		buf.WriteString(ctx.lastTag.End)
150 | 	}
151 | 	buf.WriteString("\n")
152 | 	buf.WriteString(ctx.opts.Prefix)
153 | 	for i := 0; i < ctx.level; i++ {
154 | 		buf.WriteString(ctx.opts.Indent)
155 | 	}
156 | 	if ctx.lastTag != nil {
157 | 		buf.WriteString(ctx.lastTag.Begin)
158 | 	}
159 | }
160 | 
161 | func (ctx *context) key(buf *bytes.Buffer, k string) {
162 | 	buf.WriteString(strconv.Quote(k))
163 | 	buf.WriteString(": ")
164 | }
165 | 
166 | func (ctx *context) writeValue(buf *bytes.Buffer, v interface{}, full bool) {
167 | 	switch vv := v.(type) {
168 | 	case bool:
169 | 		buf.WriteString(strconv.FormatBool(vv))
170 | 	case json.Number:
171 | 		buf.WriteString(string(vv))
172 | 	case string:
173 | 		buf.WriteString(strconv.Quote(vv))
174 | 	case []interface{}:
175 | 		if full {
176 | 			if len(vv) == 0 {
177 | 				buf.WriteString("[")
178 | 			} else {
179 | 				ctx.level++
180 | 				ctx.newline(buf, "[")
181 | 			}
182 | 			for i, v := range vv {
183 | 				ctx.writeValue(buf, v, true)
184 | 				if i != len(vv)-1 {
185 | 					ctx.newline(buf, ",")
186 | 				} else {
187 | 					ctx.level--
188 | 					ctx.newline(buf, "")
189 | 				}
190 | 			}
191 | 			buf.WriteString("]")
192 | 		} else {
193 | 			buf.WriteString("[]")
194 | 		}
195 | 	case map[string]interface{}:
196 | 		if full {
197 | 			if len(vv) == 0 {
198 | 				buf.WriteString("{")
199 | 			} else {
200 | 				ctx.level++
201 | 				ctx.newline(buf, "{")
202 | 			}
203 | 
204 | 			keys := make([]string, 0, len(vv))
205 | 			for key := range vv {
206 | 				keys = append(keys, key)
207 | 			}
208 | 			sort.Strings(keys)
209 | 
210 | 			i := 0
211 | 			for _, k := range keys {
212 | 				v := vv[k]
213 | 				ctx.key(buf, k)
214 | 				ctx.writeValue(buf, v, true)
215 | 				if i != len(vv)-1 {
216 | 					ctx.newline(buf, ",")
217 | 				} else {
218 | 					ctx.level--
219 | 					ctx.newline(buf, "")
220 | 				}
221 | 				i++
222 | 			}
223 | 			buf.WriteString("}")
224 | 		} else {
225 | 			buf.WriteString("{}")
226 | 		}
227 | 	default:
228 | 		buf.WriteString("null")
229 | 	}
230 | 
231 | 	ctx.writeTypeMaybe(buf, v)
232 | }
233 | 
234 | func (ctx *context) writeTypeMaybe(buf *bytes.Buffer, v interface{}) {
235 | 	if ctx.opts.PrintTypes {
236 | 		buf.WriteString(" ")
237 | 		ctx.writeType(buf, v)
238 | 	}
239 | }
240 | 
241 | func (ctx *context) writeType(buf *bytes.Buffer, v interface{}) {
242 | 	switch v.(type) {
243 | 	case bool:
244 | 		buf.WriteString("(boolean)")
245 | 	case json.Number:
246 | 		buf.WriteString("(number)")
247 | 	case string:
248 | 		buf.WriteString("(string)")
249 | 	case []interface{}:
250 | 		buf.WriteString("(array)")
251 | 	case map[string]interface{}:
252 | 		buf.WriteString("(object)")
253 | 	default:
254 | 		buf.WriteString("(null)")
255 | 	}
256 | }
257 | 
258 | func (ctx *context) writeMismatch(buf *bytes.Buffer, a, b interface{}) {
259 | 	ctx.writeValue(buf, a, false)
260 | 	buf.WriteString(ctx.opts.ChangedSeparator)
261 | 	ctx.writeValue(buf, b, false)
262 | }
263 | 
264 | func (ctx *context) tag(buf *bytes.Buffer, tag *Tag) {
265 | 	if ctx.lastTag == tag {
266 | 		return
267 | 	} else if ctx.lastTag != nil {
268 | 		buf.WriteString(ctx.lastTag.End)
269 | 	}
270 | 	buf.WriteString(tag.Begin)
271 | 	ctx.lastTag = tag
272 | }
273 | 
274 | func (ctx *context) result(d Difference) {
275 | 	if d == NoMatch {
276 | 		ctx.diff = NoMatch
277 | 	} else if d == SupersetMatch && ctx.diff != NoMatch {
278 | 		ctx.diff = SupersetMatch
279 | 	} else if ctx.diff != NoMatch && ctx.diff != SupersetMatch {
280 | 		ctx.diff = FullMatch
281 | 	}
282 | }
283 | 
284 | func (ctx *context) printMismatch(buf *bytes.Buffer, a, b interface{}) {
285 | 	ctx.tag(buf, &ctx.opts.Changed)
286 | 	ctx.writeMismatch(buf, a, b)
287 | }
288 | 
289 | func (ctx *context) printSkipped(buf *bytes.Buffer, n *int, strfunc func(n int) string, last bool) {
290 | 	if *n == 0 || strfunc == nil {
291 | 		return
292 | 	}
293 | 	ctx.tag(buf, &ctx.opts.Skipped)
294 | 	buf.WriteString(strfunc(*n))
295 | 	if !last {
296 | 		ctx.tag(buf, &ctx.opts.Normal)
297 | 		ctx.newline(buf, ",")
298 | 	}
299 | 	*n = 0
300 | }
301 | 
302 | func (ctx *context) finalize(buf *bytes.Buffer) string {
303 | 	ctx.terminateTag(buf)
304 | 	return buf.String()
305 | }
306 | 
307 | type collectionConfig struct {
308 | 	open    string
309 | 	close   string
310 | 	skipped func(n int) string
311 | 	value   interface{}
312 | }
313 | 
314 | type dualIterator interface {
315 | 	clone() dualIterator
316 | 	count() int
317 | 	next() (a interface{}, aOK bool, b interface{}, bOK bool, i int)
318 | 	key(buf *bytes.Buffer)
319 | }
320 | 
321 | type dualSliceIterator struct {
322 | 	a       []interface{}
323 | 	b       []interface{}
324 | 	max     int
325 | 	current int
326 | }
327 | 
328 | func (it *dualSliceIterator) clone() dualIterator {
329 | 	copy := *it
330 | 	return ©
331 | }
332 | 
333 | func (it *dualSliceIterator) count() int {
334 | 	return it.max
335 | }
336 | 
337 | func (it *dualSliceIterator) next() (a interface{}, aOK bool, b interface{}, bOK bool, i int) {
338 | 	it.current++
339 | 	i = it.current
340 | 	if i <= it.max {
341 | 		if i < len(it.a) {
342 | 			a = it.a[i]
343 | 			aOK = true
344 | 		}
345 | 		if i < len(it.b) {
346 | 			b = it.b[i]
347 | 			bOK = true
348 | 		}
349 | 	} else {
350 | 		i = -1
351 | 	}
352 | 	return
353 | }
354 | 
355 | func (it *dualSliceIterator) key(buf *bytes.Buffer) {
356 | 	// noop
357 | }
358 | 
359 | type dualMapIterator struct {
360 | 	a       map[string]interface{}
361 | 	b       map[string]interface{}
362 | 	keys    []string
363 | 	current int
364 | }
365 | 
366 | func (it *dualMapIterator) clone() dualIterator {
367 | 	copy := *it
368 | 	return ©
369 | }
370 | 
371 | func (it *dualMapIterator) count() int {
372 | 	return len(it.keys)
373 | }
374 | 
375 | func (it *dualMapIterator) next() (a interface{}, aOK bool, b interface{}, bOK bool, i int) {
376 | 	it.current++
377 | 	i = it.current
378 | 	if i < len(it.keys) {
379 | 		key := it.keys[i]
380 | 		a, aOK = it.a[key]
381 | 		b, bOK = it.b[key]
382 | 	} else {
383 | 		i = -1
384 | 	}
385 | 	return
386 | }
387 | 
388 | func (it *dualMapIterator) key(buf *bytes.Buffer) {
389 | 	key := it.keys[it.current]
390 | 	buf.WriteString(strconv.Quote(key))
391 | 	buf.WriteString(": ")
392 | }
393 | 
394 | func makeDualMapIterator(a, b map[string]interface{}) dualIterator {
395 | 	keysMap := make(map[string]struct{})
396 | 	for k := range a {
397 | 		keysMap[k] = struct{}{}
398 | 	}
399 | 	for k := range b {
400 | 		keysMap[k] = struct{}{}
401 | 	}
402 | 	keys := make([]string, 0, len(keysMap))
403 | 	for k := range keysMap {
404 | 		keys = append(keys, k)
405 | 	}
406 | 	sort.Strings(keys)
407 | 	return &dualMapIterator{
408 | 		a:       a,
409 | 		b:       b,
410 | 		keys:    keys,
411 | 		current: -1,
412 | 	}
413 | }
414 | 
415 | func makeDualSliceIterator(a, b []interface{}) dualIterator {
416 | 	max := len(a)
417 | 	if len(b) > max {
418 | 		max = len(b)
419 | 	}
420 | 	return &dualSliceIterator{
421 | 		a:       a,
422 | 		b:       b,
423 | 		max:     max,
424 | 		current: -1,
425 | 	}
426 | }
427 | 
428 | func (ctx *context) collectDiffs(it dualIterator) (diffs []string, last int) {
429 | 	ctx.level++
430 | 	last = -1
431 | 	for {
432 | 		a, aok, b, bok, i := it.next()
433 | 		if i == -1 {
434 | 			break
435 | 		}
436 | 		var diff string
437 | 		if aok && bok {
438 | 			diff = ctx.printDiff(a, b)
439 | 		}
440 | 		if len(diff) > 0 || aok != bok {
441 | 			last = i
442 | 		}
443 | 		diffs = append(diffs, diff)
444 | 	}
445 | 	ctx.level--
446 | 	return
447 | }
448 | 
449 | func (ctx *context) printCollectionDiff(cfg *collectionConfig, it dualIterator) string {
450 | 	var buf bytes.Buffer
451 | 	diffs, lastDiff := ctx.collectDiffs(it.clone())
452 | 	if ctx.opts.SkipMatches && lastDiff == -1 {
453 | 		// no diffs
454 | 		return ""
455 | 	}
456 | 
457 | 	// some diffs or empty collection
458 | 	ctx.tag(&buf, &ctx.opts.Normal)
459 | 	if it.count() == 0 {
460 | 		buf.WriteString(cfg.open)
461 | 		buf.WriteString(cfg.close)
462 | 		ctx.writeTypeMaybe(&buf, cfg.value)
463 | 		return ctx.finalize(&buf)
464 | 	} else {
465 | 		ctx.level++
466 | 		ctx.newline(&buf, cfg.open)
467 | 	}
468 | 
469 | 	noDiffSpan := 0
470 | 	for {
471 | 		va, aok, vb, bok, i := it.next()
472 | 		equals := true
473 | 		if aok && bok {
474 | 			diff := diffs[i]
475 | 			if len(diff) > 0 {
476 | 				equals = false
477 | 				ctx.printSkipped(&buf, &noDiffSpan, cfg.skipped, false)
478 | 				it.key(&buf)
479 | 				buf.WriteString(diff)
480 | 			}
481 | 		} else if aok {
482 | 			equals = false
483 | 			ctx.printSkipped(&buf, &noDiffSpan, cfg.skipped, false)
484 | 			ctx.tag(&buf, &ctx.opts.Removed)
485 | 			it.key(&buf)
486 | 			ctx.writeValue(&buf, va, true)
487 | 			ctx.result(SupersetMatch)
488 | 		} else if bok {
489 | 			equals = false
490 | 			ctx.printSkipped(&buf, &noDiffSpan, cfg.skipped, false)
491 | 			ctx.tag(&buf, &ctx.opts.Added)
492 | 			it.key(&buf)
493 | 			ctx.writeValue(&buf, vb, true)
494 | 			ctx.result(NoMatch)
495 | 		}
496 | 		if ctx.opts.SkipMatches && equals {
497 | 			noDiffSpan++
498 | 		}
499 | 
500 | 		wroteItem := !ctx.opts.SkipMatches || !equals
501 | 		willWriteMoreItems :=
502 | 			(ctx.opts.SkipMatches && i < lastDiff) ||
503 | 				(ctx.opts.SkipMatches && cfg.skipped != nil && lastDiff < it.count()-1) ||
504 | 				(!ctx.opts.SkipMatches && i < it.count()-1)
505 | 
506 | 		if wroteItem && willWriteMoreItems {
507 | 			ctx.tag(&buf, &ctx.opts.Normal)
508 | 			ctx.newline(&buf, ",")
509 | 		}
510 | 		if i == it.count()-1 {
511 | 			// we're done
512 | 			ctx.printSkipped(&buf, &noDiffSpan, cfg.skipped, true)
513 | 			ctx.level--
514 | 			ctx.tag(&buf, &ctx.opts.Normal)
515 | 			ctx.newline(&buf, "")
516 | 			break
517 | 		}
518 | 	}
519 | 
520 | 	buf.WriteString(cfg.close)
521 | 	ctx.writeTypeMaybe(&buf, cfg.value)
522 | 	return ctx.finalize(&buf)
523 | }
524 | 
525 | func (ctx *context) printDiff(a, b interface{}) string {
526 | 	var buf bytes.Buffer
527 | 
528 | 	if a == nil || b == nil {
529 | 		// either is nil, means there are just two cases:
530 | 		// 1. both are nil => match
531 | 		// 2. one of them is nil => mismatch
532 | 		if a == nil && b == nil {
533 | 			// match
534 | 			if !ctx.opts.SkipMatches {
535 | 				ctx.tag(&buf, &ctx.opts.Normal)
536 | 				ctx.writeValue(&buf, a, false)
537 | 				ctx.result(FullMatch)
538 | 			}
539 | 		} else {
540 | 			// mismatch
541 | 			ctx.printMismatch(&buf, a, b)
542 | 			ctx.result(NoMatch)
543 | 		}
544 | 		return ctx.finalize(&buf)
545 | 	}
546 | 
547 | 	ka := reflect.TypeOf(a).Kind()
548 | 	kb := reflect.TypeOf(b).Kind()
549 | 	if ka != kb {
550 | 		// Go type does not match, this is definitely a mismatch since
551 | 		// we parse JSON into interface{}
552 | 		ctx.printMismatch(&buf, a, b)
553 | 		ctx.result(NoMatch)
554 | 		return ctx.finalize(&buf)
555 | 	}
556 | 
557 | 	// big switch here handles type-specific mismatches and returns if that's the case
558 | 	// buf if control flow goes past through this switch, it's a match
559 | 	// NOTE: ka == kb at this point
560 | 	switch ka {
561 | 	case reflect.Bool:
562 | 		if a.(bool) != b.(bool) {
563 | 			ctx.printMismatch(&buf, a, b)
564 | 			ctx.result(NoMatch)
565 | 			return ctx.finalize(&buf)
566 | 		}
567 | 	case reflect.String:
568 | 		// string can be a json.Number here too (because it's a string type)
569 | 		switch aa := a.(type) {
570 | 		case json.Number:
571 | 			bb, ok := b.(json.Number)
572 | 			if !ok || !ctx.compareNumbers(aa, bb) {
573 | 				ctx.printMismatch(&buf, a, b)
574 | 				ctx.result(NoMatch)
575 | 				return ctx.finalize(&buf)
576 | 			}
577 | 		case string:
578 | 			bb, ok := b.(string)
579 | 			if !ok || aa != bb {
580 | 				ctx.printMismatch(&buf, a, b)
581 | 				ctx.result(NoMatch)
582 | 				return ctx.finalize(&buf)
583 | 			}
584 | 		}
585 | 	case reflect.Slice:
586 | 		sa, sb := a.([]interface{}), b.([]interface{})
587 | 		return ctx.printCollectionDiff(&collectionConfig{
588 | 			open:    "[",
589 | 			close:   "]",
590 | 			skipped: ctx.opts.SkippedArrayElement,
591 | 			value:   a,
592 | 		}, makeDualSliceIterator(sa, sb))
593 | 	case reflect.Map:
594 | 		ma, mb := a.(map[string]interface{}), b.(map[string]interface{})
595 | 		return ctx.printCollectionDiff(&collectionConfig{
596 | 			open:    "{",
597 | 			close:   "}",
598 | 			skipped: ctx.opts.SkippedObjectProperty,
599 | 			value:   a,
600 | 		}, makeDualMapIterator(ma, mb))
601 | 	}
602 | 	if !ctx.opts.SkipMatches {
603 | 		ctx.tag(&buf, &ctx.opts.Normal)
604 | 		ctx.writeValue(&buf, a, true)
605 | 		ctx.result(FullMatch)
606 | 	}
607 | 	return ctx.finalize(&buf)
608 | }
609 | 
610 | // Compare compares two JSON documents using given options. Returns difference type and
611 | // a string describing differences.
612 | //
613 | // FullMatch means provided arguments are deeply equal.
614 | //
615 | // SupersetMatch means first argument is a superset of a second argument. In
616 | // this context being a superset means that for each object or array in the
617 | // hierarchy which don't match exactly, it must be a superset of another one.
618 | // For example:
619 | //
620 | //	{"a": 123, "b": 456, "c": [7, 8, 9]}
621 | //
622 | // Is a superset of:
623 | //
624 | //	{"a": 123, "c": [7, 8]}
625 | //
626 | // NoMatch means there is no match.
627 | //
628 | // The rest of the difference types mean that one of or both JSON documents are
629 | // invalid JSON.
630 | //
631 | // Returned string uses a format similar to pretty printed JSON to show the
632 | // human-readable difference between provided JSON documents. It is important
633 | // to understand that returned format is not a valid JSON and is not meant
634 | // to be machine readable.
635 | func Compare(a, b []byte, opts *Options) (Difference, string) {
636 | 	return CompareStreams(bytes.NewReader(a), bytes.NewReader(b), opts)
637 | }
638 | 
639 | // CompareStreams compares two JSON documents streamed by the specified readers.
640 | // See the documentation for `Compare` for a description of the input options and return values.
641 | func CompareStreams(a, b io.Reader, opts *Options) (Difference, string) {
642 | 	var av, bv interface{}
643 | 	da := json.NewDecoder(a)
644 | 	da.UseNumber()
645 | 	db := json.NewDecoder(b)
646 | 	db.UseNumber()
647 | 	errA := da.Decode(&av)
648 | 	errB := db.Decode(&bv)
649 | 	if errA != nil && errB != nil {
650 | 		return BothArgsAreInvalidJson, "both arguments are invalid json"
651 | 	}
652 | 	if errA != nil {
653 | 		return FirstArgIsInvalidJson, "first argument is invalid json"
654 | 	}
655 | 	if errB != nil {
656 | 		return SecondArgIsInvalidJson, "second argument is invalid json"
657 | 	}
658 | 
659 | 	var buf bytes.Buffer
660 | 
661 | 	ctx := context{opts: opts}
662 | 	buf.WriteString(ctx.printDiff(av, bv))
663 | 	return ctx.diff, buf.String()
664 | }
665 | 


--------------------------------------------------------------------------------
/jsondiff_test.go:
--------------------------------------------------------------------------------
  1 | package jsondiff
  2 | 
  3 | import (
  4 | 	"bytes"
  5 | 	"encoding/json"
  6 | 	"fmt"
  7 | 	"math"
  8 | 	"strings"
  9 | 	"testing"
 10 | )
 11 | 
 12 | var compareCases = []struct {
 13 | 	a      string
 14 | 	b      string
 15 | 	result Difference
 16 | }{
 17 | 	{`{"a": 5}`, `["a"]`, NoMatch},
 18 | 	{`{"a": 5}`, `{"a": 6}`, NoMatch},
 19 | 	{`{"a": 5}`, `{"a": true}`, NoMatch},
 20 | 	{`{"a": 5}`, `{"a": 5}`, FullMatch},
 21 | 	{`{"a": 5}`, `{"a": 5, "b": 6}`, NoMatch},
 22 | 	{`{"a": 5, "b": 6}`, `{"a": 5}`, SupersetMatch},
 23 | 	{`{"a": 5, "b": 6}`, `{"b": 6}`, SupersetMatch},
 24 | 	{`{"a": null}`, `{"a": 1}`, NoMatch},
 25 | 	{`{"a": null}`, `{"a": null}`, FullMatch},
 26 | 	{`{"a": "null"}`, `{"a": null}`, NoMatch},
 27 | 	{`{"a": 3.1415}`, `{"a": 3.14156}`, NoMatch},
 28 | 	{`{"a": 3.1415}`, `{"a": 3.1415}`, FullMatch},
 29 | 	{`{"a": 4213123123}`, `{"a": "4213123123"}`, NoMatch},
 30 | 	{`{"a": 4213123123}`, `{"a": 4213123123}`, FullMatch},
 31 | }
 32 | 
 33 | func TestCompare(t *testing.T) {
 34 | 	opts := DefaultConsoleOptions()
 35 | 	opts.PrintTypes = false
 36 | 	for i, c := range compareCases {
 37 | 		result, _ := Compare([]byte(c.a), []byte(c.b), &opts)
 38 | 		if result != c.result {
 39 | 			t.Errorf("case %d failed, got: %s, expected: %s", i, result, c.result)
 40 | 		}
 41 | 	}
 42 | }
 43 | 
 44 | func TestCompareStreams(t *testing.T) {
 45 | 	opts := DefaultConsoleOptions()
 46 | 	opts.PrintTypes = false
 47 | 	for i, c := range compareCases {
 48 | 		result, _ := CompareStreams(bytes.NewReader([]byte(c.a)), bytes.NewReader([]byte(c.b)), &opts)
 49 | 		if result != c.result {
 50 | 			t.Errorf("case %d failed, got: %s, expected: %s", i, result, c.result)
 51 | 		}
 52 | 	}
 53 | }
 54 | 
 55 | type diffFlag int
 56 | 
 57 | const (
 58 | 	diffSkipMatches diffFlag = 1 << iota
 59 | 	diffNoSkipString
 60 | )
 61 | 
 62 | var diffStringCases = []struct {
 63 | 	a        string
 64 | 	b        string
 65 | 	expected string
 66 | 	flags    diffFlag
 67 | }{
 68 | 	{`{"b":"foo","a":[1,2,3],"c":"zoo","d":"Joe"}`, `{"a":[1,2,4,5],"b":"baz","c":"zoo"}`, `
 69 | {
 70 |   "a": [
 71 |     1,
 72 |     2,
 73 |     (C:3 => 4:C),
 74 |     (A:5:A)
 75 |   ],
 76 |   "b": (C:"foo" => "baz":C),
 77 |   "c": "zoo",
 78 |   (R:"d": "Joe":R)
 79 | }
 80 | 	`, 0},
 81 | 	{`{"a":[{"foo":"bar"},{"b": "c"}]}`, `{"a":[{"foo":"bar"},{"b": "d"}]}`, `
 82 | {
 83 |   "a": [
 84 |     {
 85 |       "foo": "bar"
 86 |     },
 87 |     {
 88 |       "b": (C:"c" => "d":C)
 89 |     }
 90 |   ]
 91 | }
 92 | 	`, 0},
 93 | 	{`{"b":"foo","a":[1,2,3],"c":"zoo","d":"Joe"}`, `{"a":[1,2,4,5],"b":"baz","c":"zoo"}`, `
 94 | {
 95 |   "a": [
 96 |     (S:[skipped elements:2]:S),
 97 |     (C:3 => 4:C),
 98 |     (A:5:A)
 99 |   ],
100 |   "b": (C:"foo" => "baz":C),
101 |   (S:[skipped keys:1]:S),
102 |   (R:"d": "Joe":R)
103 | }
104 | 	`, diffSkipMatches},
105 | 	{`{"a":[{"foo":"bar"},{"b": "c"}]}`, `{"a":[{"foo":"bar"},{"b": "d"}]}`, `
106 | {
107 |   "a": [
108 |     (S:[skipped elements:1]:S),
109 |     {
110 |       "b": (C:"c" => "d":C)
111 |     }
112 |   ]
113 | }
114 | 	`, diffSkipMatches},
115 | 	{`[1,2,3,4,5]`, `[1,3,3,4,5]`, `
116 | [
117 |   (S:[skipped elements:1]:S),
118 |   (C:2 => 3:C),
119 |   (S:[skipped elements:3]:S)
120 | ]
121 | 	`, diffSkipMatches},
122 | 	{`{"a":1,"b":2,"c":3}`, `{"a":1,"b":"foo","c":3}`, `
123 | {
124 |   (S:[skipped keys:1]:S),
125 |   "b": (C:2 => "foo":C),
126 |   (S:[skipped keys:1]:S)
127 | }
128 | 	`, diffSkipMatches},
129 | 	{`{"a":[1,2,3]}`, `{"b":"foo"}`, `
130 |  {
131 |   (R:"a": [:R)
132 |     (R:1,:R)
133 |     (R:2,:R)
134 |     (R:3:R)
135 |   (R:]:R),
136 |   (A:"b": "foo":A)
137 | }
138 | 	`, 0},
139 | 	{`{"b":"foo","a":[1,2,3],"c":"zoo","d":"Joe"}`, `{"a":[1,2,4,5],"b":"baz","c":"zoo"}`, `
140 | {
141 |   "a": [
142 |     (C:3 => 4:C),
143 |     (A:5:A)
144 |   ],
145 |   "b": (C:"foo" => "baz":C),
146 |   (R:"d": "Joe":R)
147 | }
148 | 	`, diffSkipMatches | diffNoSkipString},
149 | 	{`{"a":[{"foo":"bar"},{"b": "c"}]}`, `{"a":[{"foo":"bar"},{"b": "d"}]}`, `
150 | {
151 |   "a": [
152 |     {
153 |       "b": (C:"c" => "d":C)
154 |     }
155 |   ]
156 | }
157 | 	`, diffSkipMatches | diffNoSkipString},
158 | 	{`[1,2,3,4,5]`, `[1,3,3,4,5]`, `
159 | [
160 |   (C:2 => 3:C)
161 | ]
162 | 	`, diffSkipMatches | diffNoSkipString},
163 | 	{`{"a":1,"b":2,"c":3}`, `{"a":1,"b":"foo","c":3}`, `
164 | {
165 |   "b": (C:2 => "foo":C)
166 | }
167 | 	`, diffSkipMatches | diffNoSkipString},
168 | }
169 | 
170 | func TestDiffString(t *testing.T) {
171 | 	opts := DefaultConsoleOptions()
172 | 	opts.Added = Tag{Begin: "(A:", End: ":A)"}
173 | 	opts.Removed = Tag{Begin: "(R:", End: ":R)"}
174 | 	opts.Changed = Tag{Begin: "(C:", End: ":C)"}
175 | 	opts.Skipped = Tag{Begin: "(S:", End: ":S)"}
176 | 	opts.SkippedObjectProperty = func(n int) string { return fmt.Sprintf("[skipped keys:%d]", n) }
177 | 	opts.SkippedArrayElement = func(n int) string { return fmt.Sprintf("[skipped elements:%d]", n) }
178 | 	opts.Indent = "  "
179 | 	for i, c := range diffStringCases {
180 | 		t.Run(fmt.Sprint(i), func(t *testing.T) {
181 | 			lopts := opts
182 | 			if c.flags&diffSkipMatches != 0 {
183 | 				lopts.SkipMatches = true
184 | 			}
185 | 			if c.flags&diffNoSkipString != 0 {
186 | 				lopts.SkippedObjectProperty = nil
187 | 				lopts.SkippedArrayElement = nil
188 | 			}
189 | 			expected := strings.TrimSpace(c.expected)
190 | 			_, diff := Compare([]byte(c.a), []byte(c.b), &lopts)
191 | 			if diff != expected {
192 | 				t.Errorf("got:\n---\n%s\n---\nexpected:\n---\n%s\n---\n", diff, expected)
193 | 			}
194 | 		})
195 | 	}
196 | }
197 | 
198 | func TestCompareFloatsWithEpsilon(t *testing.T) {
199 | 	epsilon := math.Nextafter(1.0, 2.0) - 1.0
200 | 
201 | 	opts := DefaultConsoleOptions()
202 | 	opts.PrintTypes = false
203 | 	opts.CompareNumbers = func(an, bn json.Number) bool {
204 | 		a, err1 := an.Float64()
205 | 		b, err2 := bn.Float64()
206 | 		if err1 != nil || err2 != nil {
207 | 			// fallback to byte by byte comparison if conversion fails
208 | 			return an == bn
209 | 		}
210 | 		// Scale the epsilon based on the relative size of the numbers being compared.
211 | 		// For numbers greater than 2.0, EPSILON will be smaller than the difference between two
212 | 		// adjacent floats, so it needs to be scaled up. For numbers smaller than 1.0, EPSILON could
213 | 		// easily be larger than the numbers we're comparing and thus needs scaled down. This method
214 | 		// could still break down for numbers that are very near 0, but it's the best we can do
215 | 		// without knowing the relative scale of such numbers ahead of time.
216 | 		var scaledEpsilon = epsilon * math.Max(math.Abs(a), math.Abs(b))
217 | 		return math.Abs(a-b) < scaledEpsilon
218 | 	}
219 | 
220 | 	var floatCases = []struct {
221 | 		a      string
222 | 		b      string
223 | 		result Difference
224 | 	}{
225 | 		{`{"a": 3.1415926535897}`, `{"a": 3.141592653589700000000001}`, FullMatch},
226 | 		{`{"a": 3.1415926535897}`, `{"a": 3.1415926535898}`, NoMatch},
227 | 		{`{"a": 1}`, `{"a": 1.0000000000000000000000001}`, FullMatch},
228 | 		{`{"a": 1.0}`, `{"a": 1.0000000000000000000000001}`, FullMatch},
229 | 		// Documents how the scaled epsilon method breaks down when comparing to 0.
230 | 		{`{"a": 0.0}`, `{"a": 0.0000000000000000000000000000000000000000000001}`, NoMatch},
231 | 		// Exponential notation is parsed when UseFloats is true
232 | 		{`{"a": 1e2}`, `{"a": 10e1}`, FullMatch},
233 | 	}
234 | 	for i, c := range floatCases {
235 | 		result, _ := Compare([]byte(c.a), []byte(c.b), &opts)
236 | 		if result != c.result {
237 | 			t.Errorf("case %d failed, got: %s, expected: %s", i, result, c.result)
238 | 		}
239 | 	}
240 | }
241 | 


--------------------------------------------------------------------------------