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 +4 -4
- data/lib/diff_json.rb +2 -3
- data/lib/diff_json/diff.rb +56 -484
- data/lib/diff_json/diff/json_diffing.rb +193 -0
- data/lib/diff_json/diff/json_mapping.rb +104 -0
- data/lib/diff_json/output/html_output.rb +268 -0
- data/lib/diff_json/{undefined_value.rb → output/undefined_value.rb} +0 -0
- metadata +21 -5
- data/lib/diff_json/html_output.rb +0 -98
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5c3cd1f5a10a3800d0ff8cba80743be56fac546eebf7c22023f93a6f40b658b6
|
4
|
+
data.tar.gz: 06c2a5f2935d7023d7b86c7249c6df642f90857b47b2aada9ee43c0124ab41c9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 238428418dc783d66c171f530ba4bc7b4584e9ce0a59d3e52ee1ab77d8009d7dce06d2a74a29003cbabd1865adac02afe896a122502ea0e1db3a9fb42d3fb7aa
|
7
|
+
data.tar.gz: 77e5dab6ee29e02312cc37c82b7558007df74085738f2f7e534a8814841d4a47537f18cc80b92cd1212e2439590276eaaf7aabe71513d87a4021b718f126c1ed
|
data/lib/diff_json.rb
CHANGED
data/lib/diff_json/diff.rb
CHANGED
@@ -1,506 +1,78 @@
|
|
1
|
-
|
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
|
-
|
243
|
-
|
244
|
-
|
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
|
-
|
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
|
-
|
373
|
-
|
374
|
-
|
15
|
+
class Diff
|
16
|
+
include JsonMapping
|
17
|
+
include JsonDiffing
|
375
18
|
|
376
|
-
def
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
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
|
390
|
-
|
391
|
-
' ' * step
|
44
|
+
def json_map(version = :old)
|
45
|
+
return (version == :old ? @old_map : @new_map)
|
392
46
|
end
|
393
47
|
|
394
|
-
def
|
395
|
-
|
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
|
409
|
-
|
410
|
-
|
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
|
-
|
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
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
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
|
-
|
70
|
+
@counts
|
485
71
|
end
|
486
|
-
|
487
|
-
return check
|
488
72
|
end
|
489
73
|
|
490
|
-
def
|
491
|
-
|
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
|
File without changes
|
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.
|
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
|
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/
|
23
|
-
- lib/diff_json/
|
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
|