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,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class HashExpression < Expression
|
|
5
|
+
def initialize(pairs)
|
|
6
|
+
super()
|
|
7
|
+
|
|
8
|
+
@pairs = pairs
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
attr_reader :pairs
|
|
12
|
+
|
|
13
|
+
def ==(other)
|
|
14
|
+
return true if equal?(other)
|
|
15
|
+
|
|
16
|
+
other.instance_of?(self.class) &&
|
|
17
|
+
other.pairs.eql?(@pairs)
|
|
18
|
+
end
|
|
19
|
+
alias eql? ==
|
|
20
|
+
|
|
21
|
+
def hash
|
|
22
|
+
@hash ||= [self.class, @pairs].hash
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def variables
|
|
26
|
+
@variables ||= @pairs.flat_map { |k, v| k.variables + v.variables }.uniq
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def evaluate(values)
|
|
30
|
+
@pairs.to_h do |k, v|
|
|
31
|
+
[k.evaluate(values), v.evaluate(values)]
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def substitute(replacements)
|
|
36
|
+
return self unless replacements.keys.intersect?(variables)
|
|
37
|
+
|
|
38
|
+
substituted_pairs = @pairs.map do |k, v|
|
|
39
|
+
[k.substitute(replacements), v.substitute(replacements)]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
HashExpression.new(substituted_pairs)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def to_s
|
|
46
|
+
parts = @pairs.map do |k, v|
|
|
47
|
+
key_part = if k.is_a?(Constant) && k.value.is_a?(Symbol)
|
|
48
|
+
"#{k.value}:"
|
|
49
|
+
else
|
|
50
|
+
"#{k} =>"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
"#{key_part} #{v}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
"{ #{parts.join(', ')} }"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class ProcExpression < Expression
|
|
5
|
+
def initialize(block, substitution: nil)
|
|
6
|
+
super()
|
|
7
|
+
|
|
8
|
+
check_parameters(block.parameters)
|
|
9
|
+
|
|
10
|
+
@block = block
|
|
11
|
+
@substitution = substitution
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
attr_reader :block, :substitution
|
|
15
|
+
|
|
16
|
+
def check_parameters(parameters)
|
|
17
|
+
parameters.each_with_index do |(type, name), i|
|
|
18
|
+
case type
|
|
19
|
+
when :req, :opt
|
|
20
|
+
raise "ProcExpression cannot have more than 1 arg" if i > 0
|
|
21
|
+
when :keyreq, :key
|
|
22
|
+
if name == :actual
|
|
23
|
+
raise 'ProcExpression cannot have a kwarg called "actual"'
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def variables
|
|
30
|
+
@variables ||= begin
|
|
31
|
+
variables = @block.parameters.filter_map.with_index do |(type, name), i|
|
|
32
|
+
case type
|
|
33
|
+
when :req, :opt
|
|
34
|
+
:actual if i == 0
|
|
35
|
+
when :keyreq, :key
|
|
36
|
+
name
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
@substitution ? variables.map { @substitution[_1] || _1 } : variables
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def evaluate(values)
|
|
45
|
+
values = substitute_hash(values, @substitution) if @substitution
|
|
46
|
+
|
|
47
|
+
Utils.call_block(@block, values)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def ==(other)
|
|
51
|
+
return true if equal?(other)
|
|
52
|
+
|
|
53
|
+
other.instance_of?(ProcExpression) &&
|
|
54
|
+
other.block == @block &&
|
|
55
|
+
other.substitution == @substitution
|
|
56
|
+
end
|
|
57
|
+
alias eql? ==
|
|
58
|
+
|
|
59
|
+
def hash
|
|
60
|
+
[self.class, @block].hash
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def substitute(replacements)
|
|
64
|
+
replacements = replacements.slice(*variables)
|
|
65
|
+
|
|
66
|
+
return self if replacements.empty?
|
|
67
|
+
|
|
68
|
+
if @substitution
|
|
69
|
+
replacements = substitute_hash(@substitution, replacements)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
ProcExpression.new(@block, substitution: replacements)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def to_s
|
|
76
|
+
args_and_kwargs = Utils.inspect_block_params(@block)
|
|
77
|
+
|
|
78
|
+
if args_and_kwargs.empty?
|
|
79
|
+
"expr { ... }"
|
|
80
|
+
else
|
|
81
|
+
"expr { |#{args_and_kwargs}| ... }"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def substitute_hash(hash, substitution)
|
|
88
|
+
hash.to_h do |k, v|
|
|
89
|
+
k2 = substitution[k]
|
|
90
|
+
v2 = k2.nil? && !substitution.key?(k2) ? v : hash[k2]
|
|
91
|
+
|
|
92
|
+
[k, v2]
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class RangeExpression < Expression
|
|
5
|
+
def initialize(from, to, exclude_end: false)
|
|
6
|
+
super()
|
|
7
|
+
|
|
8
|
+
@begin = from
|
|
9
|
+
@end = to
|
|
10
|
+
@exclude_end = exclude_end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
attr_reader :begin, :end
|
|
14
|
+
|
|
15
|
+
def exclude_end?
|
|
16
|
+
@exclude_end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def ==(other)
|
|
20
|
+
return true if equal?(other)
|
|
21
|
+
|
|
22
|
+
other.instance_of?(RangeExpression) &&
|
|
23
|
+
other.begin.eql?(@begin) &&
|
|
24
|
+
other.end.eql?(@end) &&
|
|
25
|
+
other.exclude_end?.eql?(@exclude_end)
|
|
26
|
+
end
|
|
27
|
+
alias eql? ==
|
|
28
|
+
|
|
29
|
+
def hash
|
|
30
|
+
[self.class, @begin, @end, @exclude_end].hash
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def variables
|
|
34
|
+
@variables ||= (@begin.variables + @end.variables).uniq
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
PRECEDENCE = OPERATOR_PRECEDENCE[:".."]
|
|
38
|
+
|
|
39
|
+
def precedence
|
|
40
|
+
PRECEDENCE
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def evaluate(values)
|
|
44
|
+
from = @begin.evaluate(values)
|
|
45
|
+
to = @end.evaluate(values)
|
|
46
|
+
|
|
47
|
+
Range.new(from, to, @exclude_end)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def substitute(replacements)
|
|
51
|
+
return self unless replacements.keys.intersect?(variables)
|
|
52
|
+
|
|
53
|
+
from = @begin.substitute(replacements)
|
|
54
|
+
to = @end.substitute(replacements)
|
|
55
|
+
|
|
56
|
+
RangeExpression.new(from, to, exclude_end: @exclude_end)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def to_s
|
|
60
|
+
dots = @exclude_end ? "..." : ".."
|
|
61
|
+
|
|
62
|
+
"#{@begin}#{dots}#{@end}"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
##
|
|
5
|
+
# The core building block of composing expressions is a method *call*. For
|
|
6
|
+
# instance, <tt>a + b</tt> is a call where +a+ receives +b+ via its method
|
|
7
|
+
# <tt>+</tt>.
|
|
8
|
+
#
|
|
9
|
+
# Our expression builder tracks Ruby calls with Recorder objects which work
|
|
10
|
+
# like this:
|
|
11
|
+
# # let's build an AST for "Hello".upcase
|
|
12
|
+
#
|
|
13
|
+
# # explicitly by hand
|
|
14
|
+
# hello = Matcher::Constant.new("Hello")
|
|
15
|
+
# hello_upcase = Matcher::Call.new(hello, :upcase)
|
|
16
|
+
# hello_upcase.evaluate({}) # => "HELLO"
|
|
17
|
+
#
|
|
18
|
+
# # with recorder
|
|
19
|
+
# hello = Matcher::Constant.new("Hello")
|
|
20
|
+
# hello_rec = Matcher::Recorder.new(hello)
|
|
21
|
+
# hello_upcase_rec = hello_rec.upcase # the magic
|
|
22
|
+
# hello_upcase = Matcher::Recorder.to_expression(hello_upcase_rec)
|
|
23
|
+
# # => "Hello".upcase
|
|
24
|
+
# hello_upcase.evaluate({})
|
|
25
|
+
# # => "HELLO"
|
|
26
|
+
#
|
|
27
|
+
# # with builder
|
|
28
|
+
# Matcher::Expression.build do
|
|
29
|
+
# expr("Hello").upcase
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# == Recorders are nasty
|
|
33
|
+
#
|
|
34
|
+
# To do their job any call to a recorder must return a new recorder. But this
|
|
35
|
+
# also makes them ill-behaved because methods like <tt>==</tt> or
|
|
36
|
+
# <tt>is_a?</tt> don't behave like you expect them to. They don't have any
|
|
37
|
+
# methods defined (except +__id__+ and +__send__+). Instead, all calls are
|
|
38
|
+
# handled by +method_missing+.
|
|
39
|
+
#
|
|
40
|
+
# The consequence:
|
|
41
|
+
# # let r be a recorder
|
|
42
|
+
# r = Matcher::Recorder.new(Matcher::Variable.actual)
|
|
43
|
+
#
|
|
44
|
+
# # everything below returns a recorder
|
|
45
|
+
#
|
|
46
|
+
# r == nil # truthy
|
|
47
|
+
# r != r # truthy
|
|
48
|
+
# !r # truthy
|
|
49
|
+
# !!r # still truthy
|
|
50
|
+
# r.class # not Recorder (but a Recorder instance)
|
|
51
|
+
# r.object_id # not an integer
|
|
52
|
+
# r.to_s # not a string
|
|
53
|
+
#
|
|
54
|
+
# This makes them very hard to deal with, should you encounter them where you
|
|
55
|
+
# wouldn't expect them.
|
|
56
|
+
class Recorder
|
|
57
|
+
def self.recorder?(object)
|
|
58
|
+
Object.instance_method(:kind_of?)
|
|
59
|
+
.bind_call(object, Recorder)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.to_expression(recorder)
|
|
63
|
+
Object.instance_method(:instance_variable_get)
|
|
64
|
+
.bind_call(recorder, :@expression)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# NOTE: In order for Recorder to work, it can't have any methods except for
|
|
68
|
+
# the ones below.
|
|
69
|
+
|
|
70
|
+
(instance_methods - %i[__id__ __send__ object_id])
|
|
71
|
+
.each { undef_method _1 }
|
|
72
|
+
|
|
73
|
+
def initialize(expression)
|
|
74
|
+
@expression = expression
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def method_missing(method, *args, **kwargs, &block)
|
|
78
|
+
# *.hash.to_int indicates that a Hash evaluates this recorder as a key.
|
|
79
|
+
if @hash_parent # @hash_parent is set if @expression is a *.hash call.
|
|
80
|
+
to_int = method == :to_int && args.empty? && kwargs.empty? && !block
|
|
81
|
+
|
|
82
|
+
# Confirm to parent that indeed a Hash called it.
|
|
83
|
+
Object.instance_method(:instance_variable_set)
|
|
84
|
+
.bind_call(@hash_parent, :@hash_confirmed, to_int)
|
|
85
|
+
|
|
86
|
+
return @expression.receiver.hash if to_int
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# In "uncertain" state: Check if Hash called eql? on this recorder.
|
|
90
|
+
if @hash_caller # @hash_caller indicates the "uncertain" state.
|
|
91
|
+
if method == :eql? && @hash_confirmed && caller[0] == @hash_caller
|
|
92
|
+
# Return proper eql? result.
|
|
93
|
+
|
|
94
|
+
result = Recorder.recorder?(args[0]) &&
|
|
95
|
+
@expression.eql?(Recorder.to_expression(args[0]))
|
|
96
|
+
|
|
97
|
+
return result
|
|
98
|
+
else # Not a call from Hash.
|
|
99
|
+
# Leave the "uncertain" state and resume recorder behavior.
|
|
100
|
+
@hash_caller = nil
|
|
101
|
+
@hash_confirmed = false
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
expression_cache = ExpressionCache.current
|
|
106
|
+
args = args.map { Expression.of(_1, expression_cache:) }
|
|
107
|
+
kwargs = kwargs.transform_values { Expression.of(_1, expression_cache:) }
|
|
108
|
+
block = Block.build(expression_cache:, &block) if
|
|
109
|
+
block && !Matcher.settings[:pass_through_blocks]
|
|
110
|
+
|
|
111
|
+
expression = Call.new(@expression, method, args, kwargs, block)
|
|
112
|
+
expression = expression_cache[expression] if expression_cache
|
|
113
|
+
recorder = expression.to_recorder
|
|
114
|
+
|
|
115
|
+
# A Hash might evaluate this recorder as a key.
|
|
116
|
+
if method == :hash && args.empty? && kwargs.empty? && !block
|
|
117
|
+
# If the new recorder registers a *.to_int call then it was called by a
|
|
118
|
+
# Hash.
|
|
119
|
+
|
|
120
|
+
# Enter the "uncertain" state. Save the caller for later check against
|
|
121
|
+
# false positives.
|
|
122
|
+
@hash_caller = caller[0]
|
|
123
|
+
|
|
124
|
+
# Give reference to the new recorder for confirmation.
|
|
125
|
+
Object.instance_method(:instance_variable_set)
|
|
126
|
+
.bind_call(recorder, :@hash_parent, self)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
recorder
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def respond_to_missing?(...)
|
|
133
|
+
true
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class RescueLastErrorExpression < Expression
|
|
5
|
+
extend Forwardable
|
|
6
|
+
|
|
7
|
+
def initialize(expression)
|
|
8
|
+
super()
|
|
9
|
+
|
|
10
|
+
@expression = expression
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
attr_reader :expression
|
|
14
|
+
|
|
15
|
+
def_delegator :@expression, :variables
|
|
16
|
+
|
|
17
|
+
def ==(other)
|
|
18
|
+
equal?(other) ||
|
|
19
|
+
other.instance_of?(self.class) && @expression == other.expression
|
|
20
|
+
end
|
|
21
|
+
alias eql? ==
|
|
22
|
+
|
|
23
|
+
def hash
|
|
24
|
+
[self.class, @expression].hash
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
PRECEDENCE = OPERATOR_PRECEDENCE[:modifier_rescue]
|
|
28
|
+
|
|
29
|
+
def precedence
|
|
30
|
+
PRECEDENCE
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def evaluate(values)
|
|
34
|
+
@expression.evaluate(values)
|
|
35
|
+
rescue CallError => e
|
|
36
|
+
e.cause
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def substitute(replacements)
|
|
40
|
+
substitution = @expression.substitute(replacements)
|
|
41
|
+
|
|
42
|
+
RescueLastErrorExpression.new(substitution)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def to_s(**)
|
|
46
|
+
"#{@expression.to_s(**)} rescue $!"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class SetExpression < Expression
|
|
5
|
+
def initialize(items)
|
|
6
|
+
super()
|
|
7
|
+
|
|
8
|
+
@items = items
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
attr_reader :items
|
|
12
|
+
|
|
13
|
+
def ==(other)
|
|
14
|
+
return true if equal?(other)
|
|
15
|
+
|
|
16
|
+
other.instance_of?(SetExpression) &&
|
|
17
|
+
other.items.eql?(@items)
|
|
18
|
+
end
|
|
19
|
+
alias eql? ==
|
|
20
|
+
|
|
21
|
+
def hash
|
|
22
|
+
@hash ||= [self.class, @items].hash
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def variables
|
|
26
|
+
@variables ||= @items.flat_map(&:variables).uniq
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def evaluate(values)
|
|
30
|
+
@items.to_set { _1.evaluate(values) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def substitute(replacements)
|
|
34
|
+
return self unless replacements.keys.intersect?(variables)
|
|
35
|
+
|
|
36
|
+
substituted_items = @items.map { _1.substitute(replacements) }
|
|
37
|
+
|
|
38
|
+
SetExpression.new(substituted_items)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def to_s
|
|
42
|
+
"Set[#{@items.join(', ')}]"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class StringExpression < Expression
|
|
5
|
+
def initialize(parts)
|
|
6
|
+
super()
|
|
7
|
+
|
|
8
|
+
@parts = parts
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
attr_reader :parts
|
|
12
|
+
|
|
13
|
+
def ==(other)
|
|
14
|
+
return true if equal?(other)
|
|
15
|
+
|
|
16
|
+
other.instance_of?(StringExpression) &&
|
|
17
|
+
@parts.eql?(other.parts)
|
|
18
|
+
end
|
|
19
|
+
alias eql? ==
|
|
20
|
+
|
|
21
|
+
def hash
|
|
22
|
+
@hash ||= [self.class, @parts].hash
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def variables
|
|
26
|
+
@variables ||= @parts.flat_map(&:variables).uniq
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def evaluate(values)
|
|
30
|
+
@parts.map { _1.evaluate(values) }.join
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def substitute(replacements)
|
|
34
|
+
return self unless replacements.keys.intersect?(variables)
|
|
35
|
+
|
|
36
|
+
substituted_parts = @parts.map { _1.substitute(replacements) }
|
|
37
|
+
|
|
38
|
+
StringExpression.new(substituted_parts)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def to_s
|
|
42
|
+
parts = @parts.map do |part|
|
|
43
|
+
if part.is_a?(Constant) && part.value.is_a?(String)
|
|
44
|
+
part.value
|
|
45
|
+
else
|
|
46
|
+
"\#{#{part}}"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
"\"#{parts.join}\""
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class SymbolProc
|
|
5
|
+
def initialize(proc_or_symbol)
|
|
6
|
+
case proc_or_symbol
|
|
7
|
+
when Symbol
|
|
8
|
+
@symbol = proc_or_symbol
|
|
9
|
+
@proc = proc_or_symbol.to_proc
|
|
10
|
+
when Proc
|
|
11
|
+
@proc = proc_or_symbol
|
|
12
|
+
@symbol = get_symbol(proc_or_symbol)
|
|
13
|
+
else
|
|
14
|
+
raise "Expected Proc or Symbol, got #{proc_or_symbol.inspect}"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
attr_reader :symbol
|
|
19
|
+
|
|
20
|
+
def ==(other)
|
|
21
|
+
equal?(other) || other.instance_of?(SymbolProc) && @symbol == other.symbol
|
|
22
|
+
end
|
|
23
|
+
alias eql? ==
|
|
24
|
+
|
|
25
|
+
def hash
|
|
26
|
+
[self.class, @symbol].hash
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def variables
|
|
30
|
+
[]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def to_proc
|
|
34
|
+
@proc
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def to_s
|
|
38
|
+
"&#{@symbol.inspect}"
|
|
39
|
+
end
|
|
40
|
+
alias inspect to_s
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def get_symbol(proc)
|
|
45
|
+
receiver = Variable.actual # could be any
|
|
46
|
+
recorder = Recorder.new(receiver)
|
|
47
|
+
result = proc.call(recorder)
|
|
48
|
+
expression = Recorder.to_expression(result)
|
|
49
|
+
|
|
50
|
+
expression.method
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class Variable < Expression
|
|
5
|
+
WELL_KNOWN = %i[actual key value index parent original].freeze
|
|
6
|
+
|
|
7
|
+
WELL_KNOWN.each do |method|
|
|
8
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
9
|
+
def self.#{method} # def self.actual
|
|
10
|
+
@#{method} ||= Variable.new(:#{method}) # @actual ||= Variable.actual
|
|
11
|
+
end # end
|
|
12
|
+
RUBY
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.well_known?(symbol)
|
|
16
|
+
WELL_KNOWN.include?(symbol)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.cache(symbol, expression_cache: true)
|
|
20
|
+
return send(symbol) if well_known?(symbol)
|
|
21
|
+
|
|
22
|
+
expression_cache = ExpressionCache.current if expression_cache == true
|
|
23
|
+
|
|
24
|
+
if expression_cache
|
|
25
|
+
expression_cache.less_known_variable_for(symbol)
|
|
26
|
+
else
|
|
27
|
+
new(symbol)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.substitutions
|
|
32
|
+
Thread.current[:matcher_variable_substitutions]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.with_substitutions(**substitutions)
|
|
36
|
+
stack = Thread.current[:matcher_variable_substitutions] ||= HashStack.new
|
|
37
|
+
stack.push(substitutions)
|
|
38
|
+
|
|
39
|
+
begin
|
|
40
|
+
yield
|
|
41
|
+
ensure
|
|
42
|
+
stack.pop(substitutions)
|
|
43
|
+
Thread.current[:matcher_variable_substitutions] = nil if stack.empty?
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
attr_reader :symbol
|
|
48
|
+
|
|
49
|
+
def initialize(symbol)
|
|
50
|
+
super()
|
|
51
|
+
|
|
52
|
+
@symbol = symbol
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def variables
|
|
56
|
+
[@symbol]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def evaluate(values)
|
|
60
|
+
value = values[@symbol]
|
|
61
|
+
|
|
62
|
+
if value.nil? && !values.key?(@symbol)
|
|
63
|
+
raise "no value for #{@symbol.inspect}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
value
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def ==(other)
|
|
70
|
+
other.instance_of?(Variable) && other.symbol == @symbol
|
|
71
|
+
end
|
|
72
|
+
alias eql? ==
|
|
73
|
+
|
|
74
|
+
def hash
|
|
75
|
+
[self.class, @symbol].hash
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def substitute(replacements)
|
|
79
|
+
symbol = replacements[@symbol]
|
|
80
|
+
symbol ? Variable.new(symbol) : self
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def to_s
|
|
84
|
+
Variable.substitutions&.[](@symbol) || @symbol.to_s
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class HashStack
|
|
5
|
+
extend Forwardable
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@stacks = {}
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def_delegators :@stacks, :empty?, :key?, :keys
|
|
12
|
+
|
|
13
|
+
def [](key)
|
|
14
|
+
@stacks[key]&.last
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def push(hash)
|
|
18
|
+
hash.each do |k, v|
|
|
19
|
+
(@stacks[k] ||= []) << v
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def pop(hash)
|
|
24
|
+
hash.each_key do |k|
|
|
25
|
+
array = @stacks[k]
|
|
26
|
+
array.pop
|
|
27
|
+
|
|
28
|
+
@stacks.delete(k) if array.empty?
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def slice(*keys)
|
|
33
|
+
keys.each_with_object({}) do |k, h|
|
|
34
|
+
pair = @stacks.assoc(k)
|
|
35
|
+
|
|
36
|
+
next unless pair
|
|
37
|
+
|
|
38
|
+
h[k] = pair[1].last
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def merge(hash)
|
|
43
|
+
hash.default_proc = ->(h, k) { h[k] = self[k] }
|
|
44
|
+
hash
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def to_h
|
|
48
|
+
@stacks.transform_values(&:last)
|
|
49
|
+
end
|
|
50
|
+
alias to_hash to_h
|
|
51
|
+
end
|
|
52
|
+
end
|