matchers 0.1.0.pre.test

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.

Potentially problematic release.


This version of matchers might be problematic. Click here for more details.

Files changed (123) hide show
  1. checksums.yaml +7 -0
  2. data/lib/matcher/assertions.rb +21 -0
  3. data/lib/matcher/base.rb +189 -0
  4. data/lib/matcher/builder.rb +74 -0
  5. data/lib/matcher/chain.rb +60 -0
  6. data/lib/matcher/debug.rb +48 -0
  7. data/lib/matcher/errors/and_error.rb +86 -0
  8. data/lib/matcher/errors/boolean_collector.rb +51 -0
  9. data/lib/matcher/errors/element_error.rb +24 -0
  10. data/lib/matcher/errors/empty_error.rb +23 -0
  11. data/lib/matcher/errors/error.rb +39 -0
  12. data/lib/matcher/errors/error_collector.rb +99 -0
  13. data/lib/matcher/errors/nested_error.rb +96 -0
  14. data/lib/matcher/errors/or_error.rb +86 -0
  15. data/lib/matcher/expression_cache.rb +57 -0
  16. data/lib/matcher/expression_labeler.rb +91 -0
  17. data/lib/matcher/expressions/array_expression.rb +45 -0
  18. data/lib/matcher/expressions/block.rb +153 -0
  19. data/lib/matcher/expressions/call.rb +338 -0
  20. data/lib/matcher/expressions/call_error.rb +45 -0
  21. data/lib/matcher/expressions/constant.rb +53 -0
  22. data/lib/matcher/expressions/expression.rb +147 -0
  23. data/lib/matcher/expressions/expression_building.rb +258 -0
  24. data/lib/matcher/expressions/expression_walker.rb +73 -0
  25. data/lib/matcher/expressions/hash_expression.rb +59 -0
  26. data/lib/matcher/expressions/proc_expression.rb +92 -0
  27. data/lib/matcher/expressions/range_expression.rb +58 -0
  28. data/lib/matcher/expressions/recorder.rb +86 -0
  29. data/lib/matcher/expressions/rescue_last_error_expression.rb +44 -0
  30. data/lib/matcher/expressions/set_expression.rb +45 -0
  31. data/lib/matcher/expressions/string_expression.rb +53 -0
  32. data/lib/matcher/expressions/symbol_proc.rb +53 -0
  33. data/lib/matcher/expressions/variable.rb +85 -0
  34. data/lib/matcher/hash_stack.rb +53 -0
  35. data/lib/matcher/list.rb +102 -0
  36. data/lib/matcher/markers/optional.rb +80 -0
  37. data/lib/matcher/markers/others.rb +28 -0
  38. data/lib/matcher/matcher_cache.rb +18 -0
  39. data/lib/matcher/matchers/all_matcher.rb +60 -0
  40. data/lib/matcher/matchers/always_matcher.rb +28 -0
  41. data/lib/matcher/matchers/any_matcher.rb +70 -0
  42. data/lib/matcher/matchers/array_matcher.rb +35 -0
  43. data/lib/matcher/matchers/block_matcher.rb +59 -0
  44. data/lib/matcher/matchers/boolean_matcher.rb +35 -0
  45. data/lib/matcher/matchers/dig_matcher.rb +146 -0
  46. data/lib/matcher/matchers/each_matcher.rb +52 -0
  47. data/lib/matcher/matchers/each_pair_matcher.rb +119 -0
  48. data/lib/matcher/matchers/equal_matcher.rb +197 -0
  49. data/lib/matcher/matchers/equal_set_matcher.rb +99 -0
  50. data/lib/matcher/matchers/expression_matcher.rb +73 -0
  51. data/lib/matcher/matchers/filter_matcher.rb +111 -0
  52. data/lib/matcher/matchers/hash_matcher.rb +223 -0
  53. data/lib/matcher/matchers/imply_matcher.rb +81 -0
  54. data/lib/matcher/matchers/imply_some_matcher.rb +112 -0
  55. data/lib/matcher/matchers/index_by_matcher.rb +175 -0
  56. data/lib/matcher/matchers/inline_matcher.rb +99 -0
  57. data/lib/matcher/matchers/keys_matcher.rb +121 -0
  58. data/lib/matcher/matchers/kind_of_matcher.rb +35 -0
  59. data/lib/matcher/matchers/lazy_all_matcher.rb +68 -0
  60. data/lib/matcher/matchers/lazy_any_matcher.rb +68 -0
  61. data/lib/matcher/matchers/let_matcher.rb +73 -0
  62. data/lib/matcher/matchers/map_matcher.rb +129 -0
  63. data/lib/matcher/matchers/matcher_building.rb +5 -0
  64. data/lib/matcher/matchers/negated_array_matcher.rb +38 -0
  65. data/lib/matcher/matchers/negated_each_matcher.rb +36 -0
  66. data/lib/matcher/matchers/negated_each_pair_matcher.rb +38 -0
  67. data/lib/matcher/matchers/negated_imply_some_matcher.rb +46 -0
  68. data/lib/matcher/matchers/negated_matcher.rb +23 -0
  69. data/lib/matcher/matchers/negated_project_matcher.rb +31 -0
  70. data/lib/matcher/matchers/never_matcher.rb +29 -0
  71. data/lib/matcher/matchers/one_matcher.rb +70 -0
  72. data/lib/matcher/matchers/optional_matcher.rb +38 -0
  73. data/lib/matcher/matchers/parse_float_matcher.rb +86 -0
  74. data/lib/matcher/matchers/parse_integer_matcher.rb +98 -0
  75. data/lib/matcher/matchers/parse_iso8601_matcher.rb +92 -0
  76. data/lib/matcher/matchers/parse_json_matcher.rb +95 -0
  77. data/lib/matcher/matchers/project_matcher.rb +68 -0
  78. data/lib/matcher/matchers/raises_matcher.rb +124 -0
  79. data/lib/matcher/matchers/range_matcher.rb +47 -0
  80. data/lib/matcher/matchers/reference_matcher.rb +111 -0
  81. data/lib/matcher/matchers/reference_matcher_collection.rb +57 -0
  82. data/lib/matcher/matchers/regexp_matcher.rb +84 -0
  83. data/lib/matcher/messages/expected_phrasing.rb +342 -0
  84. data/lib/matcher/messages/message.rb +102 -0
  85. data/lib/matcher/messages/message_builder.rb +35 -0
  86. data/lib/matcher/messages/message_rules.rb +223 -0
  87. data/lib/matcher/messages/namespaced_message_builder.rb +19 -0
  88. data/lib/matcher/messages/phrasing.rb +57 -0
  89. data/lib/matcher/messages/standard_message_builder.rb +105 -0
  90. data/lib/matcher/once_before.rb +18 -0
  91. data/lib/matcher/optional_chain.rb +24 -0
  92. data/lib/matcher/patterns/ast_mapping.rb +42 -0
  93. data/lib/matcher/patterns/capture_hole.rb +33 -0
  94. data/lib/matcher/patterns/constant_hole.rb +14 -0
  95. data/lib/matcher/patterns/hole.rb +30 -0
  96. data/lib/matcher/patterns/method_hole.rb +58 -0
  97. data/lib/matcher/patterns/pattern.rb +92 -0
  98. data/lib/matcher/patterns/pattern_building.rb +39 -0
  99. data/lib/matcher/patterns/pattern_capture.rb +11 -0
  100. data/lib/matcher/patterns/pattern_match.rb +29 -0
  101. data/lib/matcher/patterns/variable_hole.rb +14 -0
  102. data/lib/matcher/reporter.rb +98 -0
  103. data/lib/matcher/rules/message_factory.rb +25 -0
  104. data/lib/matcher/rules/message_rule.rb +18 -0
  105. data/lib/matcher/rules/message_rule_context.rb +24 -0
  106. data/lib/matcher/rules/rule_builder.rb +29 -0
  107. data/lib/matcher/rules/rule_set.rb +57 -0
  108. data/lib/matcher/rules/transform_builder.rb +24 -0
  109. data/lib/matcher/rules/transform_mapping.rb +5 -0
  110. data/lib/matcher/rules/transform_rule.rb +21 -0
  111. data/lib/matcher/state.rb +40 -0
  112. data/lib/matcher/testing/error_builder.rb +62 -0
  113. data/lib/matcher/testing/error_checker.rb +496 -0
  114. data/lib/matcher/testing/error_testing.rb +37 -0
  115. data/lib/matcher/testing/pattern_testing.rb +11 -0
  116. data/lib/matcher/testing/pattern_testing_scope.rb +34 -0
  117. data/lib/matcher/testing.rb +102 -0
  118. data/lib/matcher/undefined.rb +10 -0
  119. data/lib/matcher/utils/mapping_utils.rb +61 -0
  120. data/lib/matcher/utils.rb +72 -0
  121. data/lib/matcher/version.rb +5 -0
  122. data/lib/matcher.rb +337 -0
  123. metadata +167 -0
@@ -0,0 +1,342 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class ExpectedPhrasing < Phrasing
5
+ define(:truthy) do
6
+ verb = verb(negated: true)
7
+ truthy_or_falsy = negated ? 'truthy' : 'falsy'
8
+
9
+ "#{verb} a #{truthy_or_falsy} value but got #{actual.inspect}"
10
+ end
11
+
12
+ define(:same) do |object|
13
+ "#{verb} same as #{object.inspect} (id=#{object.object_id})" \
14
+ "#{" but got #{actual.inspect} (id=#{actual.object_id})" if negated}"
15
+ end
16
+
17
+ define(:equal) do |value|
18
+ if negated
19
+ "#{verb} #{value.inspect} but got #{actual.inspect}"
20
+ else
21
+ "#{verb} #{actual.inspect}"
22
+ end
23
+ end
24
+
25
+ define(:less_than) do |operand|
26
+ if negated || (actual <=> operand).nil?
27
+ "#{verb} a value < #{operand.inspect} but got #{actual.inspect}"
28
+ else
29
+ phrase_negated(:greater_than_or_equal, operand)
30
+ end
31
+ end
32
+
33
+ define(:greater_than) do |operand|
34
+ if negated || (actual <=> operand).nil?
35
+ "#{verb} a value > #{operand.inspect} but got #{actual.inspect}"
36
+ else
37
+ phrase_negated(:less_than_or_equal, operand)
38
+ end
39
+ end
40
+
41
+ define(:less_than_or_equal) do |operand|
42
+ if negated || (actual <=> operand).nil?
43
+ "#{verb} a value <= #{operand.inspect} but got #{actual.inspect}"
44
+ else
45
+ phrase_negated(:greater_than, operand)
46
+ end
47
+ end
48
+
49
+ define(:greater_than_or_equal) do |operand|
50
+ if negated || (actual <=> operand).nil?
51
+ "#{verb} a value >= #{operand.inspect} but got #{actual.inspect}"
52
+ else
53
+ phrase_negated(:less_than, operand)
54
+ end
55
+ end
56
+
57
+ define(:comparable_to) do |operand|
58
+ "#{verb} a value comparable to #{operand.inspect} but got #{actual.inspect}"
59
+ end
60
+
61
+ define(:between) do |min, max, exclude_end: false|
62
+ "#{verb} value to be between #{min.inspect} and " \
63
+ "#{max.inspect}#{' (exclusive)' if exclude_end} but got #{actual.inspect}"
64
+ end
65
+
66
+ define(:length_of) do |exp, act|
67
+ "#{verb} length of #{exp}#{" but was #{act}" if negated}"
68
+ end
69
+
70
+ define(:having_key) do |key|
71
+ "#{verb} to include key #{key.inspect} but got #{actual.inspect}"
72
+ end
73
+
74
+ define(:having_index) do |index|
75
+ "#{verb} to have index #{index.inspect} but got #{actual.inspect}"
76
+ end
77
+
78
+ define(:exist) do
79
+ "#{verb} to exist#{" but got #{actual.inspect}" unless negated}"
80
+ end
81
+
82
+ define(:in) do |collection|
83
+ "#{verb} object to be included in #{collection.inspect} but got #{actual.inspect}"
84
+ end
85
+
86
+ define(:including) do |item|
87
+ "#{verb} #{item.inspect} to be included but got #{actual.inspect}"
88
+ end
89
+
90
+ define(:duplicate) do |original_index|
91
+ "#{verb} duplicate originally at index #{original_index} but got #{actual.inspect}"
92
+ end
93
+
94
+ define(:duplicate_by) do |expression, value, original_index|
95
+ "#{verb} duplicate by #{expression.inspect}=#{value.inspect} originally at index #{original_index} but got #{actual.inspect}"
96
+ end
97
+
98
+ define(:matching) do |pattern|
99
+ "#{verb} value to match #{pattern.inspect} but got #{actual.inspect}"
100
+ end
101
+
102
+ define(:valid_format) do |format|
103
+ "#{verb} a valid #{format} string but got #{actual.inspect}"
104
+ end
105
+
106
+ define(:instance_of) do |klass|
107
+ "#{verb} an instance of #{klass} but got #{actual.inspect}"
108
+ end
109
+
110
+ define(:kind_of) do |klass|
111
+ "#{verb} a kind of #{klass} but got #{actual.inspect}"
112
+ end
113
+
114
+ define(:responding_to) do |method|
115
+ "#{verb} an object responding to `#{method}' but got #{actual.inspect}"
116
+ end
117
+
118
+ define(:predicate) do |predicate|
119
+ predicate = predicate.to_s.delete_suffix('?')
120
+
121
+ "#{verb} value to be #{predicate} but got #{actual.inspect}"
122
+ end
123
+
124
+ define(:described_by) do |description|
125
+ "#{verb} #{description} but got #{actual.inspect}"
126
+ end
127
+
128
+ namespace(:expression) do
129
+ define(:truthy) do |expression, value, given|
130
+ verb = verb(negated: true)
131
+ truthy_or_falsy = negated ? 'truthy' : 'falsy'
132
+
133
+ "#{verb} #{expression} to be #{truthy_or_falsy} " \
134
+ "but got #{value.inspect}#{where_text(given, expression)}"
135
+ end
136
+
137
+ define(:same) do |left_expression, right_expression, left, right, given|
138
+ "#{verb} #{left_expression} to be same as #{right_expression} " \
139
+ "but got #{left.inspect} (id=#{left.object_id})" \
140
+ "#{" and #{right.inspect} (id=#{right.object_id})" if negated}" \
141
+ "#{where_text(given, left_expression, right_expression)}"
142
+ end
143
+
144
+ negated_comparisons =
145
+ { :== => :!=, :!= => :==, :< => :>=, :> => :<=, :<= => :>, :>= => :< }
146
+
147
+ define(:comparison) do |expression, left, right, given|
148
+ can_negate = !negated &&
149
+ (method = negated_comparisons[expression.method]) &&
150
+ (%i[== !=].include?(method) || left <=> right)
151
+
152
+ verb = if can_negate
153
+ expression = Call.new(
154
+ expression.receiver,
155
+ method,
156
+ expression.args,
157
+ expression.kwargs,
158
+ )
159
+
160
+ verb(negated: true)
161
+ else
162
+ self.verb
163
+ end
164
+
165
+ "#{verb} #{expression} but got " \
166
+ "#{left.inspect} #{expression.method} #{right.inspect}" \
167
+ "#{where_text(given, expression.receiver, expression.args[0])}"
168
+ end
169
+
170
+ define(:comparable_to) do |expression, value, operand, given|
171
+ "#{verb} #{expression} to be comparable to #{operand.inspect} " \
172
+ "but got #{value.inspect}#{where_text(given, expression)}"
173
+ end
174
+
175
+ define(:between) do |expression, value, min, max, given|
176
+ "#{verb} #{expression} to be between #{min.inspect} and #{max.inspect} " \
177
+ "but got #{value.inspect}#{where_text(given, expression)}"
178
+ end
179
+
180
+ define(:length_of) do |expression, _value, exp, act, given|
181
+ "#{verb} #{expression} to have length of #{exp}" \
182
+ "#{" but was #{act}" if negated}#{where_text(given, expression)}"
183
+ end
184
+
185
+ define(:having_key) do |expression, value, key, given|
186
+ "#{verb} #{expression} to include key #{key.inspect} " \
187
+ "but got #{value.inspect}#{where_text(given, expression)}"
188
+ end
189
+
190
+ define(:in) do |expression, value, collection, given|
191
+ "#{verb} #{expression} to be included in #{collection.inspect} " \
192
+ "but got #{value.inspect}#{where_text(given, expression)}"
193
+ end
194
+
195
+ define(:including) do |expression, value, item, given|
196
+ "#{verb} #{expression} to include #{item.inspect} " \
197
+ "but got #{value.inspect}#{where_text(given, expression)}"
198
+ end
199
+
200
+ define(:matching) do |expression, value, pattern, given|
201
+ "#{verb} #{expression} to match #{pattern.inspect} " \
202
+ "but got #{value.inspect}#{where_text(given, expression)}"
203
+ end
204
+
205
+ match_at_words = {
206
+ :== => 'at',
207
+ :!= => 'not at',
208
+ :< => 'before',
209
+ :> => 'after',
210
+ :<= => 'at or before',
211
+ :>= => 'at or after',
212
+ }
213
+
214
+ define(:match_at) do |expression, value, pattern, position, comparison, operand, given|
215
+ comparison_word = match_at_words[comparison]
216
+
217
+ message = String.new
218
+ message << "#{verb} #{expression} to match #{pattern.inspect} #{comparison_word} #{operand} "
219
+ message << "but was at #{position} " if
220
+ operand != position || comparison != (negated ? :!= : :==)
221
+ message << "for #{value.inspect}#{where_text(given, expression)}"
222
+
223
+ message
224
+ end
225
+
226
+ define(:instance_of) do |expression, value, klass, given|
227
+ "#{verb} #{expression} to be an instance of #{klass} " \
228
+ "but got #{value.inspect}#{where_text(given, expression)}"
229
+ end
230
+
231
+ define(:kind_of) do |expression, value, klass, given|
232
+ "#{verb} #{expression} to be a kind of #{klass} " \
233
+ "but got #{value.inspect}#{where_text(given, expression)}"
234
+ end
235
+
236
+ define(:responding_to) do |expression, value, method, given|
237
+ "#{verb} #{expression} to respond to `#{method}' " \
238
+ "but got #{value.inspect}#{where_text(given, expression)}"
239
+ end
240
+
241
+ define(:predicate) do |expression, value, predicate, given|
242
+ predicate = predicate.to_s.delete_suffix('?')
243
+
244
+ "#{verb} #{expression} to be #{predicate} " \
245
+ "but got #{value.inspect}#{where_text(given, expression)}"
246
+ end
247
+
248
+ define(:raising) do |expression, error, given|
249
+ error_class = error.is_a?(Class) ? error : error.class
250
+ where = where_text(given, expression)
251
+
252
+ text = "#{verb} #{expression} to raise #{error_class}#{where}"
253
+ text += ": #{error.message}" if error.is_a?(Exception)
254
+
255
+ text
256
+ end
257
+ end
258
+
259
+ namespace(:negated) do
260
+ define(:valid) do |matcher|
261
+ "#{verb} #{matcher} to be valid but got #{actual.inspect}"
262
+ end
263
+ end
264
+
265
+ namespace(:block) do
266
+ define(:satisfied) do |block_location|
267
+ "#{verb} to satisfy condition #{block_location} but got #{actual.inspect}"
268
+ end
269
+ end
270
+
271
+ namespace(:imply_some) do
272
+ def x_conditions(count)
273
+ case count
274
+ when :any
275
+ 'any condition'
276
+ when 1
277
+ 'one condition'
278
+ else
279
+ "#{count} conditions"
280
+ end
281
+ end
282
+ private :x_conditions
283
+
284
+ define(:no_condition_satisfied) do |conditions, count|
285
+ "#{negated_verb} to satisfy #{x_conditions(count)} but got #{actual.inspect} and met none of these: #{join(conditions)}"
286
+ end
287
+
288
+ define(:x_conditions_satisfied) do |conditions, count|
289
+ "#{negated_verb} to satisfy #{x_conditions(count)} but got #{actual.inspect} and met these: #{join(conditions)}"
290
+ end
291
+ end
292
+
293
+ namespace(:reference) do
294
+ define(:cyclic) do
295
+ "#{verb} a cyclic structure" \
296
+ "#{' but actual has already been visited' unless negated}"
297
+ end
298
+
299
+ define(:failed_from_cache) do
300
+ 'actual has already failed before'
301
+ end
302
+ end
303
+
304
+ namespace(:set) do
305
+ define(:equal) do |set|
306
+ "#{verb} object to be an equal set to #{set.inspect} but got #{actual.inspect}"
307
+ end
308
+ end
309
+
310
+ private
311
+
312
+ def verb(negated: self.negated)
313
+ # A message says what actual is but expected says what it is not. That's why
314
+ # the verb is counter-intuitively negated.
315
+
316
+ negated ? 'expected' : 'did not expect'
317
+ end
318
+
319
+ def negated_verb
320
+ verb(negated: !negated)
321
+ end
322
+
323
+ def join(objects)
324
+ objects.map(&:to_s).join(', ')
325
+ end
326
+
327
+ def where_text(values, *expressions)
328
+ return '' if values.empty?
329
+
330
+ except = expressions.filter_map { _1.symbol if _1.is_a?(Variable) }
331
+ symbols = expressions.flat_map(&:variables).uniq - except
332
+
333
+ return '' if symbols.empty?
334
+
335
+ list = symbols
336
+ .map { "#{_1} = #{values.fetch(_1).inspect}" }
337
+ .join(', ')
338
+
339
+ ", where #{list}"
340
+ end
341
+ end
342
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class Message
5
+ attr_reader :key, :negated, :actual, :args, :kwargs
6
+
7
+ def initialize(key, negated, actual, *args, **kwargs)
8
+ @key = key
9
+ @negated = negated
10
+ @actual = actual
11
+ @args = args
12
+ @kwargs = kwargs
13
+ end
14
+
15
+ def negate!
16
+ @negated = !@negated
17
+ end
18
+
19
+ def ==(other)
20
+ return true if equal?(other)
21
+
22
+ other.instance_of?(Message) &&
23
+ @key.eql?(other.key) &&
24
+ @negated == other.negated &&
25
+ @actual.eql?(other.actual) &&
26
+ @args.eql?(other.args) &&
27
+ @kwargs.eql?(other.kwargs)
28
+ end
29
+ alias eql? ==
30
+
31
+ def hash
32
+ @hash ||= [self.class, @key, @negated, @actual, @args, @kwargs].hash
33
+ end
34
+
35
+ def namespace
36
+ @key.is_a?(Array) ? @key[0] : nil
37
+ end
38
+
39
+ def to_standard
40
+ return self if namespace != :expression
41
+
42
+ key = @key[1]
43
+
44
+ case key
45
+ when :truthy
46
+ _expr, value = @args
47
+ Message.new(key, @negated, value)
48
+ when :same
49
+ _l_expr, _r_expr, left, right = @args
50
+ Message.new(:same, @negated, left, right)
51
+ when :comparable_to, :having_key, :in, :including, :matching, :instance_of, :kind_of, :responding_to, :predicate
52
+ _expr, value, operand = @args
53
+ Message.new(key, @negated, value, operand)
54
+ when :between, :length_of
55
+ _expr, value, operand1, operand2 = @args
56
+ Message.new(key, @negated, value, operand1, operand2)
57
+ when :comparison
58
+ expr, left, right = @args
59
+
60
+ case expr.method
61
+ when :==
62
+ Message.new(:equal, @negated, left, right)
63
+ when :!=
64
+ Message.new(:equal, !@negated, left, right)
65
+ when :<
66
+ Message.new(:less_than, @negated, left, right)
67
+ when :>
68
+ Message.new(:greater_than, @negated, left, right)
69
+ when :<=
70
+ Message.new(:less_than_or_equal, @negated, left, right)
71
+ when :>=
72
+ Message.new(:greater_than_or_equal, @negated, left, right)
73
+ else
74
+ raise "unexpected method: #{expr.method.inspect}"
75
+ end
76
+ else
77
+ self
78
+ end
79
+ end
80
+
81
+ def to_s
82
+ if @key.is_a?(Array)
83
+ namespace, key = @key
84
+ else
85
+ key = @key
86
+ end
87
+
88
+ args_and_kwargs = @args.map(&:inspect) + @kwargs.map do |k, v|
89
+ k.is_a?(Symbol) ? "#{k}: #{v.inspect}" : "#{k.inspect} => #{v.inspect}"
90
+ end
91
+
92
+ string = String.new("report(#{@actual.inspect})")
93
+ string += ".namespace(#{namespace.inspect})" if namespace
94
+ string += '.not' if @negated
95
+ string += ".#{key}"
96
+ string += "(#{args_and_kwargs.join(', ')})" unless args_and_kwargs.empty?
97
+
98
+ string
99
+ end
100
+ alias inspect to_s
101
+ end
102
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class MessageBuilder
5
+ def initialize(negated, actual)
6
+ @negated = negated
7
+ @actual = actual
8
+ end
9
+
10
+ def not
11
+ @negated = !@negated
12
+ self
13
+ end
14
+
15
+ def not_if(condition)
16
+ condition ? self.not : self
17
+ end
18
+
19
+ protected
20
+
21
+ def message(key, *, **)
22
+ Message.new(key, @negated, @actual, *, **)
23
+ end
24
+
25
+ private
26
+
27
+ def method_missing(method, *, **)
28
+ message(method, *, **)
29
+ end
30
+
31
+ def respond_to_missing?(_name, _include_private = false)
32
+ true
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,223 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ ExpressionMatcher.message_rules.configure do
5
+ # binary standard expression
6
+ standard_ops = %i[== != < > <= >= <=> =~ !~ equal? is_a? kind_of? instance_of? respond_to? key? include? in?]
7
+ message method_hole(:call, _, standard_ops, const(:operand)) do |v, e|
8
+ case e[:call].method
9
+ when :<
10
+ standard_message.less_than(v[:operand])
11
+ when :>
12
+ standard_message.greater_than(v[:operand])
13
+ when :<=
14
+ standard_message.less_than_or_equal(v[:operand])
15
+ when :>=
16
+ standard_message.greater_than_or_equal(v[:operand])
17
+ when :<=>
18
+ # <=> returns nil if operands are not comparable
19
+ standard_message.comparable_to(v[:operand])
20
+ when :==
21
+ standard_message.equal(v[:operand])
22
+ when :!=
23
+ standard_message.not.equal(v[:operand])
24
+ when :=~
25
+ standard_message.matching(v[:operand])
26
+ when :!~
27
+ standard_message.not.matching(v[:operand])
28
+ when :equal?
29
+ standard_message.same(v[:operand])
30
+ when :is_a?, :kind_of?
31
+ standard_message.kind_of(v[:operand])
32
+ when :instance_of?
33
+ standard_message.instance_of(v[:operand])
34
+ when :respond_to?
35
+ standard_message.responding_to(v[:operand])
36
+ when :key?
37
+ standard_message.having_key(v[:operand])
38
+ when :include?
39
+ standard_message.including(v[:operand])
40
+ when :in?
41
+ standard_message.in(v[:operand])
42
+ else
43
+ raise "Unexpected method: #{e[:call].method}"
44
+ end
45
+ end
46
+
47
+ # predicate
48
+ message method_hole(:predicate, _, -> { _1.end_with?('?') }) do |_v, e|
49
+ standard_message.predicate(e[:predicate].method)
50
+ end
51
+
52
+ # flip
53
+ transform(
54
+ method_hole(
55
+ :call,
56
+ hole(:operand) { !_1.variables.include?(:actual)},
57
+ %i[== != < > <= >= equal? include? in?],
58
+ hole(:actual) { _1.variables.include?(:actual) },
59
+ ),
60
+ ) do |m|
61
+ method = m[:call].expression.method
62
+
63
+ flipped_method =
64
+ case method
65
+ when :< then :>
66
+ when :> then :<
67
+ when :<= then :>=
68
+ when :>= then :<=
69
+ when :include? then :in?
70
+ when :in? then :include?
71
+ else
72
+ method
73
+ end
74
+
75
+ call(m[:call], m[:actual], flipped_method, m[:operand])
76
+ end
77
+
78
+ # length
79
+ message capture(:act, _.length) == const(:exp) do |v|
80
+ standard_message.length_of(v[:exp], v[:act])
81
+ end
82
+
83
+ # between
84
+ message _.between?(const(:min), const(:max)) do |v|
85
+ standard_message.between(v[:min], v[:max])
86
+ end
87
+
88
+ # truthy
89
+ message _ do
90
+ standard_message.truthy
91
+ end
92
+
93
+ # transform !
94
+ transform !hole(:expression), negate: true do |m|
95
+ m[:expression]
96
+ end
97
+
98
+ # transform instance_of?
99
+ transform(
100
+ hole(:obj).class == hole(:class),
101
+ hole(:class) == hole(:obj).class,
102
+ ) do |m|
103
+ call(m[:root], m[:obj], :instance_of?, m[:class])
104
+ end
105
+
106
+ # transform between?
107
+ transform(
108
+ lo { (hole(:value) >= hole(:min)) & (hole(:value) <= hole(:max)) },
109
+ ) do |m|
110
+ call(m[:root], m[:value], :between?, m[:min], m[:max])
111
+ end
112
+
113
+ # transform comparison
114
+ transform(
115
+ method_hole(:compare, hole(:lhs) <=> hole(:rhs), %i[== != < > <= >=], 0),
116
+ ) do |m|
117
+ call(m[:compare], m[:lhs], m[:compare].expression.method, m[:rhs])
118
+ end
119
+
120
+ # length expression
121
+ message capture(:act, hole(:object).length) == hole(:exp) do |v, e|
122
+ expression_message.length_of(e[:object], v[:object], v[:exp], v[:act], given)
123
+ end
124
+
125
+ # match regexp at
126
+ message(
127
+ method_hole(
128
+ :comparison,
129
+ capture(:pos, hole(:lhs) =~ hole(:rhs)),
130
+ %i[== != < > <= >=],
131
+ hole(:operand),
132
+ ),
133
+ ) do |v, e|
134
+ expression, value, pattern =
135
+ MessageRules.decompose_pattern_matching(e[:lhs], e[:rhs], v[:lhs], v[:rhs])
136
+
137
+ if expression
138
+ expression_message.match_at(
139
+ expression,
140
+ value,
141
+ pattern,
142
+ v[:pos],
143
+ e[:comparison].method,
144
+ v[:operand],
145
+ given,
146
+ )
147
+ else
148
+ expression_message.truthy(e[:comparison], v[:comparison], given)
149
+ end
150
+ end
151
+
152
+ # comparison expression
153
+ message method_hole(:comparison, hole(:lhs), %i[== != < > <= >=], hole(:rhs)) do |v, e|
154
+ expression_message.comparison(e[:comparison], v[:lhs], v[:rhs], given)
155
+ end
156
+
157
+ # general binary expression
158
+ general_ops = %i[<=> equal? is_a? kind_of? instance_of? respond_to? key? include? in?]
159
+ message method_hole(:call, hole(:lhs), general_ops, hole(:rhs)) do |v, e|
160
+ case e[:call].method
161
+ when :<=>
162
+ # <=> returns nil if operands are not comparable
163
+ expression_message.comparable_to(e[:lhs], v[:lhs], v[:rhs], given)
164
+ when :equal?
165
+ expression_message.same(e[:lhs], e[:rhs], v[:lhs], v[:rhs], given)
166
+ when :is_a?, :kind_of?
167
+ expression_message.kind_of(e[:lhs], v[:lhs], v[:rhs], given)
168
+ when :instance_of?
169
+ expression_message.instance_of(e[:lhs], v[:lhs], v[:rhs], given)
170
+ when :respond_to?
171
+ expression_message.responding_to(e[:lhs], v[:lhs], v[:rhs], given)
172
+ when :key?
173
+ expression_message.having_key(e[:lhs], v[:lhs], v[:rhs], given)
174
+ when :include?
175
+ expression_message.including(e[:lhs], v[:lhs], v[:rhs], given)
176
+ when :in?
177
+ expression_message.in(e[:lhs], v[:lhs], v[:rhs], given)
178
+ else
179
+ raise "Unexpected method: #{e[:call].method}"
180
+ end
181
+ end
182
+
183
+ # predicate expression
184
+ message method_hole(:predicate, hole(:receiver), -> { _1.end_with?('?') }) do |v, e|
185
+ expression_message.predicate(e[:receiver], v[:receiver], e[:predicate].method, given)
186
+ end
187
+
188
+ # match regexp
189
+ message method_hole(:operator, hole(:lhs), %i[=~ !~], hole(:rhs)) do |v, e|
190
+ expression, value, pattern =
191
+ MessageRules.decompose_pattern_matching(e[:lhs], e[:rhs], v[:lhs], v[:rhs])
192
+
193
+ if expression
194
+ expression_message.not_if(e[:operator].method == :!~)
195
+ .matching(expression, value, pattern, given)
196
+ else
197
+ expression_message.truthy(e[:operator], v[:operator], given)
198
+ end
199
+ end
200
+
201
+ # between expression
202
+ message hole(:value).between?(hole(:min), hole(:max)) do |v, e|
203
+ expression_message.between(e[:value], v[:value], v[:min], v[:max], given)
204
+ end
205
+
206
+ # any expression
207
+ message hole(:expression) do |v, e|
208
+ expression_message.truthy(e[:expression], v[:expression], given)
209
+ end
210
+ end
211
+
212
+ module MessageRules
213
+ def self.decompose_pattern_matching(e_lhs, e_rhs, v_lhs, v_rhs)
214
+ if v_lhs.is_a?(String) && v_rhs.is_a?(Regexp)
215
+ [e_lhs, v_lhs, v_rhs]
216
+ elsif v_lhs.is_a?(Regexp) && v_rhs.is_a?(String)
217
+ [e_rhs, v_rhs, v_lhs]
218
+ else
219
+ nil
220
+ end
221
+ end
222
+ end
223
+ end