rspec-json_matchers 0.1.0.alpha.3 → 0.1.0.alpha.4
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 +4 -4
- data/.codeclimate.yml +16 -0
- data/.rubocop.yml +477 -22
- data/.travis.yml +12 -3
- data/Appraisals +24 -4
- data/Gemfile +1 -0
- data/README.md +37 -20
- data/Rakefile +1 -0
- data/gemfiles/rspec_3_0.gemfile +2 -1
- data/gemfiles/rspec_3_1.gemfile +2 -1
- data/gemfiles/rspec_3_2.gemfile +2 -1
- data/gemfiles/rspec_3_3.gemfile +2 -1
- data/gemfiles/rspec_3_4.gemfile +7 -0
- data/gemfiles/rspec_3_5.gemfile +7 -0
- data/gemfiles/rspec_3_6.gemfile +7 -0
- data/lib/rspec/json_matchers/expectation.rb +6 -2
- data/lib/rspec/json_matchers/expectations/mixins/built_in.rb +84 -5
- data/lib/rspec/json_matchers/expectations/private.rb +4 -2
- data/lib/rspec/json_matchers/matchers.rb +0 -1
- data/lib/rspec/json_matchers/matchers/be_json_matcher.rb +0 -14
- data/lib/rspec/json_matchers/matchers/be_json_with_content_matcher.rb +144 -5
- data/lib/rspec/json_matchers/utils/key_path/extraction.rb +8 -8
- data/lib/rspec/json_matchers/utils/key_path/path.rb +1 -1
- data/lib/rspec/json_matchers/version.rb +1 -1
- data/rspec-json_matchers.gemspec +3 -2
- metadata +16 -13
- data/lib/rspec/json_matchers/comparers.rb +0 -13
- data/lib/rspec/json_matchers/comparers/abstract_comparer.rb +0 -323
- data/lib/rspec/json_matchers/comparers/comparison_result.rb +0 -22
- data/lib/rspec/json_matchers/comparers/exact_keys_comparer.rb +0 -27
- data/lib/rspec/json_matchers/comparers/include_keys_comparer.rb +0 -26
- data/lib/rspec/json_matchers/matchers/be_json_with_sizes_matcher.rb +0 -24
- data/lib/rspec/json_matchers/matchers/be_json_with_something_matcher.rb +0 -188
data/.travis.yml
CHANGED
@@ -2,18 +2,27 @@
|
|
2
2
|
# http://docs.travis-ci.com/user/workers/container-based-infrastructure/
|
3
3
|
sudo: false
|
4
4
|
language: ruby
|
5
|
+
before_install:
|
6
|
+
# This is required since 2.6.8 has issue when installing `rainbow`
|
7
|
+
# https://github.com/sickill/rainbow/issues/48
|
8
|
+
- gem update --system
|
9
|
+
- gem --version
|
5
10
|
cache:
|
6
11
|
- bundler
|
7
12
|
rvm:
|
8
|
-
- 2.
|
9
|
-
- 2.
|
10
|
-
- 2.
|
13
|
+
- 2.1.10
|
14
|
+
- 2.2.6
|
15
|
+
- 2.3.3
|
16
|
+
- 2.4.0
|
11
17
|
- ruby-head
|
12
18
|
gemfile:
|
13
19
|
- gemfiles/rspec_3_0.gemfile
|
14
20
|
- gemfiles/rspec_3_1.gemfile
|
15
21
|
- gemfiles/rspec_3_2.gemfile
|
16
22
|
- gemfiles/rspec_3_3.gemfile
|
23
|
+
- gemfiles/rspec_3_4.gemfile
|
24
|
+
- gemfiles/rspec_3_5.gemfile
|
25
|
+
- gemfiles/rspec_3_6.gemfile
|
17
26
|
matrix:
|
18
27
|
fast_finish: true
|
19
28
|
allow_failures:
|
data/Appraisals
CHANGED
@@ -1,16 +1,36 @@
|
|
1
1
|
|
2
2
|
appraise "rspec_3_0" do
|
3
|
-
gem "rspec", "3.0"
|
3
|
+
gem "rspec", "~> 3.0.0"
|
4
|
+
# https://github.com/rspec/rspec-core/issues/2205
|
5
|
+
gem "rake", "< 12"
|
4
6
|
end
|
5
7
|
|
6
8
|
appraise "rspec_3_1" do
|
7
|
-
gem "rspec", "3.1"
|
9
|
+
gem "rspec", "~> 3.1.0"
|
10
|
+
# https://github.com/rspec/rspec-core/issues/2205
|
11
|
+
gem "rake", "< 12"
|
8
12
|
end
|
9
13
|
|
10
14
|
appraise "rspec_3_2" do
|
11
|
-
gem "rspec", "3.2"
|
15
|
+
gem "rspec", "~> 3.2.0"
|
16
|
+
# https://github.com/rspec/rspec-core/issues/2205
|
17
|
+
gem "rake", "< 12"
|
12
18
|
end
|
13
19
|
|
14
20
|
appraise "rspec_3_3" do
|
15
|
-
gem "rspec", "3.3"
|
21
|
+
gem "rspec", "~> 3.3.0"
|
22
|
+
# https://github.com/rspec/rspec-core/issues/2205
|
23
|
+
gem "rake", "< 12"
|
24
|
+
end
|
25
|
+
|
26
|
+
appraise "rspec_3_4" do
|
27
|
+
gem "rspec", "~> 3.4.0"
|
28
|
+
end
|
29
|
+
|
30
|
+
appraise "rspec_3_5" do
|
31
|
+
gem "rspec", "~> 3.5.0"
|
32
|
+
end
|
33
|
+
|
34
|
+
appraise "rspec_3_6" do
|
35
|
+
gem "rspec", ">= 3.6.0.beta2", "< 3.7.0"
|
16
36
|
end
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -114,14 +114,6 @@ specify { expect({a: 1, b: 2}.to_json).to be_json.with_content({a: 1}) } # => p
|
|
114
114
|
specify { expect({a: 1}.to_json).to be_json.with_content({a: 1, b: 2}) } # => fail
|
115
115
|
```
|
116
116
|
|
117
|
-
It's possible to make examples fail when the object represented by JSON string in `subject`
|
118
|
-
contains more keys than that in expectation using `with_exact_keys`.
|
119
|
-
|
120
|
-
```ruby
|
121
|
-
# The spec can be set to fail when actual has more keys than expected
|
122
|
-
specify { expect({a: 1, b: 2}.to_json).to be_json.with_content({a: 1}).with_exact_keys } # => fail
|
123
|
-
```
|
124
|
-
|
125
117
|
A "path" can also be specified for testing deeply nested data.
|
126
118
|
|
127
119
|
```ruby
|
@@ -476,32 +468,57 @@ specify do
|
|
476
468
|
end # => fail
|
477
469
|
```
|
478
470
|
|
471
|
+
It's possible to make examples fail when the object represented by JSON string in `subject`
|
472
|
+
contains more keys than that in expectation using `HashWithContent` & `#with_exact_keys`.
|
473
|
+
`HashWithContent` is the expectation class that is automatically used when a `Hash` is passed.
|
474
|
+
|
475
|
+
|
476
|
+
```ruby
|
477
|
+
# The spec can be set to fail when actual has more keys than expected
|
478
|
+
specify do
|
479
|
+
expect({a: 1, b: 2}.to_json).
|
480
|
+
to be_json.
|
481
|
+
with_content(
|
482
|
+
expectations::HashWithContent[{a: 1}].with_exact_keys
|
483
|
+
)
|
484
|
+
# => fail
|
485
|
+
end
|
486
|
+
```
|
487
|
+
|
488
|
+
#### Custom/Complex Expectations NOT included on purpose
|
489
|
+
|
490
|
+
##### Date
|
491
|
+
In [`airborne`](https://github.com/brooklynDev/airborne) you can validate the value as a "date" (and "time").
|
492
|
+
However "date/time" is not part of the JSON specification.
|
493
|
+
Some people use a string with a format specified in ISO to represent a time, but a [Unix time](https://en.wikipedia.org/wiki/Unix_time).
|
494
|
+
So this gem does not try to be "smart" to have a "generic" expectation for "date/time".
|
495
|
+
New expectations might be added in the future, to the core gem or a new extension gem, for common formats of "date" values.
|
496
|
+
There is no clear schedule for the addition yet, so you should try to add your own expectation class to suit your application.
|
497
|
+
|
479
498
|
|
480
499
|
### Matcher `be_json.with_sizes`
|
481
500
|
|
482
|
-
|
483
|
-
|
484
|
-
|
501
|
+
Used to have in earlier alpha versions.
|
502
|
+
Indended to ease the migration from other gems but
|
503
|
+
it also makes the gem more difficult to maintain.
|
504
|
+
Removed in later alpha version(s).
|
505
|
+
|
506
|
+
Just use `ArrayWithSize`
|
507
|
+
|
485
508
|
|
486
509
|
```ruby
|
487
510
|
specify do
|
488
511
|
expect({a: [1]}.to_json).to be_json.
|
489
|
-
|
512
|
+
with_content(a: ArrayWithSize[1])
|
490
513
|
end # => pass
|
491
514
|
specify do
|
492
515
|
expect({a: [1]}.to_json).to be_json.
|
493
|
-
|
516
|
+
with_content(a: ArrayWithSize[(0..2)])
|
494
517
|
end # => pass
|
495
518
|
specify do
|
496
519
|
expect({a: [1]}.to_json).to be_json.
|
497
|
-
|
520
|
+
with_content(a: ArrayWithSize[1.1])
|
498
521
|
end # => error
|
499
|
-
|
500
|
-
# But you can no longer pass multiple "expectations" for `AnyOf` behaviour
|
501
|
-
specify do
|
502
|
-
expect({a: [1]}.to_json).to be_json.
|
503
|
-
with_content(a: [0, 1, 3])
|
504
|
-
end # => fail, expects {a: [[], [1], [1, 2, 3]])
|
505
522
|
```
|
506
523
|
|
507
524
|
|
data/Rakefile
CHANGED
data/gemfiles/rspec_3_0.gemfile
CHANGED
data/gemfiles/rspec_3_1.gemfile
CHANGED
data/gemfiles/rspec_3_2.gemfile
CHANGED
data/gemfiles/rspec_3_3.gemfile
CHANGED
@@ -91,10 +91,13 @@ module RSpec
|
|
91
91
|
OBJECT_CLASS_TO_EXPECTATION_HASH = {
|
92
92
|
Regexp => -> (obj) { Expectations::Private::MatchingRegexp[obj] },
|
93
93
|
Range => -> (obj) { Expectations::Private::InRange[obj] },
|
94
|
+
Hash => -> (obj) { Expectations::Mixins::BuiltIn::HashWithContent[obj] },
|
94
95
|
}.freeze
|
95
96
|
private_constant :OBJECT_CLASS_TO_EXPECTATION_HASH
|
96
97
|
|
97
|
-
attr_reader(
|
98
|
+
attr_reader(
|
99
|
+
:object,
|
100
|
+
)
|
98
101
|
|
99
102
|
def expectation_by_class
|
100
103
|
if instance_variable_defined?(:@expectation_by_object_class)
|
@@ -104,7 +107,8 @@ module RSpec
|
|
104
107
|
proc = OBJECT_CLASS_TO_EXPECTATION_HASH[object.class]
|
105
108
|
return nil if proc.nil?
|
106
109
|
|
107
|
-
|
110
|
+
# Assign (cache) and return
|
111
|
+
@expectation_by_object_class = proc.call(object)
|
108
112
|
end
|
109
113
|
|
110
114
|
def expectation_by_ancestors
|
@@ -1,3 +1,4 @@
|
|
1
|
+
require "set"
|
1
2
|
require "abstract_class"
|
2
3
|
|
3
4
|
require_relative "../core"
|
@@ -150,11 +151,22 @@ module RSpec
|
|
150
151
|
class ArrayWithSize < AnyOf
|
151
152
|
# `Fixnum` & `Bignum` will be returned instead of `Integer`
|
152
153
|
# in `#class` for numbers
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
154
|
+
# But since 2.4.x it will be `Integer`
|
155
|
+
EXPECTED_VALUE_CLASS_TO_EXPECTATION_CLASS_MAPPING = begin
|
156
|
+
{
|
157
|
+
Range => -> (v) { Expectations::Private::InRange[v] },
|
158
|
+
Integer => -> (v) { Expectations::Private::Eq[v] },
|
159
|
+
}.tap do |result_hash|
|
160
|
+
# This fix is similar to
|
161
|
+
# https://github.com/rails/rails/pull/26732
|
162
|
+
next if 1.class == Integer
|
163
|
+
|
164
|
+
result_hash.merge!(
|
165
|
+
Fixnum => -> (v) { Expectations::Private::Eq[v] },
|
166
|
+
Bignum => -> (v) { Expectations::Private::Eq[v] },
|
167
|
+
)
|
168
|
+
end
|
169
|
+
end.freeze
|
158
170
|
private_constant :EXPECTED_VALUE_CLASS_TO_EXPECTATION_CLASS_MAPPING
|
159
171
|
|
160
172
|
class << self
|
@@ -182,6 +194,73 @@ module RSpec
|
|
182
194
|
super(value.size)
|
183
195
|
end
|
184
196
|
end
|
197
|
+
|
198
|
+
# Takes a {Hash}
|
199
|
+
# Validates `value` to be {Hash} and
|
200
|
+
# contain expected keys & values
|
201
|
+
# Extra keys in `value` will not be treated as "unexpected"
|
202
|
+
# Unless {#with_exact_keys} is used
|
203
|
+
class HashWithContent < Expectations::Core::SingleValueCallableExpectation
|
204
|
+
EXPECTED_CLASS = ::Hash
|
205
|
+
private_constant :EXPECTED_CLASS
|
206
|
+
|
207
|
+
def expect?(value)
|
208
|
+
matches_expected_class?(value) &&
|
209
|
+
matches_content_expectations?(value) &&
|
210
|
+
matches_keys_exactly?(value)
|
211
|
+
end
|
212
|
+
|
213
|
+
# After calling this method
|
214
|
+
# Any extra key in `value` will be marked as "unexpected"
|
215
|
+
#
|
216
|
+
# By default the expectation won't care about extra keys in `value`
|
217
|
+
def with_exact_keys
|
218
|
+
@require_exact_key_matches = true
|
219
|
+
self
|
220
|
+
end
|
221
|
+
|
222
|
+
private
|
223
|
+
|
224
|
+
attr_reader :require_exact_key_matches
|
225
|
+
alias_method(
|
226
|
+
:require_exact_key_matches?,
|
227
|
+
:require_exact_key_matches,
|
228
|
+
)
|
229
|
+
attr_reader :expected_value
|
230
|
+
|
231
|
+
def matches_expected_class?(value)
|
232
|
+
value.is_a?(::Hash)
|
233
|
+
end
|
234
|
+
|
235
|
+
def matches_keys_exactly?(actual_value)
|
236
|
+
return true unless require_exact_key_matches?
|
237
|
+
|
238
|
+
::Set.new(expected_value.keys.map(&:to_s)) ==
|
239
|
+
::Set.new(actual_value.keys.map(&:to_s))
|
240
|
+
end
|
241
|
+
|
242
|
+
def matches_content_expectations?(actual_value)
|
243
|
+
expected_value.each_pair.all? do |exp_key, exp_value|
|
244
|
+
unless actual_value.key?(exp_key) ||
|
245
|
+
actual_value.key?(exp_key.to_s)
|
246
|
+
return false
|
247
|
+
end
|
248
|
+
|
249
|
+
value_from_actual = actual_value.fetch(exp_key) do
|
250
|
+
actual_value.fetch(exp_key.to_s)
|
251
|
+
end
|
252
|
+
Expectation.build(exp_value).expect?(value_from_actual)
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
def initialize(value)
|
257
|
+
unless value.is_a?(::Hash)
|
258
|
+
fail ArgumentError, "a #{EXPECTED_CLASS} is required"
|
259
|
+
end
|
260
|
+
|
261
|
+
@expected_value = value
|
262
|
+
end
|
263
|
+
end
|
185
264
|
end
|
186
265
|
end
|
187
266
|
end
|
@@ -135,8 +135,10 @@ module RSpec
|
|
135
135
|
|
136
136
|
def initialize(value)
|
137
137
|
unless value.respond_to?(:call)
|
138
|
-
fail
|
139
|
-
|
138
|
+
fail(
|
139
|
+
ArgumentError,
|
140
|
+
"an object which respond to `:call` is required",
|
141
|
+
)
|
140
142
|
end
|
141
143
|
@callable = value
|
142
144
|
end
|
@@ -45,19 +45,6 @@ module RSpec
|
|
45
45
|
BeJsonWithContentMatcher.new(expected)
|
46
46
|
end
|
47
47
|
|
48
|
-
# @api
|
49
|
-
#
|
50
|
-
# Get a matcher that try to match the content of actual
|
51
|
-
# with nested expectations about array sizes
|
52
|
-
#
|
53
|
-
# @param expected [Hash, Array, Object]
|
54
|
-
# the expectation object
|
55
|
-
#
|
56
|
-
# @return [BeJsonWithSizesMatcher] a matcher object
|
57
|
-
def with_sizes(expected)
|
58
|
-
BeJsonWithSizesMatcher.new(expected)
|
59
|
-
end
|
60
|
-
|
61
48
|
# Expectation description in spec result summary
|
62
49
|
#
|
63
50
|
# @return [String]
|
@@ -95,4 +82,3 @@ end
|
|
95
82
|
# since the classes are only required
|
96
83
|
# on runtime but not load time
|
97
84
|
require_relative "be_json_with_content_matcher"
|
98
|
-
require_relative "be_json_with_sizes_matcher"
|
@@ -1,19 +1,158 @@
|
|
1
1
|
require "json"
|
2
2
|
require "awesome_print"
|
3
3
|
|
4
|
-
require_relative "
|
5
|
-
require_relative "../comparers"
|
4
|
+
require_relative "be_json_matcher"
|
6
5
|
require_relative "../utils"
|
7
6
|
|
8
7
|
module RSpec
|
9
8
|
module JsonMatchers
|
10
9
|
module Matchers
|
11
10
|
# @api private
|
12
|
-
|
11
|
+
# @abstract
|
12
|
+
#
|
13
|
+
# Parent of matcher classes that requires {#at_path} & {#with_exact_keys}
|
14
|
+
# This is not merged with {BeJsonMatcher}
|
15
|
+
# since it should be able to be used alone
|
16
|
+
class BeJsonWithContentMatcher < BeJsonMatcher
|
17
|
+
attr_reader(
|
18
|
+
:path,
|
19
|
+
)
|
20
|
+
|
21
|
+
def initialize(expected)
|
22
|
+
@expected = expected
|
23
|
+
@path = JsonMatchers::Utils::KeyPath::Path.new("")
|
24
|
+
end
|
25
|
+
|
26
|
+
def matches?(*_args)
|
27
|
+
super && has_valid_path? && expected_and_actual_matched?
|
28
|
+
end
|
29
|
+
|
30
|
+
def does_not_match?(*args)
|
31
|
+
!matches?(*args) && has_valid_path?
|
32
|
+
end
|
33
|
+
|
34
|
+
# Override {BeJsonMatcher#actual}
|
35
|
+
# It return actual object extracted by {#path}
|
36
|
+
# And also detect & set state for path error
|
37
|
+
# (either it's invalid or fails to extract)
|
38
|
+
#
|
39
|
+
# @return [Object]
|
40
|
+
# extracted object but could be object in the middle
|
41
|
+
# when extraction failed
|
42
|
+
def actual
|
43
|
+
result = path.extract(super)
|
44
|
+
has_path_error! if result.failed?
|
45
|
+
result.object
|
46
|
+
end
|
47
|
+
|
48
|
+
# Sets the path to be used for object,
|
49
|
+
# to avoid passing a deep nested
|
50
|
+
# {Hash} or {Array} as expectation
|
51
|
+
# Defaults to "" (if this is not called)
|
52
|
+
#
|
53
|
+
# The path uses period (".") as separator for parts
|
54
|
+
# Also period cannot be used as path name as a side-effect
|
55
|
+
#
|
56
|
+
# This does NOT raise error if the path is invalid
|
57
|
+
# (like having 2 periods, 1 period at the start/end of string)
|
58
|
+
# But it will fail the example with both `should` & `should_not`
|
59
|
+
#
|
60
|
+
# @param path [String] the "path" to be used
|
61
|
+
#
|
62
|
+
# @return [BeJsonWithSomethingMatcher] the match itself
|
63
|
+
#
|
64
|
+
# @throw [TypeError] when input is not a string
|
65
|
+
def at_path(path)
|
66
|
+
@path = JsonMatchers::Utils::KeyPath::Path.new(path)
|
67
|
+
self
|
68
|
+
end
|
69
|
+
|
70
|
+
# Overrides {BeJsonMatcher#failure_message}
|
71
|
+
def failure_message
|
72
|
+
return super if has_parser_error?
|
73
|
+
failure_message_for(true)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Overrides {BeJsonMatcher#failure_message_when_negated}
|
77
|
+
def failure_message_when_negated
|
78
|
+
return super if has_parser_error?
|
79
|
+
failure_message_for(false)
|
80
|
+
end
|
81
|
+
|
13
82
|
private
|
14
83
|
|
15
|
-
|
16
|
-
|
84
|
+
attr_reader(
|
85
|
+
:expected,
|
86
|
+
)
|
87
|
+
|
88
|
+
def failure_message_for(should_match)
|
89
|
+
return invalid_path_message unless has_valid_path?
|
90
|
+
return path_error_message if has_path_error?
|
91
|
+
|
92
|
+
inspection_messages(should_match)
|
93
|
+
end
|
94
|
+
|
95
|
+
# @return [Bool] Whether `expected` & `parsed` are "equal"
|
96
|
+
def expected_and_actual_matched?
|
97
|
+
extracted_actual = actual
|
98
|
+
return false if has_path_error?
|
99
|
+
|
100
|
+
expectation = Expectation.build(expected)
|
101
|
+
expectation.expect?(extracted_actual)
|
102
|
+
end
|
103
|
+
|
104
|
+
def reasons
|
105
|
+
@reasons ||= []
|
106
|
+
end
|
107
|
+
|
108
|
+
def inspection_messages(should_match)
|
109
|
+
[
|
110
|
+
["expected", inspection_messages_prefix(should_match), "to match:"].
|
111
|
+
compact.map(&:strip).join(" "),
|
112
|
+
expected.awesome_inspect(indent: -2),
|
113
|
+
"",
|
114
|
+
"actual:",
|
115
|
+
actual.awesome_inspect(indent: -2),
|
116
|
+
"",
|
117
|
+
inspection_message_for_reason,
|
118
|
+
].join("\n")
|
119
|
+
end
|
120
|
+
|
121
|
+
def inspection_messages_prefix(should_match)
|
122
|
+
should_match ? nil : "not"
|
123
|
+
end
|
124
|
+
|
125
|
+
def inspection_message_for_reason
|
126
|
+
reasons.any? ? "reason/path: #{reasons.reverse.join('.')}" : nil
|
127
|
+
end
|
128
|
+
|
129
|
+
def original_actual
|
130
|
+
@actual
|
131
|
+
end
|
132
|
+
|
133
|
+
def has_path_error?
|
134
|
+
@has_path_error
|
135
|
+
end
|
136
|
+
|
137
|
+
def has_path_error!
|
138
|
+
@has_path_error = true
|
139
|
+
end
|
140
|
+
|
141
|
+
# For both positive and negative
|
142
|
+
def path_error_message
|
143
|
+
[
|
144
|
+
%(path "#{path}" does not exists in actual: ),
|
145
|
+
original_actual.awesome_inspect(indent: -2),
|
146
|
+
].join("\n")
|
147
|
+
end
|
148
|
+
|
149
|
+
def has_valid_path?
|
150
|
+
(path.nil? || path.valid?)
|
151
|
+
end
|
152
|
+
|
153
|
+
# For both positive and negative
|
154
|
+
def invalid_path_message
|
155
|
+
%(path "#{path}" is invalid)
|
17
156
|
end
|
18
157
|
end
|
19
158
|
end
|