rspec-json_matchers 0.1.0.alpha.1

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.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.travis.yml +20 -0
  4. data/Appraisals +16 -0
  5. data/CHANGELOG.md +7 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +497 -0
  9. data/Rakefile +14 -0
  10. data/gemfiles/rspec_3_0.gemfile +7 -0
  11. data/gemfiles/rspec_3_1.gemfile +7 -0
  12. data/gemfiles/rspec_3_2.gemfile +7 -0
  13. data/gemfiles/rspec_3_3.gemfile +7 -0
  14. data/lib/rspec/json_matchers/comparers/abstract_comparer.rb +289 -0
  15. data/lib/rspec/json_matchers/comparers/comparison_result.rb +22 -0
  16. data/lib/rspec/json_matchers/comparers/exact_keys_comparer.rb +27 -0
  17. data/lib/rspec/json_matchers/comparers/include_keys_comparer.rb +27 -0
  18. data/lib/rspec/json_matchers/comparers.rb +13 -0
  19. data/lib/rspec/json_matchers/expectation.rb +78 -0
  20. data/lib/rspec/json_matchers/expectations/abstract.rb +36 -0
  21. data/lib/rspec/json_matchers/expectations/core.rb +103 -0
  22. data/lib/rspec/json_matchers/expectations/mixins/built_in.rb +177 -0
  23. data/lib/rspec/json_matchers/expectations/private.rb +181 -0
  24. data/lib/rspec/json_matchers/expectations.rb +14 -0
  25. data/lib/rspec/json_matchers/matchers/be_json_matcher.rb +92 -0
  26. data/lib/rspec/json_matchers/matchers/be_json_with_content_matcher.rb +21 -0
  27. data/lib/rspec/json_matchers/matchers/be_json_with_sizes_matcher.rb +21 -0
  28. data/lib/rspec/json_matchers/matchers/be_json_with_something_matcher.rb +174 -0
  29. data/lib/rspec/json_matchers/matchers.rb +12 -0
  30. data/lib/rspec/json_matchers/utils/collection_keys_extractor.rb +35 -0
  31. data/lib/rspec/json_matchers/utils/key_path/extraction_result.rb +22 -0
  32. data/lib/rspec/json_matchers/utils/key_path/extractor.rb +70 -0
  33. data/lib/rspec/json_matchers/utils/key_path/path.rb +104 -0
  34. data/lib/rspec/json_matchers/utils.rb +10 -0
  35. data/lib/rspec/json_matchers/version.rb +8 -0
  36. data/lib/rspec/json_matchers.rb +15 -0
  37. data/lib/rspec-json_matchers.rb +1 -0
  38. data/rspec-json_matchers.gemspec +47 -0
  39. metadata +245 -0
@@ -0,0 +1,289 @@
1
+ require "abstract_class"
2
+ require "set"
3
+ require_relative "../expectation"
4
+ require_relative "comparison_result"
5
+
6
+ module RSpec
7
+ module JsonMatchers
8
+ module Comparers
9
+ # @api private
10
+ # @abstract
11
+ #
12
+ # The parent of all comparer classes
13
+ # It holds most of the responsibility
14
+ # The subclasses only need to implement the behaviour of matching keys
15
+ # when both expected & actual are same type of collection
16
+ class AbstractComparer
17
+ attr_reader *[
18
+ :actual,
19
+ :expected,
20
+ :reasons,
21
+ :value_matching_proc,
22
+ ]
23
+
24
+ # Creates a comparer that actually use the {value_matching_proc} for matching {#actual} and {#expected}
25
+ # This class is respossible to aggregating
26
+ # the matching result for each element of {#expected},
27
+ # and compare the keys/indices as well
28
+ #
29
+ # @param actual [Object] the actual value
30
+ # @param expected [Object] the expected value
31
+ # @param reasons [Array<String>]
32
+ # failure reasons, mostly the path parts
33
+ # @param value_matching_proc [Proc]
34
+ # the proc that actually compares the expected & actual and returns a boolean
35
+ def initialize(actual, expected, reasons, value_matching_proc)
36
+ @actual = actual
37
+ @expected = expected
38
+ @reasons = reasons
39
+
40
+ @value_matching_proc = value_matching_proc
41
+ end
42
+
43
+ # @return [Boolean]
44
+ # `true` if #actual & #expected are the same
45
+ def compare
46
+ if has_matched_value?
47
+ return ComparisonResult.new(true, reasons)
48
+ end
49
+
50
+ has_matched_collection?
51
+ end
52
+
53
+ private
54
+
55
+ def has_matched_value?
56
+ value_matching_proc.call(expected, actual)
57
+ end
58
+
59
+ def has_matched_collection?
60
+ return ComparisonResult.new(false, reasons) unless is_collection?
61
+ return ComparisonResult.new(false, reasons) unless has_matched_class?
62
+ return ComparisonResult.new(false, reasons) unless has_matched_keys?
63
+
64
+ ComparisonResult.new(has_matched_values?, reasons)
65
+ end
66
+
67
+ def is_collection?
68
+ actual.is_a?(Array) || actual.is_a?(Hash)
69
+ end
70
+
71
+ def has_matched_class?
72
+ actual.class == expected.class
73
+ end
74
+
75
+ # @note with side effect on `#reasons`
76
+ def has_matched_keys?
77
+ raise NotImplementedError
78
+ end
79
+
80
+ # @note with side effect on `#reasons`
81
+ def has_matched_values?
82
+ comparison_result = {
83
+ Array => HasMatchedArrayValues,
84
+ Hash => HasMatchedHashValues,
85
+ }.fetch(expected.class).
86
+ new(expected, actual, reasons, value_matching_proc, self.class).
87
+ comparison_result
88
+
89
+ comparison_result.matched?.tap do |matched|
90
+ @reasons = comparison_result.reasons unless matched
91
+ end
92
+ end
93
+
94
+ class HasMatchedValues
95
+ extend AbstractClass
96
+
97
+ private
98
+ attr_reader *[
99
+ :actual,
100
+ :expected,
101
+ :reasons,
102
+ :value_matching_proc,
103
+
104
+ :comparer_class,
105
+ ]
106
+ public
107
+
108
+ # Create a "matching" operation object that can return a {Comparers::ComparisonResult}
109
+ #
110
+ # @param expected [Object] the expected "thing", should be an {Enumerable}
111
+ # @param actual [Object] the actual "thing", should be an {Enumerable}
112
+ # @param reasons [Array<String>]
113
+ # failure reasons, mostly the path parts
114
+ # @param value_matching_proc [Proc]
115
+ # the proc that actually compares the expected & actual and returns a boolean
116
+ # @param comparer_class [Class<AbstractComparer>]
117
+ # the class that should be used recursively
118
+ def initialize(expected, actual, reasons, value_matching_proc, comparer_class)
119
+ @actual = actual
120
+ @expected = expected
121
+ @reasons = reasons
122
+
123
+ @value_matching_proc = value_matching_proc
124
+
125
+ @comparer_class = comparer_class
126
+ end
127
+
128
+ def comparison_result
129
+ each_element_enumerator.each do |element|
130
+ comparison_result = has_matched_value_class.new(
131
+ element,
132
+ expected,
133
+ actual,
134
+ reasons,
135
+ value_matching_proc,
136
+ comparer_class,
137
+ ).comparison_result
138
+
139
+ return comparison_result unless comparison_result.matched?
140
+ end
141
+
142
+ Comparers::ComparisonResult.new(true, reasons)
143
+ end
144
+
145
+ def each_element_enumerator
146
+ raise NotImplementedError
147
+ end
148
+
149
+ def has_matched_value_class
150
+ raise NotImplementedError
151
+ end
152
+
153
+ class HasMatchedValue
154
+ extend AbstractClass
155
+
156
+ private
157
+ attr_reader *[
158
+ :element,
159
+
160
+ :actual,
161
+ :expected,
162
+ :reasons,
163
+
164
+ :value_matching_proc,
165
+
166
+ :comparer_class,
167
+ ]
168
+ public
169
+
170
+ # Create a "matching" operation object that can return a {Comparers::ComparisonResult}
171
+ # Unlike {HasMatchedValues}, this is for an element of `expected`
172
+ #
173
+ # @param element [Integer, String, Symbol] a index/key from expected (not value)
174
+ # @param (see HasMatchedValues#initialize)
175
+ def initialize(element, expected, actual, reasons, value_matching_proc, comparer_class)
176
+ @element = element
177
+ @actual = actual
178
+ @expected = expected
179
+ @reasons = reasons
180
+
181
+ @value_matching_proc = value_matching_proc
182
+
183
+ @comparer_class = comparer_class
184
+ end
185
+
186
+ def comparison_result
187
+ return false unless actual_contain_element?
188
+
189
+ result.tap do |result|
190
+ next if result.matched?
191
+ result.reasons.push(reason)
192
+ end
193
+ end
194
+
195
+ private
196
+
197
+ def result
198
+ @result ||= comparer_class.
199
+ new(
200
+ actual_for_element,
201
+ expected_for_element,
202
+ reasons,
203
+ value_matching_proc,
204
+ ).
205
+ compare
206
+ end
207
+
208
+ def actual_contain_element?
209
+ raise NotImplementedError
210
+ end
211
+
212
+ def actual_for_element
213
+ raise NotImplementedError
214
+ end
215
+ def expected_for_element
216
+ raise NotImplementedError
217
+ end
218
+
219
+ def reason
220
+ raise NotImplementedError
221
+ end
222
+ end
223
+ end
224
+
225
+ class HasMatchedArrayValues < HasMatchedValues
226
+ def each_element_enumerator
227
+ expected.each_index
228
+ end
229
+
230
+ def has_matched_value_class
231
+ HasMatchedArrayValue
232
+ end
233
+
234
+ class HasMatchedArrayValue < HasMatchedValues::HasMatchedValue
235
+ private
236
+ alias_method :index, :element
237
+ public
238
+
239
+ def actual_contain_element?
240
+ index < actual.size
241
+ end
242
+
243
+ def actual_for_element
244
+ actual[index]
245
+ end
246
+ def expected_for_element
247
+ expected[index]
248
+ end
249
+
250
+ def reason
251
+ "[#{index}]"
252
+ end
253
+ end
254
+ end
255
+
256
+ class HasMatchedHashValues < HasMatchedValues
257
+ def each_element_enumerator
258
+ expected.each_key
259
+ end
260
+
261
+ def has_matched_value_class
262
+ HasMatchedHashValue
263
+ end
264
+
265
+ class HasMatchedHashValue < HasMatchedValues::HasMatchedValue
266
+ private
267
+ alias_method :key, :element
268
+ public
269
+
270
+ def actual_contain_element?
271
+ actual.key?(key.to_s)
272
+ end
273
+
274
+ def actual_for_element
275
+ actual[key.to_s]
276
+ end
277
+ def expected_for_element
278
+ expected[key]
279
+ end
280
+
281
+ def reason
282
+ key
283
+ end
284
+ end
285
+ end
286
+ end
287
+ end
288
+ end
289
+ end
@@ -0,0 +1,22 @@
1
+ module RSpec
2
+ module JsonMatchers
3
+ module Comparers
4
+ # @api private
5
+ #
6
+ # A value object returned by comparers
7
+ # Instead of just Boolean
8
+ class ComparisonResult
9
+ attr_reader :reasons
10
+
11
+ def initialize(matched, reasons)
12
+ @matched = matched
13
+ @reasons = reasons
14
+ end
15
+
16
+ def matched?
17
+ !!@matched
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,27 @@
1
+ require_relative "abstract_comparer"
2
+
3
+ module RSpec
4
+ module JsonMatchers
5
+ module Comparers
6
+ # @api private
7
+ #
8
+ # The comparer class that disallow actual collection
9
+ # to have more properties/elements than expected collection
10
+ class ExactKeysComparer < AbstractComparer
11
+ private
12
+
13
+ # @note with side effect on `#reasons`
14
+ def has_matched_keys?
15
+ actual_keys = Utils::CollectionKeysExtractor.extract(actual)
16
+ expected_keys = Utils::CollectionKeysExtractor.extract(expected)
17
+ (actual_keys == expected_keys).tap do |success|
18
+ unless success
19
+ diff_keys = (actual_keys - expected_keys) + (expected_keys - actual_keys)
20
+ reasons.push(diff_keys.awesome_inspect)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,27 @@
1
+ require_relative "abstract_comparer"
2
+
3
+ module RSpec
4
+ module JsonMatchers
5
+ module Comparers
6
+ # @api private
7
+ #
8
+ # The comparer class that allow actual collection
9
+ # to have more properties/elements than expected collection
10
+ class IncludeKeysComparer < AbstractComparer
11
+ private
12
+
13
+ # @note with side effect on `#reasons`
14
+ def has_matched_keys?
15
+ actual_keys = Utils::CollectionKeysExtractor.extract(actual)
16
+ expected_keys = Utils::CollectionKeysExtractor.extract(expected)
17
+ (expected_keys.subset?(actual_keys)).tap do |success|
18
+ unless success
19
+ diff_keys = (expected_keys - actual_keys)
20
+ reasons.push(diff_keys.awesome_inspect)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,13 @@
1
+ require_relative "comparers/exact_keys_comparer"
2
+ require_relative "comparers/include_keys_comparer"
3
+
4
+ module RSpec
5
+ module JsonMatchers
6
+ # @api private
7
+ #
8
+ # All classes & modules here (including itself) are for internal use only
9
+ # They could be renamed or re-structured anytime
10
+ module Comparers
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,78 @@
1
+ require "abstract_class"
2
+
3
+ module RSpec
4
+ module JsonMatchers
5
+ # Represents an expectation of an object (usually called `expected`)
6
+ # Built to avoid {Object#===} usage like other matcher gems, like `rspec-json_matcher`
7
+ # Actually `rspec-mocks` `3.x` also uses it, but only internally
8
+ #
9
+ # @api
10
+ # This class can be extended to create custom kinds of expectation
11
+ # But only used for this gem
12
+ # @abstract
13
+ # This class MUST be used after being inherited
14
+ # Subclasses MUST override {#expect?} to allow this gem to determine the test result
15
+ class Expectation
16
+ extend AbstractClass
17
+
18
+ # @abstract
19
+ # This method MUST be overridden to allow this gem to determine the test result
20
+ #
21
+ # @param value [Object] actual value to be evaluated
22
+ #
23
+ # @return [Bool] Whether the `value` is expected
24
+ def expect?(value)
25
+ raise NotImplementedError
26
+ end
27
+
28
+ class << self
29
+ # @api private
30
+ #
31
+ # "Build" an expectation object (not class) depends on `value`
32
+ #
33
+ # @param value [Object]
34
+ # expected value, could be an expectation object as well
35
+ #
36
+ # @return [Expectation]
37
+ def build(value)
38
+ return value if value.is_a?(self)
39
+
40
+ if value.is_a?(Regexp)
41
+ return Expectations::Private::MatchingRegexp[value]
42
+ end
43
+
44
+ if value.is_a?(Range)
45
+ return Expectations::Private::InRange[value]
46
+ end
47
+
48
+ if value.respond_to?(:call)
49
+ return Expectations::Private::SatisfyingCallable[value]
50
+ end
51
+
52
+ if value.is_a?(Class)
53
+ # Subclass
54
+ # See http://ruby-doc.org/core-2.2.2/Module.html#method-i-3C
55
+ if value < Expectations::Core::SingletonExpectation
56
+ return value::INSTANCE
57
+ end
58
+ return Expectations::Private::KindOf[value]
59
+ end
60
+
61
+ Expectations::Private::Eq[value]
62
+ end
63
+ # @api private
64
+ #
65
+ # "Build" expectation objects (not classes) depending on `values`
66
+ #
67
+ # @return [Array<Expectation>]
68
+ # @see .build
69
+ def build_many(values)
70
+ values.flat_map{|v| build(v) }
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ # Classes in the following file(s) are required at runtime not parse time
78
+ require_relative "expectations"
@@ -0,0 +1,36 @@
1
+ require "abstract_class"
2
+
3
+ require_relative "core"
4
+
5
+ module RSpec
6
+ module JsonMatchers
7
+ module Expectations
8
+ # @api private
9
+ # All classes within module should NOT be able to be used directly / extended
10
+ #
11
+ # Classes in this namespace are depending on {Core}
12
+ # and depended by some classes in {Expectations::Mixins::BuiltIn}
13
+ # They are all abstract too, thus the naming, but might change
14
+ # This namespace is created is to avoid require order problem when putting classes here in {Private}
15
+ module Abstract
16
+ # @abstract
17
+ # Only for reducing code duplication
18
+ #
19
+ # Verifies the value passed in is a {Numeric}
20
+ #
21
+ # @note
22
+ # {Numeric} might not be the best class to check for
23
+ # Since not all subclasses of it are expected
24
+ # But for simplicity's sake this is used until problem raised
25
+ class NumericExpectation < Expectations::Core::SingletonExpectation
26
+ extend AbstractClass
27
+
28
+ def expect?(value)
29
+ value.is_a?(Numeric)
30
+ end
31
+ end
32
+
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,103 @@
1
+ require "abstract_class"
2
+
3
+ require_relative "../expectation"
4
+
5
+ module RSpec
6
+ module JsonMatchers
7
+ module Expectations
8
+ # @api
9
+ # All classes within module should be able to be used / extended
10
+ # Unless specified otherwise
11
+ #
12
+ # All public expectation classes that can be extended
13
+ # even by classes in extension gems
14
+ module Core
15
+ # @abstract
16
+ # This class MUST be used after being inherited
17
+ # Subclasses will have a constant `INSTANCE` storing the only instance of that class
18
+ # @note
19
+ # This class assumed descendants to NOT override {.inherited} or call `super` if overridden
20
+ # Otherwise the constant `INSTANCE` won't work
21
+ # @note
22
+ # The constant `INSTANCE` will be referred with namespace,
23
+ # which eliminates the possibility of using parent class constant
24
+ # @see
25
+ # https://stackoverflow.com/questions/3174563/how-to-use-an-overridden-constant-in-an-inheritanced-class
26
+ # @note
27
+ # Pattern comes from gem rspec-mocks
28
+ # @see
29
+ # https://github.com/rspec/rspec-mocks/blob/3-2-maintenance/lib/rspec/mocks/argument_matchers.rb
30
+ class SingletonExpectation < Expectation
31
+ extend AbstractClass
32
+
33
+ private_class_method :new
34
+
35
+ def self.inherited(subclass)
36
+ subclass.const_set(:INSTANCE, subclass.send(:new))
37
+ end
38
+ end
39
+
40
+ # Allow class to be called with `.[]` instead of `.new`
41
+ #
42
+ # @abstract
43
+ # This class MUST be used after being inherited
44
+ class CallableExpectation < Expectation
45
+ extend AbstractClass
46
+
47
+ # The replacement of {.new}
48
+ # It accept any number of arguments and delegates to private {.new}
49
+ # This pattern is taken from gem `contracts`
50
+ #
51
+ # @see https://github.com/egonSchiele/contracts.ruby
52
+ #
53
+ # @return [Expectation] an expectation object
54
+ def self.[](*values)
55
+ new(*values)
56
+ end
57
+
58
+ private_class_method :new
59
+ end
60
+
61
+ # Validates exactly one value is passed in
62
+ #
63
+ # @abstract
64
+ class SingleValueCallableExpectation < CallableExpectation
65
+ EXPECTED_VALUE_SIZE = 1
66
+ private_constant :EXPECTED_VALUE_SIZE
67
+
68
+ # (see CallableExpectation.[])
69
+ # But only 1 argument is accepted
70
+ def self.[](*values)
71
+ unless values.size == EXPECTED_VALUE_SIZE
72
+ raise ArgumentError, "Exactly #{EXPECTED_VALUE_SIZE} argument is required"
73
+ end
74
+ super
75
+ end
76
+ end
77
+
78
+ # Takes any number of objects and converts into expectation objects (if not already)
79
+ #
80
+ # @abstract
81
+ class CompositeExpectation < CallableExpectation
82
+ extend AbstractClass
83
+
84
+ private
85
+ attr_reader :expectations
86
+ public
87
+
88
+ # (see CallableExpectation.[])
89
+ # Also all values will be converted into expectations
90
+ def self.[](*values)
91
+ super(build_many(values))
92
+ end
93
+
94
+ private
95
+
96
+ def initialize(expectations)
97
+ @expectations = expectations
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end