diff_json 0.1.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1efd6e578e9220d1e84dda6873ced3b68193cc011340b66bbe4f6d56e70d3546
4
- data.tar.gz: b210baa07705c2afb116ba528ace5ab8dcc3b07c8246f2eb386d45e7794fa065
3
+ metadata.gz: 5c3cd1f5a10a3800d0ff8cba80743be56fac546eebf7c22023f93a6f40b658b6
4
+ data.tar.gz: 06c2a5f2935d7023d7b86c7249c6df642f90857b47b2aada9ee43c0124ab41c9
5
5
  SHA512:
6
- metadata.gz: 13dc56cb5ab81386c536d459e2e94a7ca967802b76f68353be95d9238a1d328fd03b87e3f1e01aa1950847c6a50418f8d55c04858c3fbc3b54962343e200858a
7
- data.tar.gz: 47ac22e84aeac9b32d72c2e62db97654b0c13ada095d7f095bc506935ade353bc96634bd68a1f6f2397dfe002f39a7b7ef94796e4668cf1ff0b8eb8db44bfa62
6
+ metadata.gz: 238428418dc783d66c171f530ba4bc7b4584e9ce0a59d3e52ee1ab77d8009d7dce06d2a74a29003cbabd1865adac02afe896a122502ea0e1db3a9fb42d3fb7aa
7
+ data.tar.gz: 77e5dab6ee29e02312cc37c82b7558007df74085738f2f7e534a8814841d4a47537f18cc80b92cd1212e2439590276eaaf7aabe71513d87a4021b718f126c1ed
data/lib/diff_json.rb CHANGED
@@ -1,5 +1,4 @@
1
1
  require 'json'
2
-
3
- require_relative './diff_json/undefined_value'
2
+ require 'require_all'
4
3
  require_relative './diff_json/diff'
5
- require_relative './diff_json/html_output'
4
+ require_rel './diff_json/output'
@@ -1,506 +1,78 @@
1
- module DiffJson
2
- class Diff
3
- def initialize(old_json, new_json, **opts)
4
- @old_json = old_json
5
- @new_json = new_json
6
- @opts = {
7
- :debug => false,
8
- :diff_count_filter => {
9
- :only => ['$**'],
10
- :except => []
11
- },
12
- :ignore_object_keys => [],
13
- :generate_object_sub_diffs => {}
14
- }.merge(opts)
15
- @filtered = @opts[:diff_count_filter] != {
16
- :only => ['$**'],
17
- :except => []
18
- }
19
- @diff = {
20
- :count => {
21
- :all => 0,
22
- :insert => 0,
23
- :update => 0,
24
- :delete => 0,
25
- :move => 0
26
- },
27
- :full_diff => {
28
- :old => [],
29
- :new => []
30
- },
31
- :sub_diffs => {}
32
- }
33
-
34
- calculate
35
- end
36
-
37
- def diff
38
- return @diff[:full_diff]
39
- end
40
-
41
- def sub_diffs
42
- return @diff[:sub_diffs]
43
- end
44
-
45
- def change_count(operation = :all)
46
- return @diff[:count][operation] || 0
47
- end
48
-
49
- def retrieve_output(output_type = :stdout, **output_opts)
50
- case output_type
51
- when :stdout
52
- when :file
53
- when :html
54
- html_output = HtmlOutput.new(self, **output_opts)
55
- return html_output
56
- end
57
- end
58
-
59
- private
60
-
61
- def calculate
62
- @diff[:full_diff][:old], @diff[:full_diff][:new] = compare_elements(@old_json, @new_json)
63
-
64
- @diff[:sub_diffs].each do |key, sub_diffs|
65
- sub_diffs.each do |value, diff|
66
- diff[:old] = [] unless diff.key?(:old)
67
- diff[:new] = [] unless diff.key?(:new)
68
-
69
- diff[:old], diff[:new] = add_blank_lines(diff[:old], diff[:new])
70
- end
71
- end
72
- end
73
-
74
- def compare_elements(old_element, new_element, indent_step = 0, path = '$')
75
- debug([
76
- 'ENTER compare_elements',
77
- "Diffing #{path}"
78
- ])
79
-
80
- old_element_lines, new_element_lines = [], []
81
-
82
- if old_element == new_element
83
- debug('Equal elements, no diff required')
84
-
85
- old_element_lines = JSON.pretty_generate(old_element, max_nesting: false, quirks_mode: true).split("\n").map{|el| [' ', "#{indentation(indent_step)}#{el}"]}
86
- new_element_lines = JSON.pretty_generate(new_element, max_nesting: false, quirks_mode: true).split("\n").map{|el| [' ', "#{indentation(indent_step)}#{el}"]}
87
- else
88
- unless value_type(old_element) == value_type(new_element)
89
- debug('Opposite type element, no diff required')
90
-
91
- increment_diff_count(path, :insert)
92
- increment_diff_count(path, :delete)
93
- old_element_lines, new_element_lines = add_blank_lines(
94
- JSON.pretty_generate(old_element, max_nesting: false, quirks_mode: true).split("\n").map{|el| ['-', "#{indentation(indent_step)}#{el}"]},
95
- JSON.pretty_generate(new_element, max_nesting: false, quirks_mode: true).split("\n").map{|el| ['+', "#{indentation(indent_step)}#{el}"]}
96
- )
97
- else
98
- debug("Found #{value_type(old_element)}, diffing")
99
-
100
- increment_diff_count(path, :update)
101
- old_element_lines, new_element_lines = self.send("#{value_type(old_element)}_diff", old_element, new_element, indent_step, path)
102
- end
103
- end
104
-
105
- return old_element_lines, new_element_lines
106
- end
107
-
108
- def array_diff(old_array, new_array, indent_step, base_path)
109
- debug('ENTER array_diff')
110
-
111
- oal, nal = old_array.length, new_array.length
112
- sal = oal < nal ? oal : nal
113
- lal = oal > nal ? oal : nal
114
- old_array_lines, new_array_lines = [[' ', "#{indentation(indent_step)}["]], [[' ', "#{indentation(indent_step)}["]]
115
- next_step = indent_step + 1
116
- operations = {
117
- 'none' => [],
118
- 'arr_add_index' => [],
119
- 'arr_drop_index' => [],
120
- 'arr_send_move' => [],
121
- 'arr_receive_move' => []
122
- }
123
-
124
- # Find indices that were added or dropped, if any
125
- if oal < nal
126
- operations['arr_add_index'] += (oal..(nal - 1)).to_a
127
- elsif oal > nal
128
- operations['arr_drop_index'] += (nal..(oal - 1)).to_a
129
- end
130
-
131
- # Find 'none' and 'move_value' operations
132
- (old_array | new_array).each do |v|
133
- # For a given value, find all indices of each array that corresponds
134
- old_indices, new_indices = array_indices(old_array, v), array_indices(new_array, v)
135
- # Same index, same value, no diff necessary
136
- operations['none'] += (old_indices & new_indices)
137
-
138
- # Pull the skipped indices before calculating movements
139
- old_indices -= operations['none']
140
- new_indices -= operations['none']
141
-
142
- # Find values that were moved from one index to another
143
- if !old_indices.empty? and !new_indices.empty?
144
- max_moves = old_indices.length < new_indices.length ? old_indices.length : new_indices.length
145
- possible_moves = []
146
- # Make pairs of possible moves
147
- old_indices.each do |oi|
148
- new_indices.each do |ni|
149
- possible_moves << [(oi - ni).abs, [oi, ni]]
150
- end
151
- end
152
- # For the sake of simplicity, we'll arbitrarily decide to use the shortest moves
153
- possible_moves.sort!{|x,y| x[0] <=> y[0]}
154
- # Take the first (max_moves) moves and add their operations
155
- possible_moves[0..(max_moves - 1)].each do |move|
156
- operations['arr_send_move'] << move[1][0]
157
- operations['arr_receive_move'] << move[1][1]
158
- end
159
- end
160
- end
161
-
162
- # Add base diff for each index
163
- (0..(lal - 1)).each do |i|
164
- debug("PROCESS INDEX #{i}")
165
-
166
- item_path = "#{base_path}[#{i}]"
167
- old_item_lines, new_item_lines = [], []
168
- item_diff_operations = []
169
- last_loop = (i == (lal - 1))
170
-
171
- # Assign current known operations to each index
172
- (operations.keys).each do |operation|
173
- if operations[operation].include?(i)
174
- item_diff_operations << operation
175
- end
176
- end
177
-
178
- # Add arr_change_value, arr_add_value, and arr_drop_value operations
179
- if item_diff_operations.empty?
180
- item_diff_operations << 'arr_change_value'
181
- elsif (
182
- item_diff_operations.include?('arr_send_move') and
183
- !item_diff_operations.include?('arr_receive_move') and
184
- !item_diff_operations.include?('arr_drop_index')
185
- )
186
- item_diff_operations << 'arr_add_value'
187
- elsif (
188
- !item_diff_operations.include?('arr_send_move') and
189
- item_diff_operations.include?('arr_receive_move') and
190
- !item_diff_operations.include?('arr_add_index')
191
- )
192
- item_diff_operations << 'arr_drop_value'
193
- end
194
-
195
- # Call compare_elements for sub-elements if necessary
196
- if (!(item_diff_operations & ['none', 'arr_change_value']).empty? and
197
- is_json_element?(old_array[i]) and is_json_element?(new_array[i])
198
- )
199
- old_item_lines, new_item_lines = compare_elements(old_array[i], new_array[i], next_step, item_path)
200
- else
201
- # Grab old and new items
202
- # UndefinedValue class is here to represent the difference between explicit null and non-existent
203
- old_item = item_diff_operations.include?('arr_add_index') ? UndefinedValue.new : old_array[i]
204
- new_item = item_diff_operations.include?('arr_drop_index') ? UndefinedValue.new : new_array[i]
205
-
206
- # Figure out operators for left and right
207
- if item_diff_operations.include?('none')
208
- old_operator, new_operator = ' ', ' '
209
- elsif item_diff_operations.include?('arr_change_value')
210
- increment_diff_count(item_path, :update)
211
- old_operator, new_operator = '-', '+'
212
- elsif (item_diff_operations & ['arr_send_move', 'arr_receive_move']).length == 2
213
- increment_diff_count(item_path, :move)
214
- old_operator, new_operator = 'M', 'M'
215
- elsif item_diff_operations.include?('arr_add_value')
216
- increment_diff_count(item_path, :insert)
217
- old_operator, new_operator = 'M', '+'
218
- elsif item_diff_operations.include?('arr_drop_value')
219
- increment_diff_count(item_path, :delete)
220
- old_operator, new_operator = '-', 'M'
221
- elsif item_diff_operations.include?('arr_drop_index')
222
- if item_diff_operations.include?('arr_send_move')
223
- increment_diff_count(item_path, :move)
224
- old_operator, new_operator = 'M', ' '
225
- else
226
- increment_diff_count(item_path, :delete)
227
- old_operator, new_operator = '-', ' '
228
- end
229
- elsif item_diff_operations.include?('arr_add_index')
230
- if item_diff_operations.include?('arr_receive_move')
231
- old_operator, new_operator = ' ', 'M'
232
- else
233
- increment_diff_count(item_path, :insert)
234
- old_operator, new_operator = ' ', '+'
235
- end
236
- end
237
-
238
- # Gather lines
239
- if old_item.is_a?(UndefinedValue)
240
- new_item_lines = JSON.pretty_generate(new_item, max_nesting: false, quirks_mode: true).split("\n").map{|il| [new_operator, "#{indentation(next_step)}#{il}"]}
1
+ require_rel './diff'
241
2
 
242
- (0..(new_item_lines.length - 1)).each do |i|
243
- old_item_lines << [' ', '']
244
- end
245
- else
246
- old_item_lines = JSON.pretty_generate(old_item, max_nesting: false, quirks_mode: true).split("\n").map{|il| [old_operator, "#{indentation(next_step)}#{il}"]}
247
- end
248
-
249
- if new_item.is_a?(UndefinedValue)
250
- (0..(old_item_lines.length - 1)).each do |i|
251
- new_item_lines << [' ', '']
252
- end
253
- else
254
- new_item_lines = JSON.pretty_generate(new_item, max_nesting: false, quirks_mode: true).split("\n").map{|il| [new_operator, "#{indentation(next_step)}#{il}"]}
255
- end
256
- end
257
-
258
- unless old_item_lines.empty?
259
- old_item_lines.last[1] = "#{old_item_lines.last[1]}," if !last_loop and (old_item_lines.last[1].match(/[^\s]/))
260
- end
261
- unless new_item_lines.empty?
262
- new_item_lines.last[1] = "#{new_item_lines.last[1]}," if !last_loop and (new_item_lines.last[1].match(/[^\s]/))
263
- end
264
-
265
- add_object_sub_diff_if_required(item_path, old_item, old_item_lines) if old_item.is_a?(Hash) and old_operator == '-'
266
- add_object_sub_diff_if_required(item_path, new_item, new_item_lines, :new) if new_item.is_a?(Hash) and new_operator == '+'
267
-
268
- old_item_lines, new_item_lines = add_blank_lines(old_item_lines, new_item_lines)
269
-
270
- old_array_lines += old_item_lines
271
- new_array_lines += new_item_lines
272
- end
273
-
274
- old_array_lines << [' ', "#{indentation(indent_step)}]"]
275
- new_array_lines << [' ', "#{indentation(indent_step)}]"]
276
-
277
- return old_array_lines, new_array_lines
278
- end
279
-
280
- def object_diff(old_object, new_object, indent_step, base_path)
281
- debug('ENTER object_diff')
282
-
283
- keys = {
284
- 'all' => (old_object.keys | new_object.keys),
285
- 'common' => (old_object.keys & new_object.keys),
286
- 'add' => (new_object.keys - old_object.keys),
287
- 'drop' => (old_object.keys - new_object.keys)
288
- }
289
- old_object_lines, new_object_lines = [[' ', "#{indentation(indent_step)}{"]], [[' ', "#{indentation(indent_step)}{"]]
290
- next_step = indent_step + 1
291
-
292
- # For objects, we're taking a much simpler approach, so no movements
293
- keys['all'].each do |k|
294
- debug("PROCESS KEY #{k}")
295
-
296
- item_path = "#{base_path}{#{k}}"
297
- key_string = "#{JSON.pretty_generate(k, max_nesting: false, quirks_mode: true)}: "
298
- old_item_lines, new_item_lines = [], []
299
- last_loop = (k == keys['all'].last)
300
-
301
- if keys['common'].include?(k)
302
- if is_json_element?(old_object[k]) and is_json_element?(new_object[k]) and !@opts[:ignore_object_keys].include?(k)
303
- old_item_lines, new_item_lines = compare_elements(old_object[k], new_object[k], next_step, item_path)
304
- else
305
- if old_object[k] == new_object[k] or @opts[:ignore_object_keys].include?(k)
306
- old_item_lines = JSON.pretty_generate(old_object[k], max_nesting: false, quirks_mode: true).split("\n").map!{|il| [' ', "#{indentation(next_step)}#{il}"]}
307
- new_item_lines = JSON.pretty_generate(new_object[k], max_nesting: false, quirks_mode: true).split("\n").map!{|il| [' ', "#{indentation(next_step)}#{il}"]}
308
- else
309
- increment_diff_count(item_path, :update)
310
- old_item_lines = JSON.pretty_generate(old_object[k], max_nesting: false, quirks_mode: true).split("\n").map!{|il| ['-', "#{indentation(next_step)}#{il}"]}
311
- new_item_lines = JSON.pretty_generate(new_object[k], max_nesting: false, quirks_mode: true).split("\n").map!{|il| ['+', "#{indentation(next_step)}#{il}"]}
312
- end
313
- end
314
- else
315
- if keys['drop'].include?(k)
316
- increment_diff_count(item_path, :delete) unless @opts[:ignore_object_keys].include?(k)
317
- old_item_lines = JSON.pretty_generate(old_object[k], max_nesting: false, quirks_mode: true).split("\n").map!{|il| [@opts[:ignore_object_keys].include?(k) ? ' ' : '-', "#{indentation(next_step)}#{il}"]}
318
- new_item_lines = []
319
-
320
- (0..(old_item_lines.length - 1)).each do |i|
321
- new_item_lines << [' ', '']
322
- end
323
- elsif keys['add'].include?(k)
324
- increment_diff_count(item_path, :insert) unless @opts[:ignore_object_keys].include?(k)
325
- new_item_lines = JSON.pretty_generate(new_object[k], max_nesting: false, quirks_mode: true).split("\n").map!{|il| [@opts[:ignore_object_keys].include?(k) ? ' ' : '+', "#{indentation(next_step)}#{il}"]}
326
- old_item_lines = []
327
-
328
- (0..(new_item_lines.length - 1)).each do |i|
329
- old_item_lines << [' ', '']
330
- end
331
- end
332
- end
333
-
334
- unless old_item_lines.empty?
335
- old_item_lines[0][1].gsub!(/^(?<spaces>\s+)(?<content>.+)$/, "\\k<spaces>#{key_string}\\k<content>")
336
- old_item_lines.last[1] = "#{old_item_lines.last[1]}," if !last_loop and (old_item_lines.last[1].match(/[^\s]/))
337
- end
338
- unless new_item_lines.empty?
339
- new_item_lines[0][1].gsub!(/^(?<spaces>\s+)(?<content>.+)$/, "\\k<spaces>#{key_string}\\k<content>")
340
- new_item_lines.last[1] = "#{new_item_lines.last[1]}," if !last_loop and (new_item_lines.last[1].match(/[^\s]/))
341
- end
342
-
343
- old_item_lines, new_item_lines = add_blank_lines(old_item_lines, new_item_lines)
344
-
345
- old_object_lines += old_item_lines
346
- new_object_lines += new_item_lines
347
- end
348
-
349
- old_object_lines << [' ', "#{indentation(indent_step)}}"]
350
- new_object_lines << [' ', "#{indentation(indent_step)}}"]
351
-
352
- add_object_sub_diff_if_required(base_path, old_object, old_object_lines)
353
- add_object_sub_diff_if_required(base_path, new_object, new_object_lines, :new)
354
-
355
- return old_object_lines, new_object_lines
356
- end
357
-
358
- def debug(message)
359
- puts message if @opts[:debug]
360
- end
361
-
362
- def array_indices(array, value)
363
- indices = []
364
-
365
- array.each_with_index do |av,i|
366
- indices << i if av == value
367
- end
3
+ module DiffJson
4
+ def self.diff(old_json, new_json, return_type, diff_opts = {}, output_opts = {})
5
+ completed_diff = Diff.new(old_json, new_json, **diff_opts)
368
6
 
369
- return indices
7
+ return case return_type
8
+ when :raw
9
+ completed_diff
10
+ when :html
11
+ HtmlOutput.new(completed_diff, **output_opts)
370
12
  end
13
+ end
371
14
 
372
- def is_json_element?(object)
373
- return true if ['array', 'object'].include?(value_type(object))
374
- end
15
+ class Diff
16
+ include JsonMapping
17
+ include JsonDiffing
375
18
 
376
- def value_type(element)
377
- case class_name = element.class.name
378
- when 'Hash'
379
- return 'object'
380
- when 'NilClass'
381
- return 'null'
382
- when 'TrueClass', 'FalseClass'
383
- return 'boolean'
384
- else
385
- return class_name.downcase
386
- end
19
+ def initialize(old_json, new_json, **opts)
20
+ # Set config options
21
+ @opts = {
22
+ count_operations: {
23
+ '/**' => [:add, :replace, :remove, :move, :update]
24
+ },
25
+ ignore_paths: [],
26
+ path_sort: :sorted,
27
+ sub_diffs: {},
28
+ track_array_moves: true,
29
+ track_structure_updates: false
30
+ }.merge(opts)
31
+ # Create map of both JSON objects
32
+ @old_map = map_json(old_json, '', 0)
33
+ @new_map = map_json(new_json, '', 0)
34
+ # Gather the full list of all paths in both JSON objects in a consistent order
35
+ @all_paths = gather_paths(@old_map.keys, @new_map.keys, @opts[:path_sort] == :sorted)
36
+ # Generate diff operations list
37
+ @diff = diff_check(old_json, new_json)
38
+ # Find difference counts
39
+ @counts = find_counts(@diff)
40
+ # Gather sub-diffs
41
+ @sub_diffs = generate_sub_diffs
387
42
  end
388
43
 
389
- def indentation(step)
390
- step = 0 if step < 0
391
- ' ' * step
44
+ def json_map(version = :old)
45
+ return (version == :old ? @old_map : @new_map)
392
46
  end
393
47
 
394
- def add_blank_lines(left_lines, right_lines)
395
- if left_lines.length < right_lines.length
396
- (1..(right_lines.length - left_lines.length)).each do
397
- left_lines << [' ', '']
398
- end
399
- elsif left_lines.length > right_lines.length
400
- (1..(left_lines.length - right_lines.length)).each do
401
- right_lines << [' ', '']
402
- end
403
- end
404
-
405
- return left_lines, right_lines
48
+ def diff
49
+ return @diff
406
50
  end
407
51
 
408
- def increment_diff_count(path, operation)
409
- unless @filtered
410
- @diff[:count][operation] += 1
52
+ def paths(version = :joint)
53
+ return case version
54
+ when :old
55
+ json_map(:old).keys
56
+ when :new
57
+ json_map(:new).keys
411
58
  else
412
- do_count = false
413
-
414
- # Any path prefixes in `only` that match?
415
- if (
416
- @opts[:diff_count_filter].key?(:only) and
417
- @opts[:diff_count_filter][:only].is_a?(Array) and
418
- !@opts[:diff_count_filter][:only].empty?
419
- )
420
- @opts[:diff_count_filter][:only].each do |only_path|
421
- unless ['none', 'lower'].include?(path_inclusion(path, only_path))
422
- do_count = true
423
- break
424
- else
425
- next
426
- end
427
- end
428
- else
429
- # If :only is empty or non-existent, count everything
430
- do_count = true
431
- end
432
-
433
- # Make sure the specific path is not excluded, if we've established that we should probably include it
434
- if (
435
- do_count and
436
- @opts[:diff_count_filter].key?(:except) and
437
- @opts[:diff_count_filter][:except].is_a?(Array) and
438
- !@opts[:diff_count_filter][:except].empty?
439
- )
440
- @opts[:diff_count_filter][:except].each do |except_path|
441
- unless ['none', 'lower'].include?(path_inclusion(path, except_path))
442
- do_count = false
443
- break
444
- else
445
- next
446
- end
447
- end
448
- end
449
-
450
- # Ensure this operation is allowed for counting
451
- if (
452
- do_count and
453
- @opts[:diff_count_filter].key?(:operations) and
454
- @opts[:diff_count_filter][:operations].is_a?(Array)
455
- )
456
- do_count = false if (
457
- !@opts[:diff_count_filter][:operations].empty? and
458
- !@opts[:diff_count_filter][:operations].include?(operation)
459
- )
460
- end
461
-
462
- debug("Post-operation, #{do_count}")
463
-
464
- @diff[:count][:all] += 1 if do_count
465
- @diff[:count][operation] += 1 if do_count
59
+ @all_paths
466
60
  end
467
61
  end
468
62
 
469
- def path_inclusion(current_path, check_path)
470
- check_path_base = check_path.gsub(/\*/, '')
471
- check_path_wildcard = check_path.gsub(/[^\*]/, '') || ''
472
- check = 'lower'
473
-
474
- if current_path == check_path_base
475
- check = 'exact' if check_path_wildcard == ''
476
- check = 'lower'
477
- elsif current_path.include?(check_path_base)
478
- current_path_remainder = current_path.gsub(check_path_base, '')
479
- current_path_remainder_steps = current_path_remainder.split(/(\]\[|\]\{|\}\[|\}\{)/)
480
-
481
- check = 'level' if (current_path_remainder_steps.length == 1 and check_path_wildcard == '*')
482
- check = 'recurse' if (current_path_remainder_steps.length > 0 and check_path_wildcard == '**')
63
+ def count(count_type = :all)
64
+ return case count_type
65
+ when :ignore, :add, :replace, :remove, :move, :update
66
+ @counts[count_type] || 0
67
+ when :total
68
+ @counts.values.sum
483
69
  else
484
- check = 'none'
70
+ @counts
485
71
  end
486
-
487
- return check
488
72
  end
489
73
 
490
- def add_object_sub_diff_if_required(object_path, object, lines, side = :old)
491
- if (
492
- @opts.key?(:generate_object_sub_diffs) and
493
- @opts[:generate_object_sub_diffs].is_a?(Hash) and
494
- !@opts[:generate_object_sub_diffs].empty?
495
- )
496
- @opts[:generate_object_sub_diffs].each do |k,v|
497
- unless ['none', 'lower'].include?(path_inclusion(object_path, k))
498
- @diff[:sub_diffs][v] = {} unless @diff[:sub_diffs].key?(v)
499
- @diff[:sub_diffs][v][object[v]] = {} unless @diff[:sub_diffs][v].key?(object[v])
500
- @diff[:sub_diffs][v][object[v]][side] = lines if object.key?(v)
501
- end
502
- end
503
- end
74
+ def sub_diffs
75
+ return @sub_diffs
504
76
  end
505
77
  end
506
78
  end
@@ -0,0 +1,193 @@
1
+ module JsonDiffing
2
+ private
3
+
4
+ def diff_check(old_element, new_element, base_path = '')
5
+ diff_operations = {}
6
+
7
+ if (old_element.is_a?(Array) and new_element.is_a?(Array)) or (old_element.is_a?(Hash) and new_element.is_a?(Hash))
8
+ element_operations = case old_element.class.name
9
+ when 'Array'
10
+ diff_array(old_element, new_element, base_path)
11
+ when 'Hash'
12
+ diff_hash(old_element, new_element, base_path)
13
+ end
14
+
15
+ if @opts[:track_structure_updates]
16
+ element_operations[base_path] = [{op: :update}] if element_operations.select{|k,v| count_path?(k, "#{base_path}/*")}.length > 0
17
+ end
18
+
19
+ diff_operations.merge!(element_operations)
20
+ else
21
+ diff_operations[base_path] = [{op: :replace, path: base_path, value: new_element}] unless old_element == new_element
22
+ end
23
+
24
+ return diff_operations
25
+ end
26
+
27
+ def diff_array(old_array, new_array, base_path)
28
+ return {} if old_array == new_array
29
+
30
+ diff_operations = {}
31
+ add_drop_operations = {}
32
+ last_shared_index = (old_array.length - 1)
33
+
34
+ if @opts[:track_array_moves]
35
+ old_array_map = old_array.each_with_index.map{|v,i| [i, v]}
36
+ new_array_map = new_array.each_with_index.map{|v,i| [i, v]}
37
+ shared_elements = (old_array_map & new_array_map)
38
+ old_move_check = (old_array_map - shared_elements)
39
+ new_move_check = (new_array_map - shared_elements)
40
+ possible_moves = []
41
+ max_moves = (old_move_check.length < new_move_check.length ? old_move_check.length : new_move_check.length)
42
+
43
+ if max_moves > 0
44
+ old_move_check.each do |omc|
45
+ destinations = new_move_check.map{|v| omc[1] == v[1] ? [(omc[0] - v[0]).abs, omc[0], v[0]] : nil}.compact.sort_by{|x| x[0]}
46
+ if !destinations.empty? and possible_moves.length < max_moves
47
+ possible_moves << {op: :move, from: "#{base_path}/#{destinations.first[1]}", path: "#{base_path}/#{destinations.first[2]}"}
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ if new_array.length > old_array.length
54
+ new_array[(old_array.length)..(new_array.length - 1)].each_with_index do |value, i|
55
+ element_path = "#{base_path}/#{(old_array.length + i)}"
56
+ add_drop_operations[element_path] = [{op: :add, path: element_path, value: value}]
57
+
58
+ if @opts[:track_array_moves]
59
+ element_move_search = possible_moves.select{|x| x[:path] == element_path}
60
+ add_drop_operations[element_path] += element_move_search
61
+ end
62
+ end
63
+ elsif old_array.length > new_array.length
64
+ last_shared_index = new_array.length - 1
65
+
66
+ old_array[(new_array.length)..(old_array.length - 1)].each_with_index do |value, i|
67
+ element_path = "#{base_path}/#{(old_array.length + i)}"
68
+ add_drop_operations[element_path] = [{op: :remove, path: element_path}]
69
+
70
+ if @opts[:track_array_moves]
71
+ element_move_search = possible_moves.select{|x| x[:from] == element_path}
72
+ add_drop_operations[element_path] += element_move_search
73
+ end
74
+ end
75
+ end
76
+
77
+ (0..last_shared_index).each do |i|
78
+ index_path = "#{base_path}/#{i}"
79
+
80
+ if @opts[:track_array_moves]
81
+ element_move_search = possible_moves.select{|x| x[:from] == index_path or x[:path] == index_path}
82
+ element_move_search << {op: :replace, path: index_path, value: new_array[i]} if element_move_search.length == 1
83
+ diff_operations.merge!(element_move_search.empty? ? diff_check(old_array[i], new_array[i], index_path) : {index_path => element_move_search})
84
+ else
85
+ unless @opts[:ignore_paths].include?(index_path)
86
+ diff_operations.merge!(diff_check(old_array[i], new_array[i], index_path))
87
+ else
88
+ diff_operations[index_path] = [{op: :ignore}]
89
+ end
90
+ end
91
+ end
92
+
93
+ diff_operations.merge!(add_drop_operations)
94
+
95
+ return diff_operations
96
+ end
97
+
98
+ def diff_hash(old_hash, new_hash, base_path)
99
+ return {} if old_hash == new_hash
100
+
101
+ diff_operations = {}
102
+ old_keys, new_keys = old_hash.keys, new_hash.keys
103
+ common_keys, added_keys, dropped_keys = (old_keys & new_keys), (new_keys - old_keys), (old_keys - new_keys)
104
+
105
+ common_keys.each do |ck|
106
+ element_path = "#{base_path}/#{ck}"
107
+
108
+ unless @opts[:ignore_paths].include?(element_path)
109
+ diff_operations.merge!(diff_check(old_hash[ck], new_hash[ck], element_path))
110
+ else
111
+ diff_operations[element_path] = [{op: :ignore}]
112
+ end
113
+ end
114
+
115
+ added_keys.each do |ak|
116
+ element_path = "#{base_path}/#{ak}"
117
+ diff_operations[element_path] = [{op: :add, path: element_path, value: new_hash[ak]}]
118
+ end
119
+
120
+ dropped_keys.each do |dk|
121
+ element_path = "#{base_path}/#{dk}"
122
+ diff_operations[element_path] = [{op: :remove, path: element_path}]
123
+ end
124
+
125
+ return diff_operations
126
+ end
127
+
128
+ def generate_sub_diffs
129
+ sub_diffs = {}
130
+
131
+ @opts[:sub_diffs].each do |k,v|
132
+ sub_diff_paths = @all_paths.select{|x| count_path?(x, k)}
133
+ old_elements = @old_map.select{|k,v| sub_diff_paths.include?(k)}.values.map{|x| {x[:value][v[:key]] => x[:value]}}.reduce(:merge)
134
+ new_elements = @new_map.select{|k,v| sub_diff_paths.include?(k)}.values.map{|x| {x[:value][v[:key]] => x[:value]}}.reduce(:merge)
135
+
136
+ (old_elements.keys + new_elements.keys).uniq.each do |sub_diff_id|
137
+ sub_diffs["#{k}::#{sub_diff_id}"] = DiffJson::Diff.new((old_elements[sub_diff_id] || {}), (new_elements[sub_diff_id] || {}), **v[:opts]) unless old_elements[sub_diff_id] == new_elements[sub_diff_id]
138
+ end
139
+ end
140
+
141
+ return sub_diffs
142
+ end
143
+
144
+ def find_counts(diff_structure)
145
+ counts = {
146
+ ignore: 0,
147
+ add: 0,
148
+ replace: 0,
149
+ remove: 0
150
+ }
151
+ counts[:move] = 0 if @opts[:track_array_moves]
152
+ counts[:update] = 0 if @opts[:track_structure_updates]
153
+
154
+ diff_structure.each do |path, operations|
155
+ inclusion = path_inclusion(path)
156
+
157
+ operations.each do |op|
158
+ counts[op[:op]] += 1 if (inclusion.include?(op[:op]) and ([:ignore, :add, :replace, :remove, :update].include?(op[:op]) or (op[:op] == :move and path == op[:from])))
159
+ end
160
+ end
161
+
162
+ return counts
163
+ end
164
+
165
+ def path_inclusion(path)
166
+ if @opts[:count_operations].include?('**')
167
+ return @opts[:count_operations]['**']
168
+ else
169
+ @opts[:count_operations].each do |path_set, operations|
170
+ return operations if count_path?(path, path_set)
171
+ end
172
+
173
+ return []
174
+ end
175
+ end
176
+
177
+ def count_path?(path, inclusion)
178
+ inclusion_base = inclusion.gsub(/\/?\**$/, '')
179
+ inclusion_wildcard = /\**$/.match(inclusion)[0]
180
+
181
+ if path.include?(inclusion_base)
182
+ trailing_elements = path.gsub(/^#{inclusion_base}\/?/, '').split('/').length
183
+
184
+ return true if (
185
+ (trailing_elements == 0 and inclusion_wildcard == '') or
186
+ (trailing_elements == 1 and inclusion_wildcard == '*') or
187
+ (trailing_elements > 0 and inclusion_wildcard == '**')
188
+ )
189
+ end
190
+
191
+ return false
192
+ end
193
+ end
@@ -0,0 +1,104 @@
1
+ module JsonMapping
2
+ private
3
+
4
+ def path_indentation(path)
5
+ return 0 if path.empty?
6
+ return path.sub('/', '').split('/').length
7
+ end
8
+
9
+ def sortable_path(path)
10
+ return [''] if path.empty?
11
+ return path.split('/').map{|p| (p =~ /^\d+$/).nil? ? p : p.to_i}
12
+ end
13
+
14
+ def gather_paths(old_paths, new_paths, sort = false)
15
+ gathered_paths = []
16
+
17
+ if sort
18
+ sortable_paths = (old_paths | new_paths).map{|path| sortable_path(path)}
19
+
20
+ sortable_paths.sort! do |x,y|
21
+ last_index = x.length > y.length ? (x.length - 1) : (y.length - 1)
22
+ sort_value = nil
23
+
24
+ (0..last_index).each do |i|
25
+ next if x[i] == y[i]
26
+
27
+ sort_value = case [x[i].class.name, y[i].class.name]
28
+ when ['NilClass', 'Fixnum'], ['NilClass', 'Integer'], ['NilClass', 'String'], ['Fixnum', 'String'], ['Integer', 'String']
29
+ -1
30
+ when ['Fixnum', 'NilClass'], ['Integer', 'NilClass'], ['String', 'NilClass'], ['String', 'Fixnum'], ['String', 'Integer']
31
+ 1
32
+ else
33
+ x[i] <=> y[i]
34
+ end
35
+
36
+ break unless sort_value.nil?
37
+ end
38
+
39
+ sort_value
40
+ end
41
+
42
+ return sortable_paths.map{|path| path.join('/')}
43
+ else
44
+ ### Implementation in progress, for now, raise error
45
+ raise 'Natural sort order is WIP, for now, do not override the :path_sort option'
46
+ end
47
+
48
+ return gathered_paths
49
+ end
50
+
51
+ def element_metadata(path, value, **overrides)
52
+ hash_list = (value.is_a?(Array) ? value.map{|x| x.hash} : [])
53
+ is_structure = (value.is_a?(Array) or value.is_a?(Hash))
54
+
55
+ return {
56
+ :hash_list => hash_list,
57
+ :indentation => path_indentation(path),
58
+ :index => 0,
59
+ :key => nil,
60
+ :length => (is_structure ? value.length : nil),
61
+ :trailing_comma => false,
62
+ :type => (is_structure ? value.class.name.downcase.to_sym : :primitive),
63
+ :value => value
64
+ }.merge(overrides)
65
+ end
66
+
67
+ def map_json(json, base_path, index, parent_length = 1, **metadata_overrides)
68
+ map = {}
69
+ map[base_path] = element_metadata(base_path, json, index: index, trailing_comma: (index < (parent_length - 1)), **metadata_overrides)
70
+
71
+ if json.is_a?(Array)
72
+ json.each_with_index do |value, i|
73
+ index_path = "#{base_path}/#{i}"
74
+
75
+ if value.is_a?(Array)
76
+ map.merge!(map_json(value, index_path, i, value.length))
77
+ elsif value.is_a?(Hash)
78
+ map.merge!(map_json(value, index_path, i, value.keys.length))
79
+ else
80
+ map[index_path] = element_metadata(index_path, value, index: i, trailing_comma: (i < (json.length - 1)))
81
+ end
82
+ end
83
+ elsif json.is_a?(Hash)
84
+ json = (@opts[:path_sort] == :sorted ? json.to_a.sort.to_h : json)
85
+ key_index = 0
86
+
87
+ json.each do |key, value|
88
+ key_path = "#{base_path}/#{key}"
89
+
90
+ if value.is_a?(Array)
91
+ map.merge!(map_json(value, key_path, key_index, value.length, key: key))
92
+ elsif value.is_a?(Hash)
93
+ map.merge!(map_json(value, key_path, key_index, value.keys.length, key: key))
94
+ else
95
+ map[key_path] = element_metadata(key_path, value, index: key_index, trailing_comma: (key_index < (json.keys.length - 1)), key: key)
96
+ end
97
+
98
+ key_index = key_index.next
99
+ end
100
+ end
101
+
102
+ return map
103
+ end
104
+ end
@@ -0,0 +1,268 @@
1
+ module DiffJson
2
+ class HtmlOutput
3
+ def initialize(diff, **opts)
4
+ @diff = diff
5
+ @opts = {
6
+ table_id_prefix: 'diff_json_view_0',
7
+ markup_type: :bootstrap
8
+ }
9
+ @markup = build
10
+ end
11
+
12
+ def markup
13
+ return @markup
14
+ end
15
+
16
+ def diff
17
+ return @diff
18
+ end
19
+
20
+ private
21
+
22
+ def build
23
+ new_markup = {main: table_markup(@diff, @opts[:table_id_prefix]), sub_diffs: {}}
24
+
25
+ @diff.sub_diffs.each do |sdid, sub_diff|
26
+ new_markup[:sub_diffs][sdid] = HtmlOutput.new(sub_diff, @opts)
27
+ end
28
+
29
+ return new_markup
30
+ end
31
+
32
+ def table_markup(table_diff, table_id_prefix)
33
+ markup_lines = {left: "", right: "", full: "", sub_diffs: {}}
34
+
35
+ html_opener = self.method("html_#{@opts[:markup_type]}_opener".to_sym).call(table_id_prefix)
36
+ markup_lines[:left] = html_opener[:left]
37
+ markup_lines[:right] = html_opener[:right]
38
+ markup_lines[:full] = html_opener[:full]
39
+
40
+ hierarchy_lock = nil
41
+ structure_queue = []
42
+
43
+ table_diff.paths.each_with_index do |path, i|
44
+ skip_path = (!hierarchy_lock.nil? and !(path =~ /^#{hierarchy_lock}.+$/).nil?)
45
+ unless skip_path
46
+ if !hierarchy_lock.nil?
47
+ hierarchy_lock = nil
48
+ end
49
+
50
+ old_element, new_element = table_diff.json_map(:old)[path], table_diff.json_map(:new)[path]
51
+
52
+ operations = (table_diff.diff[path] || []).map{|op|
53
+ case op[:op]
54
+ when :ignore, :add, :replace, :remove
55
+ op[:op]
56
+ when :move
57
+ op[:from] == path ? :send_move : :receive_move
58
+ end
59
+ }.compact
60
+
61
+ if operations.empty? or operations.include?(:ignore)
62
+ left_operators = ' '
63
+ right_operators = ' '
64
+ else
65
+ left_operators = [operations.include?(:send_move) ? 'M' : ' ', (operations & [:replace, :remove]).length > 0 ? '-' : ' '].join
66
+ right_operators = [operations.include?(:receive_move) ? 'M' : ' ', (operations & [:add, :replace]).length > 0 ? '+' : ' '].join
67
+ end
68
+
69
+ if !old_element.nil? and !new_element.nil?
70
+ if (old_element[:value] == new_element[:value]) or (operations & [:ignore, :replace]).length > 0
71
+ old_lines, new_lines = balance_output(old_element[:value], new_element[:value], indentation: old_element[:indentation], old_key: old_element[:key], new_key: new_element[:key], old_comma: old_element[:trailing_comma], new_comma: new_element[:trailing_comma])
72
+ hierarchy_lock = path unless old_element[:type] == :primitive and new_element[:type] == :primitive
73
+ else
74
+ old_lines = jpg(nil, structure: true, structure_position: :open, structure_type: old_element[:type], indentation: old_element[:indentation], key: old_element[:key], trailing_comma: false)
75
+ new_lines = jpg(nil, structure: true, structure_position: :open, structure_type: new_element[:type], indentation: new_element[:indentation], key: new_element[:key], trailing_comma: false)
76
+ structure_queue.push({path: path, type: old_element[:type], indentation: old_element[:indentation], old_comma: old_element[:trailing_comma], new_comma: new_element[:trailing_comma]})
77
+ end
78
+ elsif old_element.nil?
79
+ old_lines, new_lines = balance_output(UndefinedValue.new, new_element[:value], indentation: new_element[:indentation], new_key: new_element[:key], new_comma: new_element[:trailing_comma])
80
+ hierarchy_lock = path unless new_element[:type] == :primitive
81
+ else
82
+ old_lines, new_lines = balance_output(old_element[:value], UndefinedValue.new, indentation: old_element[:indentation], old_key: old_element[:key], old_comma: old_element[:trailing_comma])
83
+ hierarchy_lock = path unless old_element[:type] == :primitive
84
+ end
85
+
86
+ compiled_lines = self.method("html_#{@opts[:markup_type]}_lines".to_sym).call(left_operators, old_lines, right_operators, new_lines)
87
+ markup_lines[:left] << compiled_lines[:left]
88
+ markup_lines[:right] << compiled_lines[:right]
89
+ markup_lines[:full] << compiled_lines[:full]
90
+ end
91
+
92
+ unless structure_queue.empty?
93
+ if i == (table_diff.paths.length - 1)
94
+ structure_queue.reverse.each do |sq|
95
+ old_lines = jpg(nil, structure: true, structure_position: :close, structure_type: sq[:type], indentation: sq[:indentation], trailing_comma: sq[:old_comma])
96
+ new_lines = jpg(nil, structure: true, structure_position: :close, structure_type: sq[:type], indentation: sq[:indentation], trailing_comma: sq[:new_comma])
97
+ compiled_lines = self.method("html_#{@opts[:markup_type]}_lines".to_sym).call(' ', old_lines, ' ', new_lines)
98
+ markup_lines[:left] << compiled_lines[:left]
99
+ markup_lines[:right] << compiled_lines[:right]
100
+ markup_lines[:full] << compiled_lines[:full]
101
+ end
102
+ else
103
+ if (table_diff.paths[(i+1)] =~ /^#{structure_queue.last[:path]}.+$/).nil?
104
+ sq = structure_queue.pop
105
+ old_lines = jpg(nil, structure: true, structure_position: :close, structure_type: sq[:type], indentation: sq[:indentation], trailing_comma: sq[:old_comma])
106
+ new_lines = jpg(nil, structure: true, structure_position: :close, structure_type: sq[:type], indentation: sq[:indentation], trailing_comma: sq[:new_comma])
107
+ compiled_lines = self.method("html_#{@opts[:markup_type]}_lines".to_sym).call(' ', old_lines, ' ', new_lines)
108
+ markup_lines[:left] << compiled_lines[:left]
109
+ markup_lines[:right] << compiled_lines[:right]
110
+ markup_lines[:full] << compiled_lines[:full]
111
+ end
112
+ end
113
+ end
114
+ end
115
+
116
+ html_closer = self.method("html_#{@opts[:markup_type]}_closer".to_sym).call
117
+ markup_lines[:left] << html_closer[:left]
118
+ markup_lines[:right] << html_closer[:right]
119
+ markup_lines[:full] << html_closer[:full]
120
+
121
+ return markup_lines
122
+ end
123
+
124
+ def html_table_opener(table_id_prefix)
125
+ compiled_lines = {}
126
+ compiled_lines[:left] = "<table id=\"#{table_id_prefix}_left\" class=\"diff-json-view diff-json-split-view-left\">\n"
127
+ compiled_lines[:right] = "<table id=\"#{table_id_prefix}_right\" class=\"diff-json-view diff-json-split-view-right\">\n"
128
+ compiled_lines[:full] = "<table id=\"#{table_id_prefix}_full\" class=\"diff-json-view diff-json-full-view\">\n"
129
+
130
+ return compiled_lines
131
+ end
132
+
133
+ def html_table_lines(left_operators, left_lines, right_operators, right_lines)
134
+ compiled_lines = {left: "", right: "", full: ""}
135
+
136
+ (0..(left_lines.length - 1)).each do |i|
137
+ compiled_lines[:left] << <<-EOL
138
+ <tr class="diff-json-view-line">
139
+ <td class="diff-json-view-line-operator"><pre>#{left_operators unless left_lines[i].empty?}</pre></td>
140
+ <td class="diff-json-view-line-content"><pre class="diff-json-line-breaker">#{left_lines[i]}</pre></td>
141
+ </tr>
142
+ EOL
143
+ compiled_lines[:right] << <<-EOL
144
+ <tr class="diff-json-view-line">
145
+ <td class="diff-json-view-line-operator"><pre>#{right_operators unless right_lines[i].empty?}</pre></td>
146
+ <td class="diff-json-view-line-content"><pre class="diff-json-line-breaker">#{right_lines[i]}</pre></td>
147
+ </tr>
148
+ EOL
149
+ compiled_lines[:full] << <<-EOL
150
+ <tr class="diff-json-view-line">
151
+ <div class="row">
152
+ <td class="diff-json-view-line-operator"><pre>#{left_operators unless left_lines[i].empty?}</pre></td>
153
+ <td class="diff-json-view-line-content"><pre class="diff-json-line-breaker">#{left_lines[i]}</pre></td>
154
+ <td class="diff-json-view-column-break"></td>
155
+ <td class="diff-json-view-line-operator"><pre>#{right_operators unless right_lines[i].empty?}</pre></td>
156
+ <td class="diff-json-view-line-content"><pre class="diff-json-line-breaker">#{right_lines[i]}</pre></td>
157
+ </div>
158
+ </tr>
159
+ EOL
160
+ end
161
+
162
+ return compiled_lines
163
+ end
164
+
165
+ def html_table_closer
166
+ compiled_lines = {}
167
+ compiled_lines[:left] = "</table>"
168
+ compiled_lines[:right] = "</table>"
169
+ compiled_lines[:full] = "</table>"
170
+
171
+ return compiled_lines
172
+ end
173
+
174
+ def html_bootstrap_opener(table_id_prefix)
175
+ compiled_lines = {}
176
+ compiled_lines[:left] = "<div id=\"#{table_id_prefix}_left\" class=\"diff-json-view diff-json-split-view-left col-xs-6 col-6\">\n"
177
+ compiled_lines[:right] = "<div id=\"#{table_id_prefix}_right\" class=\"diff-json-view diff-json-split-view-right col-xs-6 col-6\">\n"
178
+ compiled_lines[:full] = "<div id=\"#{table_id_prefix}_full\" class=\"diff-json-view diff-json-full-view col-xs-12 col-12\">\n"
179
+
180
+ return compiled_lines
181
+ end
182
+
183
+ def html_bootstrap_lines(left_operators, left_lines, right_operators, right_lines)
184
+ compiled_lines = {left: "", right: "", full: ""}
185
+
186
+ (0..(left_lines.length - 1)).each do |i|
187
+ compiled_lines[:left] << <<-EOL
188
+ <div class="diff-json-view-line row">
189
+ <div class="diff-json-view-line-operator col-xs-1"><pre>#{left_operators unless left_lines[i].empty?}</pre></div>
190
+ <div class="diff-json-view-line-content col-xs-11"><pre class="diff-json-line-breaker #{highlight_class(left_operators) unless left_lines[i].empty?}">#{left_lines[i]}</pre></div>
191
+ </div>
192
+ EOL
193
+ compiled_lines[:right] << <<-EOL
194
+ <div class="diff-json-view-line row">
195
+ <div class="diff-json-view-line-operator col-xs-1"><pre>#{right_operators unless right_lines[i].empty?}</pre></div>
196
+ <div class="diff-json-view-line-content col-xs-11"><pre class="diff-json-line-breaker #{highlight_class(right_operators) unless right_lines[i].empty?}">#{right_lines[i]}</pre></div>
197
+ </div>
198
+ EOL
199
+ compiled_lines[:full] << <<-EOL
200
+ <div class="diff-json-view-line row">
201
+ <div class="diff-json-view-line-left col-xs-6">
202
+ <pre class="diff-json-line-breaker #{highlight_class(left_operators) unless left_lines[i].empty?}">#{left_operators unless left_lines[i].empty?} #{left_lines[i]}</pre>
203
+ </div>
204
+ <div class="diff-json-view-line-right col-xs-6">
205
+ <pre class="diff-json-line-breaker #{highlight_class(right_operators) unless right_lines[i].empty?}">#{right_operators unless right_lines[i].empty?} #{right_lines[i]}</pre>
206
+ </div>
207
+ </div>
208
+ EOL
209
+ end
210
+
211
+ return compiled_lines
212
+ end
213
+
214
+ def html_bootstrap_closer
215
+ compiled_lines = {}
216
+ compiled_lines[:left] = "</div>"
217
+ compiled_lines[:right] = "</div>"
218
+ compiled_lines[:full] = "</div>"
219
+
220
+ return compiled_lines
221
+ end
222
+
223
+ def highlight_class(operators)
224
+ return '' if operators.empty?
225
+ return 'diff-json-content-ins' if operators.include?('+')
226
+ return 'diff-json-content-del' if operators.include?('-')
227
+ return 'diff-json-content-mov' if operators.include?('M')
228
+ end
229
+
230
+ def balance_output(old_element, new_element, indentation: 0, old_key: nil, new_key: nil, old_comma: false, new_comma: false)
231
+ old_lines, new_lines = jpg(old_element, indentation: indentation, key: old_key, trailing_comma: old_comma), jpg(new_element, indentation: indentation, key: new_key, trailing_comma: new_comma)
232
+ return old_lines, new_lines if old_lines.length == new_lines.length
233
+
234
+ if old_lines.length > new_lines.length
235
+ (old_lines.length - new_lines.length).times do
236
+ new_lines << ''
237
+ end
238
+ else
239
+ (new_lines.length - old_lines.length).times do
240
+ old_lines << ''
241
+ end
242
+ end
243
+
244
+ return old_lines, new_lines
245
+ end
246
+
247
+ def jpg(json_element, structure: false, structure_position: :open, structure_type: :array, indentation: 0, key: nil, trailing_comma: false)
248
+ return [] if json_element.is_a?(DiffJson::UndefinedValue)
249
+ if structure
250
+ generated_element = case [structure_position, structure_type]
251
+ when [:open, :array]
252
+ ['[']
253
+ when [:close, :array]
254
+ [']']
255
+ when [:open, :hash]
256
+ ['{']
257
+ when [:close, :hash]
258
+ ['}']
259
+ end
260
+ else
261
+ generated_element = JSON.pretty_generate(json_element, max_nesting: false, quirks_mode: true).lines
262
+ end
263
+ generated_element[0].prepend("#{key}: ") unless key.nil?
264
+ generated_element.last << ',' if trailing_comma
265
+ return generated_element.map{|line| line.prepend(' ' * indentation)}
266
+ end
267
+ end
268
+ end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: diff_json
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Josh MacLachlan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-07-19 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2019-08-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: require_all
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  description: Diffs two JSON objects and returns a left/right diff view, similar to
14
28
  the command line `diff` utility
15
29
  email: josh.t.maclachlan@gmail.com
@@ -19,8 +33,10 @@ extra_rdoc_files: []
19
33
  files:
20
34
  - lib/diff_json.rb
21
35
  - lib/diff_json/diff.rb
22
- - lib/diff_json/html_output.rb
23
- - lib/diff_json/undefined_value.rb
36
+ - lib/diff_json/diff/json_diffing.rb
37
+ - lib/diff_json/diff/json_mapping.rb
38
+ - lib/diff_json/output/html_output.rb
39
+ - lib/diff_json/output/undefined_value.rb
24
40
  homepage: https://github.com/jtmaclachlan/diff_json
25
41
  licenses:
26
42
  - GPL-2
@@ -1,98 +0,0 @@
1
- module DiffJson
2
- class HtmlOutput
3
-
4
- def initialize(diff, **opts)
5
- @diff = diff
6
- @opts = {
7
- :table_id_prefix => 'diff_json_view_0'
8
- }.merge(opts)
9
- @output = {
10
- :full_diff => {},
11
- :sub_diffs => {}
12
- }
13
-
14
- calculate
15
- end
16
-
17
- def full
18
- return @output[:full_diff][:full]
19
- end
20
-
21
- def left
22
- return @output[:full_diff][:left]
23
- end
24
-
25
- def right
26
- return @output[:full_diff][:right]
27
- end
28
-
29
- def sub_diffs
30
- return @output[:sub_diffs]
31
- end
32
-
33
- private
34
-
35
- def calculate
36
- @output[:full_diff] = table_markup(@opts[:table_id_prefix], @diff.diff)
37
-
38
- @diff.sub_diffs.each do |key, sub_diffs|
39
- sub_diffs.each do |value, diff|
40
- sub_key = "#{key}::#{value}"
41
- table_key = "#{key}_#{value}"
42
- @output[:sub_diffs][sub_key] = table_markup("#{@opts[:table_id_prefix]}_sub_diff_#{table_key}", diff)
43
- end
44
- end
45
- end
46
-
47
- def table_markup(table_id_prefix, lines)
48
- markup = {
49
- :full => "",
50
- :left => "",
51
- :right => ""
52
- }
53
-
54
- markup[:full] = "<table id=\"#{table_id_prefix}_full\" class=\"diff-json-view diff-json-full-view\">\n"
55
- markup[:left] = "<table id=\"#{table_id_prefix}_left\" class=\"diff-json-view diff-json-split-view-left\">\n"
56
- markup[:right] = "<table id=\"#{table_id_prefix}_right\" class=\"diff-json-view diff-json-split-view-right\">\n"
57
-
58
- (0..(lines[:old].length - 1)).each do |i|
59
- # Full, combined table output
60
- markup[:full] += " <tr class=\"diff-json-view-line\">\n"
61
- markup[:full] += " <td class=\"diff-json-view-line-operator\"><pre>#{lines[:old][i][0]}</pre></td>\n"
62
- markup[:full] += " <td class=\"diff-json-view-line-content #{content_highlight_class(:left, lines[:old][i][0])}\"><pre>#{lines[:old][i][1]}</pre></td>\n"
63
- markup[:full] += " <td class=\"diff-json-view-column-break\"></td>\n"
64
- markup[:full] += " <td class=\"diff-json-view-line-operator\"><pre>#{lines[:new][i][0]}</pre></td>\n"
65
- markup[:full] += " <td class=\"diff-json-view-line-content #{content_highlight_class(:right, lines[:new][i][0])}\"><pre>#{lines[:new][i][1]}</pre></td>\n"
66
- markup[:full] += " </tr>\n"
67
- # Split, left side output
68
- markup[:left] += " <tr class=\"diff-json-view-line\">\n"
69
- markup[:left] += " <td class=\"diff-json-view-line-operator\"><pre>#{lines[:old][i][0]}</pre></td>\n"
70
- markup[:left] += " <td class=\"diff-json-view-line-content #{content_highlight_class(:left, lines[:old][i][0])}\"><pre>#{lines[:old][i][1]}</pre></td>\n"
71
- markup[:left] += " </tr>\n"
72
- # Split, right side output
73
- markup[:right] += " <tr class=\"diff-json-view-line\">\n"
74
- markup[:right] += " <td class=\"diff-json-view-line-operator\"><pre>#{lines[:new][i][0]}</pre></td>\n"
75
- markup[:right] += " <td class=\"diff-json-view-line-content #{content_highlight_class(:right, lines[:new][i][0])}\"><pre>#{lines[:new][i][1]}</pre></td>\n"
76
- markup[:right] += " </tr>\n"
77
- end
78
-
79
- markup[:full] += "</table>\n"
80
- markup[:left] += "</table>\n"
81
- markup[:right] += "</table>\n"
82
-
83
- return markup
84
- end
85
-
86
- def content_highlight_class(side, operator)
87
- if operator == '-'
88
- return 'diff-json-content-del'
89
- elsif operator == '+'
90
- return 'diff-json-content-ins'
91
- elsif operator == 'M'
92
- return side == :left ? 'diff-json-content-del' : 'diff-json-content-ins'
93
- else
94
- return ''
95
- end
96
- end
97
- end
98
- end