1package jsondiff
2
3import (
4	"bytes"
5	"encoding/json"
6	"reflect"
7	"sort"
8	"strconv"
9)
10
11type Difference int
12
13const (
14	FullMatch Difference = iota
15	SupersetMatch
16	NoMatch
17	FirstArgIsInvalidJson
18	SecondArgIsInvalidJson
19	BothArgsAreInvalidJson
20)
21
22func (d Difference) String() string {
23	switch d {
24	case FullMatch:
25		return "FullMatch"
26	case SupersetMatch:
27		return "SupersetMatch"
28	case NoMatch:
29		return "NoMatch"
30	case FirstArgIsInvalidJson:
31		return "FirstArgIsInvalidJson"
32	case SecondArgIsInvalidJson:
33		return "SecondArgIsInvalidJson"
34	case BothArgsAreInvalidJson:
35		return "BothArgsAreInvalidJson"
36	}
37	return "Invalid"
38}
39
40type Tag struct {
41	Begin string
42	End   string
43}
44
45type Options struct {
46	Normal           Tag
47	Added            Tag
48	Removed          Tag
49	Changed          Tag
50	Prefix           string
51	Indent           string
52	PrintTypes       bool
53	ChangedSeparator string
54}
55
56// Provides a set of options in JSON format that are fully parseable.
57func DefaultJSONOptions() Options {
58	return Options{
59		Added:            Tag{Begin: "\"prop-added\":{", End: "}"},
60		Removed:          Tag{Begin: "\"prop-removed\":{", End: "}"},
61		Changed:          Tag{Begin: "{\"changed\":[", End: "]}"},
62		ChangedSeparator: ", ",
63		Indent:           "    ",
64	}
65}
66
67// Provides a set of options that are well suited for console output. Options
68// use ANSI foreground color escape sequences to highlight changes.
69func DefaultConsoleOptions() Options {
70	return Options{
71		Added:            Tag{Begin: "\033[0;32m", End: "\033[0m"},
72		Removed:          Tag{Begin: "\033[0;31m", End: "\033[0m"},
73		Changed:          Tag{Begin: "\033[0;33m", End: "\033[0m"},
74		ChangedSeparator: " => ",
75		Indent:           "    ",
76	}
77}
78
79// Provides a set of options that are well suited for HTML output. Works best
80// inside <pre> tag.
81func DefaultHTMLOptions() Options {
82	return Options{
83		Added:            Tag{Begin: `<span style="background-color: #8bff7f">`, End: `</span>`},
84		Removed:          Tag{Begin: `<span style="background-color: #fd7f7f">`, End: `</span>`},
85		Changed:          Tag{Begin: `<span style="background-color: #fcff7f">`, End: `</span>`},
86		ChangedSeparator: " => ",
87		Indent:           "    ",
88	}
89}
90
91type context struct {
92	opts    *Options
93	buf     bytes.Buffer
94	level   int
95	lastTag *Tag
96	diff    Difference
97}
98
99func (ctx *context) newline(s string) {
100	ctx.buf.WriteString(s)
101	if ctx.lastTag != nil {
102		ctx.buf.WriteString(ctx.lastTag.End)
103	}
104	ctx.buf.WriteString("\n")
105	ctx.buf.WriteString(ctx.opts.Prefix)
106	for i := 0; i < ctx.level; i++ {
107		ctx.buf.WriteString(ctx.opts.Indent)
108	}
109	if ctx.lastTag != nil {
110		ctx.buf.WriteString(ctx.lastTag.Begin)
111	}
112}
113
114func (ctx *context) key(k string) {
115	ctx.buf.WriteString(strconv.Quote(k))
116	ctx.buf.WriteString(": ")
117}
118
119func (ctx *context) writeValue(v interface{}, full bool) {
120	switch vv := v.(type) {
121	case bool:
122		ctx.buf.WriteString(strconv.FormatBool(vv))
123	case json.Number:
124		ctx.buf.WriteString(string(vv))
125	case string:
126		ctx.buf.WriteString(strconv.Quote(vv))
127	case []interface{}:
128		if full {
129			if len(vv) == 0 {
130				ctx.buf.WriteString("[")
131			} else {
132				ctx.level++
133				ctx.newline("[")
134			}
135			for i, v := range vv {
136				ctx.writeValue(v, true)
137				if i != len(vv)-1 {
138					ctx.newline(",")
139				} else {
140					ctx.level--
141					ctx.newline("")
142				}
143			}
144			ctx.buf.WriteString("]")
145		} else {
146			ctx.buf.WriteString("[]")
147		}
148	case map[string]interface{}:
149		if full {
150			if len(vv) == 0 {
151				ctx.buf.WriteString("{")
152			} else {
153				ctx.level++
154				ctx.newline("{")
155			}
156			i := 0
157			for k, v := range vv {
158				ctx.key(k)
159				ctx.writeValue(v, true)
160				if i != len(vv)-1 {
161					ctx.newline(",")
162				} else {
163					ctx.level--
164					ctx.newline("")
165				}
166				i++
167			}
168			ctx.buf.WriteString("}")
169		} else {
170			ctx.buf.WriteString("{}")
171		}
172	default:
173		ctx.buf.WriteString("null")
174	}
175
176	ctx.writeTypeMaybe(v)
177}
178
179func (ctx *context) writeTypeMaybe(v interface{}) {
180	if ctx.opts.PrintTypes {
181		ctx.buf.WriteString(" ")
182		ctx.writeType(v)
183	}
184}
185
186func (ctx *context) writeType(v interface{}) {
187	switch v.(type) {
188	case bool:
189		ctx.buf.WriteString("(boolean)")
190	case json.Number:
191		ctx.buf.WriteString("(number)")
192	case string:
193		ctx.buf.WriteString("(string)")
194	case []interface{}:
195		ctx.buf.WriteString("(array)")
196	case map[string]interface{}:
197		ctx.buf.WriteString("(object)")
198	default:
199		ctx.buf.WriteString("(null)")
200	}
201}
202
203func (ctx *context) writeMismatch(a, b interface{}) {
204	ctx.writeValue(a, false)
205	ctx.buf.WriteString(ctx.opts.ChangedSeparator)
206	ctx.writeValue(b, false)
207}
208
209func (ctx *context) tag(tag *Tag) {
210	if ctx.lastTag == tag {
211		return
212	} else if ctx.lastTag != nil {
213		ctx.buf.WriteString(ctx.lastTag.End)
214	}
215	ctx.buf.WriteString(tag.Begin)
216	ctx.lastTag = tag
217}
218
219func (ctx *context) result(d Difference) {
220	if d == NoMatch {
221		ctx.diff = NoMatch
222	} else if d == SupersetMatch && ctx.diff != NoMatch {
223		ctx.diff = SupersetMatch
224	} else if ctx.diff != NoMatch && ctx.diff != SupersetMatch {
225		ctx.diff = FullMatch
226	}
227}
228
229func (ctx *context) printMismatch(a, b interface{}) {
230	ctx.tag(&ctx.opts.Changed)
231	ctx.writeMismatch(a, b)
232}
233
234func (ctx *context) printDiff(a, b interface{}) {
235	if a == nil || b == nil {
236		if a == nil && b == nil {
237			ctx.tag(&ctx.opts.Normal)
238			ctx.writeValue(a, false)
239			ctx.result(FullMatch)
240		} else {
241			ctx.printMismatch(a, b)
242			ctx.result(NoMatch)
243		}
244		return
245	}
246
247	ka := reflect.TypeOf(a).Kind()
248	kb := reflect.TypeOf(b).Kind()
249	if ka != kb {
250		ctx.printMismatch(a, b)
251		ctx.result(NoMatch)
252		return
253	}
254	switch ka {
255	case reflect.Bool:
256		if a.(bool) != b.(bool) {
257			ctx.printMismatch(a, b)
258			ctx.result(NoMatch)
259			return
260		}
261	case reflect.String:
262		switch aa := a.(type) {
263		case json.Number:
264			bb, ok := b.(json.Number)
265			if !ok || aa != bb {
266				ctx.printMismatch(a, b)
267				ctx.result(NoMatch)
268				return
269			}
270		case string:
271			bb, ok := b.(string)
272			if !ok || aa != bb {
273				ctx.printMismatch(a, b)
274				ctx.result(NoMatch)
275				return
276			}
277		}
278	case reflect.Slice:
279		sa, sb := a.([]interface{}), b.([]interface{})
280		salen, sblen := len(sa), len(sb)
281		max := salen
282		if sblen > max {
283			max = sblen
284		}
285		ctx.tag(&ctx.opts.Normal)
286		if max == 0 {
287			ctx.buf.WriteString("[")
288		} else {
289			ctx.level++
290			ctx.newline("[")
291		}
292		for i := 0; i < max; i++ {
293			if i < salen && i < sblen {
294				ctx.printDiff(sa[i], sb[i])
295			} else if i < salen {
296				ctx.tag(&ctx.opts.Removed)
297				ctx.writeValue(sa[i], true)
298				ctx.result(SupersetMatch)
299			} else if i < sblen {
300				ctx.tag(&ctx.opts.Added)
301				ctx.writeValue(sb[i], true)
302				ctx.result(NoMatch)
303			}
304			ctx.tag(&ctx.opts.Normal)
305			if i != max-1 {
306				ctx.newline(",")
307			} else {
308				ctx.level--
309				ctx.newline("")
310			}
311		}
312		ctx.buf.WriteString("]")
313		ctx.writeTypeMaybe(a)
314		return
315	case reflect.Map:
316		ma, mb := a.(map[string]interface{}), b.(map[string]interface{})
317		keysMap := make(map[string]bool)
318		for k := range ma {
319			keysMap[k] = true
320		}
321		for k := range mb {
322			keysMap[k] = true
323		}
324		keys := make([]string, 0, len(keysMap))
325		for k := range keysMap {
326			keys = append(keys, k)
327		}
328		sort.Strings(keys)
329		ctx.tag(&ctx.opts.Normal)
330		if len(keys) == 0 {
331			ctx.buf.WriteString("{")
332		} else {
333			ctx.level++
334			ctx.newline("{")
335		}
336		for i, k := range keys {
337			va, aok := ma[k]
338			vb, bok := mb[k]
339			if aok && bok {
340				ctx.key(k)
341				ctx.printDiff(va, vb)
342			} else if aok {
343				ctx.tag(&ctx.opts.Removed)
344				ctx.key(k)
345				ctx.writeValue(va, true)
346				ctx.result(SupersetMatch)
347			} else if bok {
348				ctx.tag(&ctx.opts.Added)
349				ctx.key(k)
350				ctx.writeValue(vb, true)
351				ctx.result(NoMatch)
352			}
353			ctx.tag(&ctx.opts.Normal)
354			if i != len(keys)-1 {
355				ctx.newline(",")
356			} else {
357				ctx.level--
358				ctx.newline("")
359			}
360		}
361		ctx.buf.WriteString("}")
362		ctx.writeTypeMaybe(a)
363		return
364	}
365	ctx.tag(&ctx.opts.Normal)
366	ctx.writeValue(a, true)
367	ctx.result(FullMatch)
368}
369
370// Compares two JSON documents using given options. Returns difference type and
371// a string describing differences.
372//
373// FullMatch means provided arguments are deeply equal.
374//
375// SupersetMatch means first argument is a superset of a second argument. In
376// this context being a superset means that for each object or array in the
377// hierarchy which don't match exactly, it must be a superset of another one.
378// For example:
379//
380//     {"a": 123, "b": 456, "c": [7, 8, 9]}
381//
382// Is a superset of:
383//
384//     {"a": 123, "c": [7, 8]}
385//
386// NoMatch means there is no match.
387//
388// The rest of the difference types mean that one of or both JSON documents are
389// invalid JSON.
390//
391// Returned string uses a format similar to pretty printed JSON to show the
392// human-readable difference between provided JSON documents. It is important
393// to understand that returned format is not a valid JSON and is not meant
394// to be machine readable.
395func Compare(a, b []byte, opts *Options) (Difference, string) {
396	var av, bv interface{}
397	da := json.NewDecoder(bytes.NewReader(a))
398	da.UseNumber()
399	db := json.NewDecoder(bytes.NewReader(b))
400	db.UseNumber()
401	errA := da.Decode(&av)
402	errB := db.Decode(&bv)
403	if errA != nil && errB != nil {
404		return BothArgsAreInvalidJson, "both arguments are invalid json"
405	}
406	if errA != nil {
407		return FirstArgIsInvalidJson, "first argument is invalid json"
408	}
409	if errB != nil {
410		return SecondArgIsInvalidJson, "second argument is invalid json"
411	}
412
413	ctx := context{opts: opts}
414	ctx.printDiff(av, bv)
415	if ctx.lastTag != nil {
416		ctx.buf.WriteString(ctx.lastTag.End)
417	}
418	return ctx.diff, ctx.buf.String()
419}
420