diff_json 0.1.3 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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