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,59 @@
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).described_by(@description)
38
+ else
39
+ state.expected.namespace(:block).not_if(@negated).satisfied(block_location)
40
+ end
41
+ end
42
+
43
+ def block_location
44
+ Utils.block_location(@block)
45
+ end
46
+ end
47
+
48
+ module MatcherBuilding
49
+ ##
50
+ # Matches when block returns truthy
51
+ # @example
52
+ # satisfy('an even number') { |actual:| actual.even? }
53
+ # @param message [String, nil] optional description for error reporting
54
+ # @return [BlockMatcher]
55
+ def satisfy(message = nil, &block)
56
+ BlockMatcher.new(block, message)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,35 @@
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
+ @negated = negated
9
+ end
10
+
11
+ def negate
12
+ BooleanMatcher.new(negated: !@negated)
13
+ end
14
+
15
+ def validate(state)
16
+ state.errors << state.expected.not_if(@negated).in(BOOLEAN) if
17
+ @negated == BOOLEAN.include?(state.actual)
18
+ end
19
+
20
+ def to_s
21
+ "#{'~' if @negated}boolean"
22
+ end
23
+ end
24
+
25
+ module MatcherBuilding
26
+ ##
27
+ # Matches +true+ and +false+
28
+ # @example
29
+ # { available: boolean }
30
+ # @return [BooleanMatcher]
31
+ def boolean
32
+ @boolean ||= BooleanMatcher.new
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,146 @@
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(@keys, @original_matcher, optional: @optional, negated: !@negated)
17
+ end
18
+
19
+ def validate(state, &)
20
+ return validate_negated(state, &) if @negated
21
+
22
+ cur = state.actual
23
+ errors = state.errors
24
+
25
+ @keys.each do |key|
26
+ key = key.evaluate(state.values) if key.is_a?(Expression)
27
+ is_array = cur.is_a?(Array)
28
+
29
+ if is_array
30
+ unless key.is_a?(Integer)
31
+ errors << state.expected(cur).kind_of(Hash)
32
+ return nil
33
+ end
34
+ elsif !cur.is_a?(Hash)
35
+ or_error = state.new_collector.or!
36
+ or_error << state.expected(cur).kind_of(Hash)
37
+ or_error << state.expected(cur).kind_of(Array)
38
+ errors << or_error.error
39
+
40
+ return nil
41
+ end
42
+
43
+ prev = cur
44
+ cur = cur[key]
45
+
46
+ if cur.nil?
47
+ if @optional
48
+ return nil unless is_array ? index?(prev, key) : prev.key?(key)
49
+ elsif is_array
50
+ unless index?(prev, key)
51
+ errors << state.expected(prev).having_index(key)
52
+ return nil
53
+ end
54
+ else
55
+ unless prev.key?(key)
56
+ errors << state.expected(prev).having_key(key)
57
+ return nil
58
+ end
59
+ end
60
+ end
61
+
62
+ errors = errors[key]
63
+ end
64
+
65
+ errors << yield(@matcher, cur)
66
+ end
67
+
68
+ def to_s
69
+ helper = "#{'optional_' if @optional}dig"
70
+ keys = @keys.map(&:inspect).join(', ')
71
+ matcher = Matcher.parenthesize(@original_matcher)
72
+
73
+ "#{'~' if @negated}#{helper}(#{keys}) ^ #{matcher}"
74
+ end
75
+
76
+ private
77
+
78
+ def index?(array, index)
79
+ index.between?(-array.length, array.length - 1)
80
+ end
81
+
82
+ def validate_negated(state)
83
+ cur = state.actual
84
+ errors = state.errors
85
+
86
+ @keys.each do |key|
87
+ key = key.evaluate(state.values) if key.is_a?(Expression)
88
+ is_array = cur.is_a?(Array)
89
+
90
+ return nil if is_array ? !key.is_a?(Integer) : !cur.is_a?(Hash)
91
+
92
+ prev = cur
93
+ cur = cur[key]
94
+
95
+ if cur.nil?
96
+ if is_array
97
+ unless index?(prev, key)
98
+ errors << state.expected(prev).having_index(key) if @optional
99
+ return nil
100
+ end
101
+ else
102
+ unless prev.key?(key)
103
+ errors << state.expected(prev).having_key(key) if @optional
104
+ return nil
105
+ end
106
+ end
107
+ end
108
+
109
+ errors = errors[key]
110
+ end
111
+
112
+ errors << yield(@matcher, cur)
113
+ end
114
+ end
115
+
116
+ module MatcherBuilding
117
+ ##
118
+ # Matches deeply nested values
119
+ # @example
120
+ # # matches [0, { a: { "B" => 42 } }] where b: "B", but not []
121
+ # dig(1, :a, vars[:b]) ^ Integer
122
+ # @param path [Array<Expression>]
123
+ # @param optional [true, false] matches if path doesn't exist when +true+
124
+ # @return [Chain<DigMatcher>]
125
+ def dig(*path, optional: false)
126
+ path.each_with_index do |key, i|
127
+ path[i] = expression_or_value(key)
128
+ end
129
+
130
+ Chain.new do |matcher|
131
+ DigMatcher.new(path, matcher, optional:)
132
+ end
133
+ end
134
+
135
+ ##
136
+ # Matches deeply nested value only if path exists
137
+ # @example
138
+ # # matches { a: { b: 1 } } and { a: {} }, but not { a: nil }
139
+ # optional_dig(:a, :b) ^ 1
140
+ # @param path [Array<Expression>]
141
+ # @return [Chain<DigMatcher>]
142
+ def optional_dig(*path)
143
+ dig(*path, optional: true)
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class EachMatcher < Base
5
+ def initialize(matcher)
6
+ super()
7
+
8
+ @matcher = matcher
9
+ end
10
+
11
+ def negate
12
+ NegatedEachMatcher.new(@matcher)
13
+ end
14
+
15
+ def validate(state)
16
+ unless state.actual.respond_to?(:each)
17
+ state.errors << state.expected.responding_to(:each)
18
+ return
19
+ end
20
+
21
+ i = 0
22
+ state.actual.each do |item|
23
+ state.errors[i] << yield(@matcher, item, index: i, parent: state.actual)
24
+ i += 1
25
+ end
26
+ end
27
+
28
+ def to_s
29
+ "each(#{@matcher})"
30
+ end
31
+ end
32
+
33
+ module MatcherBuilding
34
+ ##
35
+ # Matches each item with matcher
36
+ # @example
37
+ # # matches [1, 2] but not [1, "foo"]
38
+ # each(Integer)
39
+ # # alternatively:
40
+ # each ^ Integer
41
+ # @overload each(matcher)
42
+ # @param matcher [Base]
43
+ # @return [EachMatcher]
44
+ # @overload each
45
+ # @return [Chain<EachMatcher>]
46
+ def each(matcher = UNDEFINED)
47
+ return Chain.new { each(_1) } if Matcher.undefined?(matcher)
48
+
49
+ EachMatcher.new(matcher_of(matcher))
50
+ end
51
+ end
52
+ end
@@ -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 MatcherBuilding
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,197 @@
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 = [Module, Range, Regexp, Hash, Array, Expression].freeze
57
+
58
+ def to_s
59
+ if IMPLICIT_MATCHER_CLASSES.any? { @value.is_a?(_1) }
60
+ "#{'~' if @negated}equal(#{@value.inspect})"
61
+ else
62
+ @negated ? "neg(#{@value.inspect})" : @value.inspect
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ def validate_helper(state, errors, exp, act)
69
+ case exp
70
+ when Array
71
+ validate_array(state, errors, exp, act)
72
+ when Hash
73
+ validate_hash(state, errors, exp, act)
74
+ when Set
75
+ validate_set(state, errors, exp, act)
76
+ else
77
+ errors << state.expected(act).not_if(@negated).equal(exp) if
78
+ @negated ^ (act != exp)
79
+ end
80
+ end
81
+
82
+ def validate_array(state, errors, exp, act)
83
+ unless act.is_a?(Array)
84
+ errors << state.expected(act).kind_of(Array)
85
+ return
86
+ end
87
+
88
+ errors << state.expected(act).length_of(exp.length, act.length) if
89
+ exp.length != act.length
90
+
91
+ [exp.length, act.length].min.times do |i|
92
+ validate_helper(state, errors[i], exp[i], act[i])
93
+ end
94
+ end
95
+
96
+ def validate_hash(state, errors, exp, act)
97
+ unless act.is_a?(Hash)
98
+ errors << state.expected(act).kind_of(Hash)
99
+ return
100
+ end
101
+
102
+ (act.keys - exp.keys).each do |key|
103
+ errors[key] << state.expected(act).not.having_key(key)
104
+ end
105
+
106
+ exp.each do |key, exp_value|
107
+ act_value = act[key]
108
+
109
+ if act_value.nil? && !act.key?(key)
110
+ errors << state.expected(act).having_key(key)
111
+ else
112
+ validate_helper(state, errors[key], exp_value, act_value)
113
+ end
114
+ end
115
+ end
116
+
117
+ def validate_set(state, errors, exp, act)
118
+ unless act.is_a?(Set)
119
+ errors << state.expected(act).kind_of(Set)
120
+ return
121
+ end
122
+
123
+ (exp - act).each do |item|
124
+ errors << state.expected(act).including(item)
125
+ end
126
+
127
+ (act - exp).each do |item|
128
+ errors << state.expected(act).not.including(item)
129
+ end
130
+ end
131
+
132
+ def validate_negated_helper(state, errors, exp, act)
133
+ case exp
134
+ when Array
135
+ validate_array_negated(state, errors, exp, act)
136
+ when Hash
137
+ validate_hash_negated(state, errors, exp, act)
138
+ when Set
139
+ validate_set_negated(state, errors, exp, act)
140
+ else
141
+ if act == exp
142
+ errors << state.expected(act).not.equal(exp)
143
+ else
144
+ throw(:valid)
145
+ end
146
+ end
147
+ end
148
+
149
+ def validate_array_negated(state, errors, exp, act)
150
+ throw(:valid) if !act.is_a?(Array) || exp.length != act.length
151
+
152
+ exp.length.times do |i|
153
+ validate_negated_helper(state, errors[i], exp[i], act[i])
154
+ end
155
+ end
156
+
157
+ def validate_hash_negated(state, errors, exp, act)
158
+ throw(:valid) unless act.is_a?(Hash)
159
+
160
+ exp_keys_set = Set.new(exp.keys)
161
+ throw(:valid) unless act.keys.all? { exp_keys_set.include?(_1) }
162
+
163
+ exp.each do |key, exp_value|
164
+ act_value = act[key]
165
+
166
+ if act_value.nil? && !act.key?(key)
167
+ throw(:valid)
168
+ else
169
+ validate_negated_helper(state, errors[key], exp_value, act_value)
170
+ end
171
+ end
172
+ end
173
+
174
+ def validate_set_negated(state, errors, exp, act)
175
+ throw(:valid) if !act.is_a?(Set) || act != exp
176
+
177
+ exp.each do |item|
178
+ errors << state.expected(act).not.including(item)
179
+ end
180
+ end
181
+ end
182
+
183
+ module MatcherBuilding
184
+ ##
185
+ # Matches equal value
186
+ # @example
187
+ # # matches String but not "foo"
188
+ # equal(String)
189
+ # @param value
190
+ # @return [EqualMatcher]
191
+ def equal(value)
192
+ value = expression_or_value(value)
193
+
194
+ EqualMatcher.cache(value, @matcher_cache)
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class EqualSetMatcher < Base
5
+ def initialize(items, negated: false)
6
+ super()
7
+
8
+ @items = items
9
+ @negated = negated
10
+ @includes_expressions = items.any? { _1.is_a?(Expression) }
11
+ end
12
+
13
+ def negate
14
+ EqualSetMatcher.new(@items, negated: !@negated)
15
+ end
16
+
17
+ def set_for(values)
18
+ if @includes_expressions
19
+ @items.to_set do |item|
20
+ item.is_a?(Expression) ? item.evalute(values) : item
21
+ end
22
+ else
23
+ @set ||= Set.new(@items)
24
+ end
25
+ end
26
+
27
+ def validate(state)
28
+ return validate_negated(state) if @negated
29
+
30
+ actual = state.actual
31
+
32
+ unless actual.respond_to?(:each)
33
+ state.errors << state.expected.responding_to(:each)
34
+ return
35
+ end
36
+
37
+ expected_set = set_for(state.values)
38
+ missing = expected_set.dup
39
+
40
+ actual.each_with_index do |act, i|
41
+ if expected_set.include?(act)
42
+ unless missing.delete?(act)
43
+ original_index = index_of(actual, act)
44
+ state.errors[i] << state.expected(act).not.duplicate(original_index)
45
+ end
46
+ else
47
+ state.errors[i] << state.expected(act).not.in(state.actual)
48
+ end
49
+ end
50
+
51
+ missing.each do |m|
52
+ state.errors << state.expected.including(m)
53
+ end
54
+ end
55
+
56
+ def to_s
57
+ "#{'~' if @negated}equal_set(#{@items.map(&:to_s).join(', ')})"
58
+ end
59
+
60
+ private
61
+
62
+ def validate_negated(state)
63
+ actual = state.actual
64
+
65
+ return unless actual.respond_to?(:each)
66
+
67
+ expected_set = set_for(state.values)
68
+ missing = expected_set.dup
69
+
70
+ actual.each do |act|
71
+ return nil if !expected_set.include?(act) || !missing.delete?(act)
72
+ end
73
+
74
+ state.errors << state.expected.namespace(:set).not.equal(@items) if missing.empty?
75
+ end
76
+
77
+ def index_of(collection, item)
78
+ collection = collection.enum_for(:each) unless
79
+ collection.respond_to?(:find_index)
80
+
81
+ collection.find_index(item)
82
+ end
83
+ end
84
+
85
+ module MatcherBuilding
86
+ ##
87
+ # Matches array elements like a set
88
+ # @example
89
+ # # matches [1, 2, 3] and [3, 2, 1] but neither [0, 1, 2] nor [1, 1, 2, 3]
90
+ # equal_set(1, 2, 3)
91
+ # @param items [Array]
92
+ # @return [EqualSetMatcher]
93
+ def equal_set(*items)
94
+ items.map! { expression_or_value(_1) }
95
+
96
+ EqualSetMatcher.new(items)
97
+ end
98
+ end
99
+ end