rspec-json_matchers 0.1.0.alpha.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.travis.yml +20 -0
- data/Appraisals +16 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +497 -0
- data/Rakefile +14 -0
- data/gemfiles/rspec_3_0.gemfile +7 -0
- data/gemfiles/rspec_3_1.gemfile +7 -0
- data/gemfiles/rspec_3_2.gemfile +7 -0
- data/gemfiles/rspec_3_3.gemfile +7 -0
- data/lib/rspec/json_matchers/comparers/abstract_comparer.rb +289 -0
- data/lib/rspec/json_matchers/comparers/comparison_result.rb +22 -0
- data/lib/rspec/json_matchers/comparers/exact_keys_comparer.rb +27 -0
- data/lib/rspec/json_matchers/comparers/include_keys_comparer.rb +27 -0
- data/lib/rspec/json_matchers/comparers.rb +13 -0
- data/lib/rspec/json_matchers/expectation.rb +78 -0
- data/lib/rspec/json_matchers/expectations/abstract.rb +36 -0
- data/lib/rspec/json_matchers/expectations/core.rb +103 -0
- data/lib/rspec/json_matchers/expectations/mixins/built_in.rb +177 -0
- data/lib/rspec/json_matchers/expectations/private.rb +181 -0
- data/lib/rspec/json_matchers/expectations.rb +14 -0
- data/lib/rspec/json_matchers/matchers/be_json_matcher.rb +92 -0
- data/lib/rspec/json_matchers/matchers/be_json_with_content_matcher.rb +21 -0
- data/lib/rspec/json_matchers/matchers/be_json_with_sizes_matcher.rb +21 -0
- data/lib/rspec/json_matchers/matchers/be_json_with_something_matcher.rb +174 -0
- data/lib/rspec/json_matchers/matchers.rb +12 -0
- data/lib/rspec/json_matchers/utils/collection_keys_extractor.rb +35 -0
- data/lib/rspec/json_matchers/utils/key_path/extraction_result.rb +22 -0
- data/lib/rspec/json_matchers/utils/key_path/extractor.rb +70 -0
- data/lib/rspec/json_matchers/utils/key_path/path.rb +104 -0
- data/lib/rspec/json_matchers/utils.rb +10 -0
- data/lib/rspec/json_matchers/version.rb +8 -0
- data/lib/rspec/json_matchers.rb +15 -0
- data/lib/rspec-json_matchers.rb +1 -0
- data/rspec-json_matchers.gemspec +47 -0
- metadata +245 -0
@@ -0,0 +1,177 @@
|
|
1
|
+
require "abstract_class"
|
2
|
+
|
3
|
+
require_relative "../core"
|
4
|
+
require_relative "../abstract"
|
5
|
+
|
6
|
+
module RSpec
|
7
|
+
module JsonMatchers
|
8
|
+
module Expectations
|
9
|
+
# @api
|
10
|
+
# The modules under this module can be included (in RSpec)
|
11
|
+
#
|
12
|
+
# If this gem or extensions gems decide to add different groups of expectations classes
|
13
|
+
# Which aim to be included in example groups
|
14
|
+
# They should add the namespace modules here
|
15
|
+
module Mixins
|
16
|
+
# @api
|
17
|
+
# All classes within module should be able to be used / extended
|
18
|
+
#
|
19
|
+
# A group of expectation classes provided by this gem
|
20
|
+
# Other extension gems (if any) should create another namespace
|
21
|
+
# if they intend to provide extra expectation classes
|
22
|
+
module BuiltIn
|
23
|
+
# Whatever the value is, it just passes
|
24
|
+
# A more verbose solution than passing {Object} in
|
25
|
+
# (That also works since everything parsed by {JSON} inherits from {Object})
|
26
|
+
#
|
27
|
+
# @example
|
28
|
+
# { key_with_unstable_content => Anything }
|
29
|
+
class Anything < Expectations::Core::SingletonExpectation
|
30
|
+
def expect?(*_args)
|
31
|
+
true
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Checks the value is a {Numeric} & less then zero
|
36
|
+
#
|
37
|
+
# @note (see Expectations::Private::NumericExpectation)
|
38
|
+
class PositiveNumber < Expectations::Abstract::NumericExpectation
|
39
|
+
def expect?(value)
|
40
|
+
super && value > 0
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Checks the value is a {Numeric} & less then zero
|
45
|
+
#
|
46
|
+
# @note (see Expectations::Private::NumericExpectation)
|
47
|
+
class NegativeNumber < Expectations::Abstract::NumericExpectation
|
48
|
+
def expect?(value)
|
49
|
+
super && value < 0
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Checks the value is a {TrueClass} or {FalseClass}
|
54
|
+
#
|
55
|
+
# @note
|
56
|
+
# The class does use name Boolean since so many gems uses it already
|
57
|
+
# You can also use gems like https://github.com/janlelis/boolean2/
|
58
|
+
class BooleanValue < Expectations::Core::SingletonExpectation
|
59
|
+
def expect?(value)
|
60
|
+
true == value || false == value
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Takes exactly one object and converts to an expectation object (if not already)
|
65
|
+
# Validates `value` to be {Array}
|
66
|
+
# And uses stored expectation for checking all elements of `value`
|
67
|
+
class ArrayOf < Expectations::Core::SingleValueCallableExpectation
|
68
|
+
private
|
69
|
+
attr_reader :children_elements_expectation
|
70
|
+
public
|
71
|
+
|
72
|
+
def expect?(value)
|
73
|
+
value.is_a?(Array) &&
|
74
|
+
(empty_allowed? || !value.empty?) &&
|
75
|
+
value.all? {|v| children_elements_expectation.expect?(v) }
|
76
|
+
end
|
77
|
+
|
78
|
+
# {Enumerable#all?} returns `true` when collection is empty
|
79
|
+
# So this method can be called to signal the expectation to do or do not expect an empty collection
|
80
|
+
#
|
81
|
+
# @param allow [Boolean]
|
82
|
+
# optional
|
83
|
+
# Should empty collection be "expected"
|
84
|
+
#
|
85
|
+
# @return [ArrayOf] the matcher itself
|
86
|
+
def allow_empty(allow = true)
|
87
|
+
@empty_allowed = !!allow
|
88
|
+
self
|
89
|
+
end
|
90
|
+
|
91
|
+
# A more verbose alias for `allow_empty(false)`
|
92
|
+
#
|
93
|
+
# @return (see #allow_empty)
|
94
|
+
def disallow_empty
|
95
|
+
allow_empty(false)
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def initialize(value)
|
101
|
+
@children_elements_expectation = Expectation.build(value)
|
102
|
+
@empty_allowed = true
|
103
|
+
end
|
104
|
+
|
105
|
+
def empty_allowed?
|
106
|
+
!!@empty_allowed
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# (see CompositeExpectation)
|
111
|
+
# It passes when any of expectation returns true
|
112
|
+
class AnyOf < Expectations::Core::CompositeExpectation
|
113
|
+
def expect?(value)
|
114
|
+
expectations.any? do |expectation|
|
115
|
+
expectation.expect?(value)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# (see CompositeExpectation)
|
121
|
+
# It passes when all of expectations return true
|
122
|
+
class AllOf < Expectations::Core::CompositeExpectation
|
123
|
+
def expect?(value)
|
124
|
+
expectations.all? do |expectation|
|
125
|
+
expectation.expect?(value)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Takes any number of {Integer} or {Range} (if not already)
|
131
|
+
# Validates `value` to be {Array}
|
132
|
+
# And the size matches any value passed in
|
133
|
+
#
|
134
|
+
# @note
|
135
|
+
# For behaviour of "and" (which should be a rare case)
|
136
|
+
# Combine {AllOf} & {ArrayWithSize}
|
137
|
+
# Or raise an issue to add support for switching to "and" with another method call
|
138
|
+
class ArrayWithSize < AnyOf
|
139
|
+
# `Fixnum` & `Bignum` will be returned instead of `Integer`
|
140
|
+
# in `#class` for numbers
|
141
|
+
EXPECTED_VALUE_CLASS_TO_EXPECTATION_CLASS_MAPPING = {
|
142
|
+
Fixnum => -> (v) { Expectations::Private::Eq[v] },
|
143
|
+
Bignum => -> (v) { Expectations::Private::Eq[v] },
|
144
|
+
Range => -> (v) { Expectations::Private::InRange[v] },
|
145
|
+
}.freeze
|
146
|
+
private_constant :EXPECTED_VALUE_CLASS_TO_EXPECTATION_CLASS_MAPPING
|
147
|
+
|
148
|
+
class << self
|
149
|
+
# Overrides {Expectation.build}
|
150
|
+
def build(value)
|
151
|
+
expectation_classes_mappings.fetch(value.class) do
|
152
|
+
-> (_) { raise ArgumentError, <<-ERR }
|
153
|
+
Expected expection(s) to be kind of
|
154
|
+
#{expectation_classes_mappings.keys.inspect}
|
155
|
+
but found #{value.inspect}
|
156
|
+
ERR
|
157
|
+
end.call(value)
|
158
|
+
end
|
159
|
+
|
160
|
+
private
|
161
|
+
|
162
|
+
# @return [Hash]
|
163
|
+
def expectation_classes_mappings
|
164
|
+
EXPECTED_VALUE_CLASS_TO_EXPECTATION_CLASS_MAPPING
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def expect?(value)
|
169
|
+
value.is_a?(Array) &&
|
170
|
+
super(value.size)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
require "abstract_class"
|
2
|
+
|
3
|
+
require_relative "core"
|
4
|
+
require_relative "mixins/built_in"
|
5
|
+
|
6
|
+
module RSpec
|
7
|
+
module JsonMatchers
|
8
|
+
module Expectations
|
9
|
+
# @api private
|
10
|
+
# All classes within module should NOT be able to be used directly / extended
|
11
|
+
#
|
12
|
+
# All classes in this module are internal expectations used when non-expectation object/class is passed in
|
13
|
+
# Extension gems should have their own namespace and should NOT add new classes to this namespace
|
14
|
+
# Classes here have dependency on {Core} & {Mixins::BuiltIn}
|
15
|
+
#
|
16
|
+
# TODO: Remove dependency on {Mixins::BuiltIn}
|
17
|
+
module Private
|
18
|
+
# @api private
|
19
|
+
# User should just pass an object in
|
20
|
+
#
|
21
|
+
# Takes exactly one object
|
22
|
+
# Use stored value & `==` for checking `value`
|
23
|
+
class Eq < Core::SingleValueCallableExpectation
|
24
|
+
private
|
25
|
+
attr_reader :expected_value
|
26
|
+
public
|
27
|
+
|
28
|
+
def expect?(value)
|
29
|
+
value == expected_value
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def initialize(value)
|
35
|
+
@expected_value = value
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# @api private
|
40
|
+
# User should just pass a class in
|
41
|
+
#
|
42
|
+
# Takes exactly one object
|
43
|
+
# Use stored class for checking `value`
|
44
|
+
#
|
45
|
+
# @note
|
46
|
+
# Might use a whitelist of acceptable classes
|
47
|
+
# and raise error if other things passed in
|
48
|
+
# in the future
|
49
|
+
class KindOf < Core::SingleValueCallableExpectation
|
50
|
+
EXPECTED_CLASS = Class
|
51
|
+
private_constant :EXPECTED_CLASS
|
52
|
+
|
53
|
+
private
|
54
|
+
attr_reader :expected_class
|
55
|
+
public
|
56
|
+
|
57
|
+
def expect?(value)
|
58
|
+
value.is_a?(expected_class)
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def initialize(value)
|
64
|
+
raise ArgumentError, "a #{EXPECTED_CLASS} is required" unless value.is_a?(EXPECTED_CLASS)
|
65
|
+
@expected_class = value
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# @api private
|
70
|
+
# User should just pass a {Range} in
|
71
|
+
#
|
72
|
+
# Takes exactly one object
|
73
|
+
# Use stored proc for checking `value`
|
74
|
+
class InRange < Core::SingleValueCallableExpectation
|
75
|
+
EXPECTED_CLASS = Range
|
76
|
+
private_constant :EXPECTED_CLASS
|
77
|
+
|
78
|
+
private
|
79
|
+
attr_reader :range
|
80
|
+
public
|
81
|
+
|
82
|
+
def expect?(value)
|
83
|
+
range.cover?(value)
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def initialize(value)
|
89
|
+
raise ArgumentError, "a #{EXPECTED_CLASS} is required" unless value.is_a?(EXPECTED_CLASS)
|
90
|
+
@range = value
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# @api private
|
95
|
+
# User should just pass a {Regexp} in
|
96
|
+
#
|
97
|
+
# Takes exactly one object
|
98
|
+
# Use stored regexp for checking `value`
|
99
|
+
class MatchingRegexp < Core::SingleValueCallableExpectation
|
100
|
+
EXPECTED_CLASS = Regexp
|
101
|
+
private_constant :EXPECTED_CLASS
|
102
|
+
|
103
|
+
private
|
104
|
+
attr_reader :regexp
|
105
|
+
public
|
106
|
+
|
107
|
+
def expect?(value)
|
108
|
+
# regex =~ string seems to be fastest
|
109
|
+
# @see https://stackoverflow.com/questions/11887145/fastest-way-to-check-if-a-string-matches-or-not-a-regexp-in-ruby
|
110
|
+
value.is_a?(String) && !!(regexp =~ value)
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def initialize(value)
|
116
|
+
raise ArgumentError, "a #{EXPECTED_CLASS} is required" unless value.is_a?(EXPECTED_CLASS)
|
117
|
+
@regexp = value
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# @api private
|
122
|
+
# User should just pass a callable in
|
123
|
+
#
|
124
|
+
# Takes exactly one object
|
125
|
+
# Use stored proc for checking `value`
|
126
|
+
class SatisfyingCallable < Core::SingleValueCallableExpectation
|
127
|
+
private
|
128
|
+
attr_reader :callable
|
129
|
+
public
|
130
|
+
|
131
|
+
def expect?(value)
|
132
|
+
callable.call(value)
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
def initialize(value)
|
138
|
+
raise ArgumentError, "an object which respond to `:call` is required" unless value.respond_to?(:call)
|
139
|
+
@callable = value
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# @api private
|
144
|
+
# Used internally for returning false
|
145
|
+
#
|
146
|
+
# Always "fail"
|
147
|
+
class Nothing < Expectations::Core::SingletonExpectation
|
148
|
+
def expect?(*_args)
|
149
|
+
false
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# @api private
|
154
|
+
# Used internally by a matcher method
|
155
|
+
#
|
156
|
+
# Comparing to {Expectations::Mixins::BuiltIn::ArrayWithSize}
|
157
|
+
# This also accepts `Hash` and `Array`, and return false for collection matching
|
158
|
+
class ArrayWithSize < Expectations::Mixins::BuiltIn::ArrayWithSize
|
159
|
+
# `Fixnum` & `Bignum` will be returned instead of `Integer`
|
160
|
+
# in `#class` for numbers
|
161
|
+
ADDITIONAL_EXPECTED_VALUE_CLASS_TO_EXPECTATION_CLASS_MAPPING = {
|
162
|
+
Array => -> (_) { Expectations::Private::Nothing::INSTANCE },
|
163
|
+
Hash => -> (_) { Expectations::Private::Nothing::INSTANCE },
|
164
|
+
}.freeze
|
165
|
+
private_constant :ADDITIONAL_EXPECTED_VALUE_CLASS_TO_EXPECTATION_CLASS_MAPPING
|
166
|
+
|
167
|
+
class << self
|
168
|
+
private
|
169
|
+
|
170
|
+
# Overrides {Expectations::Mixins::BuiltIn::ArrayWithSize.expectation_classes_mappings}
|
171
|
+
#
|
172
|
+
# @return [Hash]
|
173
|
+
def expectation_classes_mappings
|
174
|
+
super.merge(ADDITIONAL_EXPECTED_VALUE_CLASS_TO_EXPECTATION_CLASS_MAPPING)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require_relative "expectations/core"
|
2
|
+
require_relative "expectations/private"
|
3
|
+
require_relative "expectations/mixins/built_in"
|
4
|
+
|
5
|
+
module RSpec
|
6
|
+
module JsonMatchers
|
7
|
+
# This module does not mean to have any expectation class
|
8
|
+
# Use the structure like {Expectations::BuiltIn}
|
9
|
+
#
|
10
|
+
# @api private
|
11
|
+
module Expectations
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require "json"
|
2
|
+
|
3
|
+
module RSpec
|
4
|
+
module JsonMatchers
|
5
|
+
module Matchers
|
6
|
+
# @api
|
7
|
+
#
|
8
|
+
# Used for verifying actual is a valid JSON string
|
9
|
+
#
|
10
|
+
# @return [BeJsonMatcher]
|
11
|
+
def be_json
|
12
|
+
BeJsonMatcher.new
|
13
|
+
end
|
14
|
+
|
15
|
+
# @api private
|
16
|
+
class BeJsonMatcher
|
17
|
+
attr_reader :actual
|
18
|
+
|
19
|
+
def matches?(json)
|
20
|
+
@actual = JSON.parse(json)
|
21
|
+
true
|
22
|
+
rescue JSON::ParserError
|
23
|
+
@has_parser_error = true
|
24
|
+
false
|
25
|
+
end
|
26
|
+
|
27
|
+
def does_not_match?(*args)
|
28
|
+
!matches?(*args)
|
29
|
+
end
|
30
|
+
|
31
|
+
# @api
|
32
|
+
#
|
33
|
+
# Get a matcher that try to match the content of actual
|
34
|
+
# with nested various expectations
|
35
|
+
#
|
36
|
+
# @param expected [Hash, Array, Object]
|
37
|
+
# the expectation object
|
38
|
+
#
|
39
|
+
# @return [BeJsonWithContentMatcher] a matcher object
|
40
|
+
def with_content(expected)
|
41
|
+
BeJsonWithContentMatcher.new(expected)
|
42
|
+
end
|
43
|
+
|
44
|
+
# @api
|
45
|
+
#
|
46
|
+
# Get a matcher that try to match the content of actual
|
47
|
+
# with nested expectations about array sizes
|
48
|
+
#
|
49
|
+
# @param expected [Hash, Array, Object]
|
50
|
+
# the expectation object
|
51
|
+
#
|
52
|
+
# @return [BeJsonWithSizesMatcher] a matcher object
|
53
|
+
def with_sizes(expected)
|
54
|
+
BeJsonWithSizesMatcher.new(expected)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Expectation description in spec result summary
|
58
|
+
#
|
59
|
+
# @return [String]
|
60
|
+
def description
|
61
|
+
"be a valid JSON string"
|
62
|
+
end
|
63
|
+
|
64
|
+
# Failure message displayed when a positive example failed (e.g. using `should`)
|
65
|
+
#
|
66
|
+
# @return [String]
|
67
|
+
def failure_message_for_positive
|
68
|
+
"expected value to be parsed as JSON, but failed"
|
69
|
+
end
|
70
|
+
alias :failure_message :failure_message_for_positive
|
71
|
+
|
72
|
+
# Failure message displayed when a negative example failed (e.g. using `should_not`)
|
73
|
+
#
|
74
|
+
# @return [String]
|
75
|
+
def failure_message_for_negative
|
76
|
+
"expected value not to be parsed as JSON, but succeeded"
|
77
|
+
end
|
78
|
+
alias :failure_message_when_negated :failure_message_for_negative
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def has_parser_error?
|
83
|
+
!!@has_parser_error
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# These files are required since the classes are only required on runtime, not load time
|
91
|
+
require_relative "be_json_with_content_matcher"
|
92
|
+
require_relative "be_json_with_sizes_matcher"
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require "json"
|
2
|
+
require "awesome_print"
|
3
|
+
|
4
|
+
require_relative "be_json_with_something_matcher"
|
5
|
+
require_relative "../comparers"
|
6
|
+
require_relative "../utils"
|
7
|
+
|
8
|
+
module RSpec
|
9
|
+
module JsonMatchers
|
10
|
+
module Matchers
|
11
|
+
# @api private
|
12
|
+
class BeJsonWithContentMatcher < BeJsonWithSomethingMatcher
|
13
|
+
private
|
14
|
+
|
15
|
+
def value_matching_proc
|
16
|
+
-> (expected, actual) { Expectation.build(expected).expect?(actual) }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require "json"
|
2
|
+
require "awesome_print"
|
3
|
+
|
4
|
+
require_relative "be_json_with_something_matcher"
|
5
|
+
require_relative "../comparers"
|
6
|
+
require_relative "../utils"
|
7
|
+
|
8
|
+
module RSpec
|
9
|
+
module JsonMatchers
|
10
|
+
module Matchers
|
11
|
+
# @api private
|
12
|
+
class BeJsonWithSizesMatcher < BeJsonWithSomethingMatcher
|
13
|
+
private
|
14
|
+
|
15
|
+
def value_matching_proc
|
16
|
+
-> (expected, actual) { Expectations::Private::ArrayWithSize[expected].expect?(actual) }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
require "json"
|
2
|
+
require "awesome_print"
|
3
|
+
require "abstract_class"
|
4
|
+
|
5
|
+
require_relative "be_json_matcher"
|
6
|
+
require_relative "../utils"
|
7
|
+
|
8
|
+
module RSpec
|
9
|
+
module JsonMatchers
|
10
|
+
module Matchers
|
11
|
+
# @api private
|
12
|
+
# @abstract
|
13
|
+
#
|
14
|
+
# Parent of matcher classes that requires {#at_path} & {#with_exact_keys}
|
15
|
+
# This is not merged with {BeJsonMatcher} since it should be able to be used alone
|
16
|
+
class BeJsonWithSomethingMatcher < BeJsonMatcher
|
17
|
+
extend AbstractClass
|
18
|
+
|
19
|
+
attr_reader *[
|
20
|
+
:expected,
|
21
|
+
:path,
|
22
|
+
:with_exact_keys,
|
23
|
+
]
|
24
|
+
alias_method :with_exact_keys?, :with_exact_keys
|
25
|
+
|
26
|
+
def initialize(expected)
|
27
|
+
@expected = expected
|
28
|
+
@path = JsonMatchers::Utils::KeyPath::Path.new("")
|
29
|
+
@exact_match = false
|
30
|
+
end
|
31
|
+
|
32
|
+
def matches?(*_args)
|
33
|
+
super && has_valid_path? && expected_and_actual_matched?
|
34
|
+
end
|
35
|
+
|
36
|
+
def does_not_match?(*args)
|
37
|
+
!matches?(*args) && has_valid_path?
|
38
|
+
end
|
39
|
+
|
40
|
+
def description
|
41
|
+
super
|
42
|
+
end
|
43
|
+
|
44
|
+
# Override {BeJsonMatcher#actual}
|
45
|
+
# It return actual object extracted by {#path}
|
46
|
+
# And also detect & set state for path error (either it's invalid or fails to extract)
|
47
|
+
#
|
48
|
+
# @return [Object] extracted object but could be object in the middle when extraction failed
|
49
|
+
def actual
|
50
|
+
result = path.extract(super)
|
51
|
+
has_path_error! if result.failed?
|
52
|
+
result.object
|
53
|
+
end
|
54
|
+
|
55
|
+
# Sets the path to be used for object, to avoid passing a deep nested {Hash} or {Array} as expectation
|
56
|
+
# Defaults to "" (if this is not called)
|
57
|
+
# The path uses period (".") as separator for parts
|
58
|
+
# (also period cannot be used as path name as a side-effect, but who does?)
|
59
|
+
# This does NOT raise error if the path is invalid
|
60
|
+
# (like having 2 periods, 1 period at the start/end of string)
|
61
|
+
# But it will fail the example with both `should` & `should_not`
|
62
|
+
#
|
63
|
+
# @param path [String] the "path" to be used
|
64
|
+
#
|
65
|
+
# @return [BeJsonWithSomethingMatcher] the match itself
|
66
|
+
#
|
67
|
+
# @throw [TypeError] when input is not a string
|
68
|
+
def at_path(path)
|
69
|
+
@path = JsonMatchers::Utils::KeyPath::Path.new(path)
|
70
|
+
self
|
71
|
+
end
|
72
|
+
|
73
|
+
# When `exactly` is `true`,
|
74
|
+
# makes the matcher to fail the example
|
75
|
+
# when actual has more elements than expected even expectation passes
|
76
|
+
#
|
77
|
+
# When `exactly` is `true`,
|
78
|
+
# makes the matcher to pass the example
|
79
|
+
# when actual has more elements than expected and expectation passes
|
80
|
+
#
|
81
|
+
# @param exactly [Boolean] whether the matcher should match keys in actual & expected exactly
|
82
|
+
#
|
83
|
+
# @return (see #at_path)
|
84
|
+
def with_exact_keys(exactly = true)
|
85
|
+
@with_exact_keys = !!exactly
|
86
|
+
self
|
87
|
+
end
|
88
|
+
|
89
|
+
def failure_message_for_positive
|
90
|
+
return super if has_parser_error?
|
91
|
+
return invalid_path_message unless has_valid_path?
|
92
|
+
return path_error_message if has_path_error?
|
93
|
+
|
94
|
+
inspection_messages(true)
|
95
|
+
end
|
96
|
+
alias :failure_message :failure_message_for_positive
|
97
|
+
|
98
|
+
def failure_message_for_negative
|
99
|
+
return super if has_parser_error?
|
100
|
+
return invalid_path_message unless has_valid_path?
|
101
|
+
return path_error_message if has_path_error?
|
102
|
+
|
103
|
+
inspection_messages(false)
|
104
|
+
end
|
105
|
+
alias :failure_message_when_negated :failure_message_for_negative
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
# @return [Bool] Whether `expected` & `parsed` are "equal"
|
110
|
+
def expected_and_actual_matched?
|
111
|
+
extracted_actual = actual
|
112
|
+
return false if has_path_error?
|
113
|
+
result = comparer_klass.new(extracted_actual, expected, reasons, value_matching_proc).compare
|
114
|
+
|
115
|
+
result.matched?.tap do |matched|
|
116
|
+
@reasons = result.reasons unless matched
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def reasons
|
121
|
+
@reasons ||= []
|
122
|
+
end
|
123
|
+
|
124
|
+
def inspection_messages(should_match)
|
125
|
+
prefix = !!should_match ? nil : "not"
|
126
|
+
|
127
|
+
messages = [
|
128
|
+
["expected", prefix, "to match:"].compact.map(&:strip).join(" "),
|
129
|
+
expected.awesome_inspect(indent: -2),
|
130
|
+
"",
|
131
|
+
"actual:",
|
132
|
+
actual.awesome_inspect(indent: -2),
|
133
|
+
"",
|
134
|
+
]
|
135
|
+
messages.push "reason/path: #{reasons.reverse.join(".")}" unless reasons.empty?
|
136
|
+
messages.join("\n")
|
137
|
+
end
|
138
|
+
|
139
|
+
def original_actual
|
140
|
+
@actual
|
141
|
+
end
|
142
|
+
|
143
|
+
def has_path_error?
|
144
|
+
!!@has_path_error
|
145
|
+
end
|
146
|
+
|
147
|
+
def has_path_error!
|
148
|
+
@has_path_error = true
|
149
|
+
end
|
150
|
+
|
151
|
+
# For both positive and negative
|
152
|
+
def path_error_message
|
153
|
+
%Q|path "#{path}" does not exists in actual: |
|
154
|
+
[
|
155
|
+
original_actual.awesome_inspect(indent: -2),
|
156
|
+
].join("\n")
|
157
|
+
end
|
158
|
+
|
159
|
+
def has_valid_path?
|
160
|
+
(path.nil? || path.valid?)
|
161
|
+
end
|
162
|
+
|
163
|
+
# For both positive and negative
|
164
|
+
def invalid_path_message
|
165
|
+
%Q|path "#{path}" is invalid|
|
166
|
+
end
|
167
|
+
|
168
|
+
def comparer_klass
|
169
|
+
with_exact_keys? ? Comparers::ExactKeysComparer : Comparers::IncludeKeysComparer
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require_relative "matchers/be_json_matcher"
|
2
|
+
require_relative "matchers/be_json_with_content_matcher"
|
3
|
+
require_relative "matchers/be_json_with_sizes_matcher"
|
4
|
+
|
5
|
+
module RSpec
|
6
|
+
module JsonMatchers
|
7
|
+
# Mixin Module to be included into RSpec
|
8
|
+
# Other files will define the same module and add methods to this module
|
9
|
+
module Matchers
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|