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.
Files changed (127) hide show
  1. checksums.yaml +7 -0
  2. data/lib/matcher/assertions.rb +19 -0
  3. data/lib/matcher/autoload.rb +5 -0
  4. data/lib/matcher/base.rb +183 -0
  5. data/lib/matcher/compatibility.rb +34 -0
  6. data/lib/matcher/debug.rb +62 -0
  7. data/lib/matcher/dsl/builder.rb +99 -0
  8. data/lib/matcher/dsl/chain.rb +84 -0
  9. data/lib/matcher/dsl/expression_dsl.rb +306 -0
  10. data/lib/matcher/dsl/matcher_dsl.rb +5 -0
  11. data/lib/matcher/dsl/optional.rb +82 -0
  12. data/lib/matcher/dsl/optional_chain.rb +24 -0
  13. data/lib/matcher/dsl/others.rb +28 -0
  14. data/lib/matcher/errors/and_error.rb +88 -0
  15. data/lib/matcher/errors/boolean_collector.rb +51 -0
  16. data/lib/matcher/errors/element_error.rb +24 -0
  17. data/lib/matcher/errors/empty_error.rb +23 -0
  18. data/lib/matcher/errors/error.rb +39 -0
  19. data/lib/matcher/errors/error_collector.rb +100 -0
  20. data/lib/matcher/errors/nested_error.rb +98 -0
  21. data/lib/matcher/errors/or_error.rb +88 -0
  22. data/lib/matcher/expression_cache.rb +57 -0
  23. data/lib/matcher/expression_labeler.rb +96 -0
  24. data/lib/matcher/expressions/array_expression.rb +45 -0
  25. data/lib/matcher/expressions/block.rb +189 -0
  26. data/lib/matcher/expressions/call.rb +307 -0
  27. data/lib/matcher/expressions/call_error.rb +45 -0
  28. data/lib/matcher/expressions/constant.rb +53 -0
  29. data/lib/matcher/expressions/expression.rb +237 -0
  30. data/lib/matcher/expressions/expression_walker.rb +77 -0
  31. data/lib/matcher/expressions/hash_expression.rb +59 -0
  32. data/lib/matcher/expressions/proc_expression.rb +96 -0
  33. data/lib/matcher/expressions/range_expression.rb +65 -0
  34. data/lib/matcher/expressions/recorder.rb +136 -0
  35. data/lib/matcher/expressions/rescue_last_error_expression.rb +49 -0
  36. data/lib/matcher/expressions/set_expression.rb +45 -0
  37. data/lib/matcher/expressions/string_expression.rb +53 -0
  38. data/lib/matcher/expressions/symbol_proc.rb +53 -0
  39. data/lib/matcher/expressions/variable.rb +87 -0
  40. data/lib/matcher/hash_stack.rb +52 -0
  41. data/lib/matcher/list.rb +102 -0
  42. data/lib/matcher/markers.rb +7 -0
  43. data/lib/matcher/matcher_cache.rb +18 -0
  44. data/lib/matcher/matchers/all_matcher.rb +60 -0
  45. data/lib/matcher/matchers/always_matcher.rb +34 -0
  46. data/lib/matcher/matchers/any_matcher.rb +70 -0
  47. data/lib/matcher/matchers/array_matcher.rb +72 -0
  48. data/lib/matcher/matchers/block_matcher.rb +61 -0
  49. data/lib/matcher/matchers/boolean_matcher.rb +37 -0
  50. data/lib/matcher/matchers/dig_matcher.rb +149 -0
  51. data/lib/matcher/matchers/each_matcher.rb +85 -0
  52. data/lib/matcher/matchers/each_pair_matcher.rb +119 -0
  53. data/lib/matcher/matchers/equal_matcher.rb +198 -0
  54. data/lib/matcher/matchers/equal_set_matcher.rb +112 -0
  55. data/lib/matcher/matchers/expression_matcher.rb +69 -0
  56. data/lib/matcher/matchers/filter_matcher.rb +115 -0
  57. data/lib/matcher/matchers/hash_matcher.rb +315 -0
  58. data/lib/matcher/matchers/imply_matcher.rb +83 -0
  59. data/lib/matcher/matchers/imply_some_matcher.rb +116 -0
  60. data/lib/matcher/matchers/index_by_matcher.rb +177 -0
  61. data/lib/matcher/matchers/inline_matcher.rb +101 -0
  62. data/lib/matcher/matchers/keys_matcher.rb +131 -0
  63. data/lib/matcher/matchers/kind_of_matcher.rb +35 -0
  64. data/lib/matcher/matchers/lazy_all_matcher.rb +69 -0
  65. data/lib/matcher/matchers/lazy_any_matcher.rb +69 -0
  66. data/lib/matcher/matchers/let_matcher.rb +73 -0
  67. data/lib/matcher/matchers/map_matcher.rb +148 -0
  68. data/lib/matcher/matchers/negated_array_matcher.rb +38 -0
  69. data/lib/matcher/matchers/negated_each_matcher.rb +36 -0
  70. data/lib/matcher/matchers/negated_each_pair_matcher.rb +38 -0
  71. data/lib/matcher/matchers/negated_imply_some_matcher.rb +46 -0
  72. data/lib/matcher/matchers/negated_matcher.rb +25 -0
  73. data/lib/matcher/matchers/negated_project_matcher.rb +31 -0
  74. data/lib/matcher/matchers/never_matcher.rb +35 -0
  75. data/lib/matcher/matchers/one_matcher.rb +68 -0
  76. data/lib/matcher/matchers/optional_matcher.rb +38 -0
  77. data/lib/matcher/matchers/parse_float_matcher.rb +86 -0
  78. data/lib/matcher/matchers/parse_integer_matcher.rb +101 -0
  79. data/lib/matcher/matchers/parse_iso8601_helper.rb +41 -0
  80. data/lib/matcher/matchers/parse_iso8601_matcher.rb +52 -0
  81. data/lib/matcher/matchers/parse_json_helper.rb +43 -0
  82. data/lib/matcher/matchers/parse_json_matcher.rb +59 -0
  83. data/lib/matcher/matchers/project_matcher.rb +72 -0
  84. data/lib/matcher/matchers/raises_matcher.rb +131 -0
  85. data/lib/matcher/matchers/range_matcher.rb +50 -0
  86. data/lib/matcher/matchers/reference_matcher.rb +213 -0
  87. data/lib/matcher/matchers/reference_matcher_collection.rb +57 -0
  88. data/lib/matcher/matchers/regexp_matcher.rb +86 -0
  89. data/lib/matcher/messages/expected_phrasing.rb +355 -0
  90. data/lib/matcher/messages/message.rb +104 -0
  91. data/lib/matcher/messages/message_builder.rb +35 -0
  92. data/lib/matcher/messages/message_rules.rb +240 -0
  93. data/lib/matcher/messages/namespaced_message_builder.rb +19 -0
  94. data/lib/matcher/messages/phrasing.rb +59 -0
  95. data/lib/matcher/messages/standard_message_builder.rb +105 -0
  96. data/lib/matcher/patterns/ast_mapping.rb +42 -0
  97. data/lib/matcher/patterns/capture_hole.rb +33 -0
  98. data/lib/matcher/patterns/constant_hole.rb +14 -0
  99. data/lib/matcher/patterns/hole.rb +30 -0
  100. data/lib/matcher/patterns/method_hole.rb +62 -0
  101. data/lib/matcher/patterns/pattern.rb +104 -0
  102. data/lib/matcher/patterns/pattern_building.rb +39 -0
  103. data/lib/matcher/patterns/pattern_capture.rb +11 -0
  104. data/lib/matcher/patterns/pattern_match.rb +29 -0
  105. data/lib/matcher/patterns/variable_hole.rb +14 -0
  106. data/lib/matcher/reporter.rb +103 -0
  107. data/lib/matcher/rules/message_factory.rb +26 -0
  108. data/lib/matcher/rules/message_rule.rb +18 -0
  109. data/lib/matcher/rules/message_rule_context.rb +26 -0
  110. data/lib/matcher/rules/rule_builder.rb +29 -0
  111. data/lib/matcher/rules/rule_set.rb +57 -0
  112. data/lib/matcher/rules/transform_builder.rb +24 -0
  113. data/lib/matcher/rules/transform_mapping.rb +5 -0
  114. data/lib/matcher/rules/transform_rule.rb +21 -0
  115. data/lib/matcher/state.rb +40 -0
  116. data/lib/matcher/testing/error_builder.rb +62 -0
  117. data/lib/matcher/testing/error_checker.rb +514 -0
  118. data/lib/matcher/testing/error_testing.rb +37 -0
  119. data/lib/matcher/testing/pattern_testing.rb +11 -0
  120. data/lib/matcher/testing/pattern_testing_scope.rb +34 -0
  121. data/lib/matcher/testing.rb +107 -0
  122. data/lib/matcher/undefined.rb +10 -0
  123. data/lib/matcher/utils/mapping_utils.rb +61 -0
  124. data/lib/matcher/utils.rb +72 -0
  125. data/lib/matcher/version.rb +5 -0
  126. data/lib/matcher.rb +346 -0
  127. 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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ module MatcherDsl end
5
+ 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