rspec-json_matchers 0.1.0.alpha.1 → 0.1.0.alpha.2
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/.gitignore +0 -1
- data/.rubocop.yml +31 -0
- data/Gemfile +1 -1
- data/README.md +36 -0
- data/Rakefile +1 -1
- data/doc/Story.md +129 -0
- data/lib/rspec-json_matchers.rb +1 -1
- data/lib/rspec/json_matchers/comparers/abstract_comparer.rb +111 -77
- data/lib/rspec/json_matchers/comparers/comparison_result.rb +1 -1
- data/lib/rspec/json_matchers/comparers/exact_keys_comparer.rb +6 -6
- data/lib/rspec/json_matchers/comparers/include_keys_comparer.rb +5 -6
- data/lib/rspec/json_matchers/expectation.rb +81 -31
- data/lib/rspec/json_matchers/expectations/abstract.rb +4 -3
- data/lib/rspec/json_matchers/expectations/core.rb +12 -8
- data/lib/rspec/json_matchers/expectations/mixins/built_in.rb +25 -13
- data/lib/rspec/json_matchers/expectations/private.rb +40 -33
- data/lib/rspec/json_matchers/matchers.rb +3 -3
- data/lib/rspec/json_matchers/matchers/be_json_matcher.rb +16 -6
- data/lib/rspec/json_matchers/matchers/be_json_with_sizes_matcher.rb +4 -1
- data/lib/rspec/json_matchers/matchers/be_json_with_something_matcher.rb +51 -37
- data/lib/rspec/json_matchers/utils/collection_keys_extractor.rb +4 -3
- data/lib/rspec/json_matchers/utils/key_path/extraction.rb +149 -0
- data/lib/rspec/json_matchers/utils/key_path/path.rb +18 -12
- data/lib/rspec/json_matchers/version.rb +3 -1
- data/rspec-json_matchers.gemspec +7 -5
- metadata +7 -5
- data/lib/rspec/json_matchers/utils/key_path/extraction_result.rb +0 -22
- data/lib/rspec/json_matchers/utils/key_path/extractor.rb +0 -70
@@ -1,6 +1,6 @@
|
|
1
|
-
require_relative
|
2
|
-
require_relative
|
3
|
-
require_relative
|
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
4
|
|
5
5
|
module RSpec
|
6
6
|
module JsonMatchers
|
@@ -2,6 +2,8 @@ require "json"
|
|
2
2
|
|
3
3
|
module RSpec
|
4
4
|
module JsonMatchers
|
5
|
+
# Mixin Module to be included into RSpec
|
6
|
+
# Other files will define the same module and add methods to this module
|
5
7
|
module Matchers
|
6
8
|
# @api
|
7
9
|
#
|
@@ -13,6 +15,8 @@ module RSpec
|
|
13
15
|
end
|
14
16
|
|
15
17
|
# @api private
|
18
|
+
#
|
19
|
+
# The implementation for {Matchers#be_json}
|
16
20
|
class BeJsonMatcher
|
17
21
|
attr_reader :actual
|
18
22
|
|
@@ -61,32 +65,38 @@ module RSpec
|
|
61
65
|
"be a valid JSON string"
|
62
66
|
end
|
63
67
|
|
64
|
-
# Failure message displayed when a positive example failed
|
68
|
+
# Failure message displayed when a positive example failed
|
69
|
+
# (e.g. using `should`)
|
65
70
|
#
|
66
71
|
# @return [String]
|
67
72
|
def failure_message_for_positive
|
68
73
|
"expected value to be parsed as JSON, but failed"
|
69
74
|
end
|
70
|
-
|
75
|
+
alias_method :failure_message,
|
76
|
+
:failure_message_for_positive
|
71
77
|
|
72
|
-
# Failure message displayed when a negative example failed
|
78
|
+
# Failure message displayed when a negative example failed
|
79
|
+
# (e.g. using `should_not`)
|
73
80
|
#
|
74
81
|
# @return [String]
|
75
82
|
def failure_message_for_negative
|
76
83
|
"expected value not to be parsed as JSON, but succeeded"
|
77
84
|
end
|
78
|
-
|
85
|
+
alias_method :failure_message_when_negated,
|
86
|
+
:failure_message_for_negative
|
79
87
|
|
80
88
|
private
|
81
89
|
|
82
90
|
def has_parser_error?
|
83
|
-
|
91
|
+
@has_parser_error
|
84
92
|
end
|
85
93
|
end
|
86
94
|
end
|
87
95
|
end
|
88
96
|
end
|
89
97
|
|
90
|
-
# These files are required
|
98
|
+
# These files are required here
|
99
|
+
# since the classes are only required
|
100
|
+
# on runtime but not load time
|
91
101
|
require_relative "be_json_with_content_matcher"
|
92
102
|
require_relative "be_json_with_sizes_matcher"
|
@@ -13,7 +13,10 @@ module RSpec
|
|
13
13
|
private
|
14
14
|
|
15
15
|
def value_matching_proc
|
16
|
-
|
16
|
+
lambda do |expected, actual|
|
17
|
+
Expectations::Private::ArrayWithSize[expected].
|
18
|
+
expect?(actual)
|
19
|
+
end
|
17
20
|
end
|
18
21
|
end
|
19
22
|
end
|
@@ -12,15 +12,12 @@ module RSpec
|
|
12
12
|
# @abstract
|
13
13
|
#
|
14
14
|
# Parent of matcher classes that requires {#at_path} & {#with_exact_keys}
|
15
|
-
# This is not merged with {BeJsonMatcher}
|
15
|
+
# This is not merged with {BeJsonMatcher}
|
16
|
+
# since it should be able to be used alone
|
16
17
|
class BeJsonWithSomethingMatcher < BeJsonMatcher
|
17
18
|
extend AbstractClass
|
18
19
|
|
19
|
-
attr_reader
|
20
|
-
:expected,
|
21
|
-
:path,
|
22
|
-
:with_exact_keys,
|
23
|
-
]
|
20
|
+
attr_reader(*[:expected, :path, :with_exact_keys])
|
24
21
|
alias_method :with_exact_keys?, :with_exact_keys
|
25
22
|
|
26
23
|
def initialize(expected)
|
@@ -37,25 +34,28 @@ module RSpec
|
|
37
34
|
!matches?(*args) && has_valid_path?
|
38
35
|
end
|
39
36
|
|
40
|
-
def description
|
41
|
-
super
|
42
|
-
end
|
43
|
-
|
44
37
|
# Override {BeJsonMatcher#actual}
|
45
38
|
# It return actual object extracted by {#path}
|
46
|
-
# And also detect & set state for path error
|
39
|
+
# And also detect & set state for path error
|
40
|
+
# (either it's invalid or fails to extract)
|
47
41
|
#
|
48
|
-
# @return [Object]
|
42
|
+
# @return [Object]
|
43
|
+
# extracted object but could be object in the middle
|
44
|
+
# when extraction failed
|
49
45
|
def actual
|
50
46
|
result = path.extract(super)
|
51
47
|
has_path_error! if result.failed?
|
52
48
|
result.object
|
53
49
|
end
|
54
50
|
|
55
|
-
# Sets the path to be used for object,
|
51
|
+
# Sets the path to be used for object,
|
52
|
+
# to avoid passing a deep nested
|
53
|
+
# {Hash} or {Array} as expectation
|
56
54
|
# Defaults to "" (if this is not called)
|
55
|
+
#
|
57
56
|
# The path uses period (".") as separator for parts
|
58
|
-
#
|
57
|
+
# Also period cannot be used as path name as a side-effect
|
58
|
+
#
|
59
59
|
# This does NOT raise error if the path is invalid
|
60
60
|
# (like having 2 periods, 1 period at the start/end of string)
|
61
61
|
# But it will fail the example with both `should` & `should_not`
|
@@ -78,39 +78,43 @@ module RSpec
|
|
78
78
|
# makes the matcher to pass the example
|
79
79
|
# when actual has more elements than expected and expectation passes
|
80
80
|
#
|
81
|
-
# @param exactly [Boolean]
|
81
|
+
# @param exactly [Boolean]
|
82
|
+
# whether the matcher should match keys in actual & expected exactly
|
82
83
|
#
|
83
84
|
# @return (see #at_path)
|
84
85
|
def with_exact_keys(exactly = true)
|
85
|
-
@with_exact_keys =
|
86
|
+
@with_exact_keys = exactly
|
86
87
|
self
|
87
88
|
end
|
88
89
|
|
90
|
+
# Overrides {BeJsonMatcher#failure_message_for_positive}
|
89
91
|
def failure_message_for_positive
|
90
92
|
return super if has_parser_error?
|
91
|
-
|
92
|
-
return path_error_message if has_path_error?
|
93
|
-
|
94
|
-
inspection_messages(true)
|
93
|
+
failure_message_for(true)
|
95
94
|
end
|
96
|
-
alias :failure_message :failure_message_for_positive
|
97
95
|
|
96
|
+
# Overrides {BeJsonMatcher#failure_message_for_negative}
|
98
97
|
def failure_message_for_negative
|
99
98
|
return super if has_parser_error?
|
99
|
+
failure_message_for(false)
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
def failure_message_for(should_match)
|
100
105
|
return invalid_path_message unless has_valid_path?
|
101
106
|
return path_error_message if has_path_error?
|
102
107
|
|
103
|
-
inspection_messages(
|
108
|
+
inspection_messages(should_match)
|
104
109
|
end
|
105
|
-
alias :failure_message_when_negated :failure_message_for_negative
|
106
|
-
|
107
|
-
private
|
108
110
|
|
109
111
|
# @return [Bool] Whether `expected` & `parsed` are "equal"
|
110
112
|
def expected_and_actual_matched?
|
111
113
|
extracted_actual = actual
|
112
114
|
return false if has_path_error?
|
113
|
-
result = comparer_klass.
|
115
|
+
result = comparer_klass.
|
116
|
+
new(extracted_actual, expected, reasons, value_matching_proc).
|
117
|
+
compare
|
114
118
|
|
115
119
|
result.matched?.tap do |matched|
|
116
120
|
@reasons = result.reasons unless matched
|
@@ -122,18 +126,24 @@ module RSpec
|
|
122
126
|
end
|
123
127
|
|
124
128
|
def inspection_messages(should_match)
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
["expected", prefix, "to match:"].compact.map(&:strip).join(" "),
|
129
|
+
[
|
130
|
+
["expected", inspection_messages_prefix(should_match), "to match:"].
|
131
|
+
compact.map(&:strip).join(" "),
|
129
132
|
expected.awesome_inspect(indent: -2),
|
130
133
|
"",
|
131
134
|
"actual:",
|
132
135
|
actual.awesome_inspect(indent: -2),
|
133
136
|
"",
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
+
inspection_message_for_reason,
|
138
|
+
].join("\n")
|
139
|
+
end
|
140
|
+
|
141
|
+
def inspection_messages_prefix(should_match)
|
142
|
+
should_match ? nil : "not"
|
143
|
+
end
|
144
|
+
|
145
|
+
def inspection_message_for_reason
|
146
|
+
reasons.any? ? "reason/path: #{reasons.reverse.join('.')}" : nil
|
137
147
|
end
|
138
148
|
|
139
149
|
def original_actual
|
@@ -141,7 +151,7 @@ module RSpec
|
|
141
151
|
end
|
142
152
|
|
143
153
|
def has_path_error?
|
144
|
-
|
154
|
+
@has_path_error
|
145
155
|
end
|
146
156
|
|
147
157
|
def has_path_error!
|
@@ -150,8 +160,8 @@ module RSpec
|
|
150
160
|
|
151
161
|
# For both positive and negative
|
152
162
|
def path_error_message
|
153
|
-
%Q|path "#{path}" does not exists in actual: |
|
154
163
|
[
|
164
|
+
%(path "#{path}" does not exists in actual: ),
|
155
165
|
original_actual.awesome_inspect(indent: -2),
|
156
166
|
].join("\n")
|
157
167
|
end
|
@@ -162,11 +172,15 @@ module RSpec
|
|
162
172
|
|
163
173
|
# For both positive and negative
|
164
174
|
def invalid_path_message
|
165
|
-
%
|
175
|
+
%(path "#{path}" is invalid)
|
166
176
|
end
|
167
177
|
|
168
178
|
def comparer_klass
|
169
|
-
with_exact_keys?
|
179
|
+
if with_exact_keys?
|
180
|
+
Comparers::ExactKeysComparer
|
181
|
+
else
|
182
|
+
Comparers::IncludeKeysComparer
|
183
|
+
end
|
170
184
|
end
|
171
185
|
end
|
172
186
|
end
|
@@ -25,9 +25,10 @@ module RSpec
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def extract
|
28
|
-
COLLECTION_TYPE_TO_VALUE_EXTRACTION_PROC_MAP.
|
29
|
-
|
30
|
-
|
28
|
+
COLLECTION_TYPE_TO_VALUE_EXTRACTION_PROC_MAP.
|
29
|
+
fetch(collection.class) do
|
30
|
+
fail TypeError
|
31
|
+
end.call(collection)
|
31
32
|
end
|
32
33
|
end
|
33
34
|
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
require_relative "path"
|
2
|
+
|
3
|
+
module RSpec
|
4
|
+
module JsonMatchers
|
5
|
+
module Utils
|
6
|
+
# @api private
|
7
|
+
module KeyPath
|
8
|
+
# Represents an extractor that performs the extraction
|
9
|
+
# with a {#path} & {#object}
|
10
|
+
class Extraction
|
11
|
+
# Create a new extractor with the "source object"
|
12
|
+
# and the path to be used for extracting our target object
|
13
|
+
#
|
14
|
+
# @param object [Object]
|
15
|
+
# The source object to extract our target object from
|
16
|
+
# @param path [String, Path]
|
17
|
+
# the path of target object
|
18
|
+
# Will convert into {Path}
|
19
|
+
#
|
20
|
+
# @see JsonMatchers::Matchers::BeJsonWithSomethingMatcher#at_path
|
21
|
+
def initialize(object, path)
|
22
|
+
@object = object
|
23
|
+
@path = KeyPath::Path.new(path)
|
24
|
+
|
25
|
+
@failed = false
|
26
|
+
end
|
27
|
+
|
28
|
+
# Actually perform the extraction and return the result
|
29
|
+
# Since the object could be falsy,
|
30
|
+
# an object of custom class is returned instead of the object only
|
31
|
+
#
|
32
|
+
# Assume the path to be valid
|
33
|
+
#
|
34
|
+
# @return (see Path#extract)
|
35
|
+
def extract
|
36
|
+
path.each_path_part do |path_part|
|
37
|
+
result = extract_object_with_path_part(path_part)
|
38
|
+
return Result.new(object, false) if result.failed?
|
39
|
+
|
40
|
+
self.object = result.object
|
41
|
+
end
|
42
|
+
|
43
|
+
Result.new(object, true)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
attr_accessor(*[
|
49
|
+
:object,
|
50
|
+
])
|
51
|
+
attr_reader(*[
|
52
|
+
:path,
|
53
|
+
:failed,
|
54
|
+
])
|
55
|
+
alias_method :failed?, :failed
|
56
|
+
|
57
|
+
# @param path_part [String]
|
58
|
+
# One part of {#path}
|
59
|
+
def extract_object_with_path_part(path_part)
|
60
|
+
ExtractionWithOnePathPart.new(object, path_part).extract
|
61
|
+
end
|
62
|
+
|
63
|
+
def fail!
|
64
|
+
@failed = true
|
65
|
+
self
|
66
|
+
end
|
67
|
+
|
68
|
+
# @api private
|
69
|
+
#
|
70
|
+
# Internal implementation for an extraction operation with
|
71
|
+
# only one part of the path
|
72
|
+
class ExtractionWithOnePathPart
|
73
|
+
# Create a new extractor with the "source object"
|
74
|
+
# and the path to be used for extracting our target object
|
75
|
+
#
|
76
|
+
# @param object [Object]
|
77
|
+
# The source object to extract our target object from
|
78
|
+
# @param path_part [String]
|
79
|
+
# a part of {KeyPath::Path} of target object
|
80
|
+
def initialize(object, path_part)
|
81
|
+
@object = object
|
82
|
+
@path_part = path_part
|
83
|
+
end
|
84
|
+
|
85
|
+
# Actually perform the extraction and return the result
|
86
|
+
# Since the object could be falsy,
|
87
|
+
# an object of custom class is returned instead of the object only
|
88
|
+
#
|
89
|
+
# @return (see Extraction#extract)
|
90
|
+
def extract
|
91
|
+
case object
|
92
|
+
when Hash
|
93
|
+
extract_object_from_hash
|
94
|
+
when Array
|
95
|
+
extract_object_from_array
|
96
|
+
else
|
97
|
+
# Disallow non JSON collection type
|
98
|
+
Result.new(object, false)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
def extract_object_from_hash
|
105
|
+
# Allow nil as object, but disallow key to be absent
|
106
|
+
return Result.new(object, false) unless object.key?(path_part)
|
107
|
+
|
108
|
+
Result.new(object.fetch(path_part), true)
|
109
|
+
end
|
110
|
+
|
111
|
+
def extract_object_from_array
|
112
|
+
index = path_part.to_i
|
113
|
+
# Disallow index to be out of range
|
114
|
+
# Disallow negative number as index
|
115
|
+
unless (path_part =~ /\A\d+\z/) && index < object.size
|
116
|
+
return Result.new(object, false)
|
117
|
+
end
|
118
|
+
|
119
|
+
Result.new(object.slice(index), true)
|
120
|
+
end
|
121
|
+
|
122
|
+
attr_accessor(*[
|
123
|
+
:object,
|
124
|
+
])
|
125
|
+
attr_reader(*[
|
126
|
+
:path_part,
|
127
|
+
])
|
128
|
+
end
|
129
|
+
|
130
|
+
# @api private
|
131
|
+
#
|
132
|
+
# Only as a value object for internal communication
|
133
|
+
class Result
|
134
|
+
attr_reader :object, :successful
|
135
|
+
|
136
|
+
def initialize(object, successful)
|
137
|
+
@object = object
|
138
|
+
@successful = successful
|
139
|
+
end
|
140
|
+
|
141
|
+
def failed?
|
142
|
+
!successful
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -1,31 +1,37 @@
|
|
1
|
-
require_relative "
|
2
|
-
require_relative "extractor"
|
1
|
+
require_relative "extraction"
|
3
2
|
|
4
3
|
module RSpec
|
5
4
|
module JsonMatchers
|
6
5
|
module Utils
|
7
6
|
# @api private
|
8
7
|
module KeyPath
|
8
|
+
# Represents a path pointing to an element
|
9
|
+
# of a {Hash} or {Array}
|
9
10
|
class Path
|
10
11
|
# The "path part" separator
|
11
12
|
# Period is used since it's the least used char as part of a key name
|
12
13
|
# (it can never by used for index anyway)
|
13
|
-
# As a side effect this char CANNOT be used,
|
14
|
+
# As a side effect this char CANNOT be used,
|
15
|
+
# escaping is not planned to be added
|
14
16
|
PATH_PART_SPLITTER = ".".freeze
|
15
17
|
# The regular expression for checking "invalid" path
|
16
18
|
# The separator should NOT at the start/end of the string,
|
17
19
|
# or repeating itself without other chars in between
|
18
|
-
INVALID_PATH_REGEX =
|
20
|
+
INVALID_PATH_REGEX = /
|
21
|
+
(
|
19
22
|
^#{Regexp.escape(PATH_PART_SPLITTER)}
|
20
23
|
|
|
21
24
|
#{Regexp.escape(PATH_PART_SPLITTER)}{2,}
|
22
25
|
|
|
23
26
|
#{Regexp.escape(PATH_PART_SPLITTER)}$
|
24
|
-
)
|
27
|
+
)
|
28
|
+
/x.freeze
|
25
29
|
|
26
30
|
# Creates a {Path}
|
27
|
-
# with a {String} (mainly from external)
|
28
|
-
#
|
31
|
+
# with a {String} (mainly from external)
|
32
|
+
# (will store it internally)
|
33
|
+
# or a {Path} (mainly from internal)
|
34
|
+
# (will get and assign the string path it internally)
|
29
35
|
#
|
30
36
|
# @note
|
31
37
|
# It does not copy the string object since there is not need
|
@@ -43,7 +49,7 @@ module RSpec
|
|
43
49
|
when String
|
44
50
|
@string_path = path
|
45
51
|
else
|
46
|
-
|
52
|
+
fail TypeError, "Only String and Path is expected"
|
47
53
|
end
|
48
54
|
end
|
49
55
|
|
@@ -69,11 +75,11 @@ module RSpec
|
|
69
75
|
# @param object [Object]
|
70
76
|
# The "source object" to extract our "target object" from
|
71
77
|
#
|
72
|
-
# @return [
|
78
|
+
# @return [Extraction::Result] The result of object extraction
|
73
79
|
def extract(object)
|
74
|
-
return
|
75
|
-
return
|
76
|
-
|
80
|
+
return Extraction::Result.new(object, true) if empty?
|
81
|
+
return Extraction::Result.new(object, false) if invalid?
|
82
|
+
Extraction.new(object, self).extract
|
77
83
|
end
|
78
84
|
|
79
85
|
protected
|