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,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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ module NoMatcher end
5
+ module NoExpression end
6
+ module NoKey end
7
+ 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 *(other)
14
+ other = Matcher.cache(other)
15
+
16
+ if other.is_a?(AllMatcher)
17
+ AllMatcher.new(@matchers + other.matchers)
18
+ else
19
+ AllMatcher.new(@matchers + [other])
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.join(', ')})"
35
+ end
36
+ end
37
+
38
+ module MatcherDsl
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,34 @@
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 MatcherDsl
19
+ ##
20
+ # Matches always
21
+ #
22
+ # Many matchers accept child matchers, for instance the HashMatcher. But
23
+ # before they invoke a child matcher they often perform implicit checks. And
24
+ # sometimes, we are only interested in those implicit checks and don't care
25
+ # about having a child matcher.
26
+ #
27
+ # @example
28
+ # { foo: always }
29
+ # @return [AlwaysMatcher]
30
+ def always
31
+ AlwaysMatcher.instance
32
+ end
33
+ end
34
+ 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 +(other)
14
+ other = Matcher.cache(other)
15
+
16
+ if other.is_a?(AnyMatcher)
17
+ AnyMatcher.new(@matchers + other.matchers)
18
+ else
19
+ AnyMatcher.new(@matchers + [other])
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 nil 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.join(', ')})"
46
+ end
47
+ end
48
+
49
+ module MatcherDsl
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,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ ##
5
+ # == Basic array matching
6
+ #
7
+ # m = Matcher.build { [1, 2, 3] }
8
+ #
9
+ # m.match?([1, 2, 3]) # => true
10
+ # m.match([1, 2]) # > root: expected length of 3 but was 2
11
+ # m.match([1, 2, 3, 4]) # > root: expected length of 3 but was 4
12
+ #
13
+ # m.match([3, 2, 1])
14
+ # # > root[0]: expected 1 but got 3
15
+ # # > root[2]: expected 3 but got 1
16
+ #
17
+ # m = Match.build { [Integer, String] }
18
+ # m.match?([1, "foo"]) # => true
19
+ #
20
+ # == Values passed to element matchers
21
+ # +ArrayMatcher+ passes +index+ and +parent+ to its element matchers.
22
+ #
23
+ # # index or i
24
+ # m = Matcher.build { [_ == i] }
25
+ # m.match?([0]) # => true
26
+ # m.match([1]) # > root[0]: expected actual == index but got 1 == 0
27
+ #
28
+ # # parent
29
+ # m = Matcher.build do
30
+ # in_order = imply(i > 0, parent[i - 1] <= _)
31
+ # [in_order, in_order, in_order]
32
+ # end
33
+ #
34
+ # m.match?([1, 2, 3])
35
+ # # => true
36
+ # m.match([1, 2, 0])
37
+ # # > root[2]: expected actual >= parent[index - 1] but got 0 >= 2, where
38
+ # # parent = [1, 2, 0], index = 2
39
+ class ArrayMatcher < Base
40
+ def initialize(array)
41
+ super()
42
+
43
+ @array = array
44
+ end
45
+
46
+ def negate
47
+ NegatedArrayMatcher.new(@array)
48
+ end
49
+
50
+ def validate(state)
51
+ actual = state.actual
52
+ errors = state.errors
53
+
54
+ unless actual.is_a?(Array)
55
+ errors << state.expected.kind_of(Array)
56
+ return
57
+ end
58
+
59
+ if @array.length != actual.length
60
+ errors << state.expected.length_of(@array.length, actual.length)
61
+ end
62
+
63
+ [@array.length, actual.length].min.times do |i|
64
+ errors[i] << yield(@array[i], actual[i], index: i, parent: actual)
65
+ end
66
+ end
67
+
68
+ def to_s
69
+ @array.to_s
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class BlockMatcher < Base
5
+ def initialize(block, description = nil, negated: false)
6
+ super()
7
+
8
+ @block = block
9
+ @description = description
10
+ @negated = negated
11
+ end
12
+
13
+ def negate
14
+ BlockMatcher.new(@block, @description, negated: !@negated)
15
+ end
16
+
17
+ def validate(state)
18
+ result = Utils.call_block(@block, state.values)
19
+
20
+ state.errors << build_message(state) if @negated ^ !result
21
+ end
22
+
23
+ def to_s
24
+ string = @description || "-> { #{block_location} }"
25
+
26
+ if @negated
27
+ "neg(#{string})"
28
+ else
29
+ string
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def build_message(state)
36
+ if @description
37
+ state.expected.not_if(@negated)
38
+ .described_by(@description)
39
+ else
40
+ state.expected.namespace(:block).not_if(@negated)
41
+ .satisfied(block_location)
42
+ end
43
+ end
44
+
45
+ def block_location
46
+ Utils.block_location(@block)
47
+ end
48
+ end
49
+
50
+ module MatcherDsl
51
+ ##
52
+ # Matches when block returns truthy
53
+ # @example
54
+ # satisfy('an even number') { |actual:| actual.even? }
55
+ # @param message [String, nil] optional description for error reporting
56
+ # @return [BlockMatcher]
57
+ def satisfy(message = nil, &block)
58
+ BlockMatcher.new(block, message)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class BooleanMatcher < Base
5
+ BOOLEAN = [false, true].freeze
6
+
7
+ def initialize(negated: false)
8
+ super()
9
+
10
+ @negated = negated
11
+ end
12
+
13
+ def negate
14
+ BooleanMatcher.new(negated: !@negated)
15
+ end
16
+
17
+ def validate(state)
18
+ state.errors << state.expected.not_if(@negated).in(BOOLEAN) if
19
+ @negated == BOOLEAN.include?(state.actual)
20
+ end
21
+
22
+ def to_s
23
+ "#{'~' if @negated}boolean"
24
+ end
25
+ end
26
+
27
+ module MatcherDsl
28
+ ##
29
+ # Matches +true+ and +false+
30
+ # @example
31
+ # { available: boolean }
32
+ # @return [BooleanMatcher]
33
+ def boolean
34
+ @boolean ||= BooleanMatcher.new
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class DigMatcher < Base
5
+ def initialize(keys, matcher, optional: false, negated: false)
6
+ super()
7
+
8
+ @keys = keys
9
+ @matcher = negated ? ~matcher : matcher
10
+ @original_matcher = matcher
11
+ @optional = optional
12
+ @negated = negated
13
+ end
14
+
15
+ def negate
16
+ DigMatcher.new(
17
+ @keys, @original_matcher, optional: @optional, negated: !@negated
18
+ )
19
+ end
20
+
21
+ def validate(state, &)
22
+ return validate_negated(state, &) if @negated
23
+
24
+ cur = state.actual
25
+ errors = state.errors
26
+
27
+ @keys.each do |key|
28
+ key = key.evaluate(state.values) if key.is_a?(Expression)
29
+ is_array = cur.is_a?(Array)
30
+
31
+ if is_array
32
+ unless key.is_a?(Integer)
33
+ errors << state.expected(cur).kind_of(Hash)
34
+ return nil
35
+ end
36
+ elsif !cur.is_a?(Hash)
37
+ or_error = state.new_collector.or!
38
+ or_error << state.expected(cur).kind_of(Hash)
39
+ or_error << state.expected(cur).kind_of(Array)
40
+ errors << or_error.error
41
+
42
+ return nil
43
+ end
44
+
45
+ prev = cur
46
+ cur = cur[key]
47
+
48
+ if cur.nil?
49
+ if @optional
50
+ return nil unless is_array ? index?(prev, key) : prev.key?(key)
51
+ elsif is_array
52
+ unless index?(prev, key)
53
+ errors << state.expected(prev).having_index(key)
54
+ return nil
55
+ end
56
+ else
57
+ unless prev.key?(key)
58
+ errors << state.expected(prev).having_key(key)
59
+ return nil
60
+ end
61
+ end
62
+ end
63
+
64
+ errors = errors[key]
65
+ end
66
+
67
+ errors << yield(@matcher, cur)
68
+ end
69
+
70
+ def to_s
71
+ helper = "#{'optional_' if @optional}dig"
72
+ keys = @keys.map(&:inspect).join(", ")
73
+ matcher = Matcher.parenthesize(@original_matcher)
74
+
75
+ "#{'~' if @negated}#{helper}(#{keys}) ^ #{matcher}"
76
+ end
77
+
78
+ private
79
+
80
+ def index?(array, index)
81
+ index.between?(-array.length, array.length - 1)
82
+ end
83
+
84
+ def validate_negated(state)
85
+ cur = state.actual
86
+ errors = state.errors
87
+
88
+ @keys.each do |key|
89
+ key = key.evaluate(state.values) if key.is_a?(Expression)
90
+ is_array = cur.is_a?(Array)
91
+
92
+ return nil if is_array ? !key.is_a?(Integer) : !cur.is_a?(Hash)
93
+
94
+ prev = cur
95
+ cur = cur[key]
96
+
97
+ if cur.nil?
98
+ if is_array
99
+ unless index?(prev, key)
100
+ errors << state.expected(prev).having_index(key) if @optional
101
+ return nil
102
+ end
103
+ else
104
+ unless prev.key?(key)
105
+ errors << state.expected(prev).having_key(key) if @optional
106
+ return nil
107
+ end
108
+ end
109
+ end
110
+
111
+ errors = errors[key]
112
+ end
113
+
114
+ errors << yield(@matcher, cur)
115
+ end
116
+ end
117
+
118
+ module MatcherDsl
119
+ ##
120
+ # Matches deeply nested values
121
+ # @example
122
+ # # matches [0, { a: { "B" => 42 } }] where b: "B", but not []
123
+ # dig(1, :a, vars[:b]) ^ Integer
124
+ # @param path [Array<Expression>]
125
+ # @param optional [true, false] matches if path doesn't exist when +true+
126
+ # @return [Chain<DigMatcher>]
127
+ def dig(*path, optional: false)
128
+ path.each_with_index do |key, i|
129
+ path[i] = expression_or_value(key)
130
+ end
131
+
132
+ Chain.new do |matcher|
133
+ DigMatcher.new(path, matcher, optional:)
134
+ end
135
+ end
136
+
137
+ ##
138
+ # Matches deeply nested value only if path exists
139
+ # @example
140
+ # # matches { a: { b: 1 } } and { a: {} } but
141
+ # # neither { a: { b: nil } } nor { a: nil }
142
+ # optional_dig(:a, :b) ^ 1
143
+ # @param path [Array<Expression>]
144
+ # @return [Chain<DigMatcher>]
145
+ def optional_dig(*path)
146
+ dig(*path, optional: true)
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ ##
5
+ # Match each item.
6
+ # @example
7
+ # m = Matcher.build { each(Integer) }
8
+ #
9
+ # m.match?([1, 2, 3])
10
+ # # => true
11
+ # m.match([1, "foo"])
12
+ # # > root[1]: expected a kind of Integer but got "foo"
13
+ #
14
+ # # "each" passes index and parent to its item matcher:
15
+ # m = Matcher.build { each(_ == i) }
16
+ # m.match?([0, 1]) # => true
17
+ # m.match?([0, 2]) # => false
18
+ #
19
+ # m = Matcher.build { each(_ < parent.length) }
20
+ # m.match?([1, 0, 2]) # => true
21
+ # m.match?([1, 2, 3]) # => false
22
+ class EachMatcher < Base
23
+ def initialize(matcher)
24
+ super()
25
+
26
+ @matcher = matcher
27
+ end
28
+
29
+ def negate
30
+ NegatedEachMatcher.new(@matcher)
31
+ end
32
+
33
+ def validate(state)
34
+ unless state.actual.respond_to?(:each)
35
+ state.errors << state.expected.responding_to(:each)
36
+ return
37
+ end
38
+
39
+ i = 0
40
+ state.actual.each do |item|
41
+ state.errors[i] << yield(@matcher, item, index: i, parent: state.actual)
42
+ i += 1
43
+ end
44
+ end
45
+
46
+ def to_s
47
+ "each(#{@matcher})"
48
+ end
49
+ end
50
+
51
+ module MatcherDsl
52
+ ##
53
+ # Matches each item with matcher
54
+ #
55
+ # == +matcher+ values
56
+ #
57
+ # Passes +index+
58
+ #
59
+ # m = Matcher.build { each(_ == i) }
60
+ # m.match?([0, 1]) # => true
61
+ # m.match?([0, 2]) # => false
62
+ #
63
+ # Passes +parent+
64
+ #
65
+ # m = Matcher.build { each(_ < parent.length) }
66
+ # m.match?([1, 0, 2]) # => true
67
+ # m.match?([1, 2, 3]) # => false
68
+ #
69
+ # @example
70
+ # # matches [1, 2] but not [1, "foo"]
71
+ # each(Integer)
72
+ # # alternatively:
73
+ # each ^ Integer
74
+ # @overload each(matcher)
75
+ # @param matcher [Base]
76
+ # @return [EachMatcher]
77
+ # @overload each
78
+ # @return [Chain<EachMatcher>]
79
+ def each(matcher = UNDEFINED)
80
+ return Chain.new { each(_1) } if Matcher.undefined?(matcher)
81
+
82
+ EachMatcher.new(matcher_of(matcher))
83
+ end
84
+ end
85
+ end