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,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class MapMatcher < 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
|
+
MapMatcher.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
|
+
values = state.values
|
|
25
|
+
|
|
26
|
+
unless actual.respond_to?(:each)
|
|
27
|
+
state.errors << state.expected.responding_to(:each)
|
|
28
|
+
return
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
i = 0
|
|
32
|
+
mapped = []
|
|
33
|
+
mapping_failed = false
|
|
34
|
+
|
|
35
|
+
actual.each do |act|
|
|
36
|
+
mapped << @projection.evaluate(
|
|
37
|
+
values.merge(actual: act, index: i, original: actual),
|
|
38
|
+
)
|
|
39
|
+
rescue CallError => e
|
|
40
|
+
state.errors[i] << e.message_for_errors(act)
|
|
41
|
+
mapping_failed = true
|
|
42
|
+
ensure
|
|
43
|
+
i += 1
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
return if mapping_failed
|
|
47
|
+
|
|
48
|
+
errors = yield @matcher, mapped, original: actual
|
|
49
|
+
errors = map_errors2(errors) unless state.boolean?
|
|
50
|
+
|
|
51
|
+
state.errors << errors
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def to_s
|
|
55
|
+
"#{'~' if @negated}map(#{@projection}, #{@original_matcher})"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def validate_negated(state)
|
|
61
|
+
actual = state.actual
|
|
62
|
+
values = state.values
|
|
63
|
+
|
|
64
|
+
return unless actual.respond_to?(:map)
|
|
65
|
+
|
|
66
|
+
mapped = []
|
|
67
|
+
|
|
68
|
+
actual.map.with_index do |item, i|
|
|
69
|
+
mapped << @projection.evaluate(
|
|
70
|
+
values.merge(actual: item, index: i, original: actual),
|
|
71
|
+
)
|
|
72
|
+
rescue CallError
|
|
73
|
+
return if @negated
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
mapped_errors = yield @matcher, mapped, original: actual
|
|
77
|
+
|
|
78
|
+
state.errors << map_errors2(mapped_errors)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def map_errors2(errors)
|
|
82
|
+
map_errors(errors) do |nested_error|
|
|
83
|
+
key = nested_error.key
|
|
84
|
+
|
|
85
|
+
next unless index_call?(key)
|
|
86
|
+
|
|
87
|
+
NestedError.new(
|
|
88
|
+
key,
|
|
89
|
+
NestedError.new(@projection, nested_error.child),
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def mapped_base
|
|
95
|
+
@mapped_base ||= map_base(:map, @projection)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
module MatcherBuilding
|
|
100
|
+
##
|
|
101
|
+
# Maps items to another value before matching
|
|
102
|
+
# == +expression+ values
|
|
103
|
+
# - actual
|
|
104
|
+
# - index
|
|
105
|
+
# - original
|
|
106
|
+
# == +matcher+ values
|
|
107
|
+
# - original
|
|
108
|
+
# @example
|
|
109
|
+
# # matches ["1", "2"]
|
|
110
|
+
# map(_.to_i, [1, 2])
|
|
111
|
+
# # alternatively:
|
|
112
|
+
# map(_.to_i) ^ [1, 2]
|
|
113
|
+
# @overload map(expression, matcher)
|
|
114
|
+
# @param expression [Expression]
|
|
115
|
+
# @param matcher [Base]
|
|
116
|
+
# @return [MapMatcher]
|
|
117
|
+
# @overload map(expression)
|
|
118
|
+
# @param expression [Expression]
|
|
119
|
+
# @return [Chain<MapMatcher>]
|
|
120
|
+
def map(expression, matcher = UNDEFINED)
|
|
121
|
+
return Chain.new { map(expression, _1) } if Matcher.undefined?(matcher)
|
|
122
|
+
|
|
123
|
+
expression = expression_of(expression)
|
|
124
|
+
matcher = matcher_of(matcher)
|
|
125
|
+
|
|
126
|
+
MapMatcher.new(expression, matcher)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class NegatedArrayMatcher < Base
|
|
5
|
+
def initialize(array)
|
|
6
|
+
super()
|
|
7
|
+
|
|
8
|
+
@array = array
|
|
9
|
+
@neg_array = @array.map(&:~)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def negate
|
|
13
|
+
ArrayMatcher.new(@array)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def validate(state)
|
|
17
|
+
actual = state.actual
|
|
18
|
+
|
|
19
|
+
return if !actual.is_a?(Array) || @array.length != actual.length
|
|
20
|
+
|
|
21
|
+
collector = state.new_collector.or!
|
|
22
|
+
|
|
23
|
+
@array.length.times do |i|
|
|
24
|
+
result = yield @neg_array[i], actual[i], index: i, parent: actual
|
|
25
|
+
|
|
26
|
+
return if result.valid?
|
|
27
|
+
|
|
28
|
+
collector[i] << result
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
state.errors << collector.error
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def to_s
|
|
35
|
+
"neg(#{@array})"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class NegatedEachMatcher < Base
|
|
5
|
+
def initialize(matcher)
|
|
6
|
+
super()
|
|
7
|
+
|
|
8
|
+
@matcher = matcher
|
|
9
|
+
@neg_matcher = ~matcher
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def negate
|
|
13
|
+
EachMatcher.new(@matcher)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def validate(state)
|
|
17
|
+
return unless state.actual.respond_to?(:each)
|
|
18
|
+
|
|
19
|
+
collector = state.new_collector.or!
|
|
20
|
+
|
|
21
|
+
state.actual.each.with_index do |item, i|
|
|
22
|
+
result = yield @neg_matcher, item, index: i, parent: state.actual
|
|
23
|
+
|
|
24
|
+
return if result.valid?
|
|
25
|
+
|
|
26
|
+
collector[i] << result
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
state.errors << collector.error
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def to_s
|
|
33
|
+
"~each(#{@matcher})"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class NegatedEachPairMatcher < Base
|
|
5
|
+
def initialize(matcher)
|
|
6
|
+
super()
|
|
7
|
+
|
|
8
|
+
@matcher = matcher
|
|
9
|
+
@neg_matcher = ~matcher
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def negate
|
|
13
|
+
EachPairMatcher.new(@matcher)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def validate(state)
|
|
17
|
+
actual = state.actual
|
|
18
|
+
|
|
19
|
+
return unless actual.respond_to?(:each_pair)
|
|
20
|
+
|
|
21
|
+
collector = state.new_collector.or!
|
|
22
|
+
|
|
23
|
+
actual.each do |key, value|
|
|
24
|
+
result = yield(@neg_matcher, [key, value], key:, value:, parent: actual)
|
|
25
|
+
|
|
26
|
+
return if result.valid?
|
|
27
|
+
|
|
28
|
+
collector[key] << result
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
state.errors << collector.error
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def to_s
|
|
35
|
+
"~each_pair(#{@matcher})"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class NegatedImplySomeMatcher < Base
|
|
5
|
+
def initialize(matchers, else_matcher, count)
|
|
6
|
+
ImplySomeMatcher.check(matchers, else_matcher, count)
|
|
7
|
+
|
|
8
|
+
super()
|
|
9
|
+
|
|
10
|
+
@matchers = matchers
|
|
11
|
+
@neg_matchers = matchers.map(&:~)
|
|
12
|
+
@count = count
|
|
13
|
+
@else_matcher = else_matcher
|
|
14
|
+
@neg_else_matcher = @else_matcher&.~
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def negate
|
|
18
|
+
ImplySomeMatcher.new(@matchers, @else_matcher, @count)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def validate(state)
|
|
22
|
+
matchers = @neg_matchers.filter { yield(_1.condition).valid? }
|
|
23
|
+
|
|
24
|
+
if matchers.empty?
|
|
25
|
+
state.errors << yield(@neg_else_matcher) if @else_matcher
|
|
26
|
+
elsif @count == :any || matchers.length == @count
|
|
27
|
+
state.errors.or!
|
|
28
|
+
|
|
29
|
+
matchers.each do |matcher|
|
|
30
|
+
error = yield(matcher)
|
|
31
|
+
|
|
32
|
+
if error.valid?
|
|
33
|
+
state.errors.clear
|
|
34
|
+
break
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
state.errors << error
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def to_s
|
|
43
|
+
"~#{self.~}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class NegatedMatcher < Base
|
|
5
|
+
def initialize(matcher)
|
|
6
|
+
super()
|
|
7
|
+
|
|
8
|
+
@matcher = matcher
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def negate
|
|
12
|
+
@matcher
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def validate(state)
|
|
16
|
+
state.errors << state.expected.namespace(:negated).not.valid(@matcher) if yield(@matcher).valid?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_s
|
|
20
|
+
"neg(#{@matcher})"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class NegatedProjectMatcher < Base
|
|
5
|
+
def initialize(expression, matcher)
|
|
6
|
+
super()
|
|
7
|
+
|
|
8
|
+
@expression = expression
|
|
9
|
+
@matcher = matcher
|
|
10
|
+
@neg_matcher = ~matcher
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def negate
|
|
14
|
+
ProjectMatcher.new(@expression, @matcher)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def validate(state)
|
|
18
|
+
begin
|
|
19
|
+
result = @expression.evaluate(state.values)
|
|
20
|
+
rescue CallError
|
|
21
|
+
return
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
state.errors[@expression] << yield(@neg_matcher, result)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def to_s
|
|
28
|
+
"~project(#{@expression} => #{@matcher})"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class NeverMatcher < Base
|
|
5
|
+
include Singleton
|
|
6
|
+
|
|
7
|
+
def ~
|
|
8
|
+
AlwaysMatcher.instance
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def validate(state)
|
|
12
|
+
state.errors << state.report.exist
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def to_s
|
|
16
|
+
'never'
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
module MatcherBuilding
|
|
21
|
+
##
|
|
22
|
+
# Never matches. Opposite of {#always}
|
|
23
|
+
# @return [NeverMatcher]
|
|
24
|
+
# @see #always
|
|
25
|
+
def never
|
|
26
|
+
NeverMatcher.instance
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class OneMatcher < Base
|
|
5
|
+
def initialize(matchers, negated: false)
|
|
6
|
+
super()
|
|
7
|
+
|
|
8
|
+
@matchers = matchers
|
|
9
|
+
@negated = negated
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def negate
|
|
13
|
+
OneMatcher.new(@matchers, negated: !@negated)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def validate(state)
|
|
17
|
+
valid_matchers = []
|
|
18
|
+
invalid_errors = []
|
|
19
|
+
|
|
20
|
+
@matchers.each do |matcher|
|
|
21
|
+
error = yield matcher
|
|
22
|
+
|
|
23
|
+
if error.valid?
|
|
24
|
+
valid_matchers << matcher
|
|
25
|
+
else
|
|
26
|
+
invalid_errors << error
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
if @negated
|
|
31
|
+
state.errors << yield(~valid_matchers[0]) if valid_matchers.length == 1
|
|
32
|
+
else
|
|
33
|
+
if valid_matchers.length == 0
|
|
34
|
+
state.errors << OrError.from(invalid_errors)
|
|
35
|
+
elsif valid_matchers.length > 1
|
|
36
|
+
negated_matchers = valid_matchers.map(&:~)
|
|
37
|
+
any_matcher = AnyMatcher.new(negated_matchers)
|
|
38
|
+
|
|
39
|
+
state.errors << yield(any_matcher)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def to_s
|
|
45
|
+
"#{'~' if @negated}one(#{@matchers.map(&:to_s).join(', ')})"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
module MatcherBuilding
|
|
50
|
+
##
|
|
51
|
+
# Matches exactly one matcher
|
|
52
|
+
# @example
|
|
53
|
+
# # matches [1] and [2] but not [] or [1, 2]
|
|
54
|
+
# one(_.include?(1), _.include?(2))
|
|
55
|
+
# @param matchers [Array<Base>]
|
|
56
|
+
# @return [OneMatcher]
|
|
57
|
+
def one(*matchers)
|
|
58
|
+
matchers = matchers.map { matcher_of(_1) }
|
|
59
|
+
|
|
60
|
+
case matchers.count
|
|
61
|
+
when 0
|
|
62
|
+
NeverMatcher.instance
|
|
63
|
+
when 1
|
|
64
|
+
matchers[0]
|
|
65
|
+
else
|
|
66
|
+
OneMatcher.new(matchers)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class OptionalMatcher < Base
|
|
5
|
+
def self.cache(matcher, matcher_cache = MatcherCache.current)
|
|
6
|
+
return new(matcher) unless matcher_cache
|
|
7
|
+
|
|
8
|
+
cache = (matcher_cache.optional_matchers ||= {}.compare_by_identity)
|
|
9
|
+
cache[matcher] ||= new(matcher)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(matcher, negated: false)
|
|
13
|
+
super()
|
|
14
|
+
|
|
15
|
+
@matcher = negated ? ~matcher : matcher
|
|
16
|
+
@original_matcher = matcher
|
|
17
|
+
@negated = negated
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def negate
|
|
21
|
+
OptionalMatcher.new(@original_matcher, negated: !@negated)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def validate(state)
|
|
25
|
+
if state.actual.nil?
|
|
26
|
+
state.errors << state.expected.not.equal(nil) if @negated
|
|
27
|
+
else
|
|
28
|
+
state.errors << yield(@matcher)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def to_s
|
|
33
|
+
"#{'~' if @negated}optional(#{@original_matcher})"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# see Optional for optional helper
|
|
38
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class ParseFloatMatcher < Base
|
|
5
|
+
def initialize(matcher, negated: false)
|
|
6
|
+
super()
|
|
7
|
+
|
|
8
|
+
@matcher = negated ? ~matcher : matcher
|
|
9
|
+
@original_matcher = matcher
|
|
10
|
+
@negated = negated
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def negate
|
|
14
|
+
ParseFloatMatcher.new(@original_matcher, negated: !@negated)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def validate(state, &)
|
|
18
|
+
actual = state.actual
|
|
19
|
+
|
|
20
|
+
unless actual.is_a?(String)
|
|
21
|
+
state.errors << state.expected.kind_of(String) unless @negated
|
|
22
|
+
return
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
value = Float(actual)
|
|
26
|
+
|
|
27
|
+
if @matcher.is_a?(NeverMatcher)
|
|
28
|
+
state.errors << state.expected.not.valid_format(:float)
|
|
29
|
+
return
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
result = yield(@matcher, value)
|
|
33
|
+
|
|
34
|
+
return if result.valid?
|
|
35
|
+
|
|
36
|
+
float_of = Call.new(Constant.new(Kernel), :Float, [Variable.actual])
|
|
37
|
+
state.errors[float_of] << result
|
|
38
|
+
rescue ArgumentError
|
|
39
|
+
state.errors << state.expected.valid_format(:float) unless @negated
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def to_s
|
|
43
|
+
prefix = @negated ? '~' : ''
|
|
44
|
+
|
|
45
|
+
return "#{prefix}float_format" if @original_matcher.is_a?(AlwaysMatcher)
|
|
46
|
+
|
|
47
|
+
"#{prefix}parse_float(#{@original_matcher})"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
module MatcherBuilding
|
|
52
|
+
##
|
|
53
|
+
# Parses float and matches with given matcher.
|
|
54
|
+
# @example
|
|
55
|
+
# # matches "1.0"
|
|
56
|
+
# parse_float(_ > 0.0)
|
|
57
|
+
# # alternatively:
|
|
58
|
+
# parse_float ^ (_ > 0.0)
|
|
59
|
+
# # without matcher matches any float string
|
|
60
|
+
# parse_float
|
|
61
|
+
# @overload parse_float(matcher)
|
|
62
|
+
# @param matcher [Base]
|
|
63
|
+
# @return [ParseFloatMatcher]
|
|
64
|
+
# @overload parse_float
|
|
65
|
+
# @return [OptionalChain<ParseFloatMatcher>]
|
|
66
|
+
# @see #float_format
|
|
67
|
+
def parse_float(matcher = UNDEFINED)
|
|
68
|
+
return Chain.new { parse_float(_1) }.optional if
|
|
69
|
+
Matcher.undefined?(matcher)
|
|
70
|
+
|
|
71
|
+
matcher = matcher_of(matcher)
|
|
72
|
+
|
|
73
|
+
ParseFloatMatcher.new(matcher)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
##
|
|
77
|
+
# Matches float strings
|
|
78
|
+
# @example
|
|
79
|
+
# # matches { payload: "1.5" }
|
|
80
|
+
# { payload: float_format }
|
|
81
|
+
# @return [ParseFloatMatcher]
|
|
82
|
+
def float_format
|
|
83
|
+
@float_format ||= ParseFloatMatcher.new(AlwaysMatcher.instance)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class ParseIntegerMatcher < Base
|
|
5
|
+
def initialize(matcher, base: 0, negated: false)
|
|
6
|
+
super()
|
|
7
|
+
|
|
8
|
+
@matcher = negated ? ~matcher : matcher
|
|
9
|
+
@original_matcher = matcher
|
|
10
|
+
@base = base
|
|
11
|
+
@negated = negated
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def negate
|
|
15
|
+
ParseIntegerMatcher.new(@original_matcher, base: @base, negated: !@negated)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def validate(state, &)
|
|
19
|
+
actual = state.actual
|
|
20
|
+
|
|
21
|
+
unless actual.is_a?(String)
|
|
22
|
+
state.errors << state.expected.kind_of(String) unless @negated
|
|
23
|
+
return
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
value = Integer(actual)
|
|
27
|
+
|
|
28
|
+
if @matcher.is_a?(NeverMatcher)
|
|
29
|
+
state.errors << state.expected.not.valid_format(:integer)
|
|
30
|
+
return
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
result = yield(@matcher, value)
|
|
34
|
+
|
|
35
|
+
return if result.valid?
|
|
36
|
+
|
|
37
|
+
integer_of = Call.new(Constant.new(Kernel), :Integer, [Variable.actual])
|
|
38
|
+
state.errors[integer_of] << result
|
|
39
|
+
rescue ArgumentError
|
|
40
|
+
state.errors << state.expected.valid_format(:integer) unless @negated
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def to_s
|
|
44
|
+
prefix = @negated ? '~' : ''
|
|
45
|
+
|
|
46
|
+
if @original_matcher.is_a?(AlwaysMatcher)
|
|
47
|
+
args = @base == 0 ? '' : "(base: #{@base})"
|
|
48
|
+
return "#{prefix}integer_format#{args}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
base_arg = @base == 0 ? '' : ", base: #{@base}"
|
|
52
|
+
|
|
53
|
+
"#{'~' if @negated}parse_integer(#{@original_matcher}#{base_arg})"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
module MatcherBuilding
|
|
58
|
+
##
|
|
59
|
+
# Parses integer and matches with given matcher
|
|
60
|
+
# @example
|
|
61
|
+
# # matches "7"
|
|
62
|
+
# parse_integer(_.odd?)
|
|
63
|
+
# # alternatively:
|
|
64
|
+
# parse_integer ^ (_.odd?)
|
|
65
|
+
# # without matcher matches any integer string
|
|
66
|
+
# parse_integer
|
|
67
|
+
# @overload parse_integer(matcher, base: 0)
|
|
68
|
+
# @param matcher [Base]
|
|
69
|
+
# @param base [Integer]
|
|
70
|
+
# @return [ParseIntegerMatcher]
|
|
71
|
+
# @overload parse_integer(base: 0)
|
|
72
|
+
# @return [OptionalChain<ParseIntegerMatcher>]
|
|
73
|
+
# @see #integer_format
|
|
74
|
+
def parse_integer(matcher = UNDEFINED, base: 0)
|
|
75
|
+
return Chain.new { parse_integer(_1, base:) }.optional if
|
|
76
|
+
Matcher.undefined?(matcher)
|
|
77
|
+
|
|
78
|
+
matcher = matcher_of(matcher)
|
|
79
|
+
|
|
80
|
+
ParseIntegerMatcher.new(matcher, base:)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
##
|
|
84
|
+
# Matches integer strings
|
|
85
|
+
# @example
|
|
86
|
+
# # matches { payload: "42" }
|
|
87
|
+
# { payload: integer_format }
|
|
88
|
+
# @param base [Integer]
|
|
89
|
+
# @return [ParseIntegerMatcher]
|
|
90
|
+
def integer_format(base: 0)
|
|
91
|
+
if base == 0
|
|
92
|
+
@integer_format ||= ParseIntegerMatcher.new(AlwaysMatcher.instance, base:)
|
|
93
|
+
else
|
|
94
|
+
ParseIntegerMatcher.new(AlwaysMatcher.instance, base:)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|