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,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class ExpressionMatcher < Base
|
|
5
|
+
def self.cache(value, matcher_cache = MatcherCache.current, expression_cache = ExpressionCache.current)
|
|
6
|
+
return new(value) unless matcher_cache
|
|
7
|
+
|
|
8
|
+
cache = (matcher_cache.expression_matchers ||= {})
|
|
9
|
+
label = expression_cache.label(value)
|
|
10
|
+
|
|
11
|
+
cache[label] ||= new(value)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.message_rules
|
|
15
|
+
@message_rules ||= RuleSet.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
attr_reader :expression, :negated
|
|
19
|
+
|
|
20
|
+
def initialize(expression, negated: false)
|
|
21
|
+
super()
|
|
22
|
+
|
|
23
|
+
@expression = expression
|
|
24
|
+
@negated = negated
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def negate
|
|
28
|
+
ExpressionMatcher.new(@expression, negated: !@negated)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def validate(state)
|
|
32
|
+
if state.boolean?
|
|
33
|
+
state.errors << 'invalid' if @negated != !@expression.evaluate(state.values)
|
|
34
|
+
return
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
value_tree = @expression.evaluate_tree(state.values)
|
|
38
|
+
evaluation = value_tree[-1]
|
|
39
|
+
|
|
40
|
+
if @negated != !evaluation
|
|
41
|
+
rule_context = MessageRuleContext.new(self, state)
|
|
42
|
+
state.errors << message_factory.create(rule_context, value_tree)
|
|
43
|
+
end
|
|
44
|
+
rescue CallError => e
|
|
45
|
+
state.errors << e.message_for_errors(state.actual) unless @negated
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def to_s
|
|
49
|
+
if @negated
|
|
50
|
+
"neg(#{@expression})"
|
|
51
|
+
else
|
|
52
|
+
@expression.to_s
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
NEGATED_COMPARISONS = {
|
|
59
|
+
:== => :!=,
|
|
60
|
+
:!= => :==,
|
|
61
|
+
:< => :>=,
|
|
62
|
+
:> => :<=,
|
|
63
|
+
:>= => :<,
|
|
64
|
+
:<= => :>,
|
|
65
|
+
:=~ => :!~,
|
|
66
|
+
:!~ => :=~,
|
|
67
|
+
}.freeze
|
|
68
|
+
|
|
69
|
+
def message_factory
|
|
70
|
+
@message_factory ||= ExpressionMatcher.message_rules.apply(@expression)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class FilterMatcher < Base
|
|
5
|
+
include MappingUtils
|
|
6
|
+
|
|
7
|
+
def initialize(filter, matcher, negated: false)
|
|
8
|
+
super()
|
|
9
|
+
|
|
10
|
+
@filter = filter
|
|
11
|
+
@matcher = negated ? ~matcher : matcher
|
|
12
|
+
@original_matcher = matcher
|
|
13
|
+
@negated = negated
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def negate
|
|
17
|
+
FilterMatcher.new(@filter, @original_matcher, negated: !@negated)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def validate(state)
|
|
21
|
+
actual = state.actual
|
|
22
|
+
|
|
23
|
+
unless actual.respond_to?(:each)
|
|
24
|
+
state.errors << state.expected.responding_to(:each) unless @negated
|
|
25
|
+
return
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
i = 0
|
|
29
|
+
mapping = {}
|
|
30
|
+
items = []
|
|
31
|
+
failed = false
|
|
32
|
+
|
|
33
|
+
actual.each do |act|
|
|
34
|
+
filter_value = @filter.evaluate(
|
|
35
|
+
state.values.merge(actual: act, index: i, original: actual),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if filter_value
|
|
39
|
+
mapping[items.length] = i
|
|
40
|
+
items << act
|
|
41
|
+
end
|
|
42
|
+
rescue CallError => e
|
|
43
|
+
return nil if @negated
|
|
44
|
+
|
|
45
|
+
state.errors[i] << e.message_for_errors(act)
|
|
46
|
+
failed = true
|
|
47
|
+
ensure
|
|
48
|
+
i += 1
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
return if failed
|
|
52
|
+
|
|
53
|
+
errors = yield(@matcher, items, original: actual)
|
|
54
|
+
|
|
55
|
+
unless state.boolean?
|
|
56
|
+
errors = map_errors(errors) do |nested_error|
|
|
57
|
+
key = nested_error.key
|
|
58
|
+
|
|
59
|
+
next unless index_call?(key)
|
|
60
|
+
|
|
61
|
+
original_index = mapping[operand_of(key)]
|
|
62
|
+
|
|
63
|
+
NestedError.new(index_call_to(original_index), nested_error.child) if original_index
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
state.errors << errors
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def to_s
|
|
71
|
+
"#{'~' if @negated}filter(#{@filter}, #{@original_matcher})"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def mapped_base
|
|
77
|
+
@mapped_base ||= map_base(:filter, @filter)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
module MatcherBuilding
|
|
82
|
+
# Matches only filtered elements
|
|
83
|
+
# == +expression+ values
|
|
84
|
+
# - actual
|
|
85
|
+
# - index
|
|
86
|
+
# - original
|
|
87
|
+
# == +matcher+ values
|
|
88
|
+
# - original
|
|
89
|
+
# @example
|
|
90
|
+
# # matches [1, 2, 3, 4, 5]
|
|
91
|
+
# filter(_.odd?, [1, 3, 5])
|
|
92
|
+
# # alternatively:
|
|
93
|
+
# filter(_.odd?) ^ [1, 3, 5]
|
|
94
|
+
# @overload filter(expression, matcher)
|
|
95
|
+
# @param expression [Expression] matches elements for which +expression+ is truthy
|
|
96
|
+
# @param matcher
|
|
97
|
+
# @return [FilterMatcher]
|
|
98
|
+
# @overload filter(expression)
|
|
99
|
+
# @param expression [Expression] matches elements for which +expression+ is truthy
|
|
100
|
+
# @return [Chain<FilterMatcher>]
|
|
101
|
+
def filter(expression, matcher = UNDEFINED)
|
|
102
|
+
return Chain.new { filter(expression, _1) } if
|
|
103
|
+
Matcher.undefined?(matcher)
|
|
104
|
+
|
|
105
|
+
expression = expression_of(expression)
|
|
106
|
+
matcher = matcher_of(matcher)
|
|
107
|
+
|
|
108
|
+
FilterMatcher.new(expression, matcher)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class HashMatcher < Base
|
|
5
|
+
def initialize(hash, partial: false, negated: false)
|
|
6
|
+
super()
|
|
7
|
+
|
|
8
|
+
@hash = negated ? hash.transform_values(&:~) : hash
|
|
9
|
+
@original_hash = hash
|
|
10
|
+
@partial = partial
|
|
11
|
+
@negated = negated
|
|
12
|
+
@includes_others = hash.include?(Others.instance)
|
|
13
|
+
@includes_optionals = hash.each_key.any? { _1.is_a?(Optional) }
|
|
14
|
+
@includes_expressions = hash.each_key.any? { _1.is_a?(Expression) }
|
|
15
|
+
|
|
16
|
+
raise 'cannot use partial(others => ...)' if @partial && @includes_others
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def negate
|
|
20
|
+
HashMatcher.new(@original_hash, partial: @partial, negated: !@negated)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def validate(state, &)
|
|
24
|
+
return validate_negated(state, &) if @negated
|
|
25
|
+
|
|
26
|
+
actual = state.actual
|
|
27
|
+
|
|
28
|
+
unless actual.is_a?(Hash)
|
|
29
|
+
state.errors << state.expected.kind_of(Hash)
|
|
30
|
+
return
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
if @includes_expressions
|
|
34
|
+
expression_values = {}
|
|
35
|
+
|
|
36
|
+
@hash.each_key.with_index do |key, i|
|
|
37
|
+
key = key.value if key.is_a?(Optional)
|
|
38
|
+
expression_values[i] = key.evaluate(state.values) if key.is_a?(Expression)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
expected_keys = if @includes_optionals || @includes_expressions
|
|
43
|
+
@hash.keys.map.with_index do |k, i|
|
|
44
|
+
k = k.value if k.is_a?(Optional)
|
|
45
|
+
k = expression_values[i] if k.is_a?(Expression)
|
|
46
|
+
k
|
|
47
|
+
end
|
|
48
|
+
else
|
|
49
|
+
@hash.keys
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
extra_keys = actual.keys - expected_keys
|
|
53
|
+
|
|
54
|
+
if !@partial && !@includes_others
|
|
55
|
+
extra_keys.each do |key|
|
|
56
|
+
state.errors[key] << state.expected.not.having_key(key)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
@hash.each_with_index do |(key, value), i|
|
|
61
|
+
if key.is_a?(Others)
|
|
62
|
+
state.errors << yield(value, actual.slice(*extra_keys))
|
|
63
|
+
|
|
64
|
+
next
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
is_optional = key.is_a?(Optional)
|
|
68
|
+
key = key.value if is_optional
|
|
69
|
+
error_key = key
|
|
70
|
+
key = expression_values[i] if key.is_a?(Expression)
|
|
71
|
+
actual_value = actual[key]
|
|
72
|
+
|
|
73
|
+
if actual_value.nil? && !actual.key?(key)
|
|
74
|
+
state.errors << state.expected.having_key(key) unless is_optional
|
|
75
|
+
else
|
|
76
|
+
error = yield(value, actual_value, key:, parent: actual)
|
|
77
|
+
|
|
78
|
+
next if error.valid?
|
|
79
|
+
|
|
80
|
+
error_key = key_call_for(error_key) if error_key.is_a?(Expression)
|
|
81
|
+
|
|
82
|
+
state.errors[error_key] << error
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def to_s
|
|
88
|
+
if @negated
|
|
89
|
+
@partial ? "~partial(#{@original_hash})" : "neg(#{@original_hash})"
|
|
90
|
+
else
|
|
91
|
+
@partial ? "partial(#{@hash})" : @hash.to_s
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def validate_negated(state)
|
|
98
|
+
actual = state.actual
|
|
99
|
+
|
|
100
|
+
return unless actual.is_a?(Hash)
|
|
101
|
+
|
|
102
|
+
if @hash.empty?
|
|
103
|
+
if @partial
|
|
104
|
+
state.errors << state.report.kind_of(Hash)
|
|
105
|
+
elsif actual.empty?
|
|
106
|
+
state.errors << state.report.predicate(:empty?)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
return
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
if @includes_expressions
|
|
113
|
+
expression_values = {}
|
|
114
|
+
|
|
115
|
+
@hash.each_key.with_index do |key, i|
|
|
116
|
+
key = key.value if key.is_a?(Optional)
|
|
117
|
+
expression_values[i] = key.evaluate(state.values) if key.is_a?(Expression)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
expected_keys = if @includes_optionals || @includes_expressions
|
|
122
|
+
@hash.keys.map.with_index do |k, i|
|
|
123
|
+
k = k.value if k.is_a?(Optional)
|
|
124
|
+
k = expression_values[i] if k.is_a?(Expression)
|
|
125
|
+
k
|
|
126
|
+
end
|
|
127
|
+
else
|
|
128
|
+
@hash.keys
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
extra_keys = actual.keys - expected_keys
|
|
132
|
+
|
|
133
|
+
return if !@partial && !@includes_others && !extra_keys.empty?
|
|
134
|
+
|
|
135
|
+
collector = state.new_collector.or!
|
|
136
|
+
|
|
137
|
+
@hash.each_with_index do |(key, value), i|
|
|
138
|
+
if key.is_a?(Others)
|
|
139
|
+
result = yield(value, actual.slice(*extra_keys))
|
|
140
|
+
|
|
141
|
+
return if result.valid?
|
|
142
|
+
|
|
143
|
+
collector << result
|
|
144
|
+
|
|
145
|
+
next
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
is_optional = key.is_a?(Optional)
|
|
149
|
+
key = key.value if is_optional
|
|
150
|
+
error_key = key
|
|
151
|
+
key = expression_values[i] if key.is_a?(Expression)
|
|
152
|
+
actual_value = actual[key]
|
|
153
|
+
|
|
154
|
+
if actual_value.nil? && !actual.key?(key)
|
|
155
|
+
next if is_optional
|
|
156
|
+
|
|
157
|
+
return
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
result = yield(value, actual_value, key:, parent: actual)
|
|
161
|
+
|
|
162
|
+
return if result.valid?
|
|
163
|
+
|
|
164
|
+
error_key = key_call_for(error_key) if error_key.is_a?(Expression)
|
|
165
|
+
|
|
166
|
+
collector[error_key] << result
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
state.errors << if collector.empty?
|
|
170
|
+
state.report.predicate(:empty?)
|
|
171
|
+
else
|
|
172
|
+
collector.error
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def key_call_for(key)
|
|
177
|
+
Call.new(Variable.actual, :[], [key])
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
module MatcherBuilding
|
|
182
|
+
##
|
|
183
|
+
# Matches hash partially
|
|
184
|
+
# @example
|
|
185
|
+
# # matches { foo: 1, bar: 2 } but not { foo: 0, bar: 2 }
|
|
186
|
+
# partial(foo: 1)
|
|
187
|
+
# @param hash [Hash]
|
|
188
|
+
# @return [HashMatcher]
|
|
189
|
+
def partial(hash)
|
|
190
|
+
hash = hash.to_h do |k, v|
|
|
191
|
+
[expression_or_value(k), matcher_of(v)]
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
HashMatcher.new(hash, partial: true)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
##
|
|
198
|
+
# Matches nested hashes partially
|
|
199
|
+
# @example
|
|
200
|
+
# # matches { foo: { bar: 1, baz: 2 }, qux: 3 }
|
|
201
|
+
# partial_r(foo: { bar: 1 })
|
|
202
|
+
# # equivalent to:
|
|
203
|
+
# partial(foo: partial(bar: 1))
|
|
204
|
+
# @param hash [Hash]
|
|
205
|
+
# @return [HashMatcher]
|
|
206
|
+
def partial_r(hash)
|
|
207
|
+
hash = hash.to_h do |k, v|
|
|
208
|
+
[expression_or_value(k), partial_r_helper(v)]
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
HashMatcher.new(hash, partial: true)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def partial_r_helper(value)
|
|
215
|
+
if Recorder.recorder?(value) || !value.is_a?(Hash)
|
|
216
|
+
matcher_of(value)
|
|
217
|
+
else
|
|
218
|
+
partial_r(value)
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
private :partial_r_helper
|
|
222
|
+
end
|
|
223
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class ImplyMatcher < Base
|
|
5
|
+
attr_reader :condition, :matcher
|
|
6
|
+
|
|
7
|
+
def initialize(condition, matcher, negated: false)
|
|
8
|
+
super()
|
|
9
|
+
|
|
10
|
+
@condition = condition
|
|
11
|
+
@matcher = negated ? ~matcher : matcher
|
|
12
|
+
@original_matcher = matcher
|
|
13
|
+
@negated = negated
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def negate
|
|
17
|
+
ImplyMatcher.new(@condition, @original_matcher, negated: !@negated)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def validate(state, &)
|
|
21
|
+
return validate_negated(state, &) if @negated
|
|
22
|
+
|
|
23
|
+
if @condition.is_a?(ExpressionMatcher)
|
|
24
|
+
begin
|
|
25
|
+
# evaluate expression directly
|
|
26
|
+
return unless @condition.expression.evaluate(state.values)
|
|
27
|
+
rescue CallError
|
|
28
|
+
return
|
|
29
|
+
end
|
|
30
|
+
else
|
|
31
|
+
return unless yield(@condition).valid?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
state.errors << yield(@matcher)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def to_s
|
|
38
|
+
"#{'~' if @negated}imply(#{@condition}, #{@original_matcher})"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def validate_negated(state)
|
|
44
|
+
condition_errors = yield @condition
|
|
45
|
+
|
|
46
|
+
state.errors << if condition_errors.valid?
|
|
47
|
+
yield(@matcher)
|
|
48
|
+
else
|
|
49
|
+
condition_errors
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
module MatcherBuilding
|
|
55
|
+
##
|
|
56
|
+
# Matches only for given condition. Passes otherwise.
|
|
57
|
+
# @example
|
|
58
|
+
# # matches "hello" and 42 but not "hi"
|
|
59
|
+
# imply(String, _.length <= 4)
|
|
60
|
+
# # alternatively:
|
|
61
|
+
# imply(String) ^ (_.length <= 4)
|
|
62
|
+
# @overload imply(condition, matcher)
|
|
63
|
+
# @param condition [Expression]
|
|
64
|
+
# @param matcher [Base]
|
|
65
|
+
# @return [ImplyMatcher]
|
|
66
|
+
# @overload imply(condition)
|
|
67
|
+
# @param condition [Expression]
|
|
68
|
+
# @return [Chain<ImplyMatcher>]
|
|
69
|
+
# @see Base#>>
|
|
70
|
+
# @see #imply_one
|
|
71
|
+
# @see #imply_any
|
|
72
|
+
def imply(condition, matcher = UNDEFINED)
|
|
73
|
+
return Chain.new { imply(condition, _1) } if Matcher.undefined?(matcher)
|
|
74
|
+
|
|
75
|
+
condition = matcher_of(condition)
|
|
76
|
+
matcher = matcher_of(matcher)
|
|
77
|
+
|
|
78
|
+
ImplyMatcher.new(condition, matcher)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class ImplySomeMatcher < Base
|
|
5
|
+
def self.check(matchers, else_matcher, count)
|
|
6
|
+
raise "count must be a positive integer or :any. Got #{count.inspect}" if
|
|
7
|
+
count != :any && (!count.is_a?(Integer) || count <= 0)
|
|
8
|
+
|
|
9
|
+
raise 'else cannot be combined with count > 1' if
|
|
10
|
+
else_matcher && count != :any && count != 1
|
|
11
|
+
|
|
12
|
+
invalid_matcher = matchers.find { !_1.is_a?(ImplyMatcher) }
|
|
13
|
+
|
|
14
|
+
raise "Not an ImplyMatcher: #{invalid_matcher.inspect}" if invalid_matcher
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(matchers, else_matcher, count)
|
|
18
|
+
ImplySomeMatcher.check(matchers, else_matcher, count)
|
|
19
|
+
|
|
20
|
+
super()
|
|
21
|
+
|
|
22
|
+
@matchers = matchers
|
|
23
|
+
@else_matcher = else_matcher
|
|
24
|
+
@count = count
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def negate
|
|
28
|
+
NegatedImplySomeMatcher.new(@matchers, @else_matcher, @count)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def validate(state)
|
|
32
|
+
errors = state.errors
|
|
33
|
+
matchers = @matchers.filter { yield(_1.condition).valid? }
|
|
34
|
+
|
|
35
|
+
if matchers.empty?
|
|
36
|
+
errors << if @else_matcher
|
|
37
|
+
yield @else_matcher
|
|
38
|
+
else
|
|
39
|
+
state.report.namespace(:imply_some).no_condition_satisfied(@matchers.map(&:condition), @count)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
return
|
|
43
|
+
elsif @count != :any && matchers.length != @count
|
|
44
|
+
errors << state.report.namespace(:imply_some).x_conditions_satisfied(matchers.map(&:condition), @count)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
matchers.each { errors << yield(_1.matcher) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def to_s
|
|
51
|
+
args = @matchers.map(&:to_s)
|
|
52
|
+
|
|
53
|
+
case @count
|
|
54
|
+
when :any
|
|
55
|
+
method = 'imply_any'
|
|
56
|
+
when 1
|
|
57
|
+
method = 'imply_one'
|
|
58
|
+
else
|
|
59
|
+
method = 'imply_some'
|
|
60
|
+
args << "count: #{@count}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
args << "else: #{@else_matcher}" if @else_matcher
|
|
64
|
+
|
|
65
|
+
"#{method}(#{args.join(', ')})"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
module MatcherBuilding
|
|
70
|
+
##
|
|
71
|
+
# Matches exactly one implied matcher
|
|
72
|
+
# @example
|
|
73
|
+
# # matches "foo" and 1 but not "BAR", -1, or nil
|
|
74
|
+
# imply_one(
|
|
75
|
+
# imply(String, _ == _.downcase),
|
|
76
|
+
# imply(Integer, _.positive?),
|
|
77
|
+
# )
|
|
78
|
+
# @param *matchers [ImplyMatcher]
|
|
79
|
+
# @param else [Base] if no condition passed match against +else+ matcher.
|
|
80
|
+
# @return [ImplySomeMatcher]
|
|
81
|
+
# @see #imply
|
|
82
|
+
def imply_one(*matchers, else: UNDEFINED)
|
|
83
|
+
els = { else: }[:else]
|
|
84
|
+
else_matcher = Matcher.undefined?(els) ? nil : matcher_of(els)
|
|
85
|
+
|
|
86
|
+
ImplySomeMatcher.new(matchers, else_matcher, 1)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
##
|
|
90
|
+
# Matches at least one implied matcher
|
|
91
|
+
# @example
|
|
92
|
+
# # matches 9, 12, 40 but not 8, 21, 15.5
|
|
93
|
+
# imply_any(
|
|
94
|
+
# imply(_.even?, _ > 10),
|
|
95
|
+
# imply(_ % 3 == 0, _ < 20),
|
|
96
|
+
# )
|
|
97
|
+
# @param *matchers [ImplyMatcher]
|
|
98
|
+
# @param else [Base] if no condition passed match against +else+ matcher.
|
|
99
|
+
# @return [ImplySomeMatcher]
|
|
100
|
+
# @see #imply
|
|
101
|
+
def imply_any(*matchers, else: UNDEFINED)
|
|
102
|
+
els = { else: }[:else]
|
|
103
|
+
else_matcher = Matcher.undefined?(els) ? nil : matcher_of(els)
|
|
104
|
+
|
|
105
|
+
ImplySomeMatcher.new(matchers, else_matcher, :any)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def imply_some(*matchers, count:)
|
|
109
|
+
ImplySomeMatcher.new(matchers, nil, count)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|