json-diff 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 29a90b679f5bf30e17ce0b2cabd2963a9a68ea03
4
- data.tar.gz: 06c46f1cb99d0564522c30507df1f65fbcca003b
3
+ metadata.gz: 2079e5281095664f9827f175517045a3164b675b
4
+ data.tar.gz: 6a7d698b88209ed747f621ab609d3a735270fec0
5
5
  SHA512:
6
- metadata.gz: 613a0223292d0d84d7bf32a46b5f58468336251351fb1d243032172162c24a53c22f56474c1873898c492930d2283227302345febbfccdbdd9cad4c152474174
7
- data.tar.gz: c1457cdf32d04f6f368b49b7dde261751ac1c9ef7350578e766ae63a598afa37d10d7e8edb4eb30a2590df9c0cec3bfb3f5dcdfb64b54dcd30f1211161525ee7
6
+ metadata.gz: bb9cf0f504e1e0892697bee3d5b4beefa7f0eff93fadc57c9bbfccc852a7f4f5c3210a02a74d1d0bc0141887437224182ee5116c9d9e92c2a5506f634d77da0e
7
+ data.tar.gz: a045d4d6507b8834ce9bc92f8be212777777b94cc15068a362649b608887af9dcb92e8172913bfb82a2230f9c279e366ba99eff972af2a04221cba60ae17d04f
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ json-diff*.gem
data/README.md CHANGED
@@ -17,6 +17,15 @@ Outputs [RFC6902][]. Look at [hana][] for a JSON patch algorithm that can use th
17
17
  [RFC6902]: http://www.rfc-editor.org/rfc/rfc6902.txt
18
18
  [hana]: https://github.com/tenderlove/hana
19
19
 
20
+ # Options
21
+
22
+ - `include_was`: include a `was` field in remove and replace operations, to show the old value. Allows computing reverse operations without the source JSON.
23
+ - `moves`\*: include move operations. Set it to false to remove clutter.
24
+ - `additions`\*: include add operations. Se it to false to remove clutter.
25
+ - `original_indices`\*: array indices are those from the source array (for `from` fields, or `path` fields on remove operations) or the target array (for other `path` fields). It eases manual checking of differences.
26
+
27
+ \* Changing this option prevents the use of the output for JSON patching.
28
+
20
29
  # Heart
21
30
 
22
31
  - Recursive similarity computation between any two Ruby values.
@@ -34,6 +43,51 @@ Cons:
34
43
  - 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
44
  - There is a computational overhead to the default similarity computation that scales with the total number of entities in the structure.
36
45
 
46
+ # Comparisons
47
+
48
+ [HashDiff](https://github.com/liufengyun/hashdiff) — LCS, no move operation.
49
+
50
+ ```ruby
51
+ require "json-diff"
52
+ JsonDiff.diff([1, 2, 3, 4, 5], [6, 4, 3, 2])
53
+ # [{'op' => 'remove', 'path' => '/4'},
54
+ # {'op' => 'remove', 'path' => '/0'},
55
+ # {'op' => 'move', 'from' => '/0', 'path' => '/2'},
56
+ # {'op' => 'move', 'from' => '/1', 'path' => '/0'},
57
+ # {'op' => 'add', 'path' => '/0', 'value' => 6}]
58
+
59
+ require "hashdiff"
60
+ HashDiff.diff([1, 2, 3, 4, 5], [6, 4, 3, 2])
61
+ # [["-", "[0]", 1],
62
+ # ["+", "[0]", 6],
63
+ # ["+", "[1]", 4],
64
+ # ["+", "[2]", 3],
65
+ # ["-", "[6]", 5],
66
+ # ["-", "[5]", 4],
67
+ # ["-", "[4]", 3]]
68
+ ```
69
+
70
+ [jsondiff](https://github.com/francois2metz/jsondiff) — no similitude, no LCS.
71
+
72
+ ```ruby
73
+ require "json-diff"
74
+ JsonDiff.diff(
75
+ [{'code' => "ABW", 'name' => "Abbey Wood"}, {'code' => "KGX", 'name' => "Kings Cross"}],
76
+ [{'code' => "KGX", 'name' => "Kings Cross"}, {'code' => "ABW", 'name' => "Abbey Wood"}]
77
+ )
78
+ # [{'op' => 'move', 'from' => '/0', 'path' => '/1'}]
79
+
80
+ require "jsondiff"
81
+ JsonDiff.generate(
82
+ [{'code' => "ABW", 'name' => "Abbey Wood"}, {'code' => "KGX", 'name' => "Kings Cross"}],
83
+ [{'code' => "KGX", 'name' => "Kings Cross"}, {'code' => "ABW", 'name' => "Abbey Wood"}]
84
+ )
85
+ # [{:op => :replace, :path => '/0/code', :value => 'KGX'},
86
+ # {:op => :replace, :path => '/0/name', :value => 'Kings Cross'},
87
+ # {:op => :replace, :path => '/1/code', :value => 'ABW'},
88
+ # {:op => :replace, :path => '/1/name', :value => 'Abbey Wood'}]
89
+ ```
90
+
37
91
  # Plans & Bugs
38
92
 
39
93
  Roughly ordered by priority.
@@ -4,17 +4,19 @@ module JsonDiff
4
4
  path = opts[:path] || '/'
5
5
  include_addition = (opts[:additions] == nil) ? true : opts[:additions]
6
6
  include_moves = (opts[:moves] == nil) ? true : opts[:moves]
7
+ include_was = (opts[:include_was] == nil) ? false : opts[:include_was]
8
+ original_indices = (opts[:original_indices] == nil) ? false : opts[:original_indices]
7
9
 
8
10
  changes = []
9
11
 
10
12
  if before.is_a?(Hash)
11
13
  if !after.is_a?(Hash)
12
- changes << replace(path, before, after)
14
+ changes << replace(path, include_was ? before : nil, after)
13
15
  else
14
16
  lost = before.keys - after.keys
15
17
  lost.each do |key|
16
18
  inner_path = extend_json_pointer(path, key)
17
- changes << remove(inner_path, before[key])
19
+ changes << remove(inner_path, include_was ? before[key] : nil)
18
20
  end
19
21
 
20
22
  if include_addition
@@ -33,7 +35,7 @@ module JsonDiff
33
35
  end
34
36
  elsif before.is_a?(Array)
35
37
  if !after.is_a?(Array)
36
- changes << replace(path, before, after)
38
+ changes << replace(path, include_was ? before : nil, after)
37
39
  elsif before.size == 0
38
40
  if include_addition
39
41
  after.each_with_index do |item, index|
@@ -45,7 +47,7 @@ module JsonDiff
45
47
  before.each do |item|
46
48
  # Delete elements from the start.
47
49
  inner_path = extend_json_pointer(path, 0)
48
- changes << remove(inner_path, item)
50
+ changes << remove(inner_path, include_was ? item : nil)
49
51
  end
50
52
  else
51
53
  pairing = array_pairing(before, after)
@@ -63,22 +65,31 @@ module JsonDiff
63
65
  kept
64
66
  end
65
67
 
66
- array_changes(pairing)
68
+ pairing[:pairs].each do |pair|
69
+ before_index, after_index = pair
70
+ inner_path = extend_json_pointer(path, before_index)
71
+ changes += diff(before[before_index], after[after_index], opts.merge(path: inner_path))
72
+ end
73
+
74
+ if !original_indices
75
+ # Recompute indices to account for offsets from insertions and
76
+ # deletions.
77
+ pairing = array_changes(pairing)
78
+ end
67
79
 
68
80
  pairing[:removed].each do |before_index|
69
81
  inner_path = extend_json_pointer(path, before_index)
70
- changes << remove(inner_path, before[before_index])
82
+ changes << remove(inner_path, include_was ? before[before_index] : nil)
71
83
  end
72
84
 
73
85
  pairing[:pairs].each do |pair|
74
- before_index, after_index, orig_before, orig_after = pair
86
+ before_index, after_index = pair
75
87
  inner_before_path = extend_json_pointer(path, before_index)
76
88
  inner_after_path = extend_json_pointer(path, after_index)
77
89
 
78
90
  if before_index != after_index && include_moves
79
91
  changes << move(inner_before_path, inner_after_path)
80
92
  end
81
- changes += diff(before[orig_before], after[orig_after], opts.merge(path: inner_after_path))
82
93
  end
83
94
 
84
95
  if include_addition
@@ -90,7 +101,7 @@ module JsonDiff
90
101
  end
91
102
  else
92
103
  if before != after
93
- changes << replace(path, before, after)
104
+ changes << replace(path, include_was ? before : nil, after)
94
105
  end
95
106
  end
96
107
 
@@ -168,7 +179,8 @@ module JsonDiff
168
179
  }
169
180
  end
170
181
 
171
- # Compute an arbitrary notion of how probable it is that
182
+ # Compute an arbitrary notion of how probable it is that one object is the
183
+ # result of modifying the other.
172
184
  def self.similarity(before, after)
173
185
  return 0.0 if before.class != after.class
174
186
 
@@ -189,6 +201,11 @@ module JsonDiff
189
201
  before.each do |before_key, before_item|
190
202
  similarities << similarity(before_item, after[before_key])
191
203
  end
204
+ # Also consider keys' names.
205
+ before_keys = before.keys
206
+ after_keys = after.keys
207
+ key_similarity = (before_keys & after_keys).size / (before_keys | after_keys).size
208
+ similarities << key_similarity
192
209
 
193
210
  similarities.reduce(:+) / similarities.size
194
211
  elsif before.is_a?(Array)
@@ -24,24 +24,28 @@ module JsonDiff
24
24
  end
25
25
  end
26
26
 
27
+ def self.replace(path, before, after)
28
+ if before != nil
29
+ {'op' => 'replace', 'path' => path, 'was' => before, 'value' => after}
30
+ else
31
+ {'op' => 'replace', 'path' => path, 'value' => after}
32
+ end
33
+ end
34
+
27
35
  def self.add(path, value)
28
- {op: :add, path: path, value: value}
36
+ {'op' => 'add', 'path' => path, 'value' => value}
29
37
  end
30
38
 
31
39
  def self.remove(path, value)
32
40
  if value != nil
33
- {op: :remove, path: path, value: value}
41
+ {'op' => 'remove', 'path' => path, 'was' => value}
34
42
  else
35
- {op: :remove, path: path}
43
+ {'op' => 'remove', 'path' => path}
36
44
  end
37
45
  end
38
46
 
39
- def self.replace(path, value)
40
- {op: :replace, path: path, value: value}
41
- end
42
-
43
47
  def self.move(source, target)
44
- {op: :move, from: source, path: target}
48
+ {'op' => 'move', 'from' => source, 'path' => target}
45
49
  end
46
50
 
47
51
  end
@@ -1,3 +1,3 @@
1
1
  module JsonDiff
2
- VERSION = '0.1.0'
2
+ VERSION = '0.2.0'
3
3
  end
@@ -1,57 +1,119 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe JsonDiff do
4
+ # Arrays
5
+
4
6
  it "should be able to diff two empty arrays" do
5
7
  diff = JsonDiff.diff([], [])
6
8
  expect(diff).to eql([])
7
9
  end
8
10
 
9
11
  it "should be able to diff an empty array with a filled one" do
10
- diff = JsonDiff.diff([], [1, 2, 3])
12
+ diff = JsonDiff.diff([], [1, 2, 3], include_was: true)
11
13
  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},
14
+ {'op' => 'add', 'path' => "/0", 'value' => 1},
15
+ {'op' => 'add', 'path' => "/1", 'value' => 2},
16
+ {'op' => 'add', 'path' => "/2", 'value' => 3},
15
17
  ])
16
18
  end
17
19
 
18
20
  it "should be able to diff a filled array with an empty one" do
19
- diff = JsonDiff.diff([1, 2, 3], [])
21
+ diff = JsonDiff.diff([1, 2, 3], [], include_was: true)
20
22
  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},
23
+ {'op' => 'remove', 'path' => "/0", 'was' => 1},
24
+ {'op' => 'remove', 'path' => "/0", 'was' => 2},
25
+ {'op' => 'remove', 'path' => "/0", 'was' => 3},
24
26
  ])
25
27
  end
26
28
 
27
29
  it "should be able to diff a 1-array with a filled one" do
28
- diff = JsonDiff.diff([0], [1, 2, 3])
30
+ diff = JsonDiff.diff([0], [1, 2, 3], include_was: true)
29
31
  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},
32
+ {'op' => 'remove', 'path' => "/0", 'was' => 0},
33
+ {'op' => 'add', 'path' => "/0", 'value' => 1},
34
+ {'op' => 'add', 'path' => "/1", 'value' => 2},
35
+ {'op' => 'add', 'path' => "/2", 'value' => 3},
34
36
  ])
35
37
  end
36
38
 
37
39
  it "should be able to diff a filled array with a 1-array" do
38
- diff = JsonDiff.diff([1, 2, 3], [0])
40
+ diff = JsonDiff.diff([1, 2, 3], [0], include_was: true)
39
41
  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},
42
+ {'op' => 'remove', 'path' => "/2", 'was' => 3},
43
+ {'op' => 'remove', 'path' => "/1", 'was' => 2},
44
+ {'op' => 'remove', 'path' => "/0", 'was' => 1},
45
+ {'op' => 'add', 'path' => "/0", 'value' => 0},
44
46
  ])
45
47
  end
46
48
 
47
49
  it "should be able to diff two integer arrays" do
48
- diff = JsonDiff.diff([1, 2, 3, 4, 5], [6, 4, 3, 2])
50
+ diff = JsonDiff.diff([1, 2, 3, 4, 5], [6, 4, 3, 2], include_was: true)
51
+ expect(diff).to eql([
52
+ {'op' => 'remove', 'path' => "/4", 'was' => 5},
53
+ {'op' => 'remove', 'path' => "/0", 'was' => 1},
54
+ {'op' => 'move', 'from' => "/0", 'path' => "/2"},
55
+ {'op' => 'move', 'from' => "/1", 'path' => "/0"},
56
+ {'op' => 'add', 'path' => "/0", 'value' => 6},
57
+ ])
58
+ end
59
+
60
+ it "should be able to diff two arrays with mixed content" do
61
+ diff = JsonDiff.diff(["laundry", 12, {'pillar' => 0}, true], [true, {'pillar' => 1}, 3, 12], include_was: true)
62
+ expect(diff).to eql([
63
+ {'op' => 'replace', 'path' => "/2/pillar", 'was' => 0, 'value' => 1},
64
+ {'op' => 'remove', 'path' => "/0", 'was' => "laundry"},
65
+ {'op' => 'move', 'from' => "/2", 'path' => "/0"},
66
+ {'op' => 'move', 'from' => "/1", 'path' => "/2"},
67
+ {'op' => 'add', 'path' => "/2", 'value' => 3},
68
+ ])
69
+ end
70
+
71
+ # Objects
72
+
73
+ it "should be able to diff two objects with mixed content" do
74
+ diff = JsonDiff.diff(
75
+ {'string' => "laundry", 'number' => 12, 'object' => {'pillar' => 0}, 'list' => [2, 4, 1], 'bool' => false, 'null' => nil},
76
+ {'string' => "laundry", 'number' => 12, 'object' => {'pillar' => 1}, 'list' => [1, 2, 3], 'bool' => true, 'null' => nil},
77
+ include_was: true)
78
+ expect(diff).to eql([
79
+ {'op' => 'replace', 'path' => "/object/pillar", 'was' => 0, 'value' => 1},
80
+ {'op' => 'remove', 'path' => "/list/1", 'was' => 4},
81
+ {'op' => 'move', 'from' => "/list/0", 'path' => "/list/1"},
82
+ {'op' => 'add', 'path' => "/list/2", 'value' => 3},
83
+ {'op' => 'replace', 'path' => "/bool", 'was' => false, 'value' => true},
84
+ ])
85
+ end
86
+
87
+ # Options
88
+
89
+ it "should be able to diff two integer arrays with original indices" do
90
+ diff = JsonDiff.diff([1, 2, 3, 4, 5], [6, 4, 3, 2], original_indices: true)
91
+ expect(diff).to eql([
92
+ {'op' => 'remove', 'path' => "/4"},
93
+ {'op' => 'remove', 'path' => "/0"},
94
+ {'op' => 'move', 'from' => "/1", 'path' => "/3"},
95
+ {'op' => 'move', 'from' => "/3", 'path' => "/1"},
96
+ {'op' => 'add', 'path' => "/0", 'value' => 6},
97
+ ])
98
+ end
99
+
100
+ it "should be able to diff two integer arrays without move operations" do
101
+ diff = JsonDiff.diff([1, 2, 3, 4, 5], [6, 4, 3, 2], moves: false)
102
+ expect(diff).to eql([
103
+ {'op' => 'remove', 'path' => "/4"},
104
+ {'op' => 'remove', 'path' => "/0"},
105
+ {'op' => 'add', 'path' => "/0", 'value' => 6},
106
+ ])
107
+ end
108
+
109
+ it "should be able to diff two integer arrays without add operations" do
110
+ diff = JsonDiff.diff([1, 2, 3, 4, 5], [6, 4, 3, 2], additions: false)
49
111
  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},
112
+ {'op' => 'remove', 'path' => "/4"},
113
+ {'op' => 'remove', 'path' => "/0"},
114
+ {'op' => 'move', 'from' => "/0", 'path' => "/2"},
115
+ {'op' => 'move', 'from' => "/1", 'path' => "/0"},
55
116
  ])
56
117
  end
118
+
57
119
  end
@@ -0,0 +1,44 @@
1
+ require 'spec_helper'
2
+
3
+ describe JsonDiff do
4
+ # AdditionIndexMap
5
+
6
+ it "should be able to offset an index" do
7
+ aim = JsonDiff::AdditionIndexMap.new(3)
8
+ expect(aim.map(2)).to eql(2)
9
+ expect(aim.map(3)).to eql(4)
10
+ expect(aim.map(4)).to eql(5)
11
+ end
12
+
13
+ it "should be able to offset an index negatively" do
14
+ rim = JsonDiff::RemovalIndexMap.new(3)
15
+ expect(rim.map(2)).to eql(2)
16
+ expect(rim.map(3)).to eql(2)
17
+ expect(rim.map(4)).to eql(3)
18
+ end
19
+
20
+ # IndexMaps
21
+
22
+ it "should be able to offset an index with a deletion and an insertion" do
23
+ im = JsonDiff::IndexMaps.new
24
+ im.removal(2)
25
+ im.addition(4)
26
+ expect(im.map(1)).to eql(1)
27
+ expect(im.map(2)).to eql(1)
28
+ expect(im.map(3)).to eql(2)
29
+ expect(im.map(4)).to eql(3)
30
+ expect(im.map(5)).to eql(5)
31
+ end
32
+
33
+ it "should be able to offset an index with an insertion and a deletion" do
34
+ im = JsonDiff::IndexMaps.new
35
+ im.addition(2)
36
+ im.removal(4)
37
+ expect(im.map(1)).to eql(1)
38
+ expect(im.map(2)).to eql(3)
39
+ expect(im.map(3)).to eql(3)
40
+ expect(im.map(4)).to eql(4)
41
+ expect(im.map(5)).to eql(5)
42
+ end
43
+
44
+ end
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+
3
+ describe JsonDiff do
4
+ it "should convert the root to a JSON pointer" do
5
+ json_pointer = JsonDiff.json_pointer([])
6
+ expect(json_pointer).to eql("/")
7
+ end
8
+
9
+ it "should convert a path to a JSON pointer" do
10
+ json_pointer = JsonDiff.json_pointer(["path", "to", 1, "soul"])
11
+ expect(json_pointer).to eql("/path/to/1/soul")
12
+ end
13
+
14
+ it "should escape a path" do
15
+ json_pointer = JsonDiff.json_pointer(["a/b", "c%d", "e^f", "g|h", "i\\j", "k\"l", " ", "m~n"])
16
+ expect(json_pointer).to eql("/a~1b/c%d/e^f/g|h/i\\j/k\"l/ /m~0n")
17
+ end
18
+
19
+ it "should expand a path" do
20
+ json_pointer = JsonDiff.json_pointer(["path", "to", 1, "soul"])
21
+ json_pointer = JsonDiff.extend_json_pointer(json_pointer, "further")
22
+ expect(json_pointer).to eql("/path/to/1/soul/further")
23
+ end
24
+
25
+ it "should expand an empty path" do
26
+ json_pointer = JsonDiff.json_pointer([])
27
+ json_pointer = JsonDiff.extend_json_pointer(json_pointer, "further")
28
+ expect(json_pointer).to eql("/further")
29
+ end
30
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: json-diff
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Captain Train
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-06-11 00:00:00.000000000 Z
11
+ date: 2016-06-13 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Take two Ruby objects that can be serialized to JSON. Output an array
14
14
  of operations (additions, deletions, moves) that would convert the first one to
@@ -19,6 +19,7 @@ executables: []
19
19
  extensions: []
20
20
  extra_rdoc_files: []
21
21
  files:
22
+ - .gitignore
22
23
  - .rspec
23
24
  - Gemfile
24
25
  - LICENSE
@@ -32,6 +33,8 @@ files:
32
33
  - lib/json-diff/operation.rb
33
34
  - lib/json-diff/version.rb
34
35
  - spec/json-diff/diff_spec.rb
36
+ - spec/json-diff/index-map_spec.rb
37
+ - spec/json-diff/operation_spec.rb
35
38
  - spec/spec_helper.rb
36
39
  homepage: http://github.com/captaintrain/json-diff
37
40
  licenses: