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,306 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
module ExpressionDsl
|
|
5
|
+
attr_reader :assigns
|
|
6
|
+
|
|
7
|
+
def self.init(builder, build_session)
|
|
8
|
+
builder.instance_exec do
|
|
9
|
+
@expression_cache = ExpressionCache.current(build_session)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def expression_of(value)
|
|
14
|
+
Expression.of(value, expression_cache: @expression_cache)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def expression_or_value(value)
|
|
18
|
+
Expression.expression_or_value(value, expression_cache: @expression_cache)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def declare(*symbols, **assigns)
|
|
22
|
+
symbols.concat(assigns.keys - symbols)
|
|
23
|
+
|
|
24
|
+
symbols.each do |symbol|
|
|
25
|
+
define_singleton_method(symbol) do
|
|
26
|
+
vars[symbol]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
unless assigns.empty?
|
|
31
|
+
if @assigns
|
|
32
|
+
@assigns.merge!(assigns)
|
|
33
|
+
else
|
|
34
|
+
@assigns = assigns
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
UNDEFINED
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
##
|
|
42
|
+
# Turns any object into a recorder, or a block into a recorder.
|
|
43
|
+
#
|
|
44
|
+
# == Turn an object into a recorder
|
|
45
|
+
#
|
|
46
|
+
# Note, that the arguments of a call are implicitly converted to
|
|
47
|
+
# expressions. No need to use +expr+ on the right-hand-side of an operator.
|
|
48
|
+
#
|
|
49
|
+
# # BAD
|
|
50
|
+
# expr(Time).now + expr(3600)
|
|
51
|
+
#
|
|
52
|
+
# # GOOD
|
|
53
|
+
# expr(Time).now + 3600
|
|
54
|
+
#
|
|
55
|
+
# This also works for deeply nested Hashes, Arrays and Sets:
|
|
56
|
+
#
|
|
57
|
+
# my_expr = Matcher::Expression.build do
|
|
58
|
+
# expr([{ foo: Set[vars[:a]] }])
|
|
59
|
+
# end
|
|
60
|
+
#
|
|
61
|
+
# my_expr.evaluate(a: 42) # => [{:foo=>#<Set: {42}>}]
|
|
62
|
+
#
|
|
63
|
+
# == Turn a block into a recorder
|
|
64
|
+
#
|
|
65
|
+
# my_expr = Matcher::Expression.build do
|
|
66
|
+
# expr { |actual| actual ? 1 : 2 }
|
|
67
|
+
# end
|
|
68
|
+
#
|
|
69
|
+
# my_expr.evaluate(true) # => 1
|
|
70
|
+
# my_expr.inspect my_expr # => "expr { ... }"
|
|
71
|
+
#
|
|
72
|
+
# The disadvantage of block expressions is that we cannot inspect them
|
|
73
|
+
# easily. So they should be avoided if possible. Alternatively, consider
|
|
74
|
+
# using inline matchers or implement a new matcher class.
|
|
75
|
+
#
|
|
76
|
+
# @example
|
|
77
|
+
# # turn object into recorder
|
|
78
|
+
# expr(Time).now
|
|
79
|
+
# expr(1) + vars[:a]
|
|
80
|
+
# # deeply nested structures are supported
|
|
81
|
+
# expr([{ foo: Set[vars[:a]] }])
|
|
82
|
+
# # turn block into expression (not inspectable)
|
|
83
|
+
# expr { |actual| actual ? 1 : 2 }
|
|
84
|
+
# @overload expr(obj)
|
|
85
|
+
# @param obj the object to wrap
|
|
86
|
+
# @return [Recorder]
|
|
87
|
+
# @overload expr(&block)
|
|
88
|
+
# @return [Recorder]
|
|
89
|
+
def expr(obj = UNDEFINED, &block)
|
|
90
|
+
raise "obj and block given" if !Matcher.undefined?(obj) && block_given?
|
|
91
|
+
|
|
92
|
+
expression = block_given? ? ProcExpression.new(block) : expression_of(obj)
|
|
93
|
+
expression.to_recorder
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
##
|
|
97
|
+
# Builds a range expression where +from+ and +to+ can be expressions
|
|
98
|
+
# @example
|
|
99
|
+
# range(0, vars[:limit])
|
|
100
|
+
# # evaluates to 0..10 when limit: 10
|
|
101
|
+
# @param from [Expression, Object]
|
|
102
|
+
# @param to [Expression, Object]
|
|
103
|
+
# @param exclude_end [Boolean]
|
|
104
|
+
# @return [RangeExpression]
|
|
105
|
+
def range(from, to, exclude_end: false)
|
|
106
|
+
from = expression_of(from)
|
|
107
|
+
to = expression_of(to)
|
|
108
|
+
|
|
109
|
+
RangeExpression.new(from, to, exclude_end:).to_recorder
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def rescue_exception(expression)
|
|
113
|
+
expression = expression_of(expression)
|
|
114
|
+
rescue_last_error = RescueLastErrorExpression.new(expression)
|
|
115
|
+
|
|
116
|
+
expression_of(rescue_last_error).to_recorder
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
##
|
|
120
|
+
# Returns a recorder for +Kernel+, useful for calling Kernel methods
|
|
121
|
+
# @example
|
|
122
|
+
# kernel.Integer(vars[:a]) # => Integer(a)
|
|
123
|
+
# @return [Recorder]
|
|
124
|
+
def kernel
|
|
125
|
+
expr(Kernel)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
##
|
|
129
|
+
# Concatenates expressions into a string expression
|
|
130
|
+
# @example
|
|
131
|
+
# concat('foo=', vars[:foo])
|
|
132
|
+
# # evaluates to "foo=23" when foo: 23
|
|
133
|
+
# @param parts [Array<Expression, Object>]
|
|
134
|
+
# @return [Recorder]
|
|
135
|
+
def concat(*parts)
|
|
136
|
+
parts = parts.map { expression_of(_1) }
|
|
137
|
+
string_expression = StringExpression.new(parts)
|
|
138
|
+
|
|
139
|
+
expression_of(string_expression).to_recorder
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
##
|
|
143
|
+
# Returns a recorder for the actual value being matched
|
|
144
|
+
# @example
|
|
145
|
+
# _ > 10
|
|
146
|
+
# _.even?
|
|
147
|
+
# _.length == 3
|
|
148
|
+
# @return [Recorder]
|
|
149
|
+
def actual
|
|
150
|
+
vars[:actual]
|
|
151
|
+
end
|
|
152
|
+
alias _ actual
|
|
153
|
+
|
|
154
|
+
##
|
|
155
|
+
# Returns a recorder for the current hash key
|
|
156
|
+
# @example
|
|
157
|
+
# each_pair(k.to_s == v)
|
|
158
|
+
# @return [Recorder]
|
|
159
|
+
def key
|
|
160
|
+
vars[:key]
|
|
161
|
+
end
|
|
162
|
+
alias k key
|
|
163
|
+
|
|
164
|
+
##
|
|
165
|
+
# Returns a recorder for the current hash value
|
|
166
|
+
# @example
|
|
167
|
+
# each_pair(k.to_s == v)
|
|
168
|
+
# @return [Recorder]
|
|
169
|
+
def value
|
|
170
|
+
vars[:value]
|
|
171
|
+
end
|
|
172
|
+
alias v value
|
|
173
|
+
|
|
174
|
+
##
|
|
175
|
+
# Returns a recorder for the current element index
|
|
176
|
+
# @example
|
|
177
|
+
# each(_ == i)
|
|
178
|
+
# @return [Recorder]
|
|
179
|
+
def index
|
|
180
|
+
vars[:index]
|
|
181
|
+
end
|
|
182
|
+
alias i index
|
|
183
|
+
|
|
184
|
+
##
|
|
185
|
+
# Returns a recorder for the parent collection
|
|
186
|
+
# @example
|
|
187
|
+
# each(_ < parent.length)
|
|
188
|
+
# @return [Recorder]
|
|
189
|
+
def parent
|
|
190
|
+
vars[:parent]
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
##
|
|
194
|
+
# Returns a recorder for the original value before mapping
|
|
195
|
+
# @return [Recorder]
|
|
196
|
+
# @see MatcherDsl#map
|
|
197
|
+
def original
|
|
198
|
+
vars[:original]
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
##
|
|
202
|
+
# Evaluates block with +&+ and +|+ acting as +&&+ and +||+
|
|
203
|
+
#
|
|
204
|
+
# We cannot capture `&&` and `||` directly when building expressions. But as
|
|
205
|
+
# a workaround we can substitute them with `&` and `|`.
|
|
206
|
+
#
|
|
207
|
+
# lo { (_ % 4 == 0) & (_ % 100 != 0) | (_ % 400 != 0) }
|
|
208
|
+
# # => actual % 4 == 0 && actual % 100 != 0 || actual % 400 != 0
|
|
209
|
+
#
|
|
210
|
+
# Note that +&+ and +|+ have different precedence than +&&+ and +||+:
|
|
211
|
+
#
|
|
212
|
+
# Matcher::Expression.build do
|
|
213
|
+
# lo { vars[:foo] | vars[:bar] < 2 }
|
|
214
|
+
# end
|
|
215
|
+
# # => (foo || bar) < 2
|
|
216
|
+
# @return [Recorder]
|
|
217
|
+
def logical_operators(&)
|
|
218
|
+
Matcher.with_settings(logical_operators: true, &)
|
|
219
|
+
end
|
|
220
|
+
alias lo logical_operators
|
|
221
|
+
|
|
222
|
+
##
|
|
223
|
+
# Passes blocks through to the actual call instead of evaluating them
|
|
224
|
+
# as expression builders
|
|
225
|
+
# @example
|
|
226
|
+
# class Foo
|
|
227
|
+
# def initialize(foo)
|
|
228
|
+
# @foo = foo
|
|
229
|
+
# end
|
|
230
|
+
# end
|
|
231
|
+
#
|
|
232
|
+
# my_expr = Matcher::Expression.build do
|
|
233
|
+
# ptb do
|
|
234
|
+
# _.instance_exec { @foo }
|
|
235
|
+
# end
|
|
236
|
+
# end
|
|
237
|
+
#
|
|
238
|
+
# foo = Foo.new("foo")
|
|
239
|
+
#
|
|
240
|
+
# my_expr.evaluate(actual: foo) # => 'foo'
|
|
241
|
+
# @return [Recorder]
|
|
242
|
+
def pass_through_blocks(arg = UNDEFINED, &)
|
|
243
|
+
# Note that arg might be a recorder where #nil? won't work.
|
|
244
|
+
|
|
245
|
+
if Matcher.undefined?(arg)
|
|
246
|
+
Matcher.with_settings(pass_through_blocks: true, &)
|
|
247
|
+
else
|
|
248
|
+
Matcher.with_settings(pass_through_blocks: true) do
|
|
249
|
+
yield arg
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
alias ptb pass_through_blocks
|
|
254
|
+
|
|
255
|
+
##
|
|
256
|
+
# Captures calls to assignment methods
|
|
257
|
+
# @example
|
|
258
|
+
# assign { _.foo = 1 } # => actual.foo = 1
|
|
259
|
+
# assign { _[:foo] = 1 } # => actual[:foo] = 1
|
|
260
|
+
# @return [Recorder]
|
|
261
|
+
def assign
|
|
262
|
+
value = expression_of(yield)
|
|
263
|
+
call = Call.last_assign
|
|
264
|
+
|
|
265
|
+
Call.reset_last_assign
|
|
266
|
+
|
|
267
|
+
status = if call&.assignment?
|
|
268
|
+
arg = call.args.last
|
|
269
|
+
|
|
270
|
+
if value.is_a?(Constant)
|
|
271
|
+
arg.is_a?(Constant) && value.value.equal?(arg.value)
|
|
272
|
+
else
|
|
273
|
+
value.equal?(arg)
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
raise "Could not return last assignment" unless status
|
|
278
|
+
|
|
279
|
+
call.to_recorder
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
##
|
|
283
|
+
# Returns the variable factory for accessing named variables
|
|
284
|
+
# @example
|
|
285
|
+
# vars[:my_value]
|
|
286
|
+
# _ == vars[:limit]
|
|
287
|
+
# @return [VariableFactory]
|
|
288
|
+
def vars
|
|
289
|
+
@vars ||= VariableFactory.new(@expression_cache)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
class VariableFactory
|
|
293
|
+
include NoMatcher
|
|
294
|
+
include NoExpression
|
|
295
|
+
include NoKey
|
|
296
|
+
|
|
297
|
+
def initialize(expression_cache)
|
|
298
|
+
@expression_cache = expression_cache
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def [](symbol)
|
|
302
|
+
Variable.cache(symbol, expression_cache: @expression_cache).to_recorder
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class Optional
|
|
5
|
+
include NoExpression
|
|
6
|
+
|
|
7
|
+
CACHEABLE_CLASSES = [
|
|
8
|
+
NilClass,
|
|
9
|
+
FalseClass,
|
|
10
|
+
TrueClass,
|
|
11
|
+
Integer,
|
|
12
|
+
Float,
|
|
13
|
+
Symbol,
|
|
14
|
+
String,
|
|
15
|
+
Regexp,
|
|
16
|
+
Module,
|
|
17
|
+
Base,
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
def self.cache(value, matcher_cache = MatcherCache.current)
|
|
21
|
+
return new(value) if !matcher_cache ||
|
|
22
|
+
!CACHEABLE_CLASSES.include?(value.class) ||
|
|
23
|
+
value.is_a?(String) && !value.frozen?
|
|
24
|
+
|
|
25
|
+
(matcher_cache.optionals ||= {})[value] ||= new(value)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.value_of(obj)
|
|
29
|
+
obj.is_a?(Optional) ? obj.value : obj
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def initialize(value)
|
|
33
|
+
@value = value
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
attr_reader :value
|
|
37
|
+
|
|
38
|
+
def ~
|
|
39
|
+
~Matcher.cache(self)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def ==(other)
|
|
43
|
+
return true if equal?(other)
|
|
44
|
+
|
|
45
|
+
other.instance_of?(self.class) && other.value == @value
|
|
46
|
+
end
|
|
47
|
+
alias eql? ==
|
|
48
|
+
|
|
49
|
+
def hash
|
|
50
|
+
[self.class, @value].hash
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def to_s
|
|
54
|
+
"optional(#{@value.inspect})"
|
|
55
|
+
end
|
|
56
|
+
alias inspect to_s
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
module MatcherDsl
|
|
60
|
+
##
|
|
61
|
+
# Marks a hash key as optional or wraps a matcher to also accept +nil+
|
|
62
|
+
# @example
|
|
63
|
+
# # optional hash key
|
|
64
|
+
# { optional(:foo) => 1 }
|
|
65
|
+
# # matches "Hello" and nil but not 1
|
|
66
|
+
# optional(String)
|
|
67
|
+
# # alternatively:
|
|
68
|
+
# optional ^ String
|
|
69
|
+
# @overload optional(value)
|
|
70
|
+
# @param value
|
|
71
|
+
# @return [Optional]
|
|
72
|
+
# @overload optional
|
|
73
|
+
# @return [Chain]
|
|
74
|
+
def optional(value = UNDEFINED)
|
|
75
|
+
return Chain.new { optional(_1) } if Matcher.undefined?(value)
|
|
76
|
+
|
|
77
|
+
value = Expression.try_recorder(value)
|
|
78
|
+
|
|
79
|
+
Optional.new(value)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class OptionalChain
|
|
5
|
+
include NoExpression
|
|
6
|
+
include NoKey
|
|
7
|
+
extend Forwardable
|
|
8
|
+
|
|
9
|
+
def initialize(chain, fallback)
|
|
10
|
+
@chain = chain
|
|
11
|
+
@fallback = fallback
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def_delegator :@chain, :^
|
|
15
|
+
|
|
16
|
+
def ~
|
|
17
|
+
OptionalChain.new(~@chain, @fallback)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def fallback
|
|
21
|
+
@chain ^ @fallback
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class Others
|
|
5
|
+
include Singleton
|
|
6
|
+
include NoMatcher
|
|
7
|
+
include NoExpression
|
|
8
|
+
|
|
9
|
+
def to_s
|
|
10
|
+
"others"
|
|
11
|
+
end
|
|
12
|
+
alias inspect to_s
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module MatcherDsl
|
|
16
|
+
##
|
|
17
|
+
# Hash key that matches remaining entries
|
|
18
|
+
# @example
|
|
19
|
+
# {
|
|
20
|
+
# id: Integer,
|
|
21
|
+
# others => each_value(String),
|
|
22
|
+
# }
|
|
23
|
+
# @return [Others]
|
|
24
|
+
def others
|
|
25
|
+
Others.instance
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class AndError < Error
|
|
5
|
+
attr_reader :children
|
|
6
|
+
|
|
7
|
+
def self.from(children)
|
|
8
|
+
length = children.length
|
|
9
|
+
|
|
10
|
+
return EmptyError.instance if length == 0
|
|
11
|
+
|
|
12
|
+
children.reduce do |left, right|
|
|
13
|
+
if left.is_a?(AndError)
|
|
14
|
+
left << right
|
|
15
|
+
else
|
|
16
|
+
left & right
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize(children)
|
|
22
|
+
raise "children fewer than 2" if children.length < 2
|
|
23
|
+
|
|
24
|
+
super()
|
|
25
|
+
|
|
26
|
+
@children = children
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def ==(other)
|
|
30
|
+
return true if equal?(other)
|
|
31
|
+
|
|
32
|
+
other.instance_of?(AndError) &&
|
|
33
|
+
@children == other.children
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def &(other)
|
|
37
|
+
return self if other.is_a?(EmptyError)
|
|
38
|
+
|
|
39
|
+
clone << other
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def add(other)
|
|
43
|
+
case other
|
|
44
|
+
when AndError
|
|
45
|
+
right = other.children.dup
|
|
46
|
+
|
|
47
|
+
@children.each_with_index do |l, i|
|
|
48
|
+
next unless l.is_a?(NestedError)
|
|
49
|
+
|
|
50
|
+
index = right.find_index { _1.is_a?(NestedError) && _1.key == l.key }
|
|
51
|
+
|
|
52
|
+
@children[i] = l & right.delete_at(index) if index
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
@children.concat(right)
|
|
56
|
+
when NestedError
|
|
57
|
+
index = @children.find_index do |child|
|
|
58
|
+
child.is_a?(NestedError) && child.key == other.key
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
if index
|
|
62
|
+
@children[index] &= other
|
|
63
|
+
else
|
|
64
|
+
@children << other
|
|
65
|
+
end
|
|
66
|
+
else
|
|
67
|
+
@children << other
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
self
|
|
71
|
+
end
|
|
72
|
+
alias << add
|
|
73
|
+
|
|
74
|
+
def clone
|
|
75
|
+
klone = super
|
|
76
|
+
klone.instance_exec do
|
|
77
|
+
@children = @children.dup
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
klone
|
|
81
|
+
end
|
|
82
|
+
alias dup clone
|
|
83
|
+
|
|
84
|
+
def to_s
|
|
85
|
+
@children.join(" & ")
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class BooleanCollector
|
|
5
|
+
def initialize
|
|
6
|
+
@result = true
|
|
7
|
+
@mode = :and
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def or!
|
|
11
|
+
@mode = :or
|
|
12
|
+
self
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def or?
|
|
16
|
+
@mode == :or
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def and?
|
|
20
|
+
@mode == :and
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def empty?
|
|
24
|
+
@result
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
INVALID_ERROR = ElementError.new("invalid")
|
|
28
|
+
private_constant :INVALID_ERROR
|
|
29
|
+
|
|
30
|
+
def error
|
|
31
|
+
@result ? EmptyError.instance : INVALID_ERROR
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def <<(error)
|
|
35
|
+
if !error.is_a?(Error) || !error.valid?
|
|
36
|
+
@result = false
|
|
37
|
+
throw :mismatch if and?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
self.error
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def [](_key)
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def clear
|
|
48
|
+
@result = true
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class ElementError < Error
|
|
5
|
+
attr_reader :message
|
|
6
|
+
|
|
7
|
+
def initialize(message)
|
|
8
|
+
super()
|
|
9
|
+
|
|
10
|
+
@message = message
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def ==(other)
|
|
14
|
+
return true if equal?(other)
|
|
15
|
+
|
|
16
|
+
other.instance_of?(ElementError) &&
|
|
17
|
+
@message == other.message
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def to_s
|
|
21
|
+
@message.inspect
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class EmptyError < Error
|
|
5
|
+
include Singleton
|
|
6
|
+
|
|
7
|
+
def &(other)
|
|
8
|
+
other
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def |(other)
|
|
12
|
+
other
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def valid?
|
|
16
|
+
true
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_s
|
|
20
|
+
"<>"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class Error
|
|
5
|
+
def &(other)
|
|
6
|
+
case other
|
|
7
|
+
when EmptyError
|
|
8
|
+
self
|
|
9
|
+
when AndError
|
|
10
|
+
AndError.new([self].concat(other.children))
|
|
11
|
+
else
|
|
12
|
+
AndError.new([self, other])
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def |(other)
|
|
17
|
+
case other
|
|
18
|
+
when EmptyError
|
|
19
|
+
self
|
|
20
|
+
when OrError
|
|
21
|
+
OrError.new([self].concat(other.children))
|
|
22
|
+
else
|
|
23
|
+
OrError.new([self, other])
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def valid?
|
|
28
|
+
false
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def inspect
|
|
32
|
+
to_s
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def report
|
|
36
|
+
Reporter.report(self)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|