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.
- 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
|