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,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