matchers 0.1.0.pre.1
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.
- checksums.yaml +7 -0
- data/lib/matcher/assertions.rb +19 -0
- data/lib/matcher/autoload.rb +5 -0
- data/lib/matcher/base.rb +183 -0
- data/lib/matcher/compatibility.rb +34 -0
- data/lib/matcher/debug.rb +62 -0
- data/lib/matcher/dsl/builder.rb +99 -0
- data/lib/matcher/dsl/chain.rb +84 -0
- data/lib/matcher/dsl/expression_dsl.rb +306 -0
- data/lib/matcher/dsl/matcher_dsl.rb +5 -0
- data/lib/matcher/dsl/optional.rb +82 -0
- data/lib/matcher/dsl/optional_chain.rb +24 -0
- data/lib/matcher/dsl/others.rb +28 -0
- data/lib/matcher/errors/and_error.rb +88 -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 +100 -0
- data/lib/matcher/errors/nested_error.rb +98 -0
- data/lib/matcher/errors/or_error.rb +88 -0
- data/lib/matcher/expression_cache.rb +57 -0
- data/lib/matcher/expression_labeler.rb +96 -0
- data/lib/matcher/expressions/array_expression.rb +45 -0
- data/lib/matcher/expressions/block.rb +189 -0
- data/lib/matcher/expressions/call.rb +307 -0
- data/lib/matcher/expressions/call_error.rb +45 -0
- data/lib/matcher/expressions/constant.rb +53 -0
- data/lib/matcher/expressions/expression.rb +237 -0
- data/lib/matcher/expressions/expression_walker.rb +77 -0
- data/lib/matcher/expressions/hash_expression.rb +59 -0
- data/lib/matcher/expressions/proc_expression.rb +96 -0
- data/lib/matcher/expressions/range_expression.rb +65 -0
- data/lib/matcher/expressions/recorder.rb +136 -0
- data/lib/matcher/expressions/rescue_last_error_expression.rb +49 -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 +87 -0
- data/lib/matcher/hash_stack.rb +52 -0
- data/lib/matcher/list.rb +102 -0
- data/lib/matcher/markers.rb +7 -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 +34 -0
- data/lib/matcher/matchers/any_matcher.rb +70 -0
- data/lib/matcher/matchers/array_matcher.rb +72 -0
- data/lib/matcher/matchers/block_matcher.rb +61 -0
- data/lib/matcher/matchers/boolean_matcher.rb +37 -0
- data/lib/matcher/matchers/dig_matcher.rb +149 -0
- data/lib/matcher/matchers/each_matcher.rb +85 -0
- data/lib/matcher/matchers/each_pair_matcher.rb +119 -0
- data/lib/matcher/matchers/equal_matcher.rb +198 -0
- data/lib/matcher/matchers/equal_set_matcher.rb +112 -0
- data/lib/matcher/matchers/expression_matcher.rb +69 -0
- data/lib/matcher/matchers/filter_matcher.rb +115 -0
- data/lib/matcher/matchers/hash_matcher.rb +315 -0
- data/lib/matcher/matchers/imply_matcher.rb +83 -0
- data/lib/matcher/matchers/imply_some_matcher.rb +116 -0
- data/lib/matcher/matchers/index_by_matcher.rb +177 -0
- data/lib/matcher/matchers/inline_matcher.rb +101 -0
- data/lib/matcher/matchers/keys_matcher.rb +131 -0
- data/lib/matcher/matchers/kind_of_matcher.rb +35 -0
- data/lib/matcher/matchers/lazy_all_matcher.rb +69 -0
- data/lib/matcher/matchers/lazy_any_matcher.rb +69 -0
- data/lib/matcher/matchers/let_matcher.rb +73 -0
- data/lib/matcher/matchers/map_matcher.rb +148 -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 +25 -0
- data/lib/matcher/matchers/negated_project_matcher.rb +31 -0
- data/lib/matcher/matchers/never_matcher.rb +35 -0
- data/lib/matcher/matchers/one_matcher.rb +68 -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 +101 -0
- data/lib/matcher/matchers/parse_iso8601_helper.rb +41 -0
- data/lib/matcher/matchers/parse_iso8601_matcher.rb +52 -0
- data/lib/matcher/matchers/parse_json_helper.rb +43 -0
- data/lib/matcher/matchers/parse_json_matcher.rb +59 -0
- data/lib/matcher/matchers/project_matcher.rb +72 -0
- data/lib/matcher/matchers/raises_matcher.rb +131 -0
- data/lib/matcher/matchers/range_matcher.rb +50 -0
- data/lib/matcher/matchers/reference_matcher.rb +213 -0
- data/lib/matcher/matchers/reference_matcher_collection.rb +57 -0
- data/lib/matcher/matchers/regexp_matcher.rb +86 -0
- data/lib/matcher/messages/expected_phrasing.rb +355 -0
- data/lib/matcher/messages/message.rb +104 -0
- data/lib/matcher/messages/message_builder.rb +35 -0
- data/lib/matcher/messages/message_rules.rb +240 -0
- data/lib/matcher/messages/namespaced_message_builder.rb +19 -0
- data/lib/matcher/messages/phrasing.rb +59 -0
- data/lib/matcher/messages/standard_message_builder.rb +105 -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 +62 -0
- data/lib/matcher/patterns/pattern.rb +104 -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 +103 -0
- data/lib/matcher/rules/message_factory.rb +26 -0
- data/lib/matcher/rules/message_rule.rb +18 -0
- data/lib/matcher/rules/message_rule_context.rb +26 -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 +514 -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 +107 -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 +346 -0
- metadata +174 -0
|
@@ -0,0 +1,101 @@
|
|
|
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(
|
|
22
|
+
@matcher&.~, negatable: @negatable, negated: !@negated, &@block
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def receiver
|
|
27
|
+
@block.binding.receiver
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class InlineContext
|
|
31
|
+
extend Forwardable
|
|
32
|
+
|
|
33
|
+
def initialize(matcher, state, block)
|
|
34
|
+
@matcher = matcher
|
|
35
|
+
@state = state
|
|
36
|
+
@block = block
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
attr_reader :state
|
|
40
|
+
|
|
41
|
+
def_delegators :@state, *State.public_instance_methods(false) - %i[result]
|
|
42
|
+
def_delegators :@matcher, :receiver, :matcher, :negated
|
|
43
|
+
|
|
44
|
+
def _yield(matcher, act = @state.actual, **values)
|
|
45
|
+
@block.call(matcher, act, **values)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def validate(state, &block)
|
|
50
|
+
context = InlineContext.new(self, state, block)
|
|
51
|
+
context.instance_exec(&@block)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def to_s
|
|
55
|
+
args = if @matcher
|
|
56
|
+
"(#{@negated ? ~@matcher : @matcher})"
|
|
57
|
+
else
|
|
58
|
+
""
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
"#{'~' if @negated}inline#{args} { #{Utils.block_location(@block)} }"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
module MatcherDsl
|
|
66
|
+
##
|
|
67
|
+
# Creates an anonymous custom matcher
|
|
68
|
+
#
|
|
69
|
+
# If you need more control to define your matching logic then +inline+
|
|
70
|
+
# may give you an alternative to implementing a new matcher class. Within
|
|
71
|
+
# the +inline+ block you have direct access to +actual+, +errors+, +_yield+,
|
|
72
|
+
# and other state methods.
|
|
73
|
+
#
|
|
74
|
+
# @example
|
|
75
|
+
# # matches distinct arrays
|
|
76
|
+
# inline do
|
|
77
|
+
# indices = Hash.new
|
|
78
|
+
#
|
|
79
|
+
# actual.each_with_index do |e, i|
|
|
80
|
+
# if (original_index = indices[e])
|
|
81
|
+
# errors[i] << expected.not.duplicate(original_index)
|
|
82
|
+
# else
|
|
83
|
+
# indices[e] = i
|
|
84
|
+
# end
|
|
85
|
+
# end
|
|
86
|
+
# end
|
|
87
|
+
#
|
|
88
|
+
# @param matcher [Base] optionally provide a child matcher. Call the matcher
|
|
89
|
+
# with +_yield matcher, actual, **values+
|
|
90
|
+
# @param negatable [true, false] set to true if your matching logic respects
|
|
91
|
+
# the negated flag. Otherwise, the default negation implementation is
|
|
92
|
+
# used. When +negated = true+ the child matcher is automatically negated.
|
|
93
|
+
# @yield inline context
|
|
94
|
+
# @return [InlineMatcher]
|
|
95
|
+
def inline(matcher = UNDEFINED, negatable: false, &)
|
|
96
|
+
matcher = Matcher.undefined?(matcher) ? nil : matcher_of(matcher)
|
|
97
|
+
|
|
98
|
+
InlineMatcher.new(matcher, negatable:, &)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
##
|
|
5
|
+
# Match hash keys.
|
|
6
|
+
# @example
|
|
7
|
+
# m = Matcher.build { keys(:foo, :bar) }
|
|
8
|
+
#
|
|
9
|
+
# m.match?({ foo: 1, bar: 2 })
|
|
10
|
+
# # => true
|
|
11
|
+
# m.match({ foo: 1, qux: 3 })
|
|
12
|
+
# # > root: expected to include key :bar but got {:foo=>1, :qux=>3}
|
|
13
|
+
# # > root: did not expect to include key :qux but got {:foo=>1, :qux=>3}
|
|
14
|
+
class KeysMatcher < Base
|
|
15
|
+
def initialize(keys, partial: false, negated: false)
|
|
16
|
+
super()
|
|
17
|
+
|
|
18
|
+
@keys = keys
|
|
19
|
+
@partial = partial
|
|
20
|
+
@negated = negated
|
|
21
|
+
@includes_expressions = keys.any?(Expression)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def negate
|
|
25
|
+
KeysMatcher.new(@keys, partial: @partial, negated: !@negated)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def validate(state, &)
|
|
29
|
+
return validate_negated(state, &) if @negated
|
|
30
|
+
|
|
31
|
+
actual = state.actual
|
|
32
|
+
|
|
33
|
+
unless actual.is_a?(Hash)
|
|
34
|
+
state.errors << state.expected.kind_of(Hash)
|
|
35
|
+
return
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
actual_keys = actual.keys
|
|
39
|
+
|
|
40
|
+
expected_keys = if @includes_expressions
|
|
41
|
+
@keys.map { _1.is_a?(Expression) ? _1.evaluate(state.values) : _1 }
|
|
42
|
+
else
|
|
43
|
+
@keys
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
(expected_keys - actual_keys).each do |key|
|
|
47
|
+
state.errors << state.expected.having_key(key)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
return if @partial
|
|
51
|
+
|
|
52
|
+
(actual_keys - expected_keys).each do |key|
|
|
53
|
+
state.errors << state.expected.not.having_key(key)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def to_s
|
|
58
|
+
helper = @partial ? "partial_keys" : "keys"
|
|
59
|
+
args = @keys.map(&:inspect).join(", ")
|
|
60
|
+
|
|
61
|
+
"#{'~' if @negated}#{helper}(#{args})"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def validate_negated(state)
|
|
67
|
+
actual = state.actual
|
|
68
|
+
|
|
69
|
+
return unless actual.is_a?(Hash)
|
|
70
|
+
|
|
71
|
+
actual_keys = actual.keys
|
|
72
|
+
|
|
73
|
+
expected_keys = if @includes_expressions
|
|
74
|
+
@keys.map { _1.is_a?(Expression) ? _1.evaluate(state.values) : _1 }
|
|
75
|
+
else
|
|
76
|
+
@keys
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
if @partial
|
|
80
|
+
return unless (expected_keys - actual_keys).empty?
|
|
81
|
+
|
|
82
|
+
state.errors.or!
|
|
83
|
+
|
|
84
|
+
(actual_keys & expected_keys).each do |key|
|
|
85
|
+
state.errors << state.expected.not.having_key(key)
|
|
86
|
+
end
|
|
87
|
+
else
|
|
88
|
+
return if actual_keys != expected_keys
|
|
89
|
+
|
|
90
|
+
state.errors.or!
|
|
91
|
+
|
|
92
|
+
actual_keys.each do |key|
|
|
93
|
+
state.errors << state.expected.not.having_key(key)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
module MatcherDsl
|
|
100
|
+
##
|
|
101
|
+
# Matches all keys of a hash
|
|
102
|
+
# @example
|
|
103
|
+
# # matches { foo: 1, bar: 2 } but not { foo: 1, qux: 3 }
|
|
104
|
+
# keys(:foo, :bar)
|
|
105
|
+
# # using key expression, matches { "the_key" => 23 }
|
|
106
|
+
# let(my_key: :the_key) ^ keys(vars[:my_key].to_s)
|
|
107
|
+
# @param keys [Array] supports expressions
|
|
108
|
+
# @param partial [true, false] ignores extra keys when +true+
|
|
109
|
+
# @return [KeysMatcher]
|
|
110
|
+
# @see #partial_keys
|
|
111
|
+
def keys(*keys, partial: false)
|
|
112
|
+
keys.each_with_index do |key, i|
|
|
113
|
+
keys[i] = expression_or_value(key)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
KeysMatcher.new(keys, partial:)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
##
|
|
120
|
+
# Matches hash if all given keys are included. Ignores extra keys.
|
|
121
|
+
# @example
|
|
122
|
+
# # matches { foo: 1, bar: 2 } but not { bar: 2 }
|
|
123
|
+
# partial_keys(:foo)
|
|
124
|
+
# @param keys [Array] supports expressions
|
|
125
|
+
# @return [KeysMatcher]
|
|
126
|
+
# @see #keys
|
|
127
|
+
def partial_keys(*keys)
|
|
128
|
+
keys(*keys, partial: true)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
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,69 @@
|
|
|
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 &(other)
|
|
18
|
+
other = Matcher.cache(other)
|
|
19
|
+
|
|
20
|
+
if other.is_a?(LazyAllMatcher)
|
|
21
|
+
LazyAllMatcher.new(@matchers + other.matchers)
|
|
22
|
+
else
|
|
23
|
+
LazyAllMatcher.new(@matchers + [other])
|
|
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.join(', ')})"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
module MatcherDsl
|
|
45
|
+
##
|
|
46
|
+
# Matches all matchers lazily. Returns only the last match result
|
|
47
|
+
# (similar to &&).
|
|
48
|
+
# @example
|
|
49
|
+
# # matches 3 but not "foo"
|
|
50
|
+
# lazy_all(Integer, _ % 3 == 0)
|
|
51
|
+
# # alternatively:
|
|
52
|
+
# of(Integer) & _.positive?
|
|
53
|
+
# @param matchers [Array<Base>]
|
|
54
|
+
# @return [LazyAllMatcher]
|
|
55
|
+
# @see Base#&
|
|
56
|
+
# @see #lazy_any
|
|
57
|
+
# @see #all
|
|
58
|
+
def lazy_all(*matchers)
|
|
59
|
+
case matchers.length
|
|
60
|
+
when 0
|
|
61
|
+
AlwaysMatcher.instance
|
|
62
|
+
when 1
|
|
63
|
+
matcher_of(matchers[0])
|
|
64
|
+
else
|
|
65
|
+
LazyAllMatcher.new(matchers.map { matcher_of(_1) })
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
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 |(other)
|
|
18
|
+
other = Matcher.cache(other)
|
|
19
|
+
|
|
20
|
+
if other.is_a?(LazyAnyMatcher)
|
|
21
|
+
LazyAnyMatcher.new(@matchers + other.matchers)
|
|
22
|
+
else
|
|
23
|
+
LazyAnyMatcher.new(@matchers + [other])
|
|
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.join(', ')})"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
module MatcherDsl
|
|
45
|
+
##
|
|
46
|
+
# Matches any matcher lazily. Returns only the last match result
|
|
47
|
+
# (similar to ||).
|
|
48
|
+
# @example
|
|
49
|
+
# # matches "foo" and 42 but not +nil+ or +true+
|
|
50
|
+
# lazy_any(String, Integer)
|
|
51
|
+
# # alternatively:
|
|
52
|
+
# of(String) | of(Integer)
|
|
53
|
+
# @param matchers [Array<Base>]
|
|
54
|
+
# @return [LazyAnyMatcher]
|
|
55
|
+
# @see Base#|
|
|
56
|
+
# @see #lazy_all
|
|
57
|
+
# @see #any
|
|
58
|
+
def lazy_any(*matchers)
|
|
59
|
+
case matchers.length
|
|
60
|
+
when 0
|
|
61
|
+
AlwaysMatcher.instance
|
|
62
|
+
when 1
|
|
63
|
+
matcher_of(matchers[0])
|
|
64
|
+
else
|
|
65
|
+
LazyAnyMatcher.new(matchers.map { matcher_of(_1) })
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
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 MatcherDsl
|
|
47
|
+
##
|
|
48
|
+
# Sets values for the 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
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
##
|
|
5
|
+
# Map items to another value before matching.
|
|
6
|
+
# @example
|
|
7
|
+
# m = Matcher.build { map(_.to_i, [1, 2]) } # OR
|
|
8
|
+
# m = Matcher.build { map(_.to_i) ^ [1, 2] }
|
|
9
|
+
#
|
|
10
|
+
# m.match?(["1", "2"])
|
|
11
|
+
# # => true
|
|
12
|
+
# m.match(["1", :foo])
|
|
13
|
+
# # > root[1]: expected an object responding to 'to_i' but got :foo
|
|
14
|
+
# m.match(["1", "3"])
|
|
15
|
+
# # > root[1].to_i: expected 2 but got 3
|
|
16
|
+
#
|
|
17
|
+
# m = Matcher.build { map(_.to_i, _.sum == 3) }
|
|
18
|
+
# m.match?(["1", "2"])
|
|
19
|
+
# # => true
|
|
20
|
+
# m.match(["1", "2", "3"])
|
|
21
|
+
# # > root.map(&:to_i): expected actual.sum == 3 but got 6 == 3,
|
|
22
|
+
# # where actual = [1, 2, 3]
|
|
23
|
+
class MapMatcher < Base
|
|
24
|
+
include MappingUtils
|
|
25
|
+
|
|
26
|
+
def initialize(projection, matcher, negated: false)
|
|
27
|
+
super()
|
|
28
|
+
|
|
29
|
+
@projection = projection
|
|
30
|
+
@matcher = negated ? ~matcher : matcher
|
|
31
|
+
@original_matcher = matcher
|
|
32
|
+
@negated = negated
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def negate
|
|
36
|
+
MapMatcher.new(@projection, @original_matcher, negated: !@negated)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def validate(state, &)
|
|
40
|
+
return validate_negated(state, &) if @negated
|
|
41
|
+
|
|
42
|
+
actual = state.actual
|
|
43
|
+
values = state.values
|
|
44
|
+
|
|
45
|
+
unless actual.respond_to?(:each)
|
|
46
|
+
state.errors << state.expected.responding_to(:each)
|
|
47
|
+
return
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
i = 0
|
|
51
|
+
mapped = []
|
|
52
|
+
mapping_failed = false
|
|
53
|
+
|
|
54
|
+
actual.each do |act|
|
|
55
|
+
mapped << @projection.evaluate(
|
|
56
|
+
values.merge(actual: act, index: i, original: actual),
|
|
57
|
+
)
|
|
58
|
+
rescue CallError => e
|
|
59
|
+
state.errors[i] << e.message_for_errors(act)
|
|
60
|
+
mapping_failed = true
|
|
61
|
+
ensure
|
|
62
|
+
i += 1
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
return if mapping_failed
|
|
66
|
+
|
|
67
|
+
errors = yield @matcher, mapped, original: actual
|
|
68
|
+
errors = map_errors2(errors) unless state.boolean?
|
|
69
|
+
|
|
70
|
+
state.errors << errors
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def to_s
|
|
74
|
+
"#{'~' if @negated}map(#{@projection}, #{@original_matcher})"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def validate_negated(state)
|
|
80
|
+
actual = state.actual
|
|
81
|
+
values = state.values
|
|
82
|
+
|
|
83
|
+
return unless actual.respond_to?(:map)
|
|
84
|
+
|
|
85
|
+
mapped = []
|
|
86
|
+
|
|
87
|
+
actual.map.with_index do |item, i|
|
|
88
|
+
mapped << @projection.evaluate(
|
|
89
|
+
values.merge(actual: item, index: i, original: actual),
|
|
90
|
+
)
|
|
91
|
+
rescue CallError
|
|
92
|
+
return nil if @negated
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
mapped_errors = yield @matcher, mapped, original: actual
|
|
96
|
+
|
|
97
|
+
state.errors << map_errors2(mapped_errors)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def map_errors2(errors)
|
|
101
|
+
map_errors(errors) do |nested_error|
|
|
102
|
+
key = nested_error.key
|
|
103
|
+
|
|
104
|
+
next unless index_call?(key)
|
|
105
|
+
|
|
106
|
+
NestedError.new(
|
|
107
|
+
key,
|
|
108
|
+
NestedError.new(@projection, nested_error.child),
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def mapped_base
|
|
114
|
+
@mapped_base ||= map_base(:map, @projection)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
module MatcherDsl
|
|
119
|
+
##
|
|
120
|
+
# Maps items to another value before matching
|
|
121
|
+
# == +expression+ values
|
|
122
|
+
# - actual
|
|
123
|
+
# - index
|
|
124
|
+
# - original
|
|
125
|
+
# == +matcher+ values
|
|
126
|
+
# - original
|
|
127
|
+
# @example
|
|
128
|
+
# # matches ["1", "2"]
|
|
129
|
+
# map(_.to_i, [1, 2])
|
|
130
|
+
# # alternatively:
|
|
131
|
+
# map(_.to_i) ^ [1, 2]
|
|
132
|
+
# @overload map(expression, matcher)
|
|
133
|
+
# @param expression [Expression]
|
|
134
|
+
# @param matcher [Base]
|
|
135
|
+
# @return [MapMatcher]
|
|
136
|
+
# @overload map(expression)
|
|
137
|
+
# @param expression [Expression]
|
|
138
|
+
# @return [Chain<MapMatcher>]
|
|
139
|
+
def map(expression, matcher = UNDEFINED)
|
|
140
|
+
return Chain.new { map(expression, _1) } if Matcher.undefined?(matcher)
|
|
141
|
+
|
|
142
|
+
expression = expression_of(expression)
|
|
143
|
+
matcher = matcher_of(matcher)
|
|
144
|
+
|
|
145
|
+
MapMatcher.new(expression, matcher)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
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 nil 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 nil 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
|