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,101 @@
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(
22
+ @matcher&.~, negatable: @negatable, negated: !@negated, &@block
23
+ )
24
+ end
25
+
26
+ def receiver
27
+ @block.binding.receiver
28
+ end
29
+
30
+ class InlineContext
31
+ extend Forwardable
32
+
33
+ def initialize(matcher, state, block)
34
+ @matcher = matcher
35
+ @state = state
36
+ @block = block
37
+ end
38
+
39
+ attr_reader :state
40
+
41
+ def_delegators :@state, *State.public_instance_methods(false) - %i[result]
42
+ def_delegators :@matcher, :receiver, :matcher, :negated
43
+
44
+ def _yield(matcher, act = @state.actual, **values)
45
+ @block.call(matcher, act, **values)
46
+ end
47
+ end
48
+
49
+ def validate(state, &block)
50
+ context = InlineContext.new(self, state, block)
51
+ context.instance_exec(&@block)
52
+ end
53
+
54
+ def to_s
55
+ args = if @matcher
56
+ "(#{@negated ? ~@matcher : @matcher})"
57
+ else
58
+ ""
59
+ end
60
+
61
+ "#{'~' if @negated}inline#{args} { #{Utils.block_location(@block)} }"
62
+ end
63
+ end
64
+
65
+ module MatcherDsl
66
+ ##
67
+ # Creates an anonymous custom matcher
68
+ #
69
+ # If you need more control to define your matching logic then +inline+
70
+ # may give you an alternative to implementing a new matcher class. Within
71
+ # the +inline+ block you have direct access to +actual+, +errors+, +_yield+,
72
+ # and other state methods.
73
+ #
74
+ # @example
75
+ # # matches distinct arrays
76
+ # inline do
77
+ # indices = Hash.new
78
+ #
79
+ # actual.each_with_index do |e, i|
80
+ # if (original_index = indices[e])
81
+ # errors[i] << expected.not.duplicate(original_index)
82
+ # else
83
+ # indices[e] = i
84
+ # end
85
+ # end
86
+ # end
87
+ #
88
+ # @param matcher [Base] optionally provide a child matcher. Call the matcher
89
+ # with +_yield matcher, actual, **values+
90
+ # @param negatable [true, false] set to true if your matching logic respects
91
+ # the negated flag. Otherwise, the default negation implementation is
92
+ # used. When +negated = true+ the child matcher is automatically negated.
93
+ # @yield inline context
94
+ # @return [InlineMatcher]
95
+ def inline(matcher = UNDEFINED, negatable: false, &)
96
+ matcher = Matcher.undefined?(matcher) ? nil : matcher_of(matcher)
97
+
98
+ InlineMatcher.new(matcher, negatable:, &)
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ ##
5
+ # Match hash keys.
6
+ # @example
7
+ # m = Matcher.build { keys(:foo, :bar) }
8
+ #
9
+ # m.match?({ foo: 1, bar: 2 })
10
+ # # => true
11
+ # m.match({ foo: 1, qux: 3 })
12
+ # # > root: expected to include key :bar but got {:foo=>1, :qux=>3}
13
+ # # > root: did not expect to include key :qux but got {:foo=>1, :qux=>3}
14
+ class KeysMatcher < Base
15
+ def initialize(keys, partial: false, negated: false)
16
+ super()
17
+
18
+ @keys = keys
19
+ @partial = partial
20
+ @negated = negated
21
+ @includes_expressions = keys.any?(Expression)
22
+ end
23
+
24
+ def negate
25
+ KeysMatcher.new(@keys, partial: @partial, 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.is_a?(Hash)
34
+ state.errors << state.expected.kind_of(Hash)
35
+ return
36
+ end
37
+
38
+ actual_keys = actual.keys
39
+
40
+ expected_keys = if @includes_expressions
41
+ @keys.map { _1.is_a?(Expression) ? _1.evaluate(state.values) : _1 }
42
+ else
43
+ @keys
44
+ end
45
+
46
+ (expected_keys - actual_keys).each do |key|
47
+ state.errors << state.expected.having_key(key)
48
+ end
49
+
50
+ return if @partial
51
+
52
+ (actual_keys - expected_keys).each do |key|
53
+ state.errors << state.expected.not.having_key(key)
54
+ end
55
+ end
56
+
57
+ def to_s
58
+ helper = @partial ? "partial_keys" : "keys"
59
+ args = @keys.map(&:inspect).join(", ")
60
+
61
+ "#{'~' if @negated}#{helper}(#{args})"
62
+ end
63
+
64
+ private
65
+
66
+ def validate_negated(state)
67
+ actual = state.actual
68
+
69
+ return unless actual.is_a?(Hash)
70
+
71
+ actual_keys = actual.keys
72
+
73
+ expected_keys = if @includes_expressions
74
+ @keys.map { _1.is_a?(Expression) ? _1.evaluate(state.values) : _1 }
75
+ else
76
+ @keys
77
+ end
78
+
79
+ if @partial
80
+ return unless (expected_keys - actual_keys).empty?
81
+
82
+ state.errors.or!
83
+
84
+ (actual_keys & expected_keys).each do |key|
85
+ state.errors << state.expected.not.having_key(key)
86
+ end
87
+ else
88
+ return if actual_keys != expected_keys
89
+
90
+ state.errors.or!
91
+
92
+ actual_keys.each do |key|
93
+ state.errors << state.expected.not.having_key(key)
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ module MatcherDsl
100
+ ##
101
+ # Matches all keys of a hash
102
+ # @example
103
+ # # matches { foo: 1, bar: 2 } but not { foo: 1, qux: 3 }
104
+ # keys(:foo, :bar)
105
+ # # using key expression, matches { "the_key" => 23 }
106
+ # let(my_key: :the_key) ^ keys(vars[:my_key].to_s)
107
+ # @param keys [Array] supports expressions
108
+ # @param partial [true, false] ignores extra keys when +true+
109
+ # @return [KeysMatcher]
110
+ # @see #partial_keys
111
+ def keys(*keys, partial: false)
112
+ keys.each_with_index do |key, i|
113
+ keys[i] = expression_or_value(key)
114
+ end
115
+
116
+ KeysMatcher.new(keys, partial:)
117
+ end
118
+
119
+ ##
120
+ # Matches hash if all given keys are included. Ignores extra keys.
121
+ # @example
122
+ # # matches { foo: 1, bar: 2 } but not { bar: 2 }
123
+ # partial_keys(:foo)
124
+ # @param keys [Array] supports expressions
125
+ # @return [KeysMatcher]
126
+ # @see #keys
127
+ def partial_keys(*keys)
128
+ keys(*keys, partial: true)
129
+ end
130
+ end
131
+ 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,69 @@
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 &(other)
18
+ other = Matcher.cache(other)
19
+
20
+ if other.is_a?(LazyAllMatcher)
21
+ LazyAllMatcher.new(@matchers + other.matchers)
22
+ else
23
+ LazyAllMatcher.new(@matchers + [other])
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.join(', ')})"
41
+ end
42
+ end
43
+
44
+ module MatcherDsl
45
+ ##
46
+ # Matches all matchers lazily. Returns only the last match result
47
+ # (similar to &&).
48
+ # @example
49
+ # # matches 3 but not "foo"
50
+ # lazy_all(Integer, _ % 3 == 0)
51
+ # # alternatively:
52
+ # of(Integer) & _.positive?
53
+ # @param matchers [Array<Base>]
54
+ # @return [LazyAllMatcher]
55
+ # @see Base#&
56
+ # @see #lazy_any
57
+ # @see #all
58
+ def lazy_all(*matchers)
59
+ case matchers.length
60
+ when 0
61
+ AlwaysMatcher.instance
62
+ when 1
63
+ matcher_of(matchers[0])
64
+ else
65
+ LazyAllMatcher.new(matchers.map { matcher_of(_1) })
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,69 @@
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 |(other)
18
+ other = Matcher.cache(other)
19
+
20
+ if other.is_a?(LazyAnyMatcher)
21
+ LazyAnyMatcher.new(@matchers + other.matchers)
22
+ else
23
+ LazyAnyMatcher.new(@matchers + [other])
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.join(', ')})"
41
+ end
42
+ end
43
+
44
+ module MatcherDsl
45
+ ##
46
+ # Matches any matcher lazily. Returns only the last match result
47
+ # (similar to ||).
48
+ # @example
49
+ # # matches "foo" and 42 but not +nil+ or +true+
50
+ # lazy_any(String, Integer)
51
+ # # alternatively:
52
+ # of(String) | of(Integer)
53
+ # @param matchers [Array<Base>]
54
+ # @return [LazyAnyMatcher]
55
+ # @see Base#|
56
+ # @see #lazy_all
57
+ # @see #any
58
+ def lazy_any(*matchers)
59
+ case matchers.length
60
+ when 0
61
+ AlwaysMatcher.instance
62
+ when 1
63
+ matcher_of(matchers[0])
64
+ else
65
+ LazyAnyMatcher.new(matchers.map { matcher_of(_1) })
66
+ end
67
+ end
68
+ end
69
+ 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 MatcherDsl
47
+ ##
48
+ # Sets values for the 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
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ ##
5
+ # Map items to another value before matching.
6
+ # @example
7
+ # m = Matcher.build { map(_.to_i, [1, 2]) } # OR
8
+ # m = Matcher.build { map(_.to_i) ^ [1, 2] }
9
+ #
10
+ # m.match?(["1", "2"])
11
+ # # => true
12
+ # m.match(["1", :foo])
13
+ # # > root[1]: expected an object responding to 'to_i' but got :foo
14
+ # m.match(["1", "3"])
15
+ # # > root[1].to_i: expected 2 but got 3
16
+ #
17
+ # m = Matcher.build { map(_.to_i, _.sum == 3) }
18
+ # m.match?(["1", "2"])
19
+ # # => true
20
+ # m.match(["1", "2", "3"])
21
+ # # > root.map(&:to_i): expected actual.sum == 3 but got 6 == 3,
22
+ # # where actual = [1, 2, 3]
23
+ class MapMatcher < Base
24
+ include MappingUtils
25
+
26
+ def initialize(projection, matcher, negated: false)
27
+ super()
28
+
29
+ @projection = projection
30
+ @matcher = negated ? ~matcher : matcher
31
+ @original_matcher = matcher
32
+ @negated = negated
33
+ end
34
+
35
+ def negate
36
+ MapMatcher.new(@projection, @original_matcher, negated: !@negated)
37
+ end
38
+
39
+ def validate(state, &)
40
+ return validate_negated(state, &) if @negated
41
+
42
+ actual = state.actual
43
+ values = state.values
44
+
45
+ unless actual.respond_to?(:each)
46
+ state.errors << state.expected.responding_to(:each)
47
+ return
48
+ end
49
+
50
+ i = 0
51
+ mapped = []
52
+ mapping_failed = false
53
+
54
+ actual.each do |act|
55
+ mapped << @projection.evaluate(
56
+ values.merge(actual: act, index: i, original: actual),
57
+ )
58
+ rescue CallError => e
59
+ state.errors[i] << e.message_for_errors(act)
60
+ mapping_failed = true
61
+ ensure
62
+ i += 1
63
+ end
64
+
65
+ return if mapping_failed
66
+
67
+ errors = yield @matcher, mapped, original: actual
68
+ errors = map_errors2(errors) unless state.boolean?
69
+
70
+ state.errors << errors
71
+ end
72
+
73
+ def to_s
74
+ "#{'~' if @negated}map(#{@projection}, #{@original_matcher})"
75
+ end
76
+
77
+ private
78
+
79
+ def validate_negated(state)
80
+ actual = state.actual
81
+ values = state.values
82
+
83
+ return unless actual.respond_to?(:map)
84
+
85
+ mapped = []
86
+
87
+ actual.map.with_index do |item, i|
88
+ mapped << @projection.evaluate(
89
+ values.merge(actual: item, index: i, original: actual),
90
+ )
91
+ rescue CallError
92
+ return nil if @negated
93
+ end
94
+
95
+ mapped_errors = yield @matcher, mapped, original: actual
96
+
97
+ state.errors << map_errors2(mapped_errors)
98
+ end
99
+
100
+ def map_errors2(errors)
101
+ map_errors(errors) do |nested_error|
102
+ key = nested_error.key
103
+
104
+ next unless index_call?(key)
105
+
106
+ NestedError.new(
107
+ key,
108
+ NestedError.new(@projection, nested_error.child),
109
+ )
110
+ end
111
+ end
112
+
113
+ def mapped_base
114
+ @mapped_base ||= map_base(:map, @projection)
115
+ end
116
+ end
117
+
118
+ module MatcherDsl
119
+ ##
120
+ # Maps items to another value before matching
121
+ # == +expression+ values
122
+ # - actual
123
+ # - index
124
+ # - original
125
+ # == +matcher+ values
126
+ # - original
127
+ # @example
128
+ # # matches ["1", "2"]
129
+ # map(_.to_i, [1, 2])
130
+ # # alternatively:
131
+ # map(_.to_i) ^ [1, 2]
132
+ # @overload map(expression, matcher)
133
+ # @param expression [Expression]
134
+ # @param matcher [Base]
135
+ # @return [MapMatcher]
136
+ # @overload map(expression)
137
+ # @param expression [Expression]
138
+ # @return [Chain<MapMatcher>]
139
+ def map(expression, matcher = UNDEFINED)
140
+ return Chain.new { map(expression, _1) } if Matcher.undefined?(matcher)
141
+
142
+ expression = expression_of(expression)
143
+ matcher = matcher_of(matcher)
144
+
145
+ MapMatcher.new(expression, matcher)
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class NegatedArrayMatcher < Base
5
+ def initialize(array)
6
+ super()
7
+
8
+ @array = array
9
+ @neg_array = @array.map(&:~)
10
+ end
11
+
12
+ def negate
13
+ ArrayMatcher.new(@array)
14
+ end
15
+
16
+ def validate(state)
17
+ actual = state.actual
18
+
19
+ return if !actual.is_a?(Array) || @array.length != actual.length
20
+
21
+ collector = state.new_collector.or!
22
+
23
+ @array.length.times do |i|
24
+ result = yield @neg_array[i], actual[i], index: i, parent: actual
25
+
26
+ return nil if result.valid?
27
+
28
+ collector[i] << result
29
+ end
30
+
31
+ state.errors << collector.error
32
+ end
33
+
34
+ def to_s
35
+ "neg(#{@array})"
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class NegatedEachMatcher < Base
5
+ def initialize(matcher)
6
+ super()
7
+
8
+ @matcher = matcher
9
+ @neg_matcher = ~matcher
10
+ end
11
+
12
+ def negate
13
+ EachMatcher.new(@matcher)
14
+ end
15
+
16
+ def validate(state)
17
+ return unless state.actual.respond_to?(:each)
18
+
19
+ collector = state.new_collector.or!
20
+
21
+ state.actual.each.with_index do |item, i|
22
+ result = yield @neg_matcher, item, index: i, parent: state.actual
23
+
24
+ return nil if result.valid?
25
+
26
+ collector[i] << result
27
+ end
28
+
29
+ state.errors << collector.error
30
+ end
31
+
32
+ def to_s
33
+ "~each(#{@matcher})"
34
+ end
35
+ end
36
+ end