json-diff 0.1.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 +7 -0
- data/.rspec +1 -0
- data/Gemfile +6 -0
- data/LICENSE +21 -0
- data/Makefile +4 -0
- data/README.md +47 -0
- data/Rakefile +9 -0
- data/json-diff.gemspec +15 -0
- data/lib/json-diff.rb +4 -0
- data/lib/json-diff/diff.rb +303 -0
- data/lib/json-diff/index-map.rb +51 -0
- data/lib/json-diff/operation.rb +47 -0
- data/lib/json-diff/version.rb +3 -0
- data/spec/json-diff/diff_spec.rb +57 -0
- data/spec/spec_helper.rb +4 -0
- metadata +60 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 29a90b679f5bf30e17ce0b2cabd2963a9a68ea03
|
4
|
+
data.tar.gz: 06c46f1cb99d0564522c30507df1f65fbcca003b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 613a0223292d0d84d7bf32a46b5f58468336251351fb1d243032172162c24a53c22f56474c1873898c492930d2283227302345febbfccdbdd9cad4c152474174
|
7
|
+
data.tar.gz: c1457cdf32d04f6f368b49b7dde261751ac1c9ef7350578e766ae63a598afa37d10d7e8edb4eb30a2590df9c0cec3bfb3f5dcdfb64b54dcd30f1211161525ee7
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
Copyright (c) 2015 Captain Train
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
+
|
data/Makefile
ADDED
data/README.md
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# `json-diff`
|
2
|
+
|
3
|
+
*Take two Ruby objects that can be serialized to JSON. Output an array of operations (additions, deletions, moves) that would convert the first one to the second one.*
|
4
|
+
|
5
|
+
```bash
|
6
|
+
gem install json-diff # Or `gem 'json-diff'` in your Gemfile.
|
7
|
+
```
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
require 'json-diff'
|
11
|
+
JsonDiff.diff(1, 2)
|
12
|
+
#> [{:op => :replace, :path => "/", :value => 2}]
|
13
|
+
```
|
14
|
+
|
15
|
+
Outputs [RFC6902][]. Look at [hana][] for a JSON patch algorithm that can use this output.
|
16
|
+
|
17
|
+
[RFC6902]: http://www.rfc-editor.org/rfc/rfc6902.txt
|
18
|
+
[hana]: https://github.com/tenderlove/hana
|
19
|
+
|
20
|
+
# Heart
|
21
|
+
|
22
|
+
- Recursive similarity computation between any two Ruby values.
|
23
|
+
- For arrays, match elements above a certain level of similarity pairwise, and treat them as a move.
|
24
|
+
- Matching happens highest-similarity first.
|
25
|
+
- The creation of move operations is generated by detecting rings in the list of moved elements (eg, A → B → C → A).
|
26
|
+
|
27
|
+
Pros:
|
28
|
+
|
29
|
+
- For lists which are not necessarily ordered, this approach yields far better results than LCS.
|
30
|
+
- Move operations require no custom code to match elements.
|
31
|
+
|
32
|
+
Cons:
|
33
|
+
|
34
|
+
- This approach's quality is heavily reliant on how good the similarity algorithm is. Empirically, it yields sensible output. It can be improved by a user-defined procedure.
|
35
|
+
- There is a computational overhead to the default similarity computation that scales with the total number of entities in the structure.
|
36
|
+
|
37
|
+
# Plans & Bugs
|
38
|
+
|
39
|
+
Roughly ordered by priority.
|
40
|
+
|
41
|
+
- Support adding a custom procedure which computes similarities.
|
42
|
+
- Support LCS as an option. (The default will remain what yields the best results, regardless of the time it takes.)
|
43
|
+
- Support specifying a depth for similarity computation.
|
44
|
+
|
45
|
+
---
|
46
|
+
|
47
|
+
See the LICENSE file for licensing information.
|
data/Rakefile
ADDED
data/json-diff.gemspec
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
$LOAD_PATH.push File.expand_path('../lib', __FILE__)
|
2
|
+
require 'json-diff/version'
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = 'json-diff'
|
6
|
+
s.license = 'MIT'
|
7
|
+
s.version = JsonDiff::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ['Captain Train']
|
10
|
+
s.email = ['ttyl@captaintrain.com']
|
11
|
+
s.homepage = 'http://github.com/captaintrain/json-diff'
|
12
|
+
s.summary = %q{Compute the difference between two JSON-serializable Ruby objects.}
|
13
|
+
s.description = %q{Take two Ruby objects that can be serialized to JSON. Output an array of operations (additions, deletions, moves) that would convert the first one to the second one.}
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
end
|
data/lib/json-diff.rb
ADDED
@@ -0,0 +1,303 @@
|
|
1
|
+
module JsonDiff
|
2
|
+
|
3
|
+
def self.diff(before, after, opts = {})
|
4
|
+
path = opts[:path] || '/'
|
5
|
+
include_addition = (opts[:additions] == nil) ? true : opts[:additions]
|
6
|
+
include_moves = (opts[:moves] == nil) ? true : opts[:moves]
|
7
|
+
|
8
|
+
changes = []
|
9
|
+
|
10
|
+
if before.is_a?(Hash)
|
11
|
+
if !after.is_a?(Hash)
|
12
|
+
changes << replace(path, before, after)
|
13
|
+
else
|
14
|
+
lost = before.keys - after.keys
|
15
|
+
lost.each do |key|
|
16
|
+
inner_path = extend_json_pointer(path, key)
|
17
|
+
changes << remove(inner_path, before[key])
|
18
|
+
end
|
19
|
+
|
20
|
+
if include_addition
|
21
|
+
gained = after.keys - before.keys
|
22
|
+
gained.each do |key|
|
23
|
+
inner_path = extend_json_pointer(path, key)
|
24
|
+
changes << add(inner_path, after[key])
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
kept = before.keys & after.keys
|
29
|
+
kept.each do |key|
|
30
|
+
inner_path = extend_json_pointer(path, key)
|
31
|
+
changes += diff(before[key], after[key], opts.merge(path: inner_path))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
elsif before.is_a?(Array)
|
35
|
+
if !after.is_a?(Array)
|
36
|
+
changes << replace(path, before, after)
|
37
|
+
elsif before.size == 0
|
38
|
+
if include_addition
|
39
|
+
after.each_with_index do |item, index|
|
40
|
+
inner_path = extend_json_pointer(path, index)
|
41
|
+
changes << add(inner_path, item)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
elsif after.size == 0
|
45
|
+
before.each do |item|
|
46
|
+
# Delete elements from the start.
|
47
|
+
inner_path = extend_json_pointer(path, 0)
|
48
|
+
changes << remove(inner_path, item)
|
49
|
+
end
|
50
|
+
else
|
51
|
+
pairing = array_pairing(before, after)
|
52
|
+
# FIXME: detect replacements.
|
53
|
+
|
54
|
+
# All detected moves that do not reach the similarity limit are deleted
|
55
|
+
# and re-added.
|
56
|
+
pairing[:pairs].select! do |pair|
|
57
|
+
sim = pair[2]
|
58
|
+
kept = (sim >= 0.5)
|
59
|
+
if !kept
|
60
|
+
pairing[:removed] << pair[0]
|
61
|
+
pairing[:added] << pair[1]
|
62
|
+
end
|
63
|
+
kept
|
64
|
+
end
|
65
|
+
|
66
|
+
array_changes(pairing)
|
67
|
+
|
68
|
+
pairing[:removed].each do |before_index|
|
69
|
+
inner_path = extend_json_pointer(path, before_index)
|
70
|
+
changes << remove(inner_path, before[before_index])
|
71
|
+
end
|
72
|
+
|
73
|
+
pairing[:pairs].each do |pair|
|
74
|
+
before_index, after_index, orig_before, orig_after = pair
|
75
|
+
inner_before_path = extend_json_pointer(path, before_index)
|
76
|
+
inner_after_path = extend_json_pointer(path, after_index)
|
77
|
+
|
78
|
+
if before_index != after_index && include_moves
|
79
|
+
changes << move(inner_before_path, inner_after_path)
|
80
|
+
end
|
81
|
+
changes += diff(before[orig_before], after[orig_after], opts.merge(path: inner_after_path))
|
82
|
+
end
|
83
|
+
|
84
|
+
if include_addition
|
85
|
+
pairing[:added].each do |after_index|
|
86
|
+
inner_path = extend_json_pointer(path, after_index)
|
87
|
+
changes << add(inner_path, after[after_index])
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
else
|
92
|
+
if before != after
|
93
|
+
changes << replace(path, before, after)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
changes
|
98
|
+
end
|
99
|
+
|
100
|
+
# {pairs: [[before index, after index, similarity]],
|
101
|
+
# removed: [before index],
|
102
|
+
# added: [after index]}
|
103
|
+
def self.array_pairing(before, after)
|
104
|
+
# Array containing the array of similarities from before to after.
|
105
|
+
similarities = before.map do |before_item|
|
106
|
+
after.map do |after_item|
|
107
|
+
similarity(before_item, after_item)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Array containing the array of couples of indices, sorted by similarity.
|
112
|
+
indices = before.map.with_index do |before_item, before_index|
|
113
|
+
after.map.with_index do |after_item, after_index|
|
114
|
+
[before_index, after_index]
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Sort them in O(n^2 log(n)).
|
119
|
+
indices.map! do |couples|
|
120
|
+
couples.sort! do |a, b|
|
121
|
+
a_before_index = a[0]
|
122
|
+
b_before_index = b[0]
|
123
|
+
a_after_index = a[1]
|
124
|
+
b_after_index = b[1]
|
125
|
+
|
126
|
+
similarities[b_before_index][b_after_index] <=> similarities[a_before_index][a_after_index]
|
127
|
+
end
|
128
|
+
end
|
129
|
+
# Sort the toplevel.
|
130
|
+
indices.sort! do |a, b|
|
131
|
+
a_top_before_index = a[0][0]
|
132
|
+
a_top_after_index = a[0][1]
|
133
|
+
b_top_before_index = b[0][0]
|
134
|
+
b_top_after_index = b[0][1]
|
135
|
+
|
136
|
+
similarities[b_top_before_index][b_top_after_index] <=> similarities[a_top_before_index][a_top_after_index]
|
137
|
+
end
|
138
|
+
|
139
|
+
# Map from indices to boolean (true if paired).
|
140
|
+
before_paired = {}
|
141
|
+
after_paired = {}
|
142
|
+
|
143
|
+
num_pairs = [before.size, after.size].min
|
144
|
+
|
145
|
+
pairs = (0...num_pairs).map do |_|
|
146
|
+
unpaired_before_index = indices.index { |a| !before_paired[a[0][0]] }
|
147
|
+
unpaired_after_index = indices[unpaired_before_index].index { |a| !after_paired[a[1]] }
|
148
|
+
unpaired_couple = indices[unpaired_before_index][unpaired_after_index]
|
149
|
+
before_paired[unpaired_couple[0]] = true
|
150
|
+
after_paired[unpaired_couple[1]] = true
|
151
|
+
|
152
|
+
[unpaired_couple[0], unpaired_couple[1],
|
153
|
+
similarities[unpaired_couple[0]][unpaired_couple[1]]]
|
154
|
+
end
|
155
|
+
|
156
|
+
if before.size < after.size
|
157
|
+
added = after.map.with_index { |_, i| i} - after_paired.keys
|
158
|
+
removed = []
|
159
|
+
else
|
160
|
+
removed = before.map.with_index { |_, i| i } - before_paired.keys
|
161
|
+
added = []
|
162
|
+
end
|
163
|
+
|
164
|
+
{
|
165
|
+
pairs: pairs,
|
166
|
+
removed: removed,
|
167
|
+
added: added,
|
168
|
+
}
|
169
|
+
end
|
170
|
+
|
171
|
+
# Compute an arbitrary notion of how probable it is that
|
172
|
+
def self.similarity(before, after)
|
173
|
+
return 0.0 if before.class != after.class
|
174
|
+
|
175
|
+
# FIXME: call custom similarity procedure.
|
176
|
+
|
177
|
+
if before.is_a?(Hash)
|
178
|
+
if before.size == 0
|
179
|
+
if after.size == 0
|
180
|
+
return 1.0
|
181
|
+
else
|
182
|
+
return 0.0
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
# Average similarity between keys' value.
|
187
|
+
# We don't consider key renames.
|
188
|
+
similarities = []
|
189
|
+
before.each do |before_key, before_item|
|
190
|
+
similarities << similarity(before_item, after[before_key])
|
191
|
+
end
|
192
|
+
|
193
|
+
similarities.reduce(:+) / similarities.size
|
194
|
+
elsif before.is_a?(Array)
|
195
|
+
return 1.0 if before.size == 0
|
196
|
+
|
197
|
+
# The most likely match between an element in the old and the new list is
|
198
|
+
# presumably the right one, so we take the average of the maximum
|
199
|
+
# similarity between each elements of the list.
|
200
|
+
similarities = before.map do |before_item|
|
201
|
+
after.map do |after_item|
|
202
|
+
similarity(before_item, after_item)
|
203
|
+
end.max || 0.0
|
204
|
+
end
|
205
|
+
|
206
|
+
similarities.reduce(:+) / similarities.size
|
207
|
+
elsif before == after
|
208
|
+
1.0
|
209
|
+
else
|
210
|
+
0.0
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
# Input:
|
215
|
+
# {pairs: [[before index, after index, similarity]],
|
216
|
+
# removed: [before index],
|
217
|
+
# added: [after index]}
|
218
|
+
#
|
219
|
+
# Output:
|
220
|
+
# {removed: [before index],
|
221
|
+
# pairs: [[before index, after index,
|
222
|
+
# original before index, original after index]],
|
223
|
+
# added: [after index]}
|
224
|
+
def self.array_changes(pairing)
|
225
|
+
# We perform removals starting from the highest index.
|
226
|
+
# That way, they don't offset their own.
|
227
|
+
pairing[:removed].sort!.reverse!
|
228
|
+
pairing[:added].sort!
|
229
|
+
|
230
|
+
# First, map indices from before to after removals.
|
231
|
+
removal_map = IndexMaps.new
|
232
|
+
pairing[:removed].each { |rm| removal_map.removal(rm) }
|
233
|
+
# And map indices from after to before additions
|
234
|
+
# (removals, since it is reversed).
|
235
|
+
addition_map = IndexMaps.new
|
236
|
+
pairing[:added].each { |ad| addition_map.removal(ad) }
|
237
|
+
|
238
|
+
moves = {}
|
239
|
+
orig_before = {}
|
240
|
+
orig_after = {}
|
241
|
+
pairing[:pairs].each do |before, after|
|
242
|
+
mapped_before = removal_map.map(before)
|
243
|
+
mapped_after = addition_map.map(after)
|
244
|
+
orig_before[mapped_before] = before
|
245
|
+
orig_after[mapped_after] = after
|
246
|
+
moves[mapped_before] = mapped_after
|
247
|
+
end
|
248
|
+
|
249
|
+
# Now, detect rings within the pairs.
|
250
|
+
# The proof is, if whatever was at position i was sent to position j,
|
251
|
+
# whatever was at position j cannot have stayed at j.
|
252
|
+
# By induction, there is a ring.
|
253
|
+
# Oh, and a piece of the proof is that the arrays have the same length.
|
254
|
+
# Trivially. Right. Hey, this is not an interview!
|
255
|
+
rings = []
|
256
|
+
while moves.size > 0
|
257
|
+
# i goes to j. j goes to (…). k goes to i.
|
258
|
+
ring = []
|
259
|
+
pair = moves.shift
|
260
|
+
origin, target = pair
|
261
|
+
first_origin = origin
|
262
|
+
while target != first_origin
|
263
|
+
ring << origin
|
264
|
+
origin = target
|
265
|
+
target = moves[target]
|
266
|
+
moves.delete(origin)
|
267
|
+
end
|
268
|
+
ring << origin
|
269
|
+
rings << ring
|
270
|
+
end
|
271
|
+
# rings is of the form [[i,j,k], …]
|
272
|
+
|
273
|
+
# Finally, we can register the moves.
|
274
|
+
# The idea is, if the whole ring moves instantaneously,
|
275
|
+
# no element outside of the ring changed position.
|
276
|
+
pairs = []
|
277
|
+
rings.each do |ring|
|
278
|
+
orig_ring = ring.map { |i| [orig_before[i], orig_after[i]] }
|
279
|
+
ring_map = IndexMaps.new
|
280
|
+
len = ring.size
|
281
|
+
i = 0
|
282
|
+
while i < len
|
283
|
+
ni = (i + 1) % len # next i
|
284
|
+
if ring[i] != ring[ni]
|
285
|
+
pairs << [ring[i], ring[ni], orig_ring[i][0], orig_ring[ni][1]]
|
286
|
+
end
|
287
|
+
ring_map.removal(ring[i])
|
288
|
+
ring_map.addition(ring[ni])
|
289
|
+
j = i + 1
|
290
|
+
while j < len
|
291
|
+
ring[j] = ring_map.map(ring[j])
|
292
|
+
j += 1
|
293
|
+
end
|
294
|
+
i += 1
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
pairing[:pairs] = pairs
|
299
|
+
|
300
|
+
pairing
|
301
|
+
end
|
302
|
+
|
303
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module JsonDiff
|
2
|
+
|
3
|
+
class IndexMaps
|
4
|
+
def initialize
|
5
|
+
@maps = []
|
6
|
+
end
|
7
|
+
|
8
|
+
def addition(index)
|
9
|
+
@maps << AdditionIndexMap.new(index)
|
10
|
+
end
|
11
|
+
|
12
|
+
def removal(index)
|
13
|
+
@maps << RemovalIndexMap.new(index)
|
14
|
+
end
|
15
|
+
|
16
|
+
def map(index)
|
17
|
+
@maps.each do |map|
|
18
|
+
index = map.map(index)
|
19
|
+
end
|
20
|
+
index
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class IndexMap
|
25
|
+
def initialize(pivot)
|
26
|
+
@pivot = pivot
|
27
|
+
end
|
28
|
+
|
29
|
+
def map(index)
|
30
|
+
if index >= @pivot
|
31
|
+
index + 1
|
32
|
+
else
|
33
|
+
index
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class AdditionIndexMap < IndexMap
|
39
|
+
end
|
40
|
+
|
41
|
+
class RemovalIndexMap < IndexMap
|
42
|
+
def map(index)
|
43
|
+
if index >= @pivot
|
44
|
+
index - 1
|
45
|
+
else
|
46
|
+
index
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module JsonDiff
|
2
|
+
|
3
|
+
# Convert a list of strings or numbers to an RFC6901 JSON pointer.
|
4
|
+
# http://tools.ietf.org/html/rfc6901
|
5
|
+
def self.json_pointer(path)
|
6
|
+
escaped_path = path.map do |key|
|
7
|
+
if key.is_a?(String)
|
8
|
+
key.gsub('~', '~0')
|
9
|
+
.gsub('/', '~1')
|
10
|
+
else
|
11
|
+
key.to_s
|
12
|
+
end
|
13
|
+
end.join('/')
|
14
|
+
|
15
|
+
"/#{escaped_path}"
|
16
|
+
end
|
17
|
+
|
18
|
+
# Add a key to a JSON pointer.
|
19
|
+
def self.extend_json_pointer(pointer, key)
|
20
|
+
if pointer == '/'
|
21
|
+
json_pointer([key])
|
22
|
+
else
|
23
|
+
pointer + json_pointer([key])
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.add(path, value)
|
28
|
+
{op: :add, path: path, value: value}
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.remove(path, value)
|
32
|
+
if value != nil
|
33
|
+
{op: :remove, path: path, value: value}
|
34
|
+
else
|
35
|
+
{op: :remove, path: path}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.replace(path, value)
|
40
|
+
{op: :replace, path: path, value: value}
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.move(source, target)
|
44
|
+
{op: :move, from: source, path: target}
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe JsonDiff do
|
4
|
+
it "should be able to diff two empty arrays" do
|
5
|
+
diff = JsonDiff.diff([], [])
|
6
|
+
expect(diff).to eql([])
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should be able to diff an empty array with a filled one" do
|
10
|
+
diff = JsonDiff.diff([], [1, 2, 3])
|
11
|
+
expect(diff).to eql([
|
12
|
+
{op: :add, path: "/0", value: 1},
|
13
|
+
{op: :add, path: "/1", value: 2},
|
14
|
+
{op: :add, path: "/2", value: 3},
|
15
|
+
])
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should be able to diff a filled array with an empty one" do
|
19
|
+
diff = JsonDiff.diff([1, 2, 3], [])
|
20
|
+
expect(diff).to eql([
|
21
|
+
{op: :remove, path: "/0", value: 1},
|
22
|
+
{op: :remove, path: "/0", value: 2},
|
23
|
+
{op: :remove, path: "/0", value: 3},
|
24
|
+
])
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should be able to diff a 1-array with a filled one" do
|
28
|
+
diff = JsonDiff.diff([0], [1, 2, 3])
|
29
|
+
expect(diff).to eql([
|
30
|
+
{op: :remove, path: "/0", value: 0},
|
31
|
+
{op: :add, path: "/0", value: 1},
|
32
|
+
{op: :add, path: "/1", value: 2},
|
33
|
+
{op: :add, path: "/2", value: 3},
|
34
|
+
])
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should be able to diff a filled array with a 1-array" do
|
38
|
+
diff = JsonDiff.diff([1, 2, 3], [0])
|
39
|
+
expect(diff).to eql([
|
40
|
+
{op: :remove, path: "/2", value: 3},
|
41
|
+
{op: :remove, path: "/1", value: 2},
|
42
|
+
{op: :remove, path: "/0", value: 1},
|
43
|
+
{op: :add, path: "/0", value: 0},
|
44
|
+
])
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should be able to diff two integer arrays" do
|
48
|
+
diff = JsonDiff.diff([1, 2, 3, 4, 5], [6, 4, 3, 2])
|
49
|
+
expect(diff).to eql([
|
50
|
+
{op: :remove, path: "/4", value: 5},
|
51
|
+
{op: :remove, path: "/0", value: 1},
|
52
|
+
{op: :move, from: "/0", path: "/2"},
|
53
|
+
{op: :move, from: "/1", path: "/0"},
|
54
|
+
{op: :add, path: "/0", value: 6},
|
55
|
+
])
|
56
|
+
end
|
57
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: json-diff
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Captain Train
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-06-11 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Take two Ruby objects that can be serialized to JSON. Output an array
|
14
|
+
of operations (additions, deletions, moves) that would convert the first one to
|
15
|
+
the second one.
|
16
|
+
email:
|
17
|
+
- ttyl@captaintrain.com
|
18
|
+
executables: []
|
19
|
+
extensions: []
|
20
|
+
extra_rdoc_files: []
|
21
|
+
files:
|
22
|
+
- .rspec
|
23
|
+
- Gemfile
|
24
|
+
- LICENSE
|
25
|
+
- Makefile
|
26
|
+
- README.md
|
27
|
+
- Rakefile
|
28
|
+
- json-diff.gemspec
|
29
|
+
- lib/json-diff.rb
|
30
|
+
- lib/json-diff/diff.rb
|
31
|
+
- lib/json-diff/index-map.rb
|
32
|
+
- lib/json-diff/operation.rb
|
33
|
+
- lib/json-diff/version.rb
|
34
|
+
- spec/json-diff/diff_spec.rb
|
35
|
+
- spec/spec_helper.rb
|
36
|
+
homepage: http://github.com/captaintrain/json-diff
|
37
|
+
licenses:
|
38
|
+
- MIT
|
39
|
+
metadata: {}
|
40
|
+
post_install_message:
|
41
|
+
rdoc_options: []
|
42
|
+
require_paths:
|
43
|
+
- lib
|
44
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - '>='
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
requirements: []
|
55
|
+
rubyforge_project:
|
56
|
+
rubygems_version: 2.0.14
|
57
|
+
signing_key:
|
58
|
+
specification_version: 4
|
59
|
+
summary: Compute the difference between two JSON-serializable Ruby objects.
|
60
|
+
test_files: []
|