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,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class IndexByMatcher < Base
|
|
5
|
+
include MappingUtils
|
|
6
|
+
|
|
7
|
+
def initialize(projection, matcher, negated: false)
|
|
8
|
+
super()
|
|
9
|
+
|
|
10
|
+
@projection = projection
|
|
11
|
+
@matcher = negated ? ~matcher : matcher
|
|
12
|
+
@original_matcher = matcher
|
|
13
|
+
@negated = negated
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def negate
|
|
17
|
+
IndexByMatcher.new(@projection, @original_matcher, negated: !@negated)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def validate(state, &)
|
|
21
|
+
return validate_negated(state, &) if @negated
|
|
22
|
+
|
|
23
|
+
actual = state.actual
|
|
24
|
+
|
|
25
|
+
unless actual.respond_to?(:each)
|
|
26
|
+
state.errors << state.expected.responding_to(:each)
|
|
27
|
+
return
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
values = state.values
|
|
31
|
+
failed = false
|
|
32
|
+
index = {}
|
|
33
|
+
mapping = {}
|
|
34
|
+
duplicates = []
|
|
35
|
+
|
|
36
|
+
actual.each_with_index do |item, i|
|
|
37
|
+
key = @projection.evaluate(
|
|
38
|
+
values.merge(actual: item, index: i, original: actual),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
mapped_index = mapping[key]
|
|
42
|
+
|
|
43
|
+
if mapped_index
|
|
44
|
+
duplicates << [key, i, mapped_index]
|
|
45
|
+
else
|
|
46
|
+
index[key] = item
|
|
47
|
+
mapping[key] = i
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
key
|
|
51
|
+
rescue CallError => e
|
|
52
|
+
state.errors[i] << e.message_for_errors(item)
|
|
53
|
+
failed = true
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
duplicates.each do |key, i, j|
|
|
57
|
+
state.errors[i] << state.expected(actual[i]).not.duplicate_by(@projection, key, j)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
return if failed
|
|
61
|
+
|
|
62
|
+
errors = yield(@matcher, index, original: actual)
|
|
63
|
+
errors = map_errors2(errors, mapping) unless state.boolean?
|
|
64
|
+
|
|
65
|
+
state.errors << errors
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def to_s
|
|
69
|
+
"#{'~' if @negated}index_by(#{@projection}, #{@original_matcher})"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def validate_negated(state)
|
|
75
|
+
actual = state.actual
|
|
76
|
+
|
|
77
|
+
return unless actual.respond_to?(:each)
|
|
78
|
+
|
|
79
|
+
values = state.values
|
|
80
|
+
index = {}
|
|
81
|
+
mapping = {}
|
|
82
|
+
|
|
83
|
+
actual.each_with_index do |item, i|
|
|
84
|
+
key = @projection.evaluate(
|
|
85
|
+
values.merge(actual: item, index: i, original: actual),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return nil if mapping.key?(key)
|
|
89
|
+
|
|
90
|
+
index[key] = item
|
|
91
|
+
mapping[key] = i
|
|
92
|
+
|
|
93
|
+
key
|
|
94
|
+
rescue CallError
|
|
95
|
+
return nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
errors = yield(@matcher, index, original: actual)
|
|
99
|
+
|
|
100
|
+
state.errors << map_errors2(errors, mapping)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def map_errors2(error, mapping)
|
|
104
|
+
map_errors(error) do |nested_error|
|
|
105
|
+
key = nested_error.key
|
|
106
|
+
|
|
107
|
+
next unless index_call?(key)
|
|
108
|
+
|
|
109
|
+
index = mapping[operand_of(key)]
|
|
110
|
+
|
|
111
|
+
NestedError.new(index_call_to(index), nested_error.child) if index
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def mapped_base
|
|
116
|
+
return @mapped_base if @mapped_base
|
|
117
|
+
|
|
118
|
+
expression = @projection
|
|
119
|
+
with_index = expression.variables.include?(:index)
|
|
120
|
+
element = expression.free_symbol(:e)
|
|
121
|
+
parameters = [[:opt, element]]
|
|
122
|
+
parameters << %i[opt index] if with_index
|
|
123
|
+
substituted = expression.substitute(actual: element, original: :actual)
|
|
124
|
+
pair = ArrayExpression.new([substituted, Variable.new(element)])
|
|
125
|
+
block = Block.new(parameters, pair)
|
|
126
|
+
|
|
127
|
+
map = if with_index
|
|
128
|
+
enum_for_map = Call.new(Variable.actual, :map)
|
|
129
|
+
Call.new(enum_for_map, :with_index, [], {}, block)
|
|
130
|
+
else
|
|
131
|
+
Call.new(Variable.actual, :map, [], {}, block)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
@mapped_base = Call.new(map, :to_h)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
module MatcherBuilding
|
|
139
|
+
##
|
|
140
|
+
# Matches against an indexed version of actual.
|
|
141
|
+
#
|
|
142
|
+
# This is really useful when validating an array of items where the order
|
|
143
|
+
# shouldn't matter.
|
|
144
|
+
# @example
|
|
145
|
+
# # matches:
|
|
146
|
+
# # [
|
|
147
|
+
# # { name: "bar", value: 2 },
|
|
148
|
+
# # { name: "foo", value: 1 },
|
|
149
|
+
# # ]
|
|
150
|
+
# index_by(_[:name], {
|
|
151
|
+
# "foo" => { name: "foo", value: 1 },
|
|
152
|
+
# "bar" => { name: "bar", value: 2 },
|
|
153
|
+
# })
|
|
154
|
+
# # alternatively:
|
|
155
|
+
# index_by(_[:name]) ^ {
|
|
156
|
+
# "foo" => { name: "foo", value: 1 },
|
|
157
|
+
# "bar" => { name: "bar", value: 2 },
|
|
158
|
+
# }
|
|
159
|
+
# @overload index_by(expression, matcher)
|
|
160
|
+
# @param expression [Expression]
|
|
161
|
+
# @param matcher [Base]
|
|
162
|
+
# @return [IndexByMatcher]
|
|
163
|
+
# @overload index_by(expression)
|
|
164
|
+
# @param expression [Expression]
|
|
165
|
+
# @return [Chain<IndexByMatcher>]
|
|
166
|
+
def index_by(expression, matcher = UNDEFINED)
|
|
167
|
+
return Chain.new { index_by(expression, _1) } if Matcher.undefined?(matcher)
|
|
168
|
+
|
|
169
|
+
expression = expression_of(expression)
|
|
170
|
+
matcher = matcher_of(matcher)
|
|
171
|
+
|
|
172
|
+
IndexByMatcher.new(expression, matcher)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class InlineMatcher < Base
|
|
5
|
+
def initialize(matcher = nil, negatable: false, negated: false, &block)
|
|
6
|
+
raise 'no block given' unless block_given?
|
|
7
|
+
|
|
8
|
+
super()
|
|
9
|
+
|
|
10
|
+
@matcher = matcher
|
|
11
|
+
@negated = negated
|
|
12
|
+
@negatable = negatable
|
|
13
|
+
@block = block
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
attr_reader :matcher, :negated
|
|
17
|
+
|
|
18
|
+
def negate
|
|
19
|
+
return super unless @negatable
|
|
20
|
+
|
|
21
|
+
InlineMatcher.new(@matcher&.~, negatable: @negatable, negated: !@negated, &@block)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def receiver
|
|
25
|
+
@block.binding.receiver
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class InlineContext
|
|
29
|
+
extend Forwardable
|
|
30
|
+
|
|
31
|
+
def initialize(matcher, state, y)
|
|
32
|
+
@matcher = matcher
|
|
33
|
+
@state = state
|
|
34
|
+
@yield = y
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
attr_reader :state
|
|
38
|
+
|
|
39
|
+
def_delegators :@state, *State.public_instance_methods(false) - %i[result]
|
|
40
|
+
def_delegators :@matcher, :receiver, :matcher, :negated
|
|
41
|
+
|
|
42
|
+
def _yield(matcher, act = @state.actual, **values)
|
|
43
|
+
@yield.call(matcher, act, **values)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def validate(state, &block)
|
|
48
|
+
context = InlineContext.new(self, state, block)
|
|
49
|
+
context.instance_exec(&@block)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def to_s
|
|
53
|
+
args = if @matcher
|
|
54
|
+
"(#{@negated ? ~@matcher : @matcher})"
|
|
55
|
+
else
|
|
56
|
+
''
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
"#{'~' if @negated}inline#{args} { #{Utils.block_location(@block)} }"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
module MatcherBuilding
|
|
64
|
+
##
|
|
65
|
+
# Creates an anonymous custom matcher
|
|
66
|
+
#
|
|
67
|
+
# If you need more control to define your matching logic then +inline+
|
|
68
|
+
# may give you an alternative to implementing a new matcher class. Within
|
|
69
|
+
# the +inline+ block you have direct access to +actual+, +errors+, +_yield+
|
|
70
|
+
# and other state methods.
|
|
71
|
+
#
|
|
72
|
+
# @example
|
|
73
|
+
# # matches distinct arrays
|
|
74
|
+
# inline do
|
|
75
|
+
# indices = Hash.new
|
|
76
|
+
#
|
|
77
|
+
# actual.each_with_index do |e, i|
|
|
78
|
+
# if (original_index = indices[e])
|
|
79
|
+
# errors[i] << expected.not.duplicate(original_index)
|
|
80
|
+
# else
|
|
81
|
+
# indices[e] = i
|
|
82
|
+
# end
|
|
83
|
+
# end
|
|
84
|
+
# end
|
|
85
|
+
#
|
|
86
|
+
# @param matcher [Base] optionaly provide a child matcher. Call the matcher
|
|
87
|
+
# with +_yield matcher, actual, **values+
|
|
88
|
+
# @param negatable [true, false] set to true if your matching logic respects
|
|
89
|
+
# the negated flag. Otherwise, the default negation implementation is used.
|
|
90
|
+
# When +negated = true+ the child matcher is automatically negated.
|
|
91
|
+
# @yield inline context
|
|
92
|
+
# @return [InlineMatcher]
|
|
93
|
+
def inline(matcher = UNDEFINED, negatable: false, &)
|
|
94
|
+
matcher = Matcher.undefined?(matcher) ? nil : matcher_of(matcher)
|
|
95
|
+
|
|
96
|
+
InlineMatcher.new(matcher, negatable:, &)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class KeysMatcher < Base
|
|
5
|
+
def initialize(keys, partial: false, negated: false)
|
|
6
|
+
super()
|
|
7
|
+
|
|
8
|
+
@keys = keys
|
|
9
|
+
@partial = partial
|
|
10
|
+
@negated = negated
|
|
11
|
+
@includes_expressions = keys.any? { _1.is_a?(Expression) }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def negate
|
|
15
|
+
KeysMatcher.new(@keys, partial: @partial, negated: !@negated)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def validate(state, &)
|
|
19
|
+
return validate_negated(state, &) if @negated
|
|
20
|
+
|
|
21
|
+
actual = state.actual
|
|
22
|
+
|
|
23
|
+
unless actual.is_a?(Hash)
|
|
24
|
+
state.errors << state.expected.kind_of(Hash)
|
|
25
|
+
return
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
actual_keys = actual.keys
|
|
29
|
+
|
|
30
|
+
expected_keys = if @includes_expressions
|
|
31
|
+
@keys.map { _1.is_a?(Expression) ? _1.evaluate(state.values) : _1 }
|
|
32
|
+
else
|
|
33
|
+
@keys
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
(expected_keys - actual_keys).each do |key|
|
|
37
|
+
state.errors << state.expected.having_key(key)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
return if @partial
|
|
41
|
+
|
|
42
|
+
(actual_keys - expected_keys).each do |key|
|
|
43
|
+
state.errors << state.expected.not.having_key(key)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def to_s
|
|
48
|
+
helper = @partial ? 'partial_keys' : 'keys'
|
|
49
|
+
args = @keys.map(&:inspect).join(', ')
|
|
50
|
+
|
|
51
|
+
"#{'~' if @negated}#{helper}(#{args})"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def validate_negated(state)
|
|
57
|
+
actual = state.actual
|
|
58
|
+
|
|
59
|
+
return unless actual.is_a?(Hash)
|
|
60
|
+
|
|
61
|
+
actual_keys = actual.keys
|
|
62
|
+
|
|
63
|
+
expected_keys = if @includes_expressions
|
|
64
|
+
@keys.map { _1.is_a?(Expression) ? _1.evaluate(state.values) : _1 }
|
|
65
|
+
else
|
|
66
|
+
@keys
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
if @partial
|
|
70
|
+
return unless (expected_keys - actual_keys).empty?
|
|
71
|
+
|
|
72
|
+
state.errors.or!
|
|
73
|
+
|
|
74
|
+
(actual_keys & expected_keys).each do |key|
|
|
75
|
+
state.errors << state.expected.not.having_key(key)
|
|
76
|
+
end
|
|
77
|
+
else
|
|
78
|
+
return if actual_keys != expected_keys
|
|
79
|
+
|
|
80
|
+
state.errors.or!
|
|
81
|
+
|
|
82
|
+
actual_keys.each do |key|
|
|
83
|
+
state.errors << state.expected.not.having_key(key)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
module MatcherBuilding
|
|
90
|
+
##
|
|
91
|
+
# Matches all keys of a hash
|
|
92
|
+
# @example
|
|
93
|
+
# # matches { foo: 1, bar: 2 } but not { foo: 1, qux: 3 }
|
|
94
|
+
# keys(:foo, :bar)
|
|
95
|
+
# # using key expression, matches { "the_key" => 23 }
|
|
96
|
+
# let(my_key: :the_key) ^ keys(vars[:my_key].to_s)
|
|
97
|
+
# @param keys [Array] supports expressions
|
|
98
|
+
# @param partial [true, false] ignores extra keys when +true+
|
|
99
|
+
# @return [KeysMatcher]
|
|
100
|
+
# @see #partial_keys
|
|
101
|
+
def keys(*keys, partial: false)
|
|
102
|
+
keys.each_with_index do |key, i|
|
|
103
|
+
keys[i] = expression_or_value(key)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
KeysMatcher.new(keys, partial:)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
##
|
|
110
|
+
# Matches hash if all given keys are included. Ignores extra keys.
|
|
111
|
+
# @example
|
|
112
|
+
# # matches { foo: 1, bar: 2 } but not { bar: 2 }
|
|
113
|
+
# partial_keys(:foo)
|
|
114
|
+
# @param keys [Array] supports expressions
|
|
115
|
+
# @return [KeysMatcher]
|
|
116
|
+
# @see #keys
|
|
117
|
+
def partial_keys(*keys)
|
|
118
|
+
keys(*keys, partial: true)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class KindOfMatcher < Base
|
|
5
|
+
def self.cache(kind, matcher_cache = MatcherCache.current)
|
|
6
|
+
return new(kind) unless matcher_cache
|
|
7
|
+
|
|
8
|
+
(matcher_cache.kind_of_matchers ||= {})[kind] ||= new(kind)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(kind, negated: false)
|
|
12
|
+
super()
|
|
13
|
+
|
|
14
|
+
@kind = kind
|
|
15
|
+
@negated = negated
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def negate
|
|
19
|
+
KindOfMatcher.new(@kind, negated: !@negated)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def validate(state)
|
|
23
|
+
state.errors << state.expected.not_if(@negated).kind_of(@kind) if
|
|
24
|
+
state.actual.is_a?(@kind) == @negated
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def to_s
|
|
28
|
+
if @negated
|
|
29
|
+
"neg(#{@kind})"
|
|
30
|
+
else
|
|
31
|
+
@kind.to_s
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class LazyAllMatcher < Base
|
|
5
|
+
def initialize(matchers)
|
|
6
|
+
super()
|
|
7
|
+
|
|
8
|
+
@matchers = matchers
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
attr_reader :matchers
|
|
12
|
+
|
|
13
|
+
def negate
|
|
14
|
+
LazyAnyMatcher.new(@matchers.map(&:~))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def &(matcher)
|
|
18
|
+
matcher = Matcher.cache(matcher)
|
|
19
|
+
|
|
20
|
+
if matcher.is_a?(LazyAllMatcher)
|
|
21
|
+
LazyAllMatcher.new(@matchers + matcher.matchers)
|
|
22
|
+
else
|
|
23
|
+
LazyAllMatcher.new(@matchers + [matcher])
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def validate(state)
|
|
28
|
+
last_error = EmptyError.instance
|
|
29
|
+
|
|
30
|
+
@matchers.each do |matcher|
|
|
31
|
+
last_error = yield matcher
|
|
32
|
+
|
|
33
|
+
break unless last_error.valid?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
state.errors << last_error
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def to_s
|
|
40
|
+
"lazy_all(#{@matchers.map(&:to_s).join(', ')})"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
module MatcherBuilding
|
|
45
|
+
##
|
|
46
|
+
# Matches all matchers lazily. Returns only the last match result (similar to &&)
|
|
47
|
+
# @example
|
|
48
|
+
# # matches 3 but not "foo"
|
|
49
|
+
# lazy_all(Integer, _ % 3 == 0)
|
|
50
|
+
# # alternatively:
|
|
51
|
+
# of(Integer) & _.positive?
|
|
52
|
+
# @param matchers [Array<Base>]
|
|
53
|
+
# @return [LazyAllMatcher]
|
|
54
|
+
# @see Base#&
|
|
55
|
+
# @see #lazy_any
|
|
56
|
+
# @see #all
|
|
57
|
+
def lazy_all(*matchers)
|
|
58
|
+
case matchers.length
|
|
59
|
+
when 0
|
|
60
|
+
AlwaysMatcher.instance
|
|
61
|
+
when 1
|
|
62
|
+
matcher_of(matchers[0])
|
|
63
|
+
else
|
|
64
|
+
LazyAllMatcher.new(matchers.map { matcher_of(_1) })
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class LazyAnyMatcher < Base
|
|
5
|
+
def initialize(matchers)
|
|
6
|
+
super()
|
|
7
|
+
|
|
8
|
+
@matchers = matchers
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
attr_reader :matchers
|
|
12
|
+
|
|
13
|
+
def negate
|
|
14
|
+
LazyAllMatcher.new(@matchers.map(&:~))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def |(matcher)
|
|
18
|
+
matcher = Matcher.cache(matcher)
|
|
19
|
+
|
|
20
|
+
if matcher.is_a?(LazyAnyMatcher)
|
|
21
|
+
LazyAnyMatcher.new(@matchers + matcher.matchers)
|
|
22
|
+
else
|
|
23
|
+
LazyAnyMatcher.new(@matchers + [matcher])
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def validate(state)
|
|
28
|
+
last_error = EmptyError.instance
|
|
29
|
+
|
|
30
|
+
@matchers.each do |matcher|
|
|
31
|
+
last_error = yield matcher
|
|
32
|
+
|
|
33
|
+
break if last_error.valid?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
state.errors << last_error
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def to_s
|
|
40
|
+
"lazy_any(#{@matchers.map(&:to_s).join(', ')})"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
module MatcherBuilding
|
|
45
|
+
##
|
|
46
|
+
# Matches any matcher lazily. Returns only the last match result (similar to ||)
|
|
47
|
+
# @example
|
|
48
|
+
# # matches "foo" and 42 but not +nil+ or +true+
|
|
49
|
+
# lazy_any(String, Integer)
|
|
50
|
+
# # alternatively:
|
|
51
|
+
# of(String) | of(Integer)
|
|
52
|
+
# @param matchers [Array<Base>]
|
|
53
|
+
# @return [LazyAnyMatcher]
|
|
54
|
+
# @see Base#|
|
|
55
|
+
# @see #lazy_all
|
|
56
|
+
# @see #any
|
|
57
|
+
def lazy_any(*matchers)
|
|
58
|
+
case matchers.length
|
|
59
|
+
when 0
|
|
60
|
+
AlwaysMatcher.instance
|
|
61
|
+
when 1
|
|
62
|
+
matcher_of(matchers[0])
|
|
63
|
+
else
|
|
64
|
+
LazyAnyMatcher.new(matchers.map { matcher_of(_1) })
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class LetMatcher < Base
|
|
5
|
+
def initialize(assigns, matcher)
|
|
6
|
+
super()
|
|
7
|
+
|
|
8
|
+
@assigns = assigns
|
|
9
|
+
@matcher = matcher
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def negate
|
|
13
|
+
LetMatcher.new(@assigns, ~@matcher)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def validate(state)
|
|
17
|
+
assigns = @assigns.transform_values do |v|
|
|
18
|
+
v = Expression.try_recorder(v)
|
|
19
|
+
|
|
20
|
+
case v
|
|
21
|
+
when Proc
|
|
22
|
+
Utils.call_block(v, state.values)
|
|
23
|
+
when Expression
|
|
24
|
+
v.evaluate(state.values)
|
|
25
|
+
else
|
|
26
|
+
v
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
state.errors << yield(@matcher, **assigns)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def to_s
|
|
34
|
+
assign_parts = @assigns.map do |key, value|
|
|
35
|
+
if value.is_a?(Proc)
|
|
36
|
+
"#{key}: ->(#{Utils.inspect_block_params(value)}) { ... }"
|
|
37
|
+
else
|
|
38
|
+
"#{key}: #{value.inspect}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
"let(#{assign_parts.join(', ')}) ^ #{Matcher.parenthesize(@matcher)}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
module MatcherBuilding
|
|
47
|
+
##
|
|
48
|
+
# Sets values for given matcher
|
|
49
|
+
# @example
|
|
50
|
+
# # matches 1
|
|
51
|
+
# let({ a: 1 }, _ == vars[:a])
|
|
52
|
+
# # alternatively:
|
|
53
|
+
# let(a: 1) ^ (_ == vars[:a])
|
|
54
|
+
# @overload let(assigns, matcher)
|
|
55
|
+
# @param assigns [Hash]
|
|
56
|
+
# @param matcher [Base]
|
|
57
|
+
# @return [LetMatcher]
|
|
58
|
+
# @overload let(**kwargs)
|
|
59
|
+
# @param kwargs [Hash] same as assigns
|
|
60
|
+
# @return [Chain<LetMatcher>]
|
|
61
|
+
def let(assigns = nil, matcher = UNDEFINED, **kwargs)
|
|
62
|
+
raise "Cannot set both assigns and kwargs" if assigns && !kwargs.empty?
|
|
63
|
+
|
|
64
|
+
assigns ||= kwargs
|
|
65
|
+
|
|
66
|
+
return Chain.new { let(assigns, _1) } if Matcher.undefined?(matcher)
|
|
67
|
+
|
|
68
|
+
matcher = matcher_of(matcher)
|
|
69
|
+
|
|
70
|
+
LetMatcher.new(assigns, matcher)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|