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,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class ExpressionMatcher < Base
5
+ def self.cache(value, matcher_cache = MatcherCache.current, expression_cache = ExpressionCache.current)
6
+ return new(value) unless matcher_cache
7
+
8
+ cache = (matcher_cache.expression_matchers ||= {})
9
+ label = expression_cache.label(value)
10
+
11
+ cache[label] ||= new(value)
12
+ end
13
+
14
+ def self.message_rules
15
+ @message_rules ||= RuleSet.new
16
+ end
17
+
18
+ attr_reader :expression, :negated
19
+
20
+ def initialize(expression, negated: false)
21
+ super()
22
+
23
+ @expression = expression
24
+ @negated = negated
25
+ end
26
+
27
+ def negate
28
+ ExpressionMatcher.new(@expression, negated: !@negated)
29
+ end
30
+
31
+ def validate(state)
32
+ if state.boolean?
33
+ state.errors << 'invalid' if @negated != !@expression.evaluate(state.values)
34
+ return
35
+ end
36
+
37
+ value_tree = @expression.evaluate_tree(state.values)
38
+ evaluation = value_tree[-1]
39
+
40
+ if @negated != !evaluation
41
+ rule_context = MessageRuleContext.new(self, state)
42
+ state.errors << message_factory.create(rule_context, value_tree)
43
+ end
44
+ rescue CallError => e
45
+ state.errors << e.message_for_errors(state.actual) unless @negated
46
+ end
47
+
48
+ def to_s
49
+ if @negated
50
+ "neg(#{@expression})"
51
+ else
52
+ @expression.to_s
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ NEGATED_COMPARISONS = {
59
+ :== => :!=,
60
+ :!= => :==,
61
+ :< => :>=,
62
+ :> => :<=,
63
+ :>= => :<,
64
+ :<= => :>,
65
+ :=~ => :!~,
66
+ :!~ => :=~,
67
+ }.freeze
68
+
69
+ def message_factory
70
+ @message_factory ||= ExpressionMatcher.message_rules.apply(@expression)
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class FilterMatcher < Base
5
+ include MappingUtils
6
+
7
+ def initialize(filter, matcher, negated: false)
8
+ super()
9
+
10
+ @filter = filter
11
+ @matcher = negated ? ~matcher : matcher
12
+ @original_matcher = matcher
13
+ @negated = negated
14
+ end
15
+
16
+ def negate
17
+ FilterMatcher.new(@filter, @original_matcher, negated: !@negated)
18
+ end
19
+
20
+ def validate(state)
21
+ actual = state.actual
22
+
23
+ unless actual.respond_to?(:each)
24
+ state.errors << state.expected.responding_to(:each) unless @negated
25
+ return
26
+ end
27
+
28
+ i = 0
29
+ mapping = {}
30
+ items = []
31
+ failed = false
32
+
33
+ actual.each do |act|
34
+ filter_value = @filter.evaluate(
35
+ state.values.merge(actual: act, index: i, original: actual),
36
+ )
37
+
38
+ if filter_value
39
+ mapping[items.length] = i
40
+ items << act
41
+ end
42
+ rescue CallError => e
43
+ return nil if @negated
44
+
45
+ state.errors[i] << e.message_for_errors(act)
46
+ failed = true
47
+ ensure
48
+ i += 1
49
+ end
50
+
51
+ return if failed
52
+
53
+ errors = yield(@matcher, items, original: actual)
54
+
55
+ unless state.boolean?
56
+ errors = map_errors(errors) do |nested_error|
57
+ key = nested_error.key
58
+
59
+ next unless index_call?(key)
60
+
61
+ original_index = mapping[operand_of(key)]
62
+
63
+ NestedError.new(index_call_to(original_index), nested_error.child) if original_index
64
+ end
65
+ end
66
+
67
+ state.errors << errors
68
+ end
69
+
70
+ def to_s
71
+ "#{'~' if @negated}filter(#{@filter}, #{@original_matcher})"
72
+ end
73
+
74
+ private
75
+
76
+ def mapped_base
77
+ @mapped_base ||= map_base(:filter, @filter)
78
+ end
79
+ end
80
+
81
+ module MatcherBuilding
82
+ # Matches only filtered elements
83
+ # == +expression+ values
84
+ # - actual
85
+ # - index
86
+ # - original
87
+ # == +matcher+ values
88
+ # - original
89
+ # @example
90
+ # # matches [1, 2, 3, 4, 5]
91
+ # filter(_.odd?, [1, 3, 5])
92
+ # # alternatively:
93
+ # filter(_.odd?) ^ [1, 3, 5]
94
+ # @overload filter(expression, matcher)
95
+ # @param expression [Expression] matches elements for which +expression+ is truthy
96
+ # @param matcher
97
+ # @return [FilterMatcher]
98
+ # @overload filter(expression)
99
+ # @param expression [Expression] matches elements for which +expression+ is truthy
100
+ # @return [Chain<FilterMatcher>]
101
+ def filter(expression, matcher = UNDEFINED)
102
+ return Chain.new { filter(expression, _1) } if
103
+ Matcher.undefined?(matcher)
104
+
105
+ expression = expression_of(expression)
106
+ matcher = matcher_of(matcher)
107
+
108
+ FilterMatcher.new(expression, matcher)
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,223 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class HashMatcher < Base
5
+ def initialize(hash, partial: false, negated: false)
6
+ super()
7
+
8
+ @hash = negated ? hash.transform_values(&:~) : hash
9
+ @original_hash = hash
10
+ @partial = partial
11
+ @negated = negated
12
+ @includes_others = hash.include?(Others.instance)
13
+ @includes_optionals = hash.each_key.any? { _1.is_a?(Optional) }
14
+ @includes_expressions = hash.each_key.any? { _1.is_a?(Expression) }
15
+
16
+ raise 'cannot use partial(others => ...)' if @partial && @includes_others
17
+ end
18
+
19
+ def negate
20
+ HashMatcher.new(@original_hash, partial: @partial, negated: !@negated)
21
+ end
22
+
23
+ def validate(state, &)
24
+ return validate_negated(state, &) if @negated
25
+
26
+ actual = state.actual
27
+
28
+ unless actual.is_a?(Hash)
29
+ state.errors << state.expected.kind_of(Hash)
30
+ return
31
+ end
32
+
33
+ if @includes_expressions
34
+ expression_values = {}
35
+
36
+ @hash.each_key.with_index do |key, i|
37
+ key = key.value if key.is_a?(Optional)
38
+ expression_values[i] = key.evaluate(state.values) if key.is_a?(Expression)
39
+ end
40
+ end
41
+
42
+ expected_keys = if @includes_optionals || @includes_expressions
43
+ @hash.keys.map.with_index do |k, i|
44
+ k = k.value if k.is_a?(Optional)
45
+ k = expression_values[i] if k.is_a?(Expression)
46
+ k
47
+ end
48
+ else
49
+ @hash.keys
50
+ end
51
+
52
+ extra_keys = actual.keys - expected_keys
53
+
54
+ if !@partial && !@includes_others
55
+ extra_keys.each do |key|
56
+ state.errors[key] << state.expected.not.having_key(key)
57
+ end
58
+ end
59
+
60
+ @hash.each_with_index do |(key, value), i|
61
+ if key.is_a?(Others)
62
+ state.errors << yield(value, actual.slice(*extra_keys))
63
+
64
+ next
65
+ end
66
+
67
+ is_optional = key.is_a?(Optional)
68
+ key = key.value if is_optional
69
+ error_key = key
70
+ key = expression_values[i] if key.is_a?(Expression)
71
+ actual_value = actual[key]
72
+
73
+ if actual_value.nil? && !actual.key?(key)
74
+ state.errors << state.expected.having_key(key) unless is_optional
75
+ else
76
+ error = yield(value, actual_value, key:, parent: actual)
77
+
78
+ next if error.valid?
79
+
80
+ error_key = key_call_for(error_key) if error_key.is_a?(Expression)
81
+
82
+ state.errors[error_key] << error
83
+ end
84
+ end
85
+ end
86
+
87
+ def to_s
88
+ if @negated
89
+ @partial ? "~partial(#{@original_hash})" : "neg(#{@original_hash})"
90
+ else
91
+ @partial ? "partial(#{@hash})" : @hash.to_s
92
+ end
93
+ end
94
+
95
+ private
96
+
97
+ def validate_negated(state)
98
+ actual = state.actual
99
+
100
+ return unless actual.is_a?(Hash)
101
+
102
+ if @hash.empty?
103
+ if @partial
104
+ state.errors << state.report.kind_of(Hash)
105
+ elsif actual.empty?
106
+ state.errors << state.report.predicate(:empty?)
107
+ end
108
+
109
+ return
110
+ end
111
+
112
+ if @includes_expressions
113
+ expression_values = {}
114
+
115
+ @hash.each_key.with_index do |key, i|
116
+ key = key.value if key.is_a?(Optional)
117
+ expression_values[i] = key.evaluate(state.values) if key.is_a?(Expression)
118
+ end
119
+ end
120
+
121
+ expected_keys = if @includes_optionals || @includes_expressions
122
+ @hash.keys.map.with_index do |k, i|
123
+ k = k.value if k.is_a?(Optional)
124
+ k = expression_values[i] if k.is_a?(Expression)
125
+ k
126
+ end
127
+ else
128
+ @hash.keys
129
+ end
130
+
131
+ extra_keys = actual.keys - expected_keys
132
+
133
+ return if !@partial && !@includes_others && !extra_keys.empty?
134
+
135
+ collector = state.new_collector.or!
136
+
137
+ @hash.each_with_index do |(key, value), i|
138
+ if key.is_a?(Others)
139
+ result = yield(value, actual.slice(*extra_keys))
140
+
141
+ return if result.valid?
142
+
143
+ collector << result
144
+
145
+ next
146
+ end
147
+
148
+ is_optional = key.is_a?(Optional)
149
+ key = key.value if is_optional
150
+ error_key = key
151
+ key = expression_values[i] if key.is_a?(Expression)
152
+ actual_value = actual[key]
153
+
154
+ if actual_value.nil? && !actual.key?(key)
155
+ next if is_optional
156
+
157
+ return
158
+ end
159
+
160
+ result = yield(value, actual_value, key:, parent: actual)
161
+
162
+ return if result.valid?
163
+
164
+ error_key = key_call_for(error_key) if error_key.is_a?(Expression)
165
+
166
+ collector[error_key] << result
167
+ end
168
+
169
+ state.errors << if collector.empty?
170
+ state.report.predicate(:empty?)
171
+ else
172
+ collector.error
173
+ end
174
+ end
175
+
176
+ def key_call_for(key)
177
+ Call.new(Variable.actual, :[], [key])
178
+ end
179
+ end
180
+
181
+ module MatcherBuilding
182
+ ##
183
+ # Matches hash partially
184
+ # @example
185
+ # # matches { foo: 1, bar: 2 } but not { foo: 0, bar: 2 }
186
+ # partial(foo: 1)
187
+ # @param hash [Hash]
188
+ # @return [HashMatcher]
189
+ def partial(hash)
190
+ hash = hash.to_h do |k, v|
191
+ [expression_or_value(k), matcher_of(v)]
192
+ end
193
+
194
+ HashMatcher.new(hash, partial: true)
195
+ end
196
+
197
+ ##
198
+ # Matches nested hashes partially
199
+ # @example
200
+ # # matches { foo: { bar: 1, baz: 2 }, qux: 3 }
201
+ # partial_r(foo: { bar: 1 })
202
+ # # equivalent to:
203
+ # partial(foo: partial(bar: 1))
204
+ # @param hash [Hash]
205
+ # @return [HashMatcher]
206
+ def partial_r(hash)
207
+ hash = hash.to_h do |k, v|
208
+ [expression_or_value(k), partial_r_helper(v)]
209
+ end
210
+
211
+ HashMatcher.new(hash, partial: true)
212
+ end
213
+
214
+ def partial_r_helper(value)
215
+ if Recorder.recorder?(value) || !value.is_a?(Hash)
216
+ matcher_of(value)
217
+ else
218
+ partial_r(value)
219
+ end
220
+ end
221
+ private :partial_r_helper
222
+ end
223
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class ImplyMatcher < Base
5
+ attr_reader :condition, :matcher
6
+
7
+ def initialize(condition, matcher, negated: false)
8
+ super()
9
+
10
+ @condition = condition
11
+ @matcher = negated ? ~matcher : matcher
12
+ @original_matcher = matcher
13
+ @negated = negated
14
+ end
15
+
16
+ def negate
17
+ ImplyMatcher.new(@condition, @original_matcher, negated: !@negated)
18
+ end
19
+
20
+ def validate(state, &)
21
+ return validate_negated(state, &) if @negated
22
+
23
+ if @condition.is_a?(ExpressionMatcher)
24
+ begin
25
+ # evaluate expression directly
26
+ return unless @condition.expression.evaluate(state.values)
27
+ rescue CallError
28
+ return
29
+ end
30
+ else
31
+ return unless yield(@condition).valid?
32
+ end
33
+
34
+ state.errors << yield(@matcher)
35
+ end
36
+
37
+ def to_s
38
+ "#{'~' if @negated}imply(#{@condition}, #{@original_matcher})"
39
+ end
40
+
41
+ private
42
+
43
+ def validate_negated(state)
44
+ condition_errors = yield @condition
45
+
46
+ state.errors << if condition_errors.valid?
47
+ yield(@matcher)
48
+ else
49
+ condition_errors
50
+ end
51
+ end
52
+ end
53
+
54
+ module MatcherBuilding
55
+ ##
56
+ # Matches only for given condition. Passes otherwise.
57
+ # @example
58
+ # # matches "hello" and 42 but not "hi"
59
+ # imply(String, _.length <= 4)
60
+ # # alternatively:
61
+ # imply(String) ^ (_.length <= 4)
62
+ # @overload imply(condition, matcher)
63
+ # @param condition [Expression]
64
+ # @param matcher [Base]
65
+ # @return [ImplyMatcher]
66
+ # @overload imply(condition)
67
+ # @param condition [Expression]
68
+ # @return [Chain<ImplyMatcher>]
69
+ # @see Base#>>
70
+ # @see #imply_one
71
+ # @see #imply_any
72
+ def imply(condition, matcher = UNDEFINED)
73
+ return Chain.new { imply(condition, _1) } if Matcher.undefined?(matcher)
74
+
75
+ condition = matcher_of(condition)
76
+ matcher = matcher_of(matcher)
77
+
78
+ ImplyMatcher.new(condition, matcher)
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class ImplySomeMatcher < Base
5
+ def self.check(matchers, else_matcher, count)
6
+ raise "count must be a positive integer or :any. Got #{count.inspect}" if
7
+ count != :any && (!count.is_a?(Integer) || count <= 0)
8
+
9
+ raise 'else cannot be combined with count > 1' if
10
+ else_matcher && count != :any && count != 1
11
+
12
+ invalid_matcher = matchers.find { !_1.is_a?(ImplyMatcher) }
13
+
14
+ raise "Not an ImplyMatcher: #{invalid_matcher.inspect}" if invalid_matcher
15
+ end
16
+
17
+ def initialize(matchers, else_matcher, count)
18
+ ImplySomeMatcher.check(matchers, else_matcher, count)
19
+
20
+ super()
21
+
22
+ @matchers = matchers
23
+ @else_matcher = else_matcher
24
+ @count = count
25
+ end
26
+
27
+ def negate
28
+ NegatedImplySomeMatcher.new(@matchers, @else_matcher, @count)
29
+ end
30
+
31
+ def validate(state)
32
+ errors = state.errors
33
+ matchers = @matchers.filter { yield(_1.condition).valid? }
34
+
35
+ if matchers.empty?
36
+ errors << if @else_matcher
37
+ yield @else_matcher
38
+ else
39
+ state.report.namespace(:imply_some).no_condition_satisfied(@matchers.map(&:condition), @count)
40
+ end
41
+
42
+ return
43
+ elsif @count != :any && matchers.length != @count
44
+ errors << state.report.namespace(:imply_some).x_conditions_satisfied(matchers.map(&:condition), @count)
45
+ end
46
+
47
+ matchers.each { errors << yield(_1.matcher) }
48
+ end
49
+
50
+ def to_s
51
+ args = @matchers.map(&:to_s)
52
+
53
+ case @count
54
+ when :any
55
+ method = 'imply_any'
56
+ when 1
57
+ method = 'imply_one'
58
+ else
59
+ method = 'imply_some'
60
+ args << "count: #{@count}"
61
+ end
62
+
63
+ args << "else: #{@else_matcher}" if @else_matcher
64
+
65
+ "#{method}(#{args.join(', ')})"
66
+ end
67
+ end
68
+
69
+ module MatcherBuilding
70
+ ##
71
+ # Matches exactly one implied matcher
72
+ # @example
73
+ # # matches "foo" and 1 but not "BAR", -1, or nil
74
+ # imply_one(
75
+ # imply(String, _ == _.downcase),
76
+ # imply(Integer, _.positive?),
77
+ # )
78
+ # @param *matchers [ImplyMatcher]
79
+ # @param else [Base] if no condition passed match against +else+ matcher.
80
+ # @return [ImplySomeMatcher]
81
+ # @see #imply
82
+ def imply_one(*matchers, else: UNDEFINED)
83
+ els = { else: }[:else]
84
+ else_matcher = Matcher.undefined?(els) ? nil : matcher_of(els)
85
+
86
+ ImplySomeMatcher.new(matchers, else_matcher, 1)
87
+ end
88
+
89
+ ##
90
+ # Matches at least one implied matcher
91
+ # @example
92
+ # # matches 9, 12, 40 but not 8, 21, 15.5
93
+ # imply_any(
94
+ # imply(_.even?, _ > 10),
95
+ # imply(_ % 3 == 0, _ < 20),
96
+ # )
97
+ # @param *matchers [ImplyMatcher]
98
+ # @param else [Base] if no condition passed match against +else+ matcher.
99
+ # @return [ImplySomeMatcher]
100
+ # @see #imply
101
+ def imply_any(*matchers, else: UNDEFINED)
102
+ els = { else: }[:else]
103
+ else_matcher = Matcher.undefined?(els) ? nil : matcher_of(els)
104
+
105
+ ImplySomeMatcher.new(matchers, else_matcher, :any)
106
+ end
107
+
108
+ def imply_some(*matchers, count:)
109
+ ImplySomeMatcher.new(matchers, nil, count)
110
+ end
111
+ end
112
+ end