json-diff 0.1.0 → 0.2.0

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