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