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,240 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
ExpressionMatcher.message_rules.configure do
|
|
5
|
+
# binary standard expression
|
|
6
|
+
standard_ops = %i[== != < > <= >= <=> =~ !~] +
|
|
7
|
+
%i[equal? is_a? kind_of? instance_of? respond_to? key? include? in?]
|
|
8
|
+
|
|
9
|
+
message method_hole(:call, _, standard_ops, const(:operand)) do |v, e|
|
|
10
|
+
case e[:call].method
|
|
11
|
+
when :<
|
|
12
|
+
standard_message.less_than(v[:operand])
|
|
13
|
+
when :>
|
|
14
|
+
standard_message.greater_than(v[:operand])
|
|
15
|
+
when :<=
|
|
16
|
+
standard_message.less_than_or_equal(v[:operand])
|
|
17
|
+
when :>=
|
|
18
|
+
standard_message.greater_than_or_equal(v[:operand])
|
|
19
|
+
when :<=>
|
|
20
|
+
# <=> returns nil if operands are not comparable
|
|
21
|
+
standard_message.comparable_to(v[:operand])
|
|
22
|
+
when :==
|
|
23
|
+
standard_message.equal(v[:operand])
|
|
24
|
+
when :!=
|
|
25
|
+
standard_message.not.equal(v[:operand])
|
|
26
|
+
when :=~
|
|
27
|
+
standard_message.matching(v[:operand])
|
|
28
|
+
when :!~
|
|
29
|
+
standard_message.not.matching(v[:operand])
|
|
30
|
+
when :equal?
|
|
31
|
+
standard_message.same(v[:operand])
|
|
32
|
+
when :is_a?, :kind_of?
|
|
33
|
+
standard_message.kind_of(v[:operand])
|
|
34
|
+
when :instance_of?
|
|
35
|
+
standard_message.instance_of(v[:operand])
|
|
36
|
+
when :respond_to?
|
|
37
|
+
standard_message.responding_to(v[:operand])
|
|
38
|
+
when :key?
|
|
39
|
+
standard_message.having_key(v[:operand])
|
|
40
|
+
when :include?
|
|
41
|
+
standard_message.including(v[:operand])
|
|
42
|
+
when :in?
|
|
43
|
+
standard_message.in(v[:operand])
|
|
44
|
+
else
|
|
45
|
+
raise "Unexpected method: #{e[:call].method}"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# predicate
|
|
50
|
+
message method_hole(:predicate, _, -> { _1.end_with?("?") }) do |_v, e|
|
|
51
|
+
standard_message.predicate(e[:predicate].method)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# flip
|
|
55
|
+
transform(
|
|
56
|
+
method_hole(
|
|
57
|
+
:call,
|
|
58
|
+
hole(:operand) { !_1.variables.include?(:actual) },
|
|
59
|
+
%i[== != < > <= >= equal? include? in?],
|
|
60
|
+
hole(:actual) { _1.variables.include?(:actual) },
|
|
61
|
+
),
|
|
62
|
+
) do |m|
|
|
63
|
+
method = m[:call].expression.method
|
|
64
|
+
|
|
65
|
+
flipped_method =
|
|
66
|
+
case method
|
|
67
|
+
when :< then :>
|
|
68
|
+
when :> then :<
|
|
69
|
+
when :<= then :>=
|
|
70
|
+
when :>= then :<=
|
|
71
|
+
when :include? then :in?
|
|
72
|
+
when :in? then :include?
|
|
73
|
+
else
|
|
74
|
+
method
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
call(m[:call], m[:actual], flipped_method, m[:operand])
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# length
|
|
81
|
+
message capture(:act, _.length) == const(:exp) do |v|
|
|
82
|
+
standard_message.length_of(v[:exp], v[:act])
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# between
|
|
86
|
+
message _.between?(const(:min), const(:max)) do |v|
|
|
87
|
+
standard_message.between(v[:min], v[:max])
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# truthy
|
|
91
|
+
message _ do
|
|
92
|
+
standard_message.truthy
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# transform !
|
|
96
|
+
transform !hole(:expression), negate: true do |m|
|
|
97
|
+
m[:expression]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# transform instance_of?
|
|
101
|
+
transform(
|
|
102
|
+
# rubocop:disable Style/ClassEqualityComparison
|
|
103
|
+
hole(:obj).class == hole(:class),
|
|
104
|
+
hole(:class) == hole(:obj).class,
|
|
105
|
+
# rubocop:enable Style/ClassEqualityComparison
|
|
106
|
+
) do |m|
|
|
107
|
+
call(m[:root], m[:obj], :instance_of?, m[:class])
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# transform between?
|
|
111
|
+
transform(
|
|
112
|
+
lo { (hole(:value) >= hole(:min)) & (hole(:value) <= hole(:max)) },
|
|
113
|
+
) do |m|
|
|
114
|
+
call(m[:root], m[:value], :between?, m[:min], m[:max])
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# transform comparison
|
|
118
|
+
transform(
|
|
119
|
+
method_hole(:compare, hole(:lhs) <=> hole(:rhs), %i[== != < > <= >=], 0),
|
|
120
|
+
) do |m|
|
|
121
|
+
call(m[:compare], m[:lhs], m[:compare].expression.method, m[:rhs])
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# length expression
|
|
125
|
+
message capture(:act, hole(:object).length) == hole(:exp) do |v, e|
|
|
126
|
+
expression_message.length_of(
|
|
127
|
+
e[:object], v[:object], v[:exp], v[:act], given
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# match regexp at
|
|
132
|
+
message(
|
|
133
|
+
method_hole(
|
|
134
|
+
:comparison,
|
|
135
|
+
capture(:pos, hole(:lhs) =~ hole(:rhs)),
|
|
136
|
+
%i[== != < > <= >=],
|
|
137
|
+
hole(:operand),
|
|
138
|
+
),
|
|
139
|
+
) do |v, e|
|
|
140
|
+
expression, value, pattern =
|
|
141
|
+
MessageRules.decompose_pattern_matching(
|
|
142
|
+
e[:lhs], e[:rhs], v[:lhs], v[:rhs]
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if expression
|
|
146
|
+
expression_message.match_at(
|
|
147
|
+
expression,
|
|
148
|
+
value,
|
|
149
|
+
pattern,
|
|
150
|
+
v[:pos],
|
|
151
|
+
e[:comparison].method,
|
|
152
|
+
v[:operand],
|
|
153
|
+
given,
|
|
154
|
+
)
|
|
155
|
+
else
|
|
156
|
+
expression_message.truthy(e[:comparison], v[:comparison], given)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# comparison expression
|
|
161
|
+
message method_hole(
|
|
162
|
+
:comparison, hole(:lhs), %i[== != < > <= >=], hole(:rhs)
|
|
163
|
+
) do |v, e|
|
|
164
|
+
expression_message.comparison(e[:comparison], v[:lhs], v[:rhs], given)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# general binary expression
|
|
168
|
+
general_ops =
|
|
169
|
+
%i[<=> equal? is_a? kind_of? instance_of? respond_to? key? include? in?]
|
|
170
|
+
|
|
171
|
+
message method_hole(:call, hole(:lhs), general_ops, hole(:rhs)) do |v, e|
|
|
172
|
+
case e[:call].method
|
|
173
|
+
when :<=>
|
|
174
|
+
# <=> returns nil if operands are not comparable
|
|
175
|
+
expression_message.comparable_to(e[:lhs], v[:lhs], v[:rhs], given)
|
|
176
|
+
when :equal?
|
|
177
|
+
expression_message.same(e[:lhs], e[:rhs], v[:lhs], v[:rhs], given)
|
|
178
|
+
when :is_a?, :kind_of?
|
|
179
|
+
expression_message.kind_of(e[:lhs], v[:lhs], v[:rhs], given)
|
|
180
|
+
when :instance_of?
|
|
181
|
+
expression_message.instance_of(e[:lhs], v[:lhs], v[:rhs], given)
|
|
182
|
+
when :respond_to?
|
|
183
|
+
expression_message.responding_to(e[:lhs], v[:lhs], v[:rhs], given)
|
|
184
|
+
when :key?
|
|
185
|
+
expression_message.having_key(e[:lhs], v[:lhs], v[:rhs], given)
|
|
186
|
+
when :include?
|
|
187
|
+
expression_message.including(e[:lhs], v[:lhs], v[:rhs], given)
|
|
188
|
+
when :in?
|
|
189
|
+
expression_message.in(e[:lhs], v[:lhs], v[:rhs], given)
|
|
190
|
+
else
|
|
191
|
+
raise "Unexpected method: #{e[:call].method}"
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# predicate expression
|
|
196
|
+
message method_hole(
|
|
197
|
+
:predicate, hole(:receiver), -> { _1.end_with?("?") }
|
|
198
|
+
) do |v, e|
|
|
199
|
+
expression_message.predicate(
|
|
200
|
+
e[:receiver], v[:receiver], e[:predicate].method, given
|
|
201
|
+
)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# match regexp
|
|
205
|
+
message method_hole(:operator, hole(:lhs), %i[=~ !~], hole(:rhs)) do |v, e|
|
|
206
|
+
expression, value, pattern = MessageRules.decompose_pattern_matching(
|
|
207
|
+
e[:lhs], e[:rhs], v[:lhs], v[:rhs]
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
if expression
|
|
211
|
+
expression_message.not_if(e[:operator].method == :!~)
|
|
212
|
+
.matching(expression, value, pattern, given)
|
|
213
|
+
else
|
|
214
|
+
expression_message.truthy(e[:operator], v[:operator], given)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# between expression
|
|
219
|
+
message hole(:value).between?(hole(:min), hole(:max)) do |v, e|
|
|
220
|
+
expression_message.between(e[:value], v[:value], v[:min], v[:max], given)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# any expression
|
|
224
|
+
message hole(:expression) do |v, e|
|
|
225
|
+
expression_message.truthy(e[:expression], v[:expression], given)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
module MessageRules
|
|
230
|
+
def self.decompose_pattern_matching(e_lhs, e_rhs, v_lhs, v_rhs)
|
|
231
|
+
if v_lhs.is_a?(String) && v_rhs.is_a?(Regexp)
|
|
232
|
+
[e_lhs, v_lhs, v_rhs]
|
|
233
|
+
elsif v_lhs.is_a?(Regexp) && v_rhs.is_a?(String)
|
|
234
|
+
[e_rhs, v_rhs, v_lhs]
|
|
235
|
+
else
|
|
236
|
+
nil
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class NamespacedMessageBuilder < MessageBuilder
|
|
5
|
+
def initialize(negated, actual, namespace)
|
|
6
|
+
super(negated, actual)
|
|
7
|
+
|
|
8
|
+
@namespace = namespace
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
protected
|
|
12
|
+
|
|
13
|
+
def message(key, *, **)
|
|
14
|
+
key = [@namespace, key]
|
|
15
|
+
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class Phrasing
|
|
5
|
+
extend Forwardable
|
|
6
|
+
|
|
7
|
+
attr_reader :path, :message
|
|
8
|
+
|
|
9
|
+
def_delegator :@message, :actual
|
|
10
|
+
def_delegator :@message, :negated
|
|
11
|
+
|
|
12
|
+
def self.namespace(value)
|
|
13
|
+
@namespace = value
|
|
14
|
+
yield
|
|
15
|
+
ensure
|
|
16
|
+
@namespace = nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.define(key, &block)
|
|
20
|
+
key = [@namespace, key] if @namespace
|
|
21
|
+
|
|
22
|
+
dict[key] = block
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.dict
|
|
26
|
+
@dict ||= {}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.phrasing
|
|
30
|
+
->(path, message) { new(path, message).apply }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def initialize(path, message)
|
|
34
|
+
@path = path
|
|
35
|
+
@message = message
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def apply
|
|
39
|
+
phrase(@message.key, *@message.args, **@message.kwargs)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def phrase(key, *, **)
|
|
43
|
+
block = self.class.dict[key]
|
|
44
|
+
|
|
45
|
+
if block
|
|
46
|
+
instance_exec(*, **, &block)
|
|
47
|
+
else
|
|
48
|
+
"got #{actual.inspect} but found no message for " \
|
|
49
|
+
"#{'*NOT* ' if negated}#{key.inspect}"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def phrase_negated(key, *, **)
|
|
54
|
+
message = Message.new(key, !negated, actual, *, **)
|
|
55
|
+
|
|
56
|
+
self.class.new(@path, message).apply
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class StandardMessageBuilder < MessageBuilder
|
|
5
|
+
def namespace(namespace)
|
|
6
|
+
NamespacedMessageBuilder.new(@negated, @actual, namespace)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def truthy
|
|
10
|
+
message(:truthy)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def same(object)
|
|
14
|
+
message(:same, object)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def equal(value)
|
|
18
|
+
message(:equal, value)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def less_than(operand)
|
|
22
|
+
message(:less_than, operand)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def greater_than(operand)
|
|
26
|
+
message(:greater_than, operand)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def less_than_or_equal(operand)
|
|
30
|
+
message(:less_than_or_equal, operand)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def greater_than_or_equal(operand)
|
|
34
|
+
message(:greater_than_or_equal, operand)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def comparable_to(operand)
|
|
38
|
+
message(:comparable_to, operand)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def between(min, max, exclude_end: false)
|
|
42
|
+
message(:between, min, max, exclude_end:)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def length_of(exp, act)
|
|
46
|
+
message(:length_of, exp, act)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def having_key(key)
|
|
50
|
+
message(:having_key, key)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def having_index(index)
|
|
54
|
+
message(:having_index, index)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def exist
|
|
58
|
+
message(:exist)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def in(collection)
|
|
62
|
+
message(:in, collection)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def including(item)
|
|
66
|
+
message(:including, item)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def duplicate(original_index)
|
|
70
|
+
message(:duplicate, original_index)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def duplicate_by(expression, value, original_index)
|
|
74
|
+
message(:duplicate_by, expression, value, original_index)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def matching(pattern)
|
|
78
|
+
message(:matching, pattern)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def valid_format(format)
|
|
82
|
+
message(:valid_format, format)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def instance_of(klass)
|
|
86
|
+
message(:instance_of, klass)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def kind_of(klass)
|
|
90
|
+
message(:kind_of, klass)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def responding_to(method)
|
|
94
|
+
message(:responding_to, method)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def predicate(name)
|
|
98
|
+
message(:predicate, name)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def described_by(description)
|
|
102
|
+
message(:described_by, description)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class AstMapping
|
|
5
|
+
RECEIVER = 0
|
|
6
|
+
ARGS = 1
|
|
7
|
+
KWARGS = 2
|
|
8
|
+
|
|
9
|
+
def initialize(path = List.empty)
|
|
10
|
+
@path = path
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
attr_reader :path
|
|
14
|
+
|
|
15
|
+
def receiver
|
|
16
|
+
@receiver ||= AstMapping.new(@path << RECEIVER)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def args
|
|
20
|
+
@args ||= Args.new(@path << ARGS, [])
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def kwargs
|
|
24
|
+
@kwargs ||= Args.new(@path << KWARGS, {})
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class Args
|
|
28
|
+
extend Forwardable
|
|
29
|
+
|
|
30
|
+
def initialize(path, data)
|
|
31
|
+
@path = path
|
|
32
|
+
@data = data
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
attr_reader :path, :data
|
|
36
|
+
|
|
37
|
+
def [](index)
|
|
38
|
+
@data[index] ||= AstMapping.new(@path << index)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class CaptureHole < Hole
|
|
5
|
+
def initialize(key, pattern)
|
|
6
|
+
super(key)
|
|
7
|
+
|
|
8
|
+
@pattern = pattern
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
attr_reader :pattern
|
|
12
|
+
|
|
13
|
+
def match?(_expression)
|
|
14
|
+
yield @pattern
|
|
15
|
+
|
|
16
|
+
true
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def ==(other)
|
|
20
|
+
super && @pattern == other.pattern
|
|
21
|
+
end
|
|
22
|
+
alias eql? ==
|
|
23
|
+
|
|
24
|
+
def hash
|
|
25
|
+
@hash ||= [self.class, @key, @pattern].hash
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def to_s
|
|
29
|
+
"capture(#{@key.inspect}, #{@pattern})"
|
|
30
|
+
end
|
|
31
|
+
alias inspect to_s
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class Hole
|
|
5
|
+
def initialize(key, filter = nil)
|
|
6
|
+
@key = key
|
|
7
|
+
@filter = filter
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
attr_reader :key
|
|
11
|
+
|
|
12
|
+
def match?(expression)
|
|
13
|
+
!@filter || @filter.call(expression)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def ==(other)
|
|
17
|
+
other.instance_of?(self.class) && key == other.key
|
|
18
|
+
end
|
|
19
|
+
alias eql? ==
|
|
20
|
+
|
|
21
|
+
def hash
|
|
22
|
+
[self.class, @key].hash
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def to_s
|
|
26
|
+
"hole(#{@key.inspect})"
|
|
27
|
+
end
|
|
28
|
+
alias inspect to_s
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class MethodHole < Hole
|
|
5
|
+
def initialize(key, receiver, method, args, kwargs)
|
|
6
|
+
super(key)
|
|
7
|
+
|
|
8
|
+
@receiver = receiver
|
|
9
|
+
@method = method
|
|
10
|
+
@args = args
|
|
11
|
+
@kwargs = kwargs
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
attr_reader :receiver, :method, :args, :kwargs
|
|
15
|
+
|
|
16
|
+
def match?(expression)
|
|
17
|
+
if !expression.is_a?(Call) || !match_method?(expression.method)
|
|
18
|
+
return false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
yield Call.new(@receiver, expression.method, @args, @kwargs)
|
|
22
|
+
|
|
23
|
+
true
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def ==(other)
|
|
27
|
+
return true if other.equal?(self)
|
|
28
|
+
|
|
29
|
+
super &&
|
|
30
|
+
@receiver == other.receiver &&
|
|
31
|
+
@method == other.method &&
|
|
32
|
+
@args.eql?(other.receiver) &&
|
|
33
|
+
@kwargs.eql?(other.receiver)
|
|
34
|
+
end
|
|
35
|
+
alias eql? ==
|
|
36
|
+
|
|
37
|
+
def hash
|
|
38
|
+
@hash ||= [self.class, @key, @receiver, @method, @args, @kwargs].hash
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def to_s
|
|
42
|
+
args = @args.map(&:inspect)
|
|
43
|
+
kwargs = @kwargs.map { "#{_1}: #{_2.inspect}" }
|
|
44
|
+
list = [@key.inspect, @receiver, @method.inspect].concat(args, kwargs)
|
|
45
|
+
|
|
46
|
+
"method_hole(#{list.join(', ')})"
|
|
47
|
+
end
|
|
48
|
+
alias inspect to_s
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def match_method?(method)
|
|
53
|
+
if @method.is_a?(Array)
|
|
54
|
+
@method.include?(method)
|
|
55
|
+
else
|
|
56
|
+
# rubocop:disable Style/CaseEquality
|
|
57
|
+
@method === method
|
|
58
|
+
# rubocop:enable Style/CaseEquality
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class Pattern
|
|
5
|
+
class PatternBuilder
|
|
6
|
+
include PatternBuilding
|
|
7
|
+
|
|
8
|
+
def initialize(build_session: Matcher.build_session)
|
|
9
|
+
ExpressionDsl.init(self, build_session)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.build(&)
|
|
14
|
+
Matcher.with_build_session do |build_session|
|
|
15
|
+
builder = PatternBuilder.new(build_session:)
|
|
16
|
+
result = builder.instance_exec(&)
|
|
17
|
+
builder.pattern_of(result)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
extend Forwardable
|
|
22
|
+
|
|
23
|
+
def initialize(expression)
|
|
24
|
+
@expression = expression
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
attr_reader :expression
|
|
28
|
+
|
|
29
|
+
def_delegator :@expression, :to_s
|
|
30
|
+
def_delegator :@expression, :inspect
|
|
31
|
+
|
|
32
|
+
def match(expression, mapping = AstMapping.new)
|
|
33
|
+
result = PatternMatch.new
|
|
34
|
+
|
|
35
|
+
catch(:mismatch) do
|
|
36
|
+
match_helper(expression, @expression, mapping, result)
|
|
37
|
+
|
|
38
|
+
result.capture(:root, expression, mapping) unless result.include?(:root)
|
|
39
|
+
|
|
40
|
+
return result
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def match_helper(expression, pattern, mapping, result)
|
|
49
|
+
if (hole = get_hole(pattern))
|
|
50
|
+
key = hole.key
|
|
51
|
+
capture = result[key]
|
|
52
|
+
|
|
53
|
+
if capture
|
|
54
|
+
throw(:mismatch) if capture.expression != expression
|
|
55
|
+
elsif !match?(hole, expression, mapping, result)
|
|
56
|
+
throw(:mismatch)
|
|
57
|
+
else
|
|
58
|
+
result.capture(key, expression, mapping)
|
|
59
|
+
end
|
|
60
|
+
elsif expression.is_a?(Call)
|
|
61
|
+
throw(:mismatch) unless similar_call?(expression, pattern)
|
|
62
|
+
|
|
63
|
+
match_helper(
|
|
64
|
+
expression.receiver, pattern.receiver, mapping.receiver, result
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
expression.args.each_index do |i|
|
|
68
|
+
match_helper(
|
|
69
|
+
expression.args[i], pattern.args[i], mapping.args[i], result
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
expression.kwargs.each_key do |k|
|
|
74
|
+
match_helper(
|
|
75
|
+
expression.kwargs[k], pattern.kwargs[k], mapping.kwargs[k], result
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
elsif expression != pattern
|
|
79
|
+
throw(:mismatch)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def match?(pattern, expression, mapping, result)
|
|
84
|
+
pattern.match?(expression) do |p|
|
|
85
|
+
match_helper(expression, p, mapping, result)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def get_hole(expression)
|
|
90
|
+
return unless expression.is_a?(Constant)
|
|
91
|
+
|
|
92
|
+
constant = expression.value
|
|
93
|
+
constant if constant.is_a?(Hole)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def similar_call?(expression, pattern)
|
|
97
|
+
expression.instance_of?(pattern.class) &&
|
|
98
|
+
expression.method == pattern.method &&
|
|
99
|
+
expression.args.length == pattern.args.length &&
|
|
100
|
+
expression.kwargs.size == pattern.kwargs.size &&
|
|
101
|
+
expression.kwargs.keys.sort == pattern.kwargs.keys.sort
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|