matchers 0.1.0.pre.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/lib/matcher/assertions.rb +19 -0
- data/lib/matcher/autoload.rb +5 -0
- data/lib/matcher/base.rb +183 -0
- data/lib/matcher/compatibility.rb +34 -0
- data/lib/matcher/debug.rb +62 -0
- data/lib/matcher/dsl/builder.rb +99 -0
- data/lib/matcher/dsl/chain.rb +84 -0
- data/lib/matcher/dsl/expression_dsl.rb +306 -0
- data/lib/matcher/dsl/matcher_dsl.rb +5 -0
- data/lib/matcher/dsl/optional.rb +82 -0
- data/lib/matcher/dsl/optional_chain.rb +24 -0
- data/lib/matcher/dsl/others.rb +28 -0
- data/lib/matcher/errors/and_error.rb +88 -0
- data/lib/matcher/errors/boolean_collector.rb +51 -0
- data/lib/matcher/errors/element_error.rb +24 -0
- data/lib/matcher/errors/empty_error.rb +23 -0
- data/lib/matcher/errors/error.rb +39 -0
- data/lib/matcher/errors/error_collector.rb +100 -0
- data/lib/matcher/errors/nested_error.rb +98 -0
- data/lib/matcher/errors/or_error.rb +88 -0
- data/lib/matcher/expression_cache.rb +57 -0
- data/lib/matcher/expression_labeler.rb +96 -0
- data/lib/matcher/expressions/array_expression.rb +45 -0
- data/lib/matcher/expressions/block.rb +189 -0
- data/lib/matcher/expressions/call.rb +307 -0
- data/lib/matcher/expressions/call_error.rb +45 -0
- data/lib/matcher/expressions/constant.rb +53 -0
- data/lib/matcher/expressions/expression.rb +237 -0
- data/lib/matcher/expressions/expression_walker.rb +77 -0
- data/lib/matcher/expressions/hash_expression.rb +59 -0
- data/lib/matcher/expressions/proc_expression.rb +96 -0
- data/lib/matcher/expressions/range_expression.rb +65 -0
- data/lib/matcher/expressions/recorder.rb +136 -0
- data/lib/matcher/expressions/rescue_last_error_expression.rb +49 -0
- data/lib/matcher/expressions/set_expression.rb +45 -0
- data/lib/matcher/expressions/string_expression.rb +53 -0
- data/lib/matcher/expressions/symbol_proc.rb +53 -0
- data/lib/matcher/expressions/variable.rb +87 -0
- data/lib/matcher/hash_stack.rb +52 -0
- data/lib/matcher/list.rb +102 -0
- data/lib/matcher/markers.rb +7 -0
- data/lib/matcher/matcher_cache.rb +18 -0
- data/lib/matcher/matchers/all_matcher.rb +60 -0
- data/lib/matcher/matchers/always_matcher.rb +34 -0
- data/lib/matcher/matchers/any_matcher.rb +70 -0
- data/lib/matcher/matchers/array_matcher.rb +72 -0
- data/lib/matcher/matchers/block_matcher.rb +61 -0
- data/lib/matcher/matchers/boolean_matcher.rb +37 -0
- data/lib/matcher/matchers/dig_matcher.rb +149 -0
- data/lib/matcher/matchers/each_matcher.rb +85 -0
- data/lib/matcher/matchers/each_pair_matcher.rb +119 -0
- data/lib/matcher/matchers/equal_matcher.rb +198 -0
- data/lib/matcher/matchers/equal_set_matcher.rb +112 -0
- data/lib/matcher/matchers/expression_matcher.rb +69 -0
- data/lib/matcher/matchers/filter_matcher.rb +115 -0
- data/lib/matcher/matchers/hash_matcher.rb +315 -0
- data/lib/matcher/matchers/imply_matcher.rb +83 -0
- data/lib/matcher/matchers/imply_some_matcher.rb +116 -0
- data/lib/matcher/matchers/index_by_matcher.rb +177 -0
- data/lib/matcher/matchers/inline_matcher.rb +101 -0
- data/lib/matcher/matchers/keys_matcher.rb +131 -0
- data/lib/matcher/matchers/kind_of_matcher.rb +35 -0
- data/lib/matcher/matchers/lazy_all_matcher.rb +69 -0
- data/lib/matcher/matchers/lazy_any_matcher.rb +69 -0
- data/lib/matcher/matchers/let_matcher.rb +73 -0
- data/lib/matcher/matchers/map_matcher.rb +148 -0
- data/lib/matcher/matchers/negated_array_matcher.rb +38 -0
- data/lib/matcher/matchers/negated_each_matcher.rb +36 -0
- data/lib/matcher/matchers/negated_each_pair_matcher.rb +38 -0
- data/lib/matcher/matchers/negated_imply_some_matcher.rb +46 -0
- data/lib/matcher/matchers/negated_matcher.rb +25 -0
- data/lib/matcher/matchers/negated_project_matcher.rb +31 -0
- data/lib/matcher/matchers/never_matcher.rb +35 -0
- data/lib/matcher/matchers/one_matcher.rb +68 -0
- data/lib/matcher/matchers/optional_matcher.rb +38 -0
- data/lib/matcher/matchers/parse_float_matcher.rb +86 -0
- data/lib/matcher/matchers/parse_integer_matcher.rb +101 -0
- data/lib/matcher/matchers/parse_iso8601_helper.rb +41 -0
- data/lib/matcher/matchers/parse_iso8601_matcher.rb +52 -0
- data/lib/matcher/matchers/parse_json_helper.rb +43 -0
- data/lib/matcher/matchers/parse_json_matcher.rb +59 -0
- data/lib/matcher/matchers/project_matcher.rb +72 -0
- data/lib/matcher/matchers/raises_matcher.rb +131 -0
- data/lib/matcher/matchers/range_matcher.rb +50 -0
- data/lib/matcher/matchers/reference_matcher.rb +213 -0
- data/lib/matcher/matchers/reference_matcher_collection.rb +57 -0
- data/lib/matcher/matchers/regexp_matcher.rb +86 -0
- data/lib/matcher/messages/expected_phrasing.rb +355 -0
- data/lib/matcher/messages/message.rb +104 -0
- data/lib/matcher/messages/message_builder.rb +35 -0
- data/lib/matcher/messages/message_rules.rb +240 -0
- data/lib/matcher/messages/namespaced_message_builder.rb +19 -0
- data/lib/matcher/messages/phrasing.rb +59 -0
- data/lib/matcher/messages/standard_message_builder.rb +105 -0
- data/lib/matcher/patterns/ast_mapping.rb +42 -0
- data/lib/matcher/patterns/capture_hole.rb +33 -0
- data/lib/matcher/patterns/constant_hole.rb +14 -0
- data/lib/matcher/patterns/hole.rb +30 -0
- data/lib/matcher/patterns/method_hole.rb +62 -0
- data/lib/matcher/patterns/pattern.rb +104 -0
- data/lib/matcher/patterns/pattern_building.rb +39 -0
- data/lib/matcher/patterns/pattern_capture.rb +11 -0
- data/lib/matcher/patterns/pattern_match.rb +29 -0
- data/lib/matcher/patterns/variable_hole.rb +14 -0
- data/lib/matcher/reporter.rb +103 -0
- data/lib/matcher/rules/message_factory.rb +26 -0
- data/lib/matcher/rules/message_rule.rb +18 -0
- data/lib/matcher/rules/message_rule_context.rb +26 -0
- data/lib/matcher/rules/rule_builder.rb +29 -0
- data/lib/matcher/rules/rule_set.rb +57 -0
- data/lib/matcher/rules/transform_builder.rb +24 -0
- data/lib/matcher/rules/transform_mapping.rb +5 -0
- data/lib/matcher/rules/transform_rule.rb +21 -0
- data/lib/matcher/state.rb +40 -0
- data/lib/matcher/testing/error_builder.rb +62 -0
- data/lib/matcher/testing/error_checker.rb +514 -0
- data/lib/matcher/testing/error_testing.rb +37 -0
- data/lib/matcher/testing/pattern_testing.rb +11 -0
- data/lib/matcher/testing/pattern_testing_scope.rb +34 -0
- data/lib/matcher/testing.rb +107 -0
- data/lib/matcher/undefined.rb +10 -0
- data/lib/matcher/utils/mapping_utils.rb +61 -0
- data/lib/matcher/utils.rb +72 -0
- data/lib/matcher/version.rb +5 -0
- data/lib/matcher.rb +346 -0
- metadata +174 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class EachPairMatcher < Base
|
|
5
|
+
def initialize(matcher)
|
|
6
|
+
super()
|
|
7
|
+
|
|
8
|
+
@matcher = matcher
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def negate
|
|
12
|
+
NegatedEachPairMatcher.new(@matcher)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def validate(state)
|
|
16
|
+
actual = state.actual
|
|
17
|
+
|
|
18
|
+
unless actual.respond_to?(:each_pair)
|
|
19
|
+
state.errors << state.expected.responding_to(:each_pair)
|
|
20
|
+
return
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
actual.each_pair do |key, value|
|
|
24
|
+
state.errors[key] << yield(
|
|
25
|
+
@matcher,
|
|
26
|
+
[key, value],
|
|
27
|
+
key: key,
|
|
28
|
+
value: value,
|
|
29
|
+
parent: actual
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def to_s
|
|
35
|
+
"each_pair(#{@matcher})"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
module MatcherDsl
|
|
40
|
+
##
|
|
41
|
+
# Matches each hash entry
|
|
42
|
+
# == +matcher+ values
|
|
43
|
+
# - key
|
|
44
|
+
# - value
|
|
45
|
+
# - parent
|
|
46
|
+
# @example
|
|
47
|
+
# # matches { foo: "foo" } but not { foo: "bar" }
|
|
48
|
+
# each_pair(k.to_s == v)
|
|
49
|
+
# # alternatively:
|
|
50
|
+
# each_pair ^ (k.to_s == v)
|
|
51
|
+
# @overload each_pair(matcher)
|
|
52
|
+
# @param matcher [Base]
|
|
53
|
+
# @return [EachPairMatcher]
|
|
54
|
+
# @overload each_pair
|
|
55
|
+
# @return [Chain<EachPairMatcher>]
|
|
56
|
+
# @see #each_key
|
|
57
|
+
# @see #each_value
|
|
58
|
+
def each_pair(matcher = UNDEFINED)
|
|
59
|
+
return Chain.new { each_pair(_1) } if Matcher.undefined?(matcher)
|
|
60
|
+
|
|
61
|
+
EachPairMatcher.new(matcher_of(matcher))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
##
|
|
65
|
+
# Matches each hash key
|
|
66
|
+
# == +matcher+ values
|
|
67
|
+
# - key
|
|
68
|
+
# - value
|
|
69
|
+
# - parent
|
|
70
|
+
# @example
|
|
71
|
+
# # matches { foo: 1, bar: 2 } but not { "foo" => 1, "bar" => 2 }
|
|
72
|
+
# each_key(Symbol)
|
|
73
|
+
# # alternatively:
|
|
74
|
+
# each_key ^ Symbol
|
|
75
|
+
# @overload each_key(matcher)
|
|
76
|
+
# @param matcher [Base]
|
|
77
|
+
# @return [EachPairMatcher]
|
|
78
|
+
# @overload each_key
|
|
79
|
+
# @return [Chain<EachPairMatcher>]
|
|
80
|
+
# @see #each_pair
|
|
81
|
+
def each_key(matcher = UNDEFINED)
|
|
82
|
+
return Chain.new { each_key(_1) } if Matcher.undefined?(matcher)
|
|
83
|
+
|
|
84
|
+
matcher = matcher_of(matcher)
|
|
85
|
+
|
|
86
|
+
EachPairMatcher.new(
|
|
87
|
+
ProjectMatcher.new(Variable.key, matcher),
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
##
|
|
92
|
+
# Matches each hash value
|
|
93
|
+
# == +matcher+ values
|
|
94
|
+
# - key
|
|
95
|
+
# - value
|
|
96
|
+
# - parent
|
|
97
|
+
# @example
|
|
98
|
+
# # matches { a: "foo", b: "bar" } but not { a: 1, b: 2 }
|
|
99
|
+
# each_value(String)
|
|
100
|
+
# # alternatively:
|
|
101
|
+
# each_value ^ String
|
|
102
|
+
# @overload each_value(matcher)
|
|
103
|
+
# @param matcher [Base]
|
|
104
|
+
# @return [EachPairMatcher]
|
|
105
|
+
# @overload each_value
|
|
106
|
+
# @return [Chain<EachPairMatcher>]
|
|
107
|
+
# @see #each_pair
|
|
108
|
+
def each_value(matcher = UNDEFINED)
|
|
109
|
+
return Chain.new { each_value(_1) } if Matcher.undefined?(matcher)
|
|
110
|
+
|
|
111
|
+
assigns = { actual: Variable.value }
|
|
112
|
+
matcher = matcher_of(matcher)
|
|
113
|
+
|
|
114
|
+
EachPairMatcher.new(
|
|
115
|
+
LetMatcher.new(assigns, matcher),
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class EqualMatcher < Base
|
|
5
|
+
CACHEABLE_CLASSES = [
|
|
6
|
+
NilClass,
|
|
7
|
+
FalseClass,
|
|
8
|
+
TrueClass,
|
|
9
|
+
Integer,
|
|
10
|
+
Float,
|
|
11
|
+
Symbol,
|
|
12
|
+
String,
|
|
13
|
+
Regexp,
|
|
14
|
+
Module,
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
def self.cache(value, matcher_cache = MatcherCache.current)
|
|
18
|
+
return new(value) if !matcher_cache ||
|
|
19
|
+
!CACHEABLE_CLASSES.include?(value) ||
|
|
20
|
+
value.is_a?(String) && !value.frozen?
|
|
21
|
+
|
|
22
|
+
(matcher_cache.equal_matchers ||= {})[value] ||= new(value)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def initialize(value, negated: false)
|
|
26
|
+
super()
|
|
27
|
+
|
|
28
|
+
@value = value
|
|
29
|
+
@negated = negated
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def negate
|
|
33
|
+
EqualMatcher.new(@value, negated: !@negated)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def validate(state)
|
|
37
|
+
value = @value.is_a?(Expression) ? @value.evaluate(state.values) : @value
|
|
38
|
+
|
|
39
|
+
if @negated
|
|
40
|
+
errors = state.errors.or!
|
|
41
|
+
|
|
42
|
+
catch(:valid) do
|
|
43
|
+
validate_negated_helper(state, errors, value, state.actual)
|
|
44
|
+
|
|
45
|
+
# prevent clearing errors
|
|
46
|
+
return
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# caught :valid
|
|
50
|
+
errors.clear
|
|
51
|
+
else
|
|
52
|
+
validate_helper(state, state.errors, value, state.actual)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
IMPLICIT_MATCHER_CLASSES =
|
|
57
|
+
[Module, Range, Regexp, Hash, Array, Expression].freeze
|
|
58
|
+
|
|
59
|
+
def to_s
|
|
60
|
+
if IMPLICIT_MATCHER_CLASSES.any? { @value.is_a?(_1) }
|
|
61
|
+
"#{'~' if @negated}equal(#{@value.inspect})"
|
|
62
|
+
else
|
|
63
|
+
@negated ? "neg(#{@value.inspect})" : @value.inspect
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def validate_helper(state, errors, exp, act)
|
|
70
|
+
case exp
|
|
71
|
+
when Array
|
|
72
|
+
validate_array(state, errors, exp, act)
|
|
73
|
+
when Hash
|
|
74
|
+
validate_hash(state, errors, exp, act)
|
|
75
|
+
when Set
|
|
76
|
+
validate_set(state, errors, exp, act)
|
|
77
|
+
else
|
|
78
|
+
errors << state.expected(act).not_if(@negated).equal(exp) if
|
|
79
|
+
@negated ^ (act != exp)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def validate_array(state, errors, exp, act)
|
|
84
|
+
unless act.is_a?(Array)
|
|
85
|
+
errors << state.expected(act).kind_of(Array)
|
|
86
|
+
return
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
errors << state.expected(act).length_of(exp.length, act.length) if
|
|
90
|
+
exp.length != act.length
|
|
91
|
+
|
|
92
|
+
[exp.length, act.length].min.times do |i|
|
|
93
|
+
validate_helper(state, errors[i], exp[i], act[i])
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def validate_hash(state, errors, exp, act)
|
|
98
|
+
unless act.is_a?(Hash)
|
|
99
|
+
errors << state.expected(act).kind_of(Hash)
|
|
100
|
+
return
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
(act.keys - exp.keys).each do |key|
|
|
104
|
+
errors[key] << state.expected(act).not.having_key(key)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
exp.each do |key, exp_value|
|
|
108
|
+
act_value = act[key]
|
|
109
|
+
|
|
110
|
+
if act_value.nil? && !act.key?(key)
|
|
111
|
+
errors << state.expected(act).having_key(key)
|
|
112
|
+
else
|
|
113
|
+
validate_helper(state, errors[key], exp_value, act_value)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def validate_set(state, errors, exp, act)
|
|
119
|
+
unless act.is_a?(Set)
|
|
120
|
+
errors << state.expected(act).kind_of(Set)
|
|
121
|
+
return
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
(exp - act).each do |item|
|
|
125
|
+
errors << state.expected(act).including(item)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
(act - exp).each do |item|
|
|
129
|
+
errors << state.expected(act).not.including(item)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def validate_negated_helper(state, errors, exp, act)
|
|
134
|
+
case exp
|
|
135
|
+
when Array
|
|
136
|
+
validate_array_negated(state, errors, exp, act)
|
|
137
|
+
when Hash
|
|
138
|
+
validate_hash_negated(state, errors, exp, act)
|
|
139
|
+
when Set
|
|
140
|
+
validate_set_negated(state, errors, exp, act)
|
|
141
|
+
else
|
|
142
|
+
if act == exp
|
|
143
|
+
errors << state.expected(act).not.equal(exp)
|
|
144
|
+
else
|
|
145
|
+
throw(:valid)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def validate_array_negated(state, errors, exp, act)
|
|
151
|
+
throw(:valid) if !act.is_a?(Array) || exp.length != act.length
|
|
152
|
+
|
|
153
|
+
exp.length.times do |i|
|
|
154
|
+
validate_negated_helper(state, errors[i], exp[i], act[i])
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def validate_hash_negated(state, errors, exp, act)
|
|
159
|
+
throw(:valid) unless act.is_a?(Hash)
|
|
160
|
+
|
|
161
|
+
exp_keys_set = Set.new(exp.keys)
|
|
162
|
+
throw(:valid) unless act.keys.all? { exp_keys_set.include?(_1) }
|
|
163
|
+
|
|
164
|
+
exp.each do |key, exp_value|
|
|
165
|
+
act_value = act[key]
|
|
166
|
+
|
|
167
|
+
if act_value.nil? && !act.key?(key)
|
|
168
|
+
throw(:valid)
|
|
169
|
+
else
|
|
170
|
+
validate_negated_helper(state, errors[key], exp_value, act_value)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def validate_set_negated(state, errors, exp, act)
|
|
176
|
+
throw(:valid) if !act.is_a?(Set) || act != exp
|
|
177
|
+
|
|
178
|
+
exp.each do |item|
|
|
179
|
+
errors << state.expected(act).not.including(item)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
module MatcherDsl
|
|
185
|
+
##
|
|
186
|
+
# Matches equal value
|
|
187
|
+
# @example
|
|
188
|
+
# # matches String but not "foo"
|
|
189
|
+
# equal(String)
|
|
190
|
+
# @param value
|
|
191
|
+
# @return [EqualMatcher]
|
|
192
|
+
def equal(value)
|
|
193
|
+
value = expression_or_value(value)
|
|
194
|
+
|
|
195
|
+
EqualMatcher.cache(value, @matcher_cache)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
##
|
|
5
|
+
# Match array elements like a set.
|
|
6
|
+
# @example
|
|
7
|
+
# m = Matcher.build { equal_set(1, 2, 3) }
|
|
8
|
+
#
|
|
9
|
+
# m.match?([1, 2, 3]) # => true
|
|
10
|
+
# m.match?([3, 2, 1]) # => true
|
|
11
|
+
#
|
|
12
|
+
# m.match([1, 1, 3])
|
|
13
|
+
# # > root[1]: did not expect duplicate originally at index 0 but got 1
|
|
14
|
+
# # > root: expected 2 to be included but got [1, 1, 3]
|
|
15
|
+
class EqualSetMatcher < Base
|
|
16
|
+
def initialize(items, negated: false)
|
|
17
|
+
super()
|
|
18
|
+
|
|
19
|
+
@items = items
|
|
20
|
+
@negated = negated
|
|
21
|
+
@includes_expressions = items.any?(Expression)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def negate
|
|
25
|
+
EqualSetMatcher.new(@items, negated: !@negated)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def validate(state)
|
|
29
|
+
return validate_negated(state) if @negated
|
|
30
|
+
|
|
31
|
+
actual = state.actual
|
|
32
|
+
|
|
33
|
+
unless actual.respond_to?(:each)
|
|
34
|
+
state.errors << state.expected.responding_to(:each)
|
|
35
|
+
return
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
expected_set = item_set(state.values)
|
|
39
|
+
missing = expected_set.dup
|
|
40
|
+
|
|
41
|
+
actual.each_with_index do |act, i|
|
|
42
|
+
if expected_set.include?(act)
|
|
43
|
+
unless missing.delete?(act)
|
|
44
|
+
original_index = index_of(actual, act)
|
|
45
|
+
state.errors[i] << state.expected(act).not.duplicate(original_index)
|
|
46
|
+
end
|
|
47
|
+
else
|
|
48
|
+
state.errors[i] << state.expected(act).not.in(state.actual)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
missing.each do |m|
|
|
53
|
+
state.errors << state.expected.including(m)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def to_s
|
|
58
|
+
"#{'~' if @negated}equal_set(#{@items.join(', ')})"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def item_set(values)
|
|
64
|
+
if @includes_expressions
|
|
65
|
+
@items.to_set do |item|
|
|
66
|
+
item.is_a?(Expression) ? item.evaluate(values) : item
|
|
67
|
+
end
|
|
68
|
+
else
|
|
69
|
+
@item_set ||= Set.new(@items)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def validate_negated(state)
|
|
74
|
+
actual = state.actual
|
|
75
|
+
|
|
76
|
+
return unless actual.respond_to?(:each)
|
|
77
|
+
|
|
78
|
+
expected_set = item_set(state.values)
|
|
79
|
+
missing = expected_set.dup
|
|
80
|
+
|
|
81
|
+
actual.each do |act|
|
|
82
|
+
return nil if !expected_set.include?(act) || !missing.delete?(act)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
return unless missing.empty?
|
|
86
|
+
|
|
87
|
+
state.errors << state.expected.namespace(:set).not.equal(@items)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def index_of(collection, item)
|
|
91
|
+
collection = collection.enum_for(:each) unless
|
|
92
|
+
collection.respond_to?(:find_index)
|
|
93
|
+
|
|
94
|
+
collection.find_index(item)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
module MatcherDsl
|
|
99
|
+
##
|
|
100
|
+
# Matches array elements like a set
|
|
101
|
+
# @example
|
|
102
|
+
# # matches [1, 2, 3] and [3, 2, 1] but neither [0, 1, 2] nor [1, 1, 2, 3]
|
|
103
|
+
# equal_set(1, 2, 3)
|
|
104
|
+
# @param items [Array]
|
|
105
|
+
# @return [EqualSetMatcher]
|
|
106
|
+
def equal_set(*items)
|
|
107
|
+
items.map! { expression_or_value(_1) }
|
|
108
|
+
|
|
109
|
+
EqualSetMatcher.new(items)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class ExpressionMatcher < Base
|
|
5
|
+
def self.cache(
|
|
6
|
+
value,
|
|
7
|
+
matcher_cache = MatcherCache.current,
|
|
8
|
+
expression_cache = ExpressionCache.current
|
|
9
|
+
)
|
|
10
|
+
return new(value) unless matcher_cache
|
|
11
|
+
|
|
12
|
+
cache = (matcher_cache.expression_matchers ||= {})
|
|
13
|
+
label = expression_cache.label(value)
|
|
14
|
+
|
|
15
|
+
cache[label] ||= new(value)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.message_rules
|
|
19
|
+
@message_rules ||= RuleSet.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
attr_reader :expression, :negated
|
|
23
|
+
|
|
24
|
+
def initialize(expression, negated: false)
|
|
25
|
+
super()
|
|
26
|
+
|
|
27
|
+
@expression = expression
|
|
28
|
+
@negated = negated
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def negate
|
|
32
|
+
ExpressionMatcher.new(@expression, negated: !@negated)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def validate(state)
|
|
36
|
+
if state.boolean?
|
|
37
|
+
if @negated != !@expression.evaluate(state.values)
|
|
38
|
+
state.errors << "invalid"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
return
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
value_tree = @expression.evaluate_tree(state.values)
|
|
45
|
+
evaluation = value_tree[-1]
|
|
46
|
+
|
|
47
|
+
if @negated != !evaluation
|
|
48
|
+
rule_context = MessageRuleContext.new(self, state)
|
|
49
|
+
state.errors << message_factory.create(rule_context, value_tree)
|
|
50
|
+
end
|
|
51
|
+
rescue CallError => e
|
|
52
|
+
state.errors << e.message_for_errors(state.actual) unless @negated
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def to_s
|
|
56
|
+
if @negated
|
|
57
|
+
"neg(#{@expression})"
|
|
58
|
+
else
|
|
59
|
+
@expression.to_s
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def message_factory
|
|
66
|
+
@message_factory ||= ExpressionMatcher.message_rules.apply(@expression)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class FilterMatcher < Base
|
|
5
|
+
include MappingUtils
|
|
6
|
+
|
|
7
|
+
def initialize(filter, matcher, negated: false)
|
|
8
|
+
super()
|
|
9
|
+
|
|
10
|
+
@filter = filter
|
|
11
|
+
@matcher = negated ? ~matcher : matcher
|
|
12
|
+
@original_matcher = matcher
|
|
13
|
+
@negated = negated
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def negate
|
|
17
|
+
FilterMatcher.new(@filter, @original_matcher, negated: !@negated)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def validate(state)
|
|
21
|
+
actual = state.actual
|
|
22
|
+
|
|
23
|
+
unless actual.respond_to?(:each)
|
|
24
|
+
state.errors << state.expected.responding_to(:each) unless @negated
|
|
25
|
+
return
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
i = 0
|
|
29
|
+
mapping = {}
|
|
30
|
+
items = []
|
|
31
|
+
failed = false
|
|
32
|
+
|
|
33
|
+
actual.each do |act|
|
|
34
|
+
filter_value = @filter.evaluate(
|
|
35
|
+
state.values.merge(actual: act, index: i, original: actual),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if filter_value
|
|
39
|
+
mapping[items.length] = i
|
|
40
|
+
items << act
|
|
41
|
+
end
|
|
42
|
+
rescue CallError => e
|
|
43
|
+
return nil if @negated
|
|
44
|
+
|
|
45
|
+
state.errors[i] << e.message_for_errors(act)
|
|
46
|
+
failed = true
|
|
47
|
+
ensure
|
|
48
|
+
i += 1
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
return if failed
|
|
52
|
+
|
|
53
|
+
errors = yield(@matcher, items, original: actual)
|
|
54
|
+
|
|
55
|
+
unless state.boolean?
|
|
56
|
+
errors = map_errors(errors) do |nested_error|
|
|
57
|
+
key = nested_error.key
|
|
58
|
+
|
|
59
|
+
next unless index_call?(key)
|
|
60
|
+
|
|
61
|
+
original_index = mapping[operand_of(key)]
|
|
62
|
+
|
|
63
|
+
if original_index
|
|
64
|
+
NestedError.new(index_call_to(original_index), nested_error.child)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
state.errors << errors
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def to_s
|
|
73
|
+
"#{'~' if @negated}filter(#{@filter}, #{@original_matcher})"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def mapped_base
|
|
79
|
+
@mapped_base ||= map_base(:filter, @filter)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
module MatcherDsl
|
|
84
|
+
# Matches only filtered elements
|
|
85
|
+
# == +expression+ values
|
|
86
|
+
# - actual
|
|
87
|
+
# - index
|
|
88
|
+
# - original
|
|
89
|
+
# == +matcher+ values
|
|
90
|
+
# - original
|
|
91
|
+
# @example
|
|
92
|
+
# # matches [1, 2, 3, 4, 5]
|
|
93
|
+
# filter(_.odd?, [1, 3, 5])
|
|
94
|
+
# # alternatively:
|
|
95
|
+
# filter(_.odd?) ^ [1, 3, 5]
|
|
96
|
+
# @overload filter(expression, matcher)
|
|
97
|
+
# @param expression [Expression] matches elements for which +expression+
|
|
98
|
+
# is truthy
|
|
99
|
+
# @param matcher
|
|
100
|
+
# @return [FilterMatcher]
|
|
101
|
+
# @overload filter(expression)
|
|
102
|
+
# @param expression [Expression] matches elements for which +expression+
|
|
103
|
+
# is truthy
|
|
104
|
+
# @return [Chain<FilterMatcher>]
|
|
105
|
+
def filter(expression, matcher = UNDEFINED)
|
|
106
|
+
return Chain.new { filter(expression, _1) } if
|
|
107
|
+
Matcher.undefined?(matcher)
|
|
108
|
+
|
|
109
|
+
expression = expression_of(expression)
|
|
110
|
+
matcher = matcher_of(matcher)
|
|
111
|
+
|
|
112
|
+
FilterMatcher.new(expression, matcher)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|