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.
- checksums.yaml +7 -0
- data/lib/matcher/assertions.rb +21 -0
- data/lib/matcher/base.rb +189 -0
- data/lib/matcher/builder.rb +74 -0
- data/lib/matcher/chain.rb +60 -0
- data/lib/matcher/debug.rb +48 -0
- data/lib/matcher/errors/and_error.rb +86 -0
- data/lib/matcher/errors/boolean_collector.rb +51 -0
- data/lib/matcher/errors/element_error.rb +24 -0
- data/lib/matcher/errors/empty_error.rb +23 -0
- data/lib/matcher/errors/error.rb +39 -0
- data/lib/matcher/errors/error_collector.rb +99 -0
- data/lib/matcher/errors/nested_error.rb +96 -0
- data/lib/matcher/errors/or_error.rb +86 -0
- data/lib/matcher/expression_cache.rb +57 -0
- data/lib/matcher/expression_labeler.rb +91 -0
- data/lib/matcher/expressions/array_expression.rb +45 -0
- data/lib/matcher/expressions/block.rb +153 -0
- data/lib/matcher/expressions/call.rb +338 -0
- data/lib/matcher/expressions/call_error.rb +45 -0
- data/lib/matcher/expressions/constant.rb +53 -0
- data/lib/matcher/expressions/expression.rb +147 -0
- data/lib/matcher/expressions/expression_building.rb +258 -0
- data/lib/matcher/expressions/expression_walker.rb +73 -0
- data/lib/matcher/expressions/hash_expression.rb +59 -0
- data/lib/matcher/expressions/proc_expression.rb +92 -0
- data/lib/matcher/expressions/range_expression.rb +58 -0
- data/lib/matcher/expressions/recorder.rb +86 -0
- data/lib/matcher/expressions/rescue_last_error_expression.rb +44 -0
- data/lib/matcher/expressions/set_expression.rb +45 -0
- data/lib/matcher/expressions/string_expression.rb +53 -0
- data/lib/matcher/expressions/symbol_proc.rb +53 -0
- data/lib/matcher/expressions/variable.rb +85 -0
- data/lib/matcher/hash_stack.rb +53 -0
- data/lib/matcher/list.rb +102 -0
- data/lib/matcher/markers/optional.rb +80 -0
- data/lib/matcher/markers/others.rb +28 -0
- data/lib/matcher/matcher_cache.rb +18 -0
- data/lib/matcher/matchers/all_matcher.rb +60 -0
- data/lib/matcher/matchers/always_matcher.rb +28 -0
- data/lib/matcher/matchers/any_matcher.rb +70 -0
- data/lib/matcher/matchers/array_matcher.rb +35 -0
- data/lib/matcher/matchers/block_matcher.rb +59 -0
- data/lib/matcher/matchers/boolean_matcher.rb +35 -0
- data/lib/matcher/matchers/dig_matcher.rb +146 -0
- data/lib/matcher/matchers/each_matcher.rb +52 -0
- data/lib/matcher/matchers/each_pair_matcher.rb +119 -0
- data/lib/matcher/matchers/equal_matcher.rb +197 -0
- data/lib/matcher/matchers/equal_set_matcher.rb +99 -0
- data/lib/matcher/matchers/expression_matcher.rb +73 -0
- data/lib/matcher/matchers/filter_matcher.rb +111 -0
- data/lib/matcher/matchers/hash_matcher.rb +223 -0
- data/lib/matcher/matchers/imply_matcher.rb +81 -0
- data/lib/matcher/matchers/imply_some_matcher.rb +112 -0
- data/lib/matcher/matchers/index_by_matcher.rb +175 -0
- data/lib/matcher/matchers/inline_matcher.rb +99 -0
- data/lib/matcher/matchers/keys_matcher.rb +121 -0
- data/lib/matcher/matchers/kind_of_matcher.rb +35 -0
- data/lib/matcher/matchers/lazy_all_matcher.rb +68 -0
- data/lib/matcher/matchers/lazy_any_matcher.rb +68 -0
- data/lib/matcher/matchers/let_matcher.rb +73 -0
- data/lib/matcher/matchers/map_matcher.rb +129 -0
- data/lib/matcher/matchers/matcher_building.rb +5 -0
- data/lib/matcher/matchers/negated_array_matcher.rb +38 -0
- data/lib/matcher/matchers/negated_each_matcher.rb +36 -0
- data/lib/matcher/matchers/negated_each_pair_matcher.rb +38 -0
- data/lib/matcher/matchers/negated_imply_some_matcher.rb +46 -0
- data/lib/matcher/matchers/negated_matcher.rb +23 -0
- data/lib/matcher/matchers/negated_project_matcher.rb +31 -0
- data/lib/matcher/matchers/never_matcher.rb +29 -0
- data/lib/matcher/matchers/one_matcher.rb +70 -0
- data/lib/matcher/matchers/optional_matcher.rb +38 -0
- data/lib/matcher/matchers/parse_float_matcher.rb +86 -0
- data/lib/matcher/matchers/parse_integer_matcher.rb +98 -0
- data/lib/matcher/matchers/parse_iso8601_matcher.rb +92 -0
- data/lib/matcher/matchers/parse_json_matcher.rb +95 -0
- data/lib/matcher/matchers/project_matcher.rb +68 -0
- data/lib/matcher/matchers/raises_matcher.rb +124 -0
- data/lib/matcher/matchers/range_matcher.rb +47 -0
- data/lib/matcher/matchers/reference_matcher.rb +111 -0
- data/lib/matcher/matchers/reference_matcher_collection.rb +57 -0
- data/lib/matcher/matchers/regexp_matcher.rb +84 -0
- data/lib/matcher/messages/expected_phrasing.rb +342 -0
- data/lib/matcher/messages/message.rb +102 -0
- data/lib/matcher/messages/message_builder.rb +35 -0
- data/lib/matcher/messages/message_rules.rb +223 -0
- data/lib/matcher/messages/namespaced_message_builder.rb +19 -0
- data/lib/matcher/messages/phrasing.rb +57 -0
- data/lib/matcher/messages/standard_message_builder.rb +105 -0
- data/lib/matcher/once_before.rb +18 -0
- data/lib/matcher/optional_chain.rb +24 -0
- data/lib/matcher/patterns/ast_mapping.rb +42 -0
- data/lib/matcher/patterns/capture_hole.rb +33 -0
- data/lib/matcher/patterns/constant_hole.rb +14 -0
- data/lib/matcher/patterns/hole.rb +30 -0
- data/lib/matcher/patterns/method_hole.rb +58 -0
- data/lib/matcher/patterns/pattern.rb +92 -0
- data/lib/matcher/patterns/pattern_building.rb +39 -0
- data/lib/matcher/patterns/pattern_capture.rb +11 -0
- data/lib/matcher/patterns/pattern_match.rb +29 -0
- data/lib/matcher/patterns/variable_hole.rb +14 -0
- data/lib/matcher/reporter.rb +98 -0
- data/lib/matcher/rules/message_factory.rb +25 -0
- data/lib/matcher/rules/message_rule.rb +18 -0
- data/lib/matcher/rules/message_rule_context.rb +24 -0
- data/lib/matcher/rules/rule_builder.rb +29 -0
- data/lib/matcher/rules/rule_set.rb +57 -0
- data/lib/matcher/rules/transform_builder.rb +24 -0
- data/lib/matcher/rules/transform_mapping.rb +5 -0
- data/lib/matcher/rules/transform_rule.rb +21 -0
- data/lib/matcher/state.rb +40 -0
- data/lib/matcher/testing/error_builder.rb +62 -0
- data/lib/matcher/testing/error_checker.rb +496 -0
- data/lib/matcher/testing/error_testing.rb +37 -0
- data/lib/matcher/testing/pattern_testing.rb +11 -0
- data/lib/matcher/testing/pattern_testing_scope.rb +34 -0
- data/lib/matcher/testing.rb +102 -0
- data/lib/matcher/undefined.rb +10 -0
- data/lib/matcher/utils/mapping_utils.rb +61 -0
- data/lib/matcher/utils.rb +72 -0
- data/lib/matcher/version.rb +5 -0
- data/lib/matcher.rb +337 -0
- 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
|