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,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class IndexByMatcher < Base
5
+ include MappingUtils
6
+
7
+ def initialize(projection, matcher, negated: false)
8
+ super()
9
+
10
+ @projection = projection
11
+ @matcher = negated ? ~matcher : matcher
12
+ @original_matcher = matcher
13
+ @negated = negated
14
+ end
15
+
16
+ def negate
17
+ IndexByMatcher.new(@projection, @original_matcher, negated: !@negated)
18
+ end
19
+
20
+ def validate(state, &)
21
+ return validate_negated(state, &) if @negated
22
+
23
+ actual = state.actual
24
+
25
+ unless actual.respond_to?(:each)
26
+ state.errors << state.expected.responding_to(:each)
27
+ return
28
+ end
29
+
30
+ values = state.values
31
+ failed = false
32
+ index = {}
33
+ mapping = {}
34
+ duplicates = []
35
+
36
+ actual.each_with_index do |item, i|
37
+ key = @projection.evaluate(
38
+ values.merge(actual: item, index: i, original: actual),
39
+ )
40
+
41
+ mapped_index = mapping[key]
42
+
43
+ if mapped_index
44
+ duplicates << [key, i, mapped_index]
45
+ else
46
+ index[key] = item
47
+ mapping[key] = i
48
+ end
49
+
50
+ key
51
+ rescue CallError => e
52
+ state.errors[i] << e.message_for_errors(item)
53
+ failed = true
54
+ end
55
+
56
+ duplicates.each do |key, i, j|
57
+ state.errors[i] << state.expected(actual[i]).not.duplicate_by(@projection, key, j)
58
+ end
59
+
60
+ return if failed
61
+
62
+ errors = yield(@matcher, index, original: actual)
63
+ errors = map_errors2(errors, mapping) unless state.boolean?
64
+
65
+ state.errors << errors
66
+ end
67
+
68
+ def to_s
69
+ "#{'~' if @negated}index_by(#{@projection}, #{@original_matcher})"
70
+ end
71
+
72
+ private
73
+
74
+ def validate_negated(state)
75
+ actual = state.actual
76
+
77
+ return unless actual.respond_to?(:each)
78
+
79
+ values = state.values
80
+ index = {}
81
+ mapping = {}
82
+
83
+ actual.each_with_index do |item, i|
84
+ key = @projection.evaluate(
85
+ values.merge(actual: item, index: i, original: actual),
86
+ )
87
+
88
+ return nil if mapping.key?(key)
89
+
90
+ index[key] = item
91
+ mapping[key] = i
92
+
93
+ key
94
+ rescue CallError
95
+ return nil
96
+ end
97
+
98
+ errors = yield(@matcher, index, original: actual)
99
+
100
+ state.errors << map_errors2(errors, mapping)
101
+ end
102
+
103
+ def map_errors2(error, mapping)
104
+ map_errors(error) do |nested_error|
105
+ key = nested_error.key
106
+
107
+ next unless index_call?(key)
108
+
109
+ index = mapping[operand_of(key)]
110
+
111
+ NestedError.new(index_call_to(index), nested_error.child) if index
112
+ end
113
+ end
114
+
115
+ def mapped_base
116
+ return @mapped_base if @mapped_base
117
+
118
+ expression = @projection
119
+ with_index = expression.variables.include?(:index)
120
+ element = expression.free_symbol(:e)
121
+ parameters = [[:opt, element]]
122
+ parameters << %i[opt index] if with_index
123
+ substituted = expression.substitute(actual: element, original: :actual)
124
+ pair = ArrayExpression.new([substituted, Variable.new(element)])
125
+ block = Block.new(parameters, pair)
126
+
127
+ map = if with_index
128
+ enum_for_map = Call.new(Variable.actual, :map)
129
+ Call.new(enum_for_map, :with_index, [], {}, block)
130
+ else
131
+ Call.new(Variable.actual, :map, [], {}, block)
132
+ end
133
+
134
+ @mapped_base = Call.new(map, :to_h)
135
+ end
136
+ end
137
+
138
+ module MatcherBuilding
139
+ ##
140
+ # Matches against an indexed version of actual.
141
+ #
142
+ # This is really useful when validating an array of items where the order
143
+ # shouldn't matter.
144
+ # @example
145
+ # # matches:
146
+ # # [
147
+ # # { name: "bar", value: 2 },
148
+ # # { name: "foo", value: 1 },
149
+ # # ]
150
+ # index_by(_[:name], {
151
+ # "foo" => { name: "foo", value: 1 },
152
+ # "bar" => { name: "bar", value: 2 },
153
+ # })
154
+ # # alternatively:
155
+ # index_by(_[:name]) ^ {
156
+ # "foo" => { name: "foo", value: 1 },
157
+ # "bar" => { name: "bar", value: 2 },
158
+ # }
159
+ # @overload index_by(expression, matcher)
160
+ # @param expression [Expression]
161
+ # @param matcher [Base]
162
+ # @return [IndexByMatcher]
163
+ # @overload index_by(expression)
164
+ # @param expression [Expression]
165
+ # @return [Chain<IndexByMatcher>]
166
+ def index_by(expression, matcher = UNDEFINED)
167
+ return Chain.new { index_by(expression, _1) } if Matcher.undefined?(matcher)
168
+
169
+ expression = expression_of(expression)
170
+ matcher = matcher_of(matcher)
171
+
172
+ IndexByMatcher.new(expression, matcher)
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class InlineMatcher < Base
5
+ def initialize(matcher = nil, negatable: false, negated: false, &block)
6
+ raise 'no block given' unless block_given?
7
+
8
+ super()
9
+
10
+ @matcher = matcher
11
+ @negated = negated
12
+ @negatable = negatable
13
+ @block = block
14
+ end
15
+
16
+ attr_reader :matcher, :negated
17
+
18
+ def negate
19
+ return super unless @negatable
20
+
21
+ InlineMatcher.new(@matcher&.~, negatable: @negatable, negated: !@negated, &@block)
22
+ end
23
+
24
+ def receiver
25
+ @block.binding.receiver
26
+ end
27
+
28
+ class InlineContext
29
+ extend Forwardable
30
+
31
+ def initialize(matcher, state, y)
32
+ @matcher = matcher
33
+ @state = state
34
+ @yield = y
35
+ end
36
+
37
+ attr_reader :state
38
+
39
+ def_delegators :@state, *State.public_instance_methods(false) - %i[result]
40
+ def_delegators :@matcher, :receiver, :matcher, :negated
41
+
42
+ def _yield(matcher, act = @state.actual, **values)
43
+ @yield.call(matcher, act, **values)
44
+ end
45
+ end
46
+
47
+ def validate(state, &block)
48
+ context = InlineContext.new(self, state, block)
49
+ context.instance_exec(&@block)
50
+ end
51
+
52
+ def to_s
53
+ args = if @matcher
54
+ "(#{@negated ? ~@matcher : @matcher})"
55
+ else
56
+ ''
57
+ end
58
+
59
+ "#{'~' if @negated}inline#{args} { #{Utils.block_location(@block)} }"
60
+ end
61
+ end
62
+
63
+ module MatcherBuilding
64
+ ##
65
+ # Creates an anonymous custom matcher
66
+ #
67
+ # If you need more control to define your matching logic then +inline+
68
+ # may give you an alternative to implementing a new matcher class. Within
69
+ # the +inline+ block you have direct access to +actual+, +errors+, +_yield+
70
+ # and other state methods.
71
+ #
72
+ # @example
73
+ # # matches distinct arrays
74
+ # inline do
75
+ # indices = Hash.new
76
+ #
77
+ # actual.each_with_index do |e, i|
78
+ # if (original_index = indices[e])
79
+ # errors[i] << expected.not.duplicate(original_index)
80
+ # else
81
+ # indices[e] = i
82
+ # end
83
+ # end
84
+ # end
85
+ #
86
+ # @param matcher [Base] optionaly provide a child matcher. Call the matcher
87
+ # with +_yield matcher, actual, **values+
88
+ # @param negatable [true, false] set to true if your matching logic respects
89
+ # the negated flag. Otherwise, the default negation implementation is used.
90
+ # When +negated = true+ the child matcher is automatically negated.
91
+ # @yield inline context
92
+ # @return [InlineMatcher]
93
+ def inline(matcher = UNDEFINED, negatable: false, &)
94
+ matcher = Matcher.undefined?(matcher) ? nil : matcher_of(matcher)
95
+
96
+ InlineMatcher.new(matcher, negatable:, &)
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class KeysMatcher < Base
5
+ def initialize(keys, partial: false, negated: false)
6
+ super()
7
+
8
+ @keys = keys
9
+ @partial = partial
10
+ @negated = negated
11
+ @includes_expressions = keys.any? { _1.is_a?(Expression) }
12
+ end
13
+
14
+ def negate
15
+ KeysMatcher.new(@keys, partial: @partial, negated: !@negated)
16
+ end
17
+
18
+ def validate(state, &)
19
+ return validate_negated(state, &) if @negated
20
+
21
+ actual = state.actual
22
+
23
+ unless actual.is_a?(Hash)
24
+ state.errors << state.expected.kind_of(Hash)
25
+ return
26
+ end
27
+
28
+ actual_keys = actual.keys
29
+
30
+ expected_keys = if @includes_expressions
31
+ @keys.map { _1.is_a?(Expression) ? _1.evaluate(state.values) : _1 }
32
+ else
33
+ @keys
34
+ end
35
+
36
+ (expected_keys - actual_keys).each do |key|
37
+ state.errors << state.expected.having_key(key)
38
+ end
39
+
40
+ return if @partial
41
+
42
+ (actual_keys - expected_keys).each do |key|
43
+ state.errors << state.expected.not.having_key(key)
44
+ end
45
+ end
46
+
47
+ def to_s
48
+ helper = @partial ? 'partial_keys' : 'keys'
49
+ args = @keys.map(&:inspect).join(', ')
50
+
51
+ "#{'~' if @negated}#{helper}(#{args})"
52
+ end
53
+
54
+ private
55
+
56
+ def validate_negated(state)
57
+ actual = state.actual
58
+
59
+ return unless actual.is_a?(Hash)
60
+
61
+ actual_keys = actual.keys
62
+
63
+ expected_keys = if @includes_expressions
64
+ @keys.map { _1.is_a?(Expression) ? _1.evaluate(state.values) : _1 }
65
+ else
66
+ @keys
67
+ end
68
+
69
+ if @partial
70
+ return unless (expected_keys - actual_keys).empty?
71
+
72
+ state.errors.or!
73
+
74
+ (actual_keys & expected_keys).each do |key|
75
+ state.errors << state.expected.not.having_key(key)
76
+ end
77
+ else
78
+ return if actual_keys != expected_keys
79
+
80
+ state.errors.or!
81
+
82
+ actual_keys.each do |key|
83
+ state.errors << state.expected.not.having_key(key)
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ module MatcherBuilding
90
+ ##
91
+ # Matches all keys of a hash
92
+ # @example
93
+ # # matches { foo: 1, bar: 2 } but not { foo: 1, qux: 3 }
94
+ # keys(:foo, :bar)
95
+ # # using key expression, matches { "the_key" => 23 }
96
+ # let(my_key: :the_key) ^ keys(vars[:my_key].to_s)
97
+ # @param keys [Array] supports expressions
98
+ # @param partial [true, false] ignores extra keys when +true+
99
+ # @return [KeysMatcher]
100
+ # @see #partial_keys
101
+ def keys(*keys, partial: false)
102
+ keys.each_with_index do |key, i|
103
+ keys[i] = expression_or_value(key)
104
+ end
105
+
106
+ KeysMatcher.new(keys, partial:)
107
+ end
108
+
109
+ ##
110
+ # Matches hash if all given keys are included. Ignores extra keys.
111
+ # @example
112
+ # # matches { foo: 1, bar: 2 } but not { bar: 2 }
113
+ # partial_keys(:foo)
114
+ # @param keys [Array] supports expressions
115
+ # @return [KeysMatcher]
116
+ # @see #keys
117
+ def partial_keys(*keys)
118
+ keys(*keys, partial: true)
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class KindOfMatcher < Base
5
+ def self.cache(kind, matcher_cache = MatcherCache.current)
6
+ return new(kind) unless matcher_cache
7
+
8
+ (matcher_cache.kind_of_matchers ||= {})[kind] ||= new(kind)
9
+ end
10
+
11
+ def initialize(kind, negated: false)
12
+ super()
13
+
14
+ @kind = kind
15
+ @negated = negated
16
+ end
17
+
18
+ def negate
19
+ KindOfMatcher.new(@kind, negated: !@negated)
20
+ end
21
+
22
+ def validate(state)
23
+ state.errors << state.expected.not_if(@negated).kind_of(@kind) if
24
+ state.actual.is_a?(@kind) == @negated
25
+ end
26
+
27
+ def to_s
28
+ if @negated
29
+ "neg(#{@kind})"
30
+ else
31
+ @kind.to_s
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class LazyAllMatcher < Base
5
+ def initialize(matchers)
6
+ super()
7
+
8
+ @matchers = matchers
9
+ end
10
+
11
+ attr_reader :matchers
12
+
13
+ def negate
14
+ LazyAnyMatcher.new(@matchers.map(&:~))
15
+ end
16
+
17
+ def &(matcher)
18
+ matcher = Matcher.cache(matcher)
19
+
20
+ if matcher.is_a?(LazyAllMatcher)
21
+ LazyAllMatcher.new(@matchers + matcher.matchers)
22
+ else
23
+ LazyAllMatcher.new(@matchers + [matcher])
24
+ end
25
+ end
26
+
27
+ def validate(state)
28
+ last_error = EmptyError.instance
29
+
30
+ @matchers.each do |matcher|
31
+ last_error = yield matcher
32
+
33
+ break unless last_error.valid?
34
+ end
35
+
36
+ state.errors << last_error
37
+ end
38
+
39
+ def to_s
40
+ "lazy_all(#{@matchers.map(&:to_s).join(', ')})"
41
+ end
42
+ end
43
+
44
+ module MatcherBuilding
45
+ ##
46
+ # Matches all matchers lazily. Returns only the last match result (similar to &&)
47
+ # @example
48
+ # # matches 3 but not "foo"
49
+ # lazy_all(Integer, _ % 3 == 0)
50
+ # # alternatively:
51
+ # of(Integer) & _.positive?
52
+ # @param matchers [Array<Base>]
53
+ # @return [LazyAllMatcher]
54
+ # @see Base#&
55
+ # @see #lazy_any
56
+ # @see #all
57
+ def lazy_all(*matchers)
58
+ case matchers.length
59
+ when 0
60
+ AlwaysMatcher.instance
61
+ when 1
62
+ matcher_of(matchers[0])
63
+ else
64
+ LazyAllMatcher.new(matchers.map { matcher_of(_1) })
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class LazyAnyMatcher < Base
5
+ def initialize(matchers)
6
+ super()
7
+
8
+ @matchers = matchers
9
+ end
10
+
11
+ attr_reader :matchers
12
+
13
+ def negate
14
+ LazyAllMatcher.new(@matchers.map(&:~))
15
+ end
16
+
17
+ def |(matcher)
18
+ matcher = Matcher.cache(matcher)
19
+
20
+ if matcher.is_a?(LazyAnyMatcher)
21
+ LazyAnyMatcher.new(@matchers + matcher.matchers)
22
+ else
23
+ LazyAnyMatcher.new(@matchers + [matcher])
24
+ end
25
+ end
26
+
27
+ def validate(state)
28
+ last_error = EmptyError.instance
29
+
30
+ @matchers.each do |matcher|
31
+ last_error = yield matcher
32
+
33
+ break if last_error.valid?
34
+ end
35
+
36
+ state.errors << last_error
37
+ end
38
+
39
+ def to_s
40
+ "lazy_any(#{@matchers.map(&:to_s).join(', ')})"
41
+ end
42
+ end
43
+
44
+ module MatcherBuilding
45
+ ##
46
+ # Matches any matcher lazily. Returns only the last match result (similar to ||)
47
+ # @example
48
+ # # matches "foo" and 42 but not +nil+ or +true+
49
+ # lazy_any(String, Integer)
50
+ # # alternatively:
51
+ # of(String) | of(Integer)
52
+ # @param matchers [Array<Base>]
53
+ # @return [LazyAnyMatcher]
54
+ # @see Base#|
55
+ # @see #lazy_all
56
+ # @see #any
57
+ def lazy_any(*matchers)
58
+ case matchers.length
59
+ when 0
60
+ AlwaysMatcher.instance
61
+ when 1
62
+ matcher_of(matchers[0])
63
+ else
64
+ LazyAnyMatcher.new(matchers.map { matcher_of(_1) })
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class LetMatcher < Base
5
+ def initialize(assigns, matcher)
6
+ super()
7
+
8
+ @assigns = assigns
9
+ @matcher = matcher
10
+ end
11
+
12
+ def negate
13
+ LetMatcher.new(@assigns, ~@matcher)
14
+ end
15
+
16
+ def validate(state)
17
+ assigns = @assigns.transform_values do |v|
18
+ v = Expression.try_recorder(v)
19
+
20
+ case v
21
+ when Proc
22
+ Utils.call_block(v, state.values)
23
+ when Expression
24
+ v.evaluate(state.values)
25
+ else
26
+ v
27
+ end
28
+ end
29
+
30
+ state.errors << yield(@matcher, **assigns)
31
+ end
32
+
33
+ def to_s
34
+ assign_parts = @assigns.map do |key, value|
35
+ if value.is_a?(Proc)
36
+ "#{key}: ->(#{Utils.inspect_block_params(value)}) { ... }"
37
+ else
38
+ "#{key}: #{value.inspect}"
39
+ end
40
+ end
41
+
42
+ "let(#{assign_parts.join(', ')}) ^ #{Matcher.parenthesize(@matcher)}"
43
+ end
44
+ end
45
+
46
+ module MatcherBuilding
47
+ ##
48
+ # Sets values for given matcher
49
+ # @example
50
+ # # matches 1
51
+ # let({ a: 1 }, _ == vars[:a])
52
+ # # alternatively:
53
+ # let(a: 1) ^ (_ == vars[:a])
54
+ # @overload let(assigns, matcher)
55
+ # @param assigns [Hash]
56
+ # @param matcher [Base]
57
+ # @return [LetMatcher]
58
+ # @overload let(**kwargs)
59
+ # @param kwargs [Hash] same as assigns
60
+ # @return [Chain<LetMatcher>]
61
+ def let(assigns = nil, matcher = UNDEFINED, **kwargs)
62
+ raise "Cannot set both assigns and kwargs" if assigns && !kwargs.empty?
63
+
64
+ assigns ||= kwargs
65
+
66
+ return Chain.new { let(assigns, _1) } if Matcher.undefined?(matcher)
67
+
68
+ matcher = matcher_of(matcher)
69
+
70
+ LetMatcher.new(assigns, matcher)
71
+ end
72
+ end
73
+ end