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,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class StringExpression < Expression
5
+ def initialize(parts)
6
+ super()
7
+
8
+ @parts = parts
9
+ end
10
+
11
+ attr_reader :parts
12
+
13
+ def ==(other)
14
+ return true if equal?(other)
15
+
16
+ other.instance_of?(StringExpression) &&
17
+ @parts.eql?(other.parts)
18
+ end
19
+ alias eql? ==
20
+
21
+ def hash
22
+ @hash ||= [self.class, @parts].hash
23
+ end
24
+
25
+ def variables
26
+ @variables ||= @parts.flat_map(&:variables).uniq
27
+ end
28
+
29
+ def evaluate(values)
30
+ @parts.map { _1.evaluate(values) }.join
31
+ end
32
+
33
+ def substitute(replacements)
34
+ return self unless replacements.keys.intersect?(variables)
35
+
36
+ substituted_parts = @parts.map { _1.substitute(replacements) }
37
+
38
+ StringExpression.new(substituted_parts)
39
+ end
40
+
41
+ def to_s
42
+ parts = @parts.map do |part|
43
+ if part.is_a?(Constant) && part.value.is_a?(String)
44
+ part.value
45
+ else
46
+ "\#{#{part}}"
47
+ end
48
+ end
49
+
50
+ "\"#{parts.join}\""
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class SymbolProc
5
+ def initialize(proc_or_symbol)
6
+ case proc_or_symbol
7
+ when Symbol
8
+ @symbol = proc_or_symbol
9
+ @proc = proc_or_symbol.to_proc
10
+ when Proc
11
+ @proc = proc_or_symbol
12
+ @symbol = get_symbol(proc_or_symbol)
13
+ else
14
+ raise "Expected Proc or Symbol, got #{proc_or_symbol.inspect}"
15
+ end
16
+ end
17
+
18
+ attr_reader :symbol
19
+
20
+ def ==(other)
21
+ equal?(other) || other.instance_of?(SymbolProc) && @symbol == other.symbol
22
+ end
23
+ alias eql? ==
24
+
25
+ def hash
26
+ [self.class, @symbol].hash
27
+ end
28
+
29
+ def variables
30
+ []
31
+ end
32
+
33
+ def to_proc
34
+ @proc
35
+ end
36
+
37
+ def to_s
38
+ "&#{@symbol.inspect}"
39
+ end
40
+ alias inspect to_s
41
+
42
+ private
43
+
44
+ def get_symbol(proc)
45
+ receiver = Variable.actual # could be any
46
+ recorder = Recorder.new(receiver)
47
+ result = proc.call(recorder)
48
+ expression = Recorder.to_expression(result)
49
+
50
+ expression.method
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class Variable < Expression
5
+ WELL_KNOWN = %i[actual key value index parent original].freeze
6
+
7
+ WELL_KNOWN.each do |method|
8
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
9
+ def self.#{method} # def self.actual
10
+ @#{method} ||= Variable.new(:#{method}) # @actual ||= Variable.actual
11
+ end # end
12
+ RUBY
13
+ end
14
+
15
+ def self.well_known?(symbol)
16
+ WELL_KNOWN.include?(symbol)
17
+ end
18
+
19
+ def self.cache(symbol, expression_cache: true)
20
+ return send(symbol) if well_known?(symbol)
21
+
22
+ expression_cache = ExpressionCache.current if expression_cache == true
23
+
24
+ if expression_cache
25
+ expression_cache.less_known_variable_for(symbol)
26
+ else
27
+ new(symbol)
28
+ end
29
+ end
30
+
31
+ def self.substitutions
32
+ Thread.current[:matcher_variable_substitution_stack]
33
+ end
34
+
35
+ def self.with_substitutions(**substitutions)
36
+ stack = (Thread.current[:matcher_variable_substitution_stack] ||= HashStack.new)
37
+ stack.push(substitutions)
38
+
39
+ begin
40
+ yield
41
+ ensure
42
+ stack.pop(substitutions)
43
+ Thread.current[:matcher_variable_substitution_stack] = nil if stack.empty?
44
+ end
45
+ end
46
+
47
+ attr_reader :symbol
48
+
49
+ def initialize(symbol)
50
+ super()
51
+
52
+ @symbol = symbol
53
+ end
54
+
55
+ def variables
56
+ [@symbol]
57
+ end
58
+
59
+ def evaluate(values)
60
+ value = values[@symbol]
61
+
62
+ raise "no value for #{@symbol.inspect}" if value.nil? && !values.key?(@symbol)
63
+
64
+ value
65
+ end
66
+
67
+ def ==(other)
68
+ other.instance_of?(Variable) && other.symbol == @symbol
69
+ end
70
+ alias eql? ==
71
+
72
+ def hash
73
+ [self.class, @symbol].hash
74
+ end
75
+
76
+ def substitute(replacements)
77
+ symbol = replacements[@symbol]
78
+ symbol ? Variable.new(symbol) : self
79
+ end
80
+
81
+ def to_s
82
+ Variable.substitutions&.[](@symbol) || @symbol.to_s
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class HashStack
5
+ extend Forwardable
6
+
7
+ def initialize
8
+ @stacks = {}
9
+ end
10
+
11
+ def_delegators :@stacks, :empty?, :key?, :keys
12
+ def_delegators :to_h, :merge
13
+
14
+ def [](key)
15
+ @stacks[key]&.last
16
+ end
17
+
18
+ def push(hash)
19
+ hash.each do |k, v|
20
+ (@stacks[k] ||= []) << v
21
+ end
22
+ end
23
+
24
+ def pop(hash)
25
+ hash.each_key do |k|
26
+ array = @stacks[k]
27
+ array.pop
28
+
29
+ @stacks.delete(k) if array.empty?
30
+ end
31
+ end
32
+
33
+ def slice(*keys)
34
+ keys.each_with_object({}) do |k, h|
35
+ pair = @stacks.assoc(k)
36
+
37
+ next unless pair
38
+
39
+ h[k] = pair[1].last
40
+ end
41
+ end
42
+
43
+ def merge(hash)
44
+ hash.default_proc = ->(h, k) { h[k] = self[k] }
45
+ hash
46
+ end
47
+
48
+ def to_h
49
+ @stacks.transform_values(&:last)
50
+ end
51
+ alias to_hash to_h
52
+ end
53
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class List
5
+ include Enumerable
6
+
7
+ def self.empty
8
+ EmptyList.instance
9
+ end
10
+
11
+ def self.one(item)
12
+ NonEmptyList.new(item)
13
+ end
14
+
15
+ def to_s
16
+ to_a.to_s
17
+ end
18
+ alias inspect to_s
19
+ end
20
+
21
+ class EmptyList < List
22
+ include Singleton
23
+
24
+ def add(item)
25
+ NonEmptyList.new(item)
26
+ end
27
+ alias << add
28
+
29
+ def empty?
30
+ true
31
+ end
32
+
33
+ def last
34
+ nil
35
+ end
36
+
37
+ def each
38
+ self
39
+ end
40
+
41
+ def reverse_each
42
+ return to_enum(:reverse_each) unless block_given?
43
+
44
+ self
45
+ end
46
+
47
+ def to_a
48
+ []
49
+ end
50
+ end
51
+
52
+ class NonEmptyList < List
53
+ attr_reader :head, :tail
54
+
55
+ def initialize(head, tail = nil)
56
+ super()
57
+
58
+ @head = head
59
+ @tail = tail
60
+ end
61
+
62
+ def empty?
63
+ false
64
+ end
65
+
66
+ def add(item)
67
+ NonEmptyList.new(item, self)
68
+ end
69
+ alias << add
70
+
71
+ def last
72
+ @tail&.last || @head
73
+ end
74
+
75
+ def hash
76
+ @hash ||= [self.class, @head, @tail].hash
77
+ end
78
+
79
+ def ==(other)
80
+ return true if equal?(other)
81
+
82
+ other.instance_of?(NonEmptyList) &&
83
+ @head.eql?(other.head) &&
84
+ @tail.eql?(other.tail)
85
+ end
86
+ alias eql? ==
87
+
88
+ def each
89
+ c = self
90
+
91
+ while c
92
+ yield c.head
93
+ c = c.tail
94
+ end
95
+ end
96
+
97
+ def reverse_each(&)
98
+ @tail&.reverse_each(&)
99
+ yield @head
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class Optional
5
+ include NoExpression
6
+
7
+ CACHEABLE_CLASSES = [
8
+ NilClass,
9
+ FalseClass,
10
+ TrueClass,
11
+ Integer,
12
+ Float,
13
+ Symbol,
14
+ String,
15
+ Regexp,
16
+ Module,
17
+ Base,
18
+ ].freeze
19
+
20
+ def self.cache(value, matcher_cache = MatcherCache.current)
21
+ return new(value) if !matcher_cache ||
22
+ !CACHEABLE_CLASSES.include?(value.class) ||
23
+ value.is_a?(String) && !value.frozen?
24
+
25
+ (matcher_cache.optionals ||= {})[value] ||= new(value)
26
+ end
27
+
28
+ def self.value_of(obj)
29
+ obj.is_a?(Optional) ? obj.value : obj
30
+ end
31
+
32
+ def initialize(value)
33
+ @value = value
34
+ end
35
+
36
+ attr_reader :value
37
+
38
+ def ~
39
+ ~Matcher.cache(self)
40
+ end
41
+
42
+ def ==(other)
43
+ return true if equal?(other)
44
+
45
+ other.instance_of?(self.class) && other.value == @value
46
+ end
47
+ alias eql? ==
48
+
49
+ def hash
50
+ [self.class, @value].hash
51
+ end
52
+
53
+ def to_s
54
+ "optional(#{@value.inspect})"
55
+ end
56
+ alias inspect to_s
57
+ end
58
+
59
+ module MatcherBuilding
60
+ ##
61
+ # Marks a hash key as optional or wraps a matcher to also accept +nil+
62
+ # @example
63
+ # # optional hash key
64
+ # { optional(:foo) => 1 }
65
+ # # match nil or String
66
+ # optional(String)
67
+ # @overload optional(value)
68
+ # @param value
69
+ # @return [Optional]
70
+ # @overload optional
71
+ # @return [Chain]
72
+ def optional(value = UNDEFINED)
73
+ return Chain.new { optional(_1) } if Matcher.undefined?(value)
74
+
75
+ value = Expression.try_recorder(value)
76
+
77
+ Optional.new(value)
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class Others
5
+ include Singleton
6
+ include NoMatcher
7
+ include NoExpression
8
+
9
+ def to_s
10
+ 'others'
11
+ end
12
+ alias inspect to_s
13
+ end
14
+
15
+ module MatcherBuilding
16
+ ##
17
+ # Hash key that matches remaining entries
18
+ # @example
19
+ # {
20
+ # id: Integer,
21
+ # others => each_value(String),
22
+ # }
23
+ # @return [Others]
24
+ def others
25
+ Others.instance
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ MatcherCache = Struct.new(
5
+ :negated_matchers,
6
+ :equal_matchers,
7
+ :expression_matchers,
8
+ :kind_of_matchers,
9
+ :optionals,
10
+ :optional_matchers,
11
+ :range_matchers,
12
+ :regexp_matchers,
13
+ ) do
14
+ def self.current(build_session = Matcher.build_session)
15
+ build_session[:_matcher_cache] ||= new if build_session
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class AllMatcher < Base
5
+ def initialize(matchers)
6
+ super()
7
+
8
+ @matchers = matchers
9
+ end
10
+
11
+ attr_reader :matchers
12
+
13
+ def *(matcher)
14
+ matcher = Matcher.cache(matcher)
15
+
16
+ if matcher.is_a?(AllMatcher)
17
+ AllMatcher.new(@matchers + matcher.matchers)
18
+ else
19
+ AllMatcher.new(@matchers + [matcher])
20
+ end
21
+ end
22
+
23
+ def negate
24
+ AnyMatcher.new(@matchers.map(&:~))
25
+ end
26
+
27
+ def validate(state)
28
+ @matchers.each do |matcher|
29
+ state.errors << yield(matcher)
30
+ end
31
+ end
32
+
33
+ def to_s
34
+ "all(#{@matchers.map(&:to_s).join(', ')})"
35
+ end
36
+ end
37
+
38
+ module MatcherBuilding
39
+ ##
40
+ # Matches all matchers
41
+ # @example
42
+ # # matches 12 but not 9 or 13
43
+ # all(_ > 10, _.even?)
44
+ # # alternatively:
45
+ # of(_ > 10) * of(_.even?)
46
+ # @param matchers [Array<Base>]
47
+ # @return [AllMatcher]
48
+ # @see Base#*
49
+ def all(*matchers)
50
+ case matchers.length
51
+ when 0
52
+ AlwaysMatcher.instance
53
+ when 1
54
+ matcher_of(matchers[0])
55
+ else
56
+ AllMatcher.new(matchers.map { matcher_of(_1) })
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class AlwaysMatcher < Base
5
+ include Singleton
6
+
7
+ def ~
8
+ NeverMatcher.instance
9
+ end
10
+
11
+ def validate(_state) end
12
+
13
+ def to_s
14
+ 'always'
15
+ end
16
+ end
17
+
18
+ module MatcherBuilding
19
+ ##
20
+ # Matches always
21
+ # @example
22
+ # { foo: always }
23
+ # @return [AlwaysMatcher]
24
+ def always
25
+ AlwaysMatcher.instance
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class AnyMatcher < Base
5
+ def initialize(matchers)
6
+ super()
7
+
8
+ @matchers = matchers
9
+ end
10
+
11
+ attr_reader :matchers
12
+
13
+ def +(matcher)
14
+ matcher = Matcher.cache(matcher)
15
+
16
+ if matcher.is_a?(AnyMatcher)
17
+ AnyMatcher.new(@matchers + matcher.matchers)
18
+ else
19
+ AnyMatcher.new(@matchers + [matcher])
20
+ end
21
+ end
22
+
23
+ def negate
24
+ AllMatcher.new(@matchers.map(&:~))
25
+ end
26
+
27
+ def validate(state)
28
+ if @matchers.empty?
29
+ state.errors << state.report.exist
30
+ return
31
+ end
32
+
33
+ sub_errors = @matchers.map do |matcher|
34
+ sub_error = yield matcher
35
+
36
+ return if sub_error.valid?
37
+
38
+ sub_error
39
+ end
40
+
41
+ state.errors << OrError.from(sub_errors)
42
+ end
43
+
44
+ def to_s
45
+ "any(#{@matchers.map(&:to_s).join(', ')})"
46
+ end
47
+ end
48
+
49
+ module MatcherBuilding
50
+ ##
51
+ # Matches any matcher
52
+ # @example
53
+ # # matches "foo" and 1 but not 1.5
54
+ # any(String, Integer)
55
+ # # alternatively:
56
+ # of(String) + of(Integer)
57
+ # @param matchers [Array<Base>]
58
+ # @return [AnyMatcher]
59
+ def any(*matchers)
60
+ case matchers.length
61
+ when 0
62
+ NeverMatcher.instance
63
+ when 1
64
+ matcher_of(matchers[0])
65
+ else
66
+ AnyMatcher.new(matchers.map { matcher_of(_1) })
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class ArrayMatcher < Base
5
+ def initialize(array)
6
+ super()
7
+
8
+ @array = array
9
+ end
10
+
11
+ def negate
12
+ NegatedArrayMatcher.new(@array)
13
+ end
14
+
15
+ def validate(state)
16
+ actual = state.actual
17
+ errors = state.errors
18
+
19
+ unless actual.is_a?(Array)
20
+ errors << state.expected.kind_of(Array)
21
+ return
22
+ end
23
+
24
+ errors << state.expected.length_of(@array.length, actual.length) if @array.length != actual.length
25
+
26
+ [@array.length, actual.length].min.times do |i|
27
+ errors[i] << yield(@array[i], actual[i], index: i, parent: actual)
28
+ end
29
+ end
30
+
31
+ def to_s
32
+ @array.to_s
33
+ end
34
+ end
35
+ end