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,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class ErrorCollector
5
+ def self.error_from(obj)
6
+ case obj
7
+ when String
8
+ ElementError.new(obj)
9
+ when Message
10
+ ElementError.new(obj)
11
+ when Error
12
+ obj
13
+ else
14
+ "Unexpected error object: #{obj.inspect}"
15
+ end
16
+ end
17
+
18
+ attr_reader :error
19
+
20
+ def initialize
21
+ @error = EmptyError.instance
22
+ @mode = :and
23
+ end
24
+
25
+ def empty?
26
+ @error.valid?
27
+ end
28
+
29
+ def or!
30
+ @mode = :or
31
+ self
32
+ end
33
+
34
+ def or?
35
+ @mode == :or
36
+ end
37
+
38
+ def and?
39
+ @mode == :and
40
+ end
41
+
42
+ class Brackets
43
+ def initialize(parent, key)
44
+ @parent = parent
45
+ @key = key
46
+ end
47
+
48
+ def error
49
+ @parent.error
50
+ end
51
+
52
+ def <<(error)
53
+ error = ErrorCollector.error_from(error)
54
+
55
+ return error if error.is_a?(EmptyError) || @key == Variable.actual
56
+
57
+ key = @key
58
+ key = Call.new(Variable.actual, :[], [Constant.new(key)]) unless key.is_a?(Expression)
59
+
60
+ @parent << NestedError.new(key, error)
61
+
62
+ @parent.error
63
+ end
64
+
65
+ def [](key)
66
+ Brackets.new(self, key)
67
+ end
68
+ end
69
+
70
+ def <<(error)
71
+ return @error if error.is_a?(EmptyError)
72
+
73
+ error = ErrorCollector.error_from(error)
74
+
75
+ case @error
76
+ when EmptyError
77
+ @error = error
78
+ when and? ? AndError : OrError
79
+ @error << error
80
+ else
81
+ if and?
82
+ @error &= error
83
+ else
84
+ @error |= error
85
+ end
86
+ end
87
+
88
+ @error
89
+ end
90
+
91
+ def [](key)
92
+ Brackets.new(self, key)
93
+ end
94
+
95
+ def clear
96
+ @error = EmptyError.instance
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class NestedError < Error
5
+ def self.from(key, child)
6
+ return child if child.is_a?(EmptyError) || key == Variable.actual
7
+
8
+ key = Call.new(Variable.actual, :[], [Constant.new(key)]) unless key.is_a?(Expression)
9
+
10
+ NestedError.new(key, child)
11
+ end
12
+
13
+ def self.key_to_s(key, path)
14
+ remaining = 20 + path.length
15
+ key.visit do |expr|
16
+ next if !expr.is_a?(Variable) || expr.symbol != :actual
17
+
18
+ remaining -= path.length
19
+ break if remaining < 0
20
+ end
21
+
22
+ if remaining >= 0 && key.variables.include?(:actual)
23
+ Variable.with_substitutions(actual: path) do
24
+ key.to_s
25
+ end
26
+ else
27
+ "#{path} -> #{key}"
28
+ end
29
+ end
30
+
31
+ attr_reader :key, :child
32
+
33
+ def initialize(key, child)
34
+ super()
35
+
36
+ @key = key
37
+ @child = child
38
+ end
39
+
40
+ def ==(other)
41
+ return true if equal?(other)
42
+
43
+ other.instance_of?(NestedError) &&
44
+ @key.eql?(other.key) &&
45
+ @child == other.child
46
+ end
47
+
48
+ def &(other)
49
+ return self if other.is_a?(EmptyError)
50
+
51
+ if other.is_a?(NestedError) && @key == other.key
52
+ NestedError.new(@key, @child & other.child)
53
+ elsif other.is_a?(AndError)
54
+ errors = other.children
55
+ index = errors.find_index { _1.is_a?(NestedError) && _1.key == @key }
56
+
57
+ if index
58
+ new_errors = errors.dup
59
+ new_errors[index] = self & errors[index]
60
+ else
61
+ new_errors = [self] + errors
62
+ end
63
+
64
+ AndError.new(new_errors)
65
+ else
66
+ AndError.new([self, other])
67
+ end
68
+ end
69
+
70
+ def |(other)
71
+ return self if other.is_a?(EmptyError)
72
+
73
+ if other.is_a?(NestedError) && other.key == @key
74
+ NestedError.new(@key, @child | other.child)
75
+ elsif other.is_a?(OrError)
76
+ errors = other.children
77
+ index = errors.find_index { _1.is_a?(NestedError) && _1.key == @key }
78
+
79
+ if index
80
+ new_errors = errors.dup
81
+ new_errors[index] = self | errors[index]
82
+ else
83
+ new_errors = [self] + errors
84
+ end
85
+
86
+ OrError.new(new_errors)
87
+ else
88
+ OrError.new([self, other])
89
+ end
90
+ end
91
+
92
+ def to_s
93
+ "#{@key} -> #{@child}"
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class OrError < Error
5
+ attr_reader :children
6
+
7
+ def self.from(children)
8
+ length = children.length
9
+
10
+ return EmptyError.instance if length == 0
11
+
12
+ children.reduce do |left, right|
13
+ if left.is_a?(OrError)
14
+ left << right
15
+ else
16
+ left | right
17
+ end
18
+ end
19
+ end
20
+
21
+ def initialize(children)
22
+ raise 'children fewer than 2' if children.length < 2
23
+
24
+ super()
25
+
26
+ @children = children
27
+ end
28
+
29
+ def ==(other)
30
+ return true if equal?(other)
31
+
32
+ other.instance_of?(OrError) &&
33
+ @children == other.children
34
+ end
35
+
36
+ def |(other)
37
+ return self if other.is_a?(EmptyError)
38
+
39
+ clone << other
40
+ end
41
+
42
+ def add(other)
43
+ case other
44
+ when OrError
45
+ right = other.children.dup
46
+
47
+ @children.each_with_index do |l, i|
48
+ next unless l.is_a?(NestedError)
49
+
50
+ index = right.find_index { _1.is_a?(NestedError) && _1.key == l.key }
51
+
52
+ @children[i] = l | right.delete_at(index) if index
53
+ end
54
+
55
+ @children.concat(right)
56
+ when NestedError
57
+ index = @children.find_index { _1.is_a?(NestedError) && _1.key == other.key }
58
+
59
+ if index
60
+ @children[index] |= other
61
+ else
62
+ @children << other
63
+ end
64
+ else
65
+ @children << other
66
+ end
67
+
68
+ self
69
+ end
70
+ alias << add
71
+
72
+ def clone
73
+ klone = super
74
+ klone.instance_exec do
75
+ @children = @children.dup
76
+ end
77
+
78
+ klone
79
+ end
80
+ alias dup clone
81
+
82
+ def to_s
83
+ "(#{@children.map(&:to_s).join(' | ')})"
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class ExpressionCache < ExpressionLabeler
5
+ def self.current(build_session = Matcher.build_session)
6
+ build_session[:_expression_cache] ||= new if build_session
7
+ end
8
+
9
+ def initialize
10
+ super
11
+
12
+ @cache = Hash.new.compare_by_identity
13
+ @index = [Variable.actual]
14
+ end
15
+
16
+ def [](expression)
17
+ @index[label(expression)]
18
+ end
19
+
20
+ def label(expression, actual_label = ROOT)
21
+ cached_label = @cache[expression]
22
+
23
+ return cached_label if cached_label
24
+
25
+ count = @label_count
26
+ label = super
27
+
28
+ if label > count
29
+ if expression.is_a?(Variable)
30
+ symbol = expression.symbol
31
+ expression = Variable.send(symbol) if Variable.well_known?(symbol)
32
+ end
33
+
34
+ @cache[expression] = label
35
+ @index[label] = expression
36
+ end
37
+
38
+ label
39
+ end
40
+
41
+ def constant_for(value)
42
+ label = label_for(@constant_labels, value)
43
+ @index[label] ||= Constant.new(value)
44
+ end
45
+
46
+ def variable_for(symbol)
47
+ return Variable.send(symbol) if Variable.well_known?(symbol)
48
+
49
+ less_known_variable_for(symbol)
50
+ end
51
+
52
+ def less_known_variable_for(symbol)
53
+ label = label_for(@variable_labels, symbol)
54
+ @index[label] ||= Variable.new(symbol)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class ExpressionLabeler
5
+ ROOT = 0
6
+
7
+ def initialize
8
+ @label_count = 0
9
+
10
+ @constant_labels = {}
11
+ @variable_labels = {}
12
+ @call_labels = {}
13
+ @block_labels = {}
14
+ @proc_labels = {}
15
+ @array_labels = {}
16
+ @set_labels = {}
17
+ @hash_labels = {}
18
+ @range_labels = {}
19
+ @string_labels = {}
20
+ @rescue_labels = {}
21
+ end
22
+
23
+ def label(expression, actual_label = ROOT)
24
+ case expression
25
+ when Constant
26
+ label_for(@constant_labels, expression.value)
27
+ when Variable
28
+ if expression.symbol == :actual
29
+ actual_label
30
+ else
31
+ label_for(@variable_labels, expression.symbol)
32
+ end
33
+ when Call
34
+ receiver_l = label(expression.receiver, actual_label)
35
+ args_l = expression.args.map { label(_1, actual_label) }
36
+ kwargs_l = expression.kwargs.transform_values { label(_1, actual_label) }
37
+ block_l = label_for_block(expression.block)
38
+
39
+ label_for(@call_labels, [receiver_l, expression.method, args_l, kwargs_l, block_l])
40
+ when ProcExpression
41
+ label_for(@proc_labels, expression.block)
42
+ when ArrayExpression
43
+ items_l = expression.items.map { label(_1, actual_label) }
44
+
45
+ label_for(@array_labels, items_l)
46
+ when SetExpression
47
+ items_l = expression.items.map { label(_1, actual_label) }
48
+
49
+ label_for(@set_labels, items_l)
50
+ when HashExpression
51
+ pairs_l = expression.pairs.flat_map do |k, v|
52
+ [label(k, actual_label), label(v, actual_label)]
53
+ end
54
+
55
+ label_for(@hash_labels, pairs_l)
56
+ when RangeExpression
57
+ begin_l = label(expression.begin, actual_label)
58
+ end_l = label(expression.end, actual_label)
59
+
60
+ label_for(@range_labels, [begin_l, end_l, expression.exclude_end?])
61
+ when StringExpression
62
+ parts_l = expression.parts.map { label(_1, actual_label) }
63
+
64
+ label_for(@string_labels, parts_l)
65
+ when RescueLastErrorExpression
66
+ expression_l = label(expression.expression, actual_label)
67
+
68
+ label_for(@rescue_labels, expression_l)
69
+ else
70
+ raise "unexpected expression: #{expression.inspect}"
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def label_for(index, key)
77
+ index[key] ||= (@label_count += 1)
78
+ end
79
+
80
+ def label_for_block(block)
81
+ case block
82
+ when Block
83
+ label_for(@block_labels, [block.parameters, label(block.expression)])
84
+ when SymbolProc
85
+ label_for(@block_labels, block.symbol)
86
+ else # nil, Proc
87
+ label_for(@block_labels, block)
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class ArrayExpression < 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?(ArrayExpression) &&
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.map { _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
+ ArrayExpression.new(substituted_items)
39
+ end
40
+
41
+ def to_s
42
+ "[#{@items.map(&:to_s).join(', ')}]"
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class Block
5
+ class Context
6
+ attr_reader :expression, :values
7
+
8
+ def initialize(expression, values)
9
+ @expression = expression
10
+ @values = values
11
+ end
12
+
13
+ def evaluate(values)
14
+ values.merge!(@values) { |_k, _l, r| r }
15
+
16
+ @expression.evaluate(values)
17
+ end
18
+ end
19
+
20
+ def self.build(expression_cache: nil, &block)
21
+ return SymbolProc.new(block) if
22
+ block.parameters == [[:req], [:rest]] &&
23
+ /\(&:(\w+[!?]?|".*")\)/.match?(block.to_s)
24
+
25
+ parameters = block.parameters
26
+ args = []
27
+ kwargs = {}
28
+ parameter_names = Set.new
29
+ variable_object_ids = Set.new
30
+
31
+ parameters.each do |type, name|
32
+ parameter_names << name
33
+ variable = Variable.cache(name, expression_cache:)
34
+ variable_object_ids << variable.object_id
35
+
36
+ case type
37
+ when :req, :opt
38
+ args << variable.to_recorder
39
+ when :key, :keyreq
40
+ kwargs[name] = variable.to_recorder
41
+ when :rest
42
+ raise "*#{name unless name == :*} not allowed"
43
+ when :keyrest
44
+ raise "**#{name unless name == :**} not allowed"
45
+ when :block
46
+ raise "&#{name unless name == :&} not allowed"
47
+ end
48
+ end
49
+
50
+ result = block.call(*args, **kwargs)
51
+ expression = Expression.of(result, expression_cache:)
52
+
53
+ return SymbolProc.new(expression.method) if
54
+ args.length == 1 &&
55
+ expression.is_a?(Call) &&
56
+ expression.unary? &&
57
+ expression.receiver == Recorder.to_expression(args[0])
58
+
59
+ ExpressionWalker.each_variable(expression) do |variable|
60
+ raise "parameter `#{variable.symbol}' shadows an outer variable" if
61
+ parameter_names.include?(variable.symbol) &&
62
+ !variable_object_ids.include?(variable.object_id)
63
+ end
64
+
65
+ new(parameters, expression)
66
+ end
67
+
68
+ attr_reader :parameters, :expression
69
+
70
+ def initialize(parameters, expression)
71
+ @parameters = parameters
72
+ @expression = expression
73
+ end
74
+
75
+ def ==(other)
76
+ return true if equal?(other)
77
+
78
+ other.instance_of?(Block) &&
79
+ @parameters.eql?(other.parameters) &&
80
+ @expression == other.expression
81
+ end
82
+ alias eql? ==
83
+
84
+ def hash
85
+ [self.class, @parameters, @expression].hash
86
+ end
87
+
88
+ def variables
89
+ @variables ||= @expression.variables - @parameters.map { _2 }
90
+ end
91
+
92
+ def substitute(replacements)
93
+ replacements = replacements.slice(*variables)
94
+
95
+ return self if replacements.empty?
96
+
97
+ expression = @expression.substitute(replacements)
98
+
99
+ Block.new(@parameters, expression)
100
+ end
101
+
102
+ def to_proc(values: nil)
103
+ @proc ||= begin
104
+ kwlist = @parameters.map { |_type, name| "#{name}:" }.join(', ')
105
+
106
+ instance_eval(<<~RUBY, __FILE__, __LINE__ + 1)
107
+ ->(#{arg_list}) { evaluate({ #{kwlist} }) } # ->(arg, kwarg:) { evaluate({ arg:, kwarg: }) }
108
+ RUBY
109
+ end
110
+
111
+ if values && !variables.empty?
112
+ values = values.slice(*variables)
113
+
114
+ return @proc if values.empty?
115
+
116
+ context = Context.new(@expression, values)
117
+
118
+ lambda do |*args, **kwargs|
119
+ context.instance_exec(*args, **kwargs, &@proc)
120
+ end
121
+ else
122
+ @proc
123
+ end
124
+ end
125
+
126
+ def to_s(as_block: false)
127
+ if @parameters.empty?
128
+ as_block ? "{ #{@expression} }" : "-> { #{@expression} }"
129
+ else
130
+ args = arg_list
131
+ as_block ? "{ |#{args}| #{@expression} }" : "->(#{args}) { #{@expression} }"
132
+ end
133
+ end
134
+ alias inspect to_s
135
+
136
+ private
137
+
138
+ def arg_list
139
+ @parameters.map do |type, name|
140
+ case type
141
+ when :req, :opt
142
+ name
143
+ when :key, :keyreq
144
+ "#{name}:"
145
+ end
146
+ end.join(', ')
147
+ end
148
+
149
+ def evaluate(values)
150
+ @expression.evaluate(**values)
151
+ end
152
+ end
153
+ end