rspec-json_matchers 0.1.0.alpha.3 → 0.1.0.alpha.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|