json-deep-compare 0.0.1 → 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.
- data/lib/json-deep-compare.rb +135 -48
- data/test/json_deep_compare_test.rb +114 -39
- metadata +4 -4
data/lib/json-deep-compare.rb
CHANGED
@@ -1,114 +1,201 @@
|
|
1
1
|
module JsonDeepCompare
|
2
|
-
VERSION = '0.0
|
2
|
+
VERSION = '0.1.0'
|
3
|
+
|
4
|
+
class DocumentComparison
|
5
|
+
def initialize(lval, rval, options = {})
|
6
|
+
if exclusions = options[:exclusions]
|
7
|
+
options[:exclusions] = [exclusions] unless exclusions.is_a?(Array)
|
8
|
+
else
|
9
|
+
options[:exclusions] = []
|
10
|
+
end
|
11
|
+
@root_comparisons = []
|
12
|
+
@root_comparisons << NodeComparison.new(lval, rval, ":root", options)
|
13
|
+
@root_comparisons << NodeComparison.new(rval, lval, ":root", options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def difference_messages
|
17
|
+
ldiffs = @root_comparisons.first.differences
|
18
|
+
rdiffs = @root_comparisons.last.differences
|
19
|
+
differences = ldiffs.dup
|
20
|
+
ldiff_selectors = ldiffs.map { |ldiff| ldiff.selector }
|
21
|
+
rdiffs.each do |rdiff|
|
22
|
+
unless ldiff_selectors.include?(rdiff.selector)
|
23
|
+
differences << rdiff.reverse
|
24
|
+
end
|
25
|
+
end
|
26
|
+
differences = differences.sort_by &:selector
|
27
|
+
differences.map(&:message).join("\n")
|
28
|
+
end
|
29
|
+
|
30
|
+
def equal?
|
31
|
+
@root_comparisons.all?(&:equal?)
|
32
|
+
end
|
33
|
+
end
|
3
34
|
|
4
35
|
class NodeComparison
|
5
36
|
ExcerptPadding = 15
|
6
|
-
attr_reader :
|
37
|
+
attr_reader :lval, :rval, :selector
|
7
38
|
|
8
|
-
def initialize(
|
9
|
-
@
|
10
|
-
@selector = options[:selector] || ':root'
|
11
|
-
@exclusions = options[:exclusions]
|
12
|
-
@exclusions = [@exclusions] unless @exclusions.is_a?(Array)
|
39
|
+
def initialize(lval, rval, selector, options = {})
|
40
|
+
@lval, @rval, @selector, @options = lval, rval, selector, options
|
13
41
|
@children = []
|
14
|
-
if
|
15
|
-
if
|
16
|
-
|
42
|
+
if lval.is_a?(Hash)
|
43
|
+
if rval.is_a?(Hash)
|
44
|
+
lval.each do |key, left_sub_value|
|
17
45
|
@children << NodeComparison.new(
|
18
|
-
left_sub_value,
|
19
|
-
|
46
|
+
left_sub_value,
|
47
|
+
rval[key],
|
48
|
+
"#{selector} > .#{key}",
|
49
|
+
options
|
20
50
|
)
|
21
51
|
end
|
22
52
|
end
|
23
|
-
elsif
|
24
|
-
if
|
25
|
-
|
53
|
+
elsif lval.is_a?(Array)
|
54
|
+
if rval.is_a?(Array)
|
55
|
+
lval.each_with_index do |left_sub_value, i|
|
26
56
|
@children << NodeComparison.new(
|
27
|
-
left_sub_value,
|
28
|
-
|
29
|
-
|
57
|
+
left_sub_value,
|
58
|
+
rval[i],
|
59
|
+
"#{selector} :nth-child(#{i+1})",
|
60
|
+
options
|
30
61
|
)
|
31
62
|
end
|
32
63
|
end
|
33
64
|
end
|
34
65
|
end
|
35
66
|
|
36
|
-
def
|
37
|
-
|
38
|
-
if str.length >= 40
|
39
|
-
"#{value.class.name} #{str[0..37]}..."
|
40
|
-
else
|
41
|
-
str
|
42
|
-
end
|
67
|
+
def blank?(value)
|
68
|
+
value.respond_to?(:empty?) ? value.empty? : !value
|
43
69
|
end
|
44
70
|
|
45
|
-
def
|
46
|
-
|
71
|
+
def blank_equality?
|
72
|
+
@options[:blank_equality]
|
73
|
+
end
|
74
|
+
|
75
|
+
def differences
|
76
|
+
if equal?
|
77
|
+
[]
|
78
|
+
else
|
47
79
|
if leaf?
|
48
80
|
if excerptable_difference?
|
49
|
-
excerpted_difference
|
81
|
+
[excerpted_difference]
|
50
82
|
else
|
51
|
-
|
83
|
+
[Difference.new(
|
84
|
+
@selector, "expected to be :lval but was :rval",
|
85
|
+
lval: value_inspect(@lval), rval: value_inspect(@rval)
|
86
|
+
)]
|
52
87
|
end
|
53
88
|
else
|
54
|
-
@children.
|
89
|
+
@children.map(&:differences).compact.flatten
|
55
90
|
end
|
56
91
|
end
|
57
92
|
end
|
58
93
|
|
59
94
|
def equal?
|
60
95
|
if leaf?
|
61
|
-
|
96
|
+
if selector_excluded?
|
97
|
+
true
|
98
|
+
elsif equality_proc
|
99
|
+
equality_proc.call(@lval, @rval)
|
100
|
+
else
|
101
|
+
@lval == @rval || (blank_equality? && blank?(@lval) && blank?(@rval))
|
102
|
+
end
|
62
103
|
else
|
63
104
|
@children.all?(&:equal?)
|
64
105
|
end
|
65
106
|
end
|
66
107
|
|
108
|
+
def equality_proc
|
109
|
+
@options[:equality]
|
110
|
+
end
|
111
|
+
|
67
112
|
def excerptable_difference?
|
68
|
-
@
|
69
|
-
@
|
70
|
-
@right_value.size > ExcerptPadding * 2
|
113
|
+
@lval.is_a?(String) and @rval.is_a?(String) && (
|
114
|
+
@lval.size > ExcerptPadding * 2 || @rval.size > ExcerptPadding * 2
|
71
115
|
)
|
72
116
|
end
|
73
117
|
|
74
118
|
def excerpted_difference
|
75
|
-
difference_start = (0..@
|
76
|
-
@left_value[i] != @right_value[i]
|
77
|
-
}
|
119
|
+
difference_start = (0..@lval.length).detect { |i| @lval[i] != @rval[i] }
|
78
120
|
range_start = if difference_start > ExcerptPadding
|
79
121
|
difference_start - ExcerptPadding
|
80
122
|
else
|
81
123
|
0
|
82
124
|
end
|
83
|
-
left_excerpt = @
|
84
|
-
|
85
|
-
]
|
86
|
-
right_excerpt = @right_value[
|
87
|
-
range_start..difference_start+ExcerptPadding
|
88
|
-
]
|
125
|
+
left_excerpt = @lval[range_start..difference_start+ExcerptPadding]
|
126
|
+
right_excerpt = @rval[range_start..difference_start+ExcerptPadding]
|
89
127
|
if difference_start - ExcerptPadding > 0
|
90
128
|
left_excerpt = "..." + left_excerpt
|
91
129
|
right_excerpt = "..." + right_excerpt
|
92
130
|
end
|
93
|
-
if difference_start + ExcerptPadding < @
|
131
|
+
if difference_start + ExcerptPadding < @lval.length
|
94
132
|
left_excerpt = left_excerpt + '...'
|
95
133
|
end
|
96
|
-
if difference_start + ExcerptPadding < @
|
134
|
+
if difference_start + ExcerptPadding < @rval.length
|
97
135
|
right_excerpt = right_excerpt + '...'
|
98
136
|
end
|
99
|
-
|
137
|
+
Difference.new(
|
138
|
+
@selector,
|
139
|
+
"differs starting at char :difference_start: :lval differs from :rval",
|
140
|
+
difference_start: difference_start.to_s,
|
141
|
+
lval: left_excerpt.inspect, rval: right_excerpt.inspect
|
142
|
+
)
|
100
143
|
end
|
101
144
|
|
102
145
|
def leaf?
|
103
146
|
@children.empty?
|
104
147
|
end
|
148
|
+
|
149
|
+
def selector_excluded?
|
150
|
+
@options[:exclusions].any? { |exclusion|
|
151
|
+
if exclusion.is_a?(String)
|
152
|
+
exclusion == @selector
|
153
|
+
else
|
154
|
+
@selector =~ exclusion
|
155
|
+
end
|
156
|
+
}
|
157
|
+
end
|
158
|
+
|
159
|
+
def value_inspect(value)
|
160
|
+
str = value.inspect
|
161
|
+
if str.length >= 40
|
162
|
+
"#{value.class.name} #{str[0..37]}..."
|
163
|
+
else
|
164
|
+
str
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
class Difference
|
169
|
+
attr_reader :selector
|
170
|
+
|
171
|
+
def initialize(selector, msg_template, variables)
|
172
|
+
@selector, @msg_template, @variables =
|
173
|
+
selector, msg_template, variables
|
174
|
+
end
|
175
|
+
|
176
|
+
def message
|
177
|
+
msg = @msg_template
|
178
|
+
@variables.each do |name, value|
|
179
|
+
msg = msg.gsub(/#{name.inspect}/, value)
|
180
|
+
end
|
181
|
+
"#{@selector.inspect} #{msg}"
|
182
|
+
end
|
183
|
+
|
184
|
+
def reverse
|
185
|
+
reversed_variables = {lval: @variables[:rval], rval: @variables[:lval]}
|
186
|
+
(@variables.keys - [:lval, :rval]).each do |other_var|
|
187
|
+
reversed_variables[other_var] = @variables[other_var]
|
188
|
+
end
|
189
|
+
Difference.new(@selector, @msg_template, reversed_variables)
|
190
|
+
end
|
191
|
+
end
|
105
192
|
end
|
106
193
|
|
107
194
|
module Assertions
|
108
195
|
def assert_json_equal(expected, actual, exclusions = nil)
|
109
|
-
comparison =
|
196
|
+
comparison = DocumentComparison.new(expected, actual, exclusions: exclusions)
|
110
197
|
unless comparison.equal?
|
111
|
-
fail comparison.
|
198
|
+
fail comparison.difference_messages
|
112
199
|
end
|
113
200
|
end
|
114
201
|
end
|
@@ -2,7 +2,41 @@ require 'test/unit'
|
|
2
2
|
$: << '.'
|
3
3
|
require 'lib/json-deep-compare'
|
4
4
|
|
5
|
-
class
|
5
|
+
class DocumentComparisonTestCase < Test::Unit::TestCase
|
6
|
+
def assert_symmetrically_different(
|
7
|
+
lval, rval, message_template = nil, sub_pairs = nil
|
8
|
+
)
|
9
|
+
sub_pairs = [sub_pairs] if sub_pairs and sub_pairs.first.is_a?(String)
|
10
|
+
comparison1 = JsonDeepCompare::DocumentComparison.new(lval, rval)
|
11
|
+
assert(
|
12
|
+
!comparison1.equal?,
|
13
|
+
"Comparison of #{lval.inspect} to #{rval.inspect} should not have been equal"
|
14
|
+
)
|
15
|
+
if message_template
|
16
|
+
message1 = message_template
|
17
|
+
sub_pairs.each_with_index do |sub_pair, i|
|
18
|
+
message1 = message1.
|
19
|
+
sub(/:left#{i}/, sub_pair.first).
|
20
|
+
sub(/:right#{i}/, sub_pair.last)
|
21
|
+
end
|
22
|
+
assert_equal(message1, comparison1.difference_messages)
|
23
|
+
end
|
24
|
+
comparison2 = JsonDeepCompare::DocumentComparison.new(rval, lval)
|
25
|
+
assert(
|
26
|
+
!comparison2.equal?,
|
27
|
+
"Comparison of #{rval.inspect} to #{lval.inspect} should not have been equal"
|
28
|
+
)
|
29
|
+
if message_template
|
30
|
+
message2 = message_template
|
31
|
+
sub_pairs.each_with_index do |sub_pair, i|
|
32
|
+
message2 = message2.
|
33
|
+
sub(/:left#{i}/, sub_pair.last).
|
34
|
+
sub(/:right#{i}/, sub_pair.first)
|
35
|
+
end
|
36
|
+
assert_equal(message2, comparison2.difference_messages)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
6
40
|
def test_detects_difference_in_atoms
|
7
41
|
left_value = {
|
8
42
|
'total_rows' => 2,
|
@@ -30,29 +64,36 @@ class NodeComparisonTestCase < Test::Unit::TestCase
|
|
30
64
|
}
|
31
65
|
]
|
32
66
|
}
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
"
|
37
|
-
comparison.difference_message
|
67
|
+
assert_symmetrically_different(
|
68
|
+
left_value, right_value,
|
69
|
+
"\":root > .rows :nth-child(1) > .doc > .sub_document > .one\" expected to be :left0 but was :right0",
|
70
|
+
["two".inspect, "1".inspect]
|
38
71
|
)
|
39
72
|
end
|
40
73
|
|
41
74
|
def test_detects_differences_in_keys
|
42
75
|
left_value = {"doc" => {"foo" => "bar"}}
|
43
76
|
right_value = {"doc" => {"biz" => "bang", "bing" => nil}}
|
44
|
-
|
45
|
-
|
77
|
+
assert_symmetrically_different(left_value, right_value)
|
78
|
+
end
|
79
|
+
|
80
|
+
def test_missing_keys
|
81
|
+
lval = {"outer" => {"key1" => "value1"}}
|
82
|
+
rval = {"outer" => {"key1" => "value1", "key2" => {"foo" => "bar"}}}
|
83
|
+
assert_symmetrically_different(
|
84
|
+
lval, rval,
|
85
|
+
"\":root > .outer > .key2\" expected to be :left0 but was :right0",
|
86
|
+
["nil", {"foo" => "bar"}.inspect]
|
87
|
+
)
|
46
88
|
end
|
47
89
|
|
48
90
|
def test_detects_difference_in_type
|
49
91
|
lval = {"foo" => {'bar' => 'bang'}}
|
50
92
|
rval = {"foo" => [3,4,5]}
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
"
|
55
|
-
comparison.difference_message
|
93
|
+
assert_symmetrically_different(
|
94
|
+
lval, rval,
|
95
|
+
"\":root > .foo\" expected to be :left0 but was :right0",
|
96
|
+
[{"bar" => "bang"}.inspect, [3,4,5].inspect]
|
56
97
|
)
|
57
98
|
end
|
58
99
|
|
@@ -63,64 +104,98 @@ class NodeComparisonTestCase < Test::Unit::TestCase
|
|
63
104
|
end
|
64
105
|
lval = {"foo" => lval_hash}
|
65
106
|
rval = {"foo" => [3,4,5]}
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
107
|
+
assert_symmetrically_different(
|
108
|
+
lval, rval,
|
109
|
+
"\":root > .foo\" expected to be :left0 but was :right0",
|
110
|
+
[
|
111
|
+
"Hash {\"A\"=>\"A\", \"B\"=>\"B\", \"C\"=>\"C\", \"D\"=>\"D...",
|
112
|
+
[3,4,5].inspect
|
113
|
+
]
|
71
114
|
)
|
72
115
|
end
|
73
116
|
|
74
117
|
def test_lists_multliple_differences
|
75
118
|
lval = {'one' => {'two' => 'three', 'four' => 'five'}, 'six' => 'seven'}
|
76
119
|
rval = {'one' => {'two' => 'TWO', 'four' => 'five'}, 'six' => 'SIX'}
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
"
|
81
|
-
comparison.difference_message
|
120
|
+
assert_symmetrically_different(
|
121
|
+
lval, rval,
|
122
|
+
"\":root > .one > .two\" expected to be :left0 but was :right0\n\":root > .six\" expected to be :left1 but was :right1",
|
123
|
+
[["three".inspect, "TWO".inspect], ["seven".inspect, "SIX".inspect]]
|
82
124
|
)
|
83
125
|
end
|
84
126
|
|
85
127
|
def test_detects_difference_between_array_and_nil
|
86
128
|
lval = {'one' => [1,2,3]}
|
87
129
|
rval = {'one' => nil}
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
comparison.difference_message
|
130
|
+
assert_symmetrically_different(
|
131
|
+
lval, rval,
|
132
|
+
"\":root > .one\" expected to be :left0 but was :right0",
|
133
|
+
[[1,2,3].inspect, 'nil']
|
93
134
|
)
|
94
135
|
end
|
95
136
|
|
96
137
|
def test_shows_first_string_difference
|
97
138
|
lval = {'body' => "Four score and seven years ago our fathers brought forth on this continent, a new nation, conceived in Liberty, and dedicated to the proposition that all men are created equal."}
|
98
139
|
rval = {'body' => "Four score and seven years ago our fathers brought forth on this continent, a new nation, conceived in liberty, and dedicated to the proposition that all men are created equal."}
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
140
|
+
assert_symmetrically_different(
|
141
|
+
lval, rval,
|
142
|
+
"\":root > .body\" differs starting at char 103: :left0 differs from :right0",
|
143
|
+
[
|
144
|
+
"..., conceived in Liberty, and ded...".inspect,
|
145
|
+
"..., conceived in liberty, and ded...".inspect
|
146
|
+
]
|
104
147
|
)
|
105
148
|
end
|
106
149
|
|
107
150
|
def test_shows_string_difference_start
|
108
151
|
lval = {'rev' => "22-23c92a95665bb692313229c8224b7088"}
|
109
152
|
rval = {'rev' => "23-54a5106f8c522a57d6d4c6963bc36611"}
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
"
|
114
|
-
comparison.difference_message
|
153
|
+
assert_symmetrically_different(
|
154
|
+
lval, rval,
|
155
|
+
"\":root > .rev\" differs starting at char 1: :left0 differs from :right0",
|
156
|
+
["22-23c92a95665bb6...".inspect, "23-54a5106f8c522a...".inspect]
|
115
157
|
)
|
116
158
|
end
|
117
159
|
|
118
160
|
def test_simple_exclusion
|
119
161
|
lval = {'one' => 'two', 'three' => 'four'}
|
120
162
|
rval = {'one' => 'two', 'three' => 'THREE'}
|
121
|
-
comparison = JsonDeepCompare::
|
163
|
+
comparison = JsonDeepCompare::DocumentComparison.new(
|
122
164
|
lval, rval, exclusions: [":root > .three"]
|
123
165
|
)
|
124
166
|
assert comparison.equal?
|
125
167
|
end
|
168
|
+
|
169
|
+
def test_exclusion_regexp
|
170
|
+
lval = {'one' => 'two', 'three' => 'four'}
|
171
|
+
rval = {'one' => 'two', 'three' => 'THREE'}
|
172
|
+
comparison = JsonDeepCompare::DocumentComparison.new(
|
173
|
+
lval, rval, exclusions: [/> \.three$/]
|
174
|
+
)
|
175
|
+
assert comparison.equal?
|
176
|
+
end
|
177
|
+
|
178
|
+
def test_blank_equality_option
|
179
|
+
lval = {'one' => 'two', 'three' => ''}
|
180
|
+
rval = {'one' => 'two', 'three' => nil}
|
181
|
+
comparison = JsonDeepCompare::DocumentComparison.new(
|
182
|
+
lval, rval, blank_equality: true
|
183
|
+
)
|
184
|
+
assert comparison.equal?
|
185
|
+
end
|
186
|
+
|
187
|
+
def test_equality_proc_option
|
188
|
+
lval = {'one' => 2, 'three' => "He says 'hi'"}
|
189
|
+
rval = {'one' => 2, 'three' => "He says \"hi\""}
|
190
|
+
comparison = JsonDeepCompare::DocumentComparison.new(
|
191
|
+
lval, rval, equality: Proc.new { |lval, rval|
|
192
|
+
if lval.is_a?(String) && rval.is_a?(String)
|
193
|
+
lval.gsub(/"/, "'") == rval.gsub(/"/, "'")
|
194
|
+
else
|
195
|
+
lval == rval
|
196
|
+
end
|
197
|
+
}
|
198
|
+
)
|
199
|
+
assert comparison.equal?
|
200
|
+
end
|
126
201
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: json-deep-compare
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2014-01
|
12
|
+
date: 2014-02-01 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|
@@ -73,7 +73,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
73
73
|
version: '0'
|
74
74
|
segments:
|
75
75
|
- 0
|
76
|
-
hash:
|
76
|
+
hash: 634763774305586060
|
77
77
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
78
78
|
none: false
|
79
79
|
requirements:
|
@@ -82,7 +82,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
82
82
|
version: '0'
|
83
83
|
segments:
|
84
84
|
- 0
|
85
|
-
hash:
|
85
|
+
hash: 634763774305586060
|
86
86
|
requirements: []
|
87
87
|
rubyforge_project:
|
88
88
|
rubygems_version: 1.8.23
|