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,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class EachPairMatcher < Base
5
+ def initialize(matcher)
6
+ super()
7
+
8
+ @matcher = matcher
9
+ end
10
+
11
+ def negate
12
+ NegatedEachPairMatcher.new(@matcher)
13
+ end
14
+
15
+ def validate(state)
16
+ actual = state.actual
17
+
18
+ unless actual.respond_to?(:each_pair)
19
+ state.errors << state.expected.responding_to(:each_pair)
20
+ return
21
+ end
22
+
23
+ actual.each_pair do |key, value|
24
+ state.errors[key] << yield(
25
+ @matcher,
26
+ [key, value],
27
+ key: key,
28
+ value: value,
29
+ parent: actual
30
+ )
31
+ end
32
+ end
33
+
34
+ def to_s
35
+ "each_pair(#{@matcher})"
36
+ end
37
+ end
38
+
39
+ module MatcherDsl
40
+ ##
41
+ # Matches each hash entry
42
+ # == +matcher+ values
43
+ # - key
44
+ # - value
45
+ # - parent
46
+ # @example
47
+ # # matches { foo: "foo" } but not { foo: "bar" }
48
+ # each_pair(k.to_s == v)
49
+ # # alternatively:
50
+ # each_pair ^ (k.to_s == v)
51
+ # @overload each_pair(matcher)
52
+ # @param matcher [Base]
53
+ # @return [EachPairMatcher]
54
+ # @overload each_pair
55
+ # @return [Chain<EachPairMatcher>]
56
+ # @see #each_key
57
+ # @see #each_value
58
+ def each_pair(matcher = UNDEFINED)
59
+ return Chain.new { each_pair(_1) } if Matcher.undefined?(matcher)
60
+
61
+ EachPairMatcher.new(matcher_of(matcher))
62
+ end
63
+
64
+ ##
65
+ # Matches each hash key
66
+ # == +matcher+ values
67
+ # - key
68
+ # - value
69
+ # - parent
70
+ # @example
71
+ # # matches { foo: 1, bar: 2 } but not { "foo" => 1, "bar" => 2 }
72
+ # each_key(Symbol)
73
+ # # alternatively:
74
+ # each_key ^ Symbol
75
+ # @overload each_key(matcher)
76
+ # @param matcher [Base]
77
+ # @return [EachPairMatcher]
78
+ # @overload each_key
79
+ # @return [Chain<EachPairMatcher>]
80
+ # @see #each_pair
81
+ def each_key(matcher = UNDEFINED)
82
+ return Chain.new { each_key(_1) } if Matcher.undefined?(matcher)
83
+
84
+ matcher = matcher_of(matcher)
85
+
86
+ EachPairMatcher.new(
87
+ ProjectMatcher.new(Variable.key, matcher),
88
+ )
89
+ end
90
+
91
+ ##
92
+ # Matches each hash value
93
+ # == +matcher+ values
94
+ # - key
95
+ # - value
96
+ # - parent
97
+ # @example
98
+ # # matches { a: "foo", b: "bar" } but not { a: 1, b: 2 }
99
+ # each_value(String)
100
+ # # alternatively:
101
+ # each_value ^ String
102
+ # @overload each_value(matcher)
103
+ # @param matcher [Base]
104
+ # @return [EachPairMatcher]
105
+ # @overload each_value
106
+ # @return [Chain<EachPairMatcher>]
107
+ # @see #each_pair
108
+ def each_value(matcher = UNDEFINED)
109
+ return Chain.new { each_value(_1) } if Matcher.undefined?(matcher)
110
+
111
+ assigns = { actual: Variable.value }
112
+ matcher = matcher_of(matcher)
113
+
114
+ EachPairMatcher.new(
115
+ LetMatcher.new(assigns, matcher),
116
+ )
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class EqualMatcher < Base
5
+ CACHEABLE_CLASSES = [
6
+ NilClass,
7
+ FalseClass,
8
+ TrueClass,
9
+ Integer,
10
+ Float,
11
+ Symbol,
12
+ String,
13
+ Regexp,
14
+ Module,
15
+ ].freeze
16
+
17
+ def self.cache(value, matcher_cache = MatcherCache.current)
18
+ return new(value) if !matcher_cache ||
19
+ !CACHEABLE_CLASSES.include?(value) ||
20
+ value.is_a?(String) && !value.frozen?
21
+
22
+ (matcher_cache.equal_matchers ||= {})[value] ||= new(value)
23
+ end
24
+
25
+ def initialize(value, negated: false)
26
+ super()
27
+
28
+ @value = value
29
+ @negated = negated
30
+ end
31
+
32
+ def negate
33
+ EqualMatcher.new(@value, negated: !@negated)
34
+ end
35
+
36
+ def validate(state)
37
+ value = @value.is_a?(Expression) ? @value.evaluate(state.values) : @value
38
+
39
+ if @negated
40
+ errors = state.errors.or!
41
+
42
+ catch(:valid) do
43
+ validate_negated_helper(state, errors, value, state.actual)
44
+
45
+ # prevent clearing errors
46
+ return
47
+ end
48
+
49
+ # caught :valid
50
+ errors.clear
51
+ else
52
+ validate_helper(state, state.errors, value, state.actual)
53
+ end
54
+ end
55
+
56
+ IMPLICIT_MATCHER_CLASSES =
57
+ [Module, Range, Regexp, Hash, Array, Expression].freeze
58
+
59
+ def to_s
60
+ if IMPLICIT_MATCHER_CLASSES.any? { @value.is_a?(_1) }
61
+ "#{'~' if @negated}equal(#{@value.inspect})"
62
+ else
63
+ @negated ? "neg(#{@value.inspect})" : @value.inspect
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ def validate_helper(state, errors, exp, act)
70
+ case exp
71
+ when Array
72
+ validate_array(state, errors, exp, act)
73
+ when Hash
74
+ validate_hash(state, errors, exp, act)
75
+ when Set
76
+ validate_set(state, errors, exp, act)
77
+ else
78
+ errors << state.expected(act).not_if(@negated).equal(exp) if
79
+ @negated ^ (act != exp)
80
+ end
81
+ end
82
+
83
+ def validate_array(state, errors, exp, act)
84
+ unless act.is_a?(Array)
85
+ errors << state.expected(act).kind_of(Array)
86
+ return
87
+ end
88
+
89
+ errors << state.expected(act).length_of(exp.length, act.length) if
90
+ exp.length != act.length
91
+
92
+ [exp.length, act.length].min.times do |i|
93
+ validate_helper(state, errors[i], exp[i], act[i])
94
+ end
95
+ end
96
+
97
+ def validate_hash(state, errors, exp, act)
98
+ unless act.is_a?(Hash)
99
+ errors << state.expected(act).kind_of(Hash)
100
+ return
101
+ end
102
+
103
+ (act.keys - exp.keys).each do |key|
104
+ errors[key] << state.expected(act).not.having_key(key)
105
+ end
106
+
107
+ exp.each do |key, exp_value|
108
+ act_value = act[key]
109
+
110
+ if act_value.nil? && !act.key?(key)
111
+ errors << state.expected(act).having_key(key)
112
+ else
113
+ validate_helper(state, errors[key], exp_value, act_value)
114
+ end
115
+ end
116
+ end
117
+
118
+ def validate_set(state, errors, exp, act)
119
+ unless act.is_a?(Set)
120
+ errors << state.expected(act).kind_of(Set)
121
+ return
122
+ end
123
+
124
+ (exp - act).each do |item|
125
+ errors << state.expected(act).including(item)
126
+ end
127
+
128
+ (act - exp).each do |item|
129
+ errors << state.expected(act).not.including(item)
130
+ end
131
+ end
132
+
133
+ def validate_negated_helper(state, errors, exp, act)
134
+ case exp
135
+ when Array
136
+ validate_array_negated(state, errors, exp, act)
137
+ when Hash
138
+ validate_hash_negated(state, errors, exp, act)
139
+ when Set
140
+ validate_set_negated(state, errors, exp, act)
141
+ else
142
+ if act == exp
143
+ errors << state.expected(act).not.equal(exp)
144
+ else
145
+ throw(:valid)
146
+ end
147
+ end
148
+ end
149
+
150
+ def validate_array_negated(state, errors, exp, act)
151
+ throw(:valid) if !act.is_a?(Array) || exp.length != act.length
152
+
153
+ exp.length.times do |i|
154
+ validate_negated_helper(state, errors[i], exp[i], act[i])
155
+ end
156
+ end
157
+
158
+ def validate_hash_negated(state, errors, exp, act)
159
+ throw(:valid) unless act.is_a?(Hash)
160
+
161
+ exp_keys_set = Set.new(exp.keys)
162
+ throw(:valid) unless act.keys.all? { exp_keys_set.include?(_1) }
163
+
164
+ exp.each do |key, exp_value|
165
+ act_value = act[key]
166
+
167
+ if act_value.nil? && !act.key?(key)
168
+ throw(:valid)
169
+ else
170
+ validate_negated_helper(state, errors[key], exp_value, act_value)
171
+ end
172
+ end
173
+ end
174
+
175
+ def validate_set_negated(state, errors, exp, act)
176
+ throw(:valid) if !act.is_a?(Set) || act != exp
177
+
178
+ exp.each do |item|
179
+ errors << state.expected(act).not.including(item)
180
+ end
181
+ end
182
+ end
183
+
184
+ module MatcherDsl
185
+ ##
186
+ # Matches equal value
187
+ # @example
188
+ # # matches String but not "foo"
189
+ # equal(String)
190
+ # @param value
191
+ # @return [EqualMatcher]
192
+ def equal(value)
193
+ value = expression_or_value(value)
194
+
195
+ EqualMatcher.cache(value, @matcher_cache)
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ ##
5
+ # Match array elements like a set.
6
+ # @example
7
+ # m = Matcher.build { equal_set(1, 2, 3) }
8
+ #
9
+ # m.match?([1, 2, 3]) # => true
10
+ # m.match?([3, 2, 1]) # => true
11
+ #
12
+ # m.match([1, 1, 3])
13
+ # # > root[1]: did not expect duplicate originally at index 0 but got 1
14
+ # # > root: expected 2 to be included but got [1, 1, 3]
15
+ class EqualSetMatcher < Base
16
+ def initialize(items, negated: false)
17
+ super()
18
+
19
+ @items = items
20
+ @negated = negated
21
+ @includes_expressions = items.any?(Expression)
22
+ end
23
+
24
+ def negate
25
+ EqualSetMatcher.new(@items, negated: !@negated)
26
+ end
27
+
28
+ def validate(state)
29
+ return validate_negated(state) if @negated
30
+
31
+ actual = state.actual
32
+
33
+ unless actual.respond_to?(:each)
34
+ state.errors << state.expected.responding_to(:each)
35
+ return
36
+ end
37
+
38
+ expected_set = item_set(state.values)
39
+ missing = expected_set.dup
40
+
41
+ actual.each_with_index do |act, i|
42
+ if expected_set.include?(act)
43
+ unless missing.delete?(act)
44
+ original_index = index_of(actual, act)
45
+ state.errors[i] << state.expected(act).not.duplicate(original_index)
46
+ end
47
+ else
48
+ state.errors[i] << state.expected(act).not.in(state.actual)
49
+ end
50
+ end
51
+
52
+ missing.each do |m|
53
+ state.errors << state.expected.including(m)
54
+ end
55
+ end
56
+
57
+ def to_s
58
+ "#{'~' if @negated}equal_set(#{@items.join(', ')})"
59
+ end
60
+
61
+ private
62
+
63
+ def item_set(values)
64
+ if @includes_expressions
65
+ @items.to_set do |item|
66
+ item.is_a?(Expression) ? item.evaluate(values) : item
67
+ end
68
+ else
69
+ @item_set ||= Set.new(@items)
70
+ end
71
+ end
72
+
73
+ def validate_negated(state)
74
+ actual = state.actual
75
+
76
+ return unless actual.respond_to?(:each)
77
+
78
+ expected_set = item_set(state.values)
79
+ missing = expected_set.dup
80
+
81
+ actual.each do |act|
82
+ return nil if !expected_set.include?(act) || !missing.delete?(act)
83
+ end
84
+
85
+ return unless missing.empty?
86
+
87
+ state.errors << state.expected.namespace(:set).not.equal(@items)
88
+ end
89
+
90
+ def index_of(collection, item)
91
+ collection = collection.enum_for(:each) unless
92
+ collection.respond_to?(:find_index)
93
+
94
+ collection.find_index(item)
95
+ end
96
+ end
97
+
98
+ module MatcherDsl
99
+ ##
100
+ # Matches array elements like a set
101
+ # @example
102
+ # # matches [1, 2, 3] and [3, 2, 1] but neither [0, 1, 2] nor [1, 1, 2, 3]
103
+ # equal_set(1, 2, 3)
104
+ # @param items [Array]
105
+ # @return [EqualSetMatcher]
106
+ def equal_set(*items)
107
+ items.map! { expression_or_value(_1) }
108
+
109
+ EqualSetMatcher.new(items)
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class ExpressionMatcher < Base
5
+ def self.cache(
6
+ value,
7
+ matcher_cache = MatcherCache.current,
8
+ expression_cache = ExpressionCache.current
9
+ )
10
+ return new(value) unless matcher_cache
11
+
12
+ cache = (matcher_cache.expression_matchers ||= {})
13
+ label = expression_cache.label(value)
14
+
15
+ cache[label] ||= new(value)
16
+ end
17
+
18
+ def self.message_rules
19
+ @message_rules ||= RuleSet.new
20
+ end
21
+
22
+ attr_reader :expression, :negated
23
+
24
+ def initialize(expression, negated: false)
25
+ super()
26
+
27
+ @expression = expression
28
+ @negated = negated
29
+ end
30
+
31
+ def negate
32
+ ExpressionMatcher.new(@expression, negated: !@negated)
33
+ end
34
+
35
+ def validate(state)
36
+ if state.boolean?
37
+ if @negated != !@expression.evaluate(state.values)
38
+ state.errors << "invalid"
39
+ end
40
+
41
+ return
42
+ end
43
+
44
+ value_tree = @expression.evaluate_tree(state.values)
45
+ evaluation = value_tree[-1]
46
+
47
+ if @negated != !evaluation
48
+ rule_context = MessageRuleContext.new(self, state)
49
+ state.errors << message_factory.create(rule_context, value_tree)
50
+ end
51
+ rescue CallError => e
52
+ state.errors << e.message_for_errors(state.actual) unless @negated
53
+ end
54
+
55
+ def to_s
56
+ if @negated
57
+ "neg(#{@expression})"
58
+ else
59
+ @expression.to_s
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def message_factory
66
+ @message_factory ||= ExpressionMatcher.message_rules.apply(@expression)
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,115 @@
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
+ if original_index
64
+ NestedError.new(index_call_to(original_index), nested_error.child)
65
+ end
66
+ end
67
+ end
68
+
69
+ state.errors << errors
70
+ end
71
+
72
+ def to_s
73
+ "#{'~' if @negated}filter(#{@filter}, #{@original_matcher})"
74
+ end
75
+
76
+ private
77
+
78
+ def mapped_base
79
+ @mapped_base ||= map_base(:filter, @filter)
80
+ end
81
+ end
82
+
83
+ module MatcherDsl
84
+ # Matches only filtered elements
85
+ # == +expression+ values
86
+ # - actual
87
+ # - index
88
+ # - original
89
+ # == +matcher+ values
90
+ # - original
91
+ # @example
92
+ # # matches [1, 2, 3, 4, 5]
93
+ # filter(_.odd?, [1, 3, 5])
94
+ # # alternatively:
95
+ # filter(_.odd?) ^ [1, 3, 5]
96
+ # @overload filter(expression, matcher)
97
+ # @param expression [Expression] matches elements for which +expression+
98
+ # is truthy
99
+ # @param matcher
100
+ # @return [FilterMatcher]
101
+ # @overload filter(expression)
102
+ # @param expression [Expression] matches elements for which +expression+
103
+ # is truthy
104
+ # @return [Chain<FilterMatcher>]
105
+ def filter(expression, matcher = UNDEFINED)
106
+ return Chain.new { filter(expression, _1) } if
107
+ Matcher.undefined?(matcher)
108
+
109
+ expression = expression_of(expression)
110
+ matcher = matcher_of(matcher)
111
+
112
+ FilterMatcher.new(expression, matcher)
113
+ end
114
+ end
115
+ end