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,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class StringExpression < Expression
|
|
5
|
+
def initialize(parts)
|
|
6
|
+
super()
|
|
7
|
+
|
|
8
|
+
@parts = parts
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
attr_reader :parts
|
|
12
|
+
|
|
13
|
+
def ==(other)
|
|
14
|
+
return true if equal?(other)
|
|
15
|
+
|
|
16
|
+
other.instance_of?(StringExpression) &&
|
|
17
|
+
@parts.eql?(other.parts)
|
|
18
|
+
end
|
|
19
|
+
alias eql? ==
|
|
20
|
+
|
|
21
|
+
def hash
|
|
22
|
+
@hash ||= [self.class, @parts].hash
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def variables
|
|
26
|
+
@variables ||= @parts.flat_map(&:variables).uniq
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def evaluate(values)
|
|
30
|
+
@parts.map { _1.evaluate(values) }.join
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def substitute(replacements)
|
|
34
|
+
return self unless replacements.keys.intersect?(variables)
|
|
35
|
+
|
|
36
|
+
substituted_parts = @parts.map { _1.substitute(replacements) }
|
|
37
|
+
|
|
38
|
+
StringExpression.new(substituted_parts)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def to_s
|
|
42
|
+
parts = @parts.map do |part|
|
|
43
|
+
if part.is_a?(Constant) && part.value.is_a?(String)
|
|
44
|
+
part.value
|
|
45
|
+
else
|
|
46
|
+
"\#{#{part}}"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
"\"#{parts.join}\""
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class SymbolProc
|
|
5
|
+
def initialize(proc_or_symbol)
|
|
6
|
+
case proc_or_symbol
|
|
7
|
+
when Symbol
|
|
8
|
+
@symbol = proc_or_symbol
|
|
9
|
+
@proc = proc_or_symbol.to_proc
|
|
10
|
+
when Proc
|
|
11
|
+
@proc = proc_or_symbol
|
|
12
|
+
@symbol = get_symbol(proc_or_symbol)
|
|
13
|
+
else
|
|
14
|
+
raise "Expected Proc or Symbol, got #{proc_or_symbol.inspect}"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
attr_reader :symbol
|
|
19
|
+
|
|
20
|
+
def ==(other)
|
|
21
|
+
equal?(other) || other.instance_of?(SymbolProc) && @symbol == other.symbol
|
|
22
|
+
end
|
|
23
|
+
alias eql? ==
|
|
24
|
+
|
|
25
|
+
def hash
|
|
26
|
+
[self.class, @symbol].hash
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def variables
|
|
30
|
+
[]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def to_proc
|
|
34
|
+
@proc
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def to_s
|
|
38
|
+
"&#{@symbol.inspect}"
|
|
39
|
+
end
|
|
40
|
+
alias inspect to_s
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def get_symbol(proc)
|
|
45
|
+
receiver = Variable.actual # could be any
|
|
46
|
+
recorder = Recorder.new(receiver)
|
|
47
|
+
result = proc.call(recorder)
|
|
48
|
+
expression = Recorder.to_expression(result)
|
|
49
|
+
|
|
50
|
+
expression.method
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class Variable < Expression
|
|
5
|
+
WELL_KNOWN = %i[actual key value index parent original].freeze
|
|
6
|
+
|
|
7
|
+
WELL_KNOWN.each do |method|
|
|
8
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
9
|
+
def self.#{method} # def self.actual
|
|
10
|
+
@#{method} ||= Variable.new(:#{method}) # @actual ||= Variable.actual
|
|
11
|
+
end # end
|
|
12
|
+
RUBY
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.well_known?(symbol)
|
|
16
|
+
WELL_KNOWN.include?(symbol)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.cache(symbol, expression_cache: true)
|
|
20
|
+
return send(symbol) if well_known?(symbol)
|
|
21
|
+
|
|
22
|
+
expression_cache = ExpressionCache.current if expression_cache == true
|
|
23
|
+
|
|
24
|
+
if expression_cache
|
|
25
|
+
expression_cache.less_known_variable_for(symbol)
|
|
26
|
+
else
|
|
27
|
+
new(symbol)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.substitutions
|
|
32
|
+
Thread.current[:matcher_variable_substitution_stack]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.with_substitutions(**substitutions)
|
|
36
|
+
stack = (Thread.current[:matcher_variable_substitution_stack] ||= HashStack.new)
|
|
37
|
+
stack.push(substitutions)
|
|
38
|
+
|
|
39
|
+
begin
|
|
40
|
+
yield
|
|
41
|
+
ensure
|
|
42
|
+
stack.pop(substitutions)
|
|
43
|
+
Thread.current[:matcher_variable_substitution_stack] = nil if stack.empty?
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
attr_reader :symbol
|
|
48
|
+
|
|
49
|
+
def initialize(symbol)
|
|
50
|
+
super()
|
|
51
|
+
|
|
52
|
+
@symbol = symbol
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def variables
|
|
56
|
+
[@symbol]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def evaluate(values)
|
|
60
|
+
value = values[@symbol]
|
|
61
|
+
|
|
62
|
+
raise "no value for #{@symbol.inspect}" if value.nil? && !values.key?(@symbol)
|
|
63
|
+
|
|
64
|
+
value
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def ==(other)
|
|
68
|
+
other.instance_of?(Variable) && other.symbol == @symbol
|
|
69
|
+
end
|
|
70
|
+
alias eql? ==
|
|
71
|
+
|
|
72
|
+
def hash
|
|
73
|
+
[self.class, @symbol].hash
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def substitute(replacements)
|
|
77
|
+
symbol = replacements[@symbol]
|
|
78
|
+
symbol ? Variable.new(symbol) : self
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def to_s
|
|
82
|
+
Variable.substitutions&.[](@symbol) || @symbol.to_s
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class HashStack
|
|
5
|
+
extend Forwardable
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@stacks = {}
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def_delegators :@stacks, :empty?, :key?, :keys
|
|
12
|
+
def_delegators :to_h, :merge
|
|
13
|
+
|
|
14
|
+
def [](key)
|
|
15
|
+
@stacks[key]&.last
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def push(hash)
|
|
19
|
+
hash.each do |k, v|
|
|
20
|
+
(@stacks[k] ||= []) << v
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def pop(hash)
|
|
25
|
+
hash.each_key do |k|
|
|
26
|
+
array = @stacks[k]
|
|
27
|
+
array.pop
|
|
28
|
+
|
|
29
|
+
@stacks.delete(k) if array.empty?
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def slice(*keys)
|
|
34
|
+
keys.each_with_object({}) do |k, h|
|
|
35
|
+
pair = @stacks.assoc(k)
|
|
36
|
+
|
|
37
|
+
next unless pair
|
|
38
|
+
|
|
39
|
+
h[k] = pair[1].last
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def merge(hash)
|
|
44
|
+
hash.default_proc = ->(h, k) { h[k] = self[k] }
|
|
45
|
+
hash
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def to_h
|
|
49
|
+
@stacks.transform_values(&:last)
|
|
50
|
+
end
|
|
51
|
+
alias to_hash to_h
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/matcher/list.rb
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class List
|
|
5
|
+
include Enumerable
|
|
6
|
+
|
|
7
|
+
def self.empty
|
|
8
|
+
EmptyList.instance
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.one(item)
|
|
12
|
+
NonEmptyList.new(item)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def to_s
|
|
16
|
+
to_a.to_s
|
|
17
|
+
end
|
|
18
|
+
alias inspect to_s
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class EmptyList < List
|
|
22
|
+
include Singleton
|
|
23
|
+
|
|
24
|
+
def add(item)
|
|
25
|
+
NonEmptyList.new(item)
|
|
26
|
+
end
|
|
27
|
+
alias << add
|
|
28
|
+
|
|
29
|
+
def empty?
|
|
30
|
+
true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def last
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def each
|
|
38
|
+
self
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def reverse_each
|
|
42
|
+
return to_enum(:reverse_each) unless block_given?
|
|
43
|
+
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def to_a
|
|
48
|
+
[]
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class NonEmptyList < List
|
|
53
|
+
attr_reader :head, :tail
|
|
54
|
+
|
|
55
|
+
def initialize(head, tail = nil)
|
|
56
|
+
super()
|
|
57
|
+
|
|
58
|
+
@head = head
|
|
59
|
+
@tail = tail
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def empty?
|
|
63
|
+
false
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def add(item)
|
|
67
|
+
NonEmptyList.new(item, self)
|
|
68
|
+
end
|
|
69
|
+
alias << add
|
|
70
|
+
|
|
71
|
+
def last
|
|
72
|
+
@tail&.last || @head
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def hash
|
|
76
|
+
@hash ||= [self.class, @head, @tail].hash
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def ==(other)
|
|
80
|
+
return true if equal?(other)
|
|
81
|
+
|
|
82
|
+
other.instance_of?(NonEmptyList) &&
|
|
83
|
+
@head.eql?(other.head) &&
|
|
84
|
+
@tail.eql?(other.tail)
|
|
85
|
+
end
|
|
86
|
+
alias eql? ==
|
|
87
|
+
|
|
88
|
+
def each
|
|
89
|
+
c = self
|
|
90
|
+
|
|
91
|
+
while c
|
|
92
|
+
yield c.head
|
|
93
|
+
c = c.tail
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def reverse_each(&)
|
|
98
|
+
@tail&.reverse_each(&)
|
|
99
|
+
yield @head
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class Optional
|
|
5
|
+
include NoExpression
|
|
6
|
+
|
|
7
|
+
CACHEABLE_CLASSES = [
|
|
8
|
+
NilClass,
|
|
9
|
+
FalseClass,
|
|
10
|
+
TrueClass,
|
|
11
|
+
Integer,
|
|
12
|
+
Float,
|
|
13
|
+
Symbol,
|
|
14
|
+
String,
|
|
15
|
+
Regexp,
|
|
16
|
+
Module,
|
|
17
|
+
Base,
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
def self.cache(value, matcher_cache = MatcherCache.current)
|
|
21
|
+
return new(value) if !matcher_cache ||
|
|
22
|
+
!CACHEABLE_CLASSES.include?(value.class) ||
|
|
23
|
+
value.is_a?(String) && !value.frozen?
|
|
24
|
+
|
|
25
|
+
(matcher_cache.optionals ||= {})[value] ||= new(value)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.value_of(obj)
|
|
29
|
+
obj.is_a?(Optional) ? obj.value : obj
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def initialize(value)
|
|
33
|
+
@value = value
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
attr_reader :value
|
|
37
|
+
|
|
38
|
+
def ~
|
|
39
|
+
~Matcher.cache(self)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def ==(other)
|
|
43
|
+
return true if equal?(other)
|
|
44
|
+
|
|
45
|
+
other.instance_of?(self.class) && other.value == @value
|
|
46
|
+
end
|
|
47
|
+
alias eql? ==
|
|
48
|
+
|
|
49
|
+
def hash
|
|
50
|
+
[self.class, @value].hash
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def to_s
|
|
54
|
+
"optional(#{@value.inspect})"
|
|
55
|
+
end
|
|
56
|
+
alias inspect to_s
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
module MatcherBuilding
|
|
60
|
+
##
|
|
61
|
+
# Marks a hash key as optional or wraps a matcher to also accept +nil+
|
|
62
|
+
# @example
|
|
63
|
+
# # optional hash key
|
|
64
|
+
# { optional(:foo) => 1 }
|
|
65
|
+
# # match nil or String
|
|
66
|
+
# optional(String)
|
|
67
|
+
# @overload optional(value)
|
|
68
|
+
# @param value
|
|
69
|
+
# @return [Optional]
|
|
70
|
+
# @overload optional
|
|
71
|
+
# @return [Chain]
|
|
72
|
+
def optional(value = UNDEFINED)
|
|
73
|
+
return Chain.new { optional(_1) } if Matcher.undefined?(value)
|
|
74
|
+
|
|
75
|
+
value = Expression.try_recorder(value)
|
|
76
|
+
|
|
77
|
+
Optional.new(value)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class Others
|
|
5
|
+
include Singleton
|
|
6
|
+
include NoMatcher
|
|
7
|
+
include NoExpression
|
|
8
|
+
|
|
9
|
+
def to_s
|
|
10
|
+
'others'
|
|
11
|
+
end
|
|
12
|
+
alias inspect to_s
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module MatcherBuilding
|
|
16
|
+
##
|
|
17
|
+
# Hash key that matches remaining entries
|
|
18
|
+
# @example
|
|
19
|
+
# {
|
|
20
|
+
# id: Integer,
|
|
21
|
+
# others => each_value(String),
|
|
22
|
+
# }
|
|
23
|
+
# @return [Others]
|
|
24
|
+
def others
|
|
25
|
+
Others.instance
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
MatcherCache = Struct.new(
|
|
5
|
+
:negated_matchers,
|
|
6
|
+
:equal_matchers,
|
|
7
|
+
:expression_matchers,
|
|
8
|
+
:kind_of_matchers,
|
|
9
|
+
:optionals,
|
|
10
|
+
:optional_matchers,
|
|
11
|
+
:range_matchers,
|
|
12
|
+
:regexp_matchers,
|
|
13
|
+
) do
|
|
14
|
+
def self.current(build_session = Matcher.build_session)
|
|
15
|
+
build_session[:_matcher_cache] ||= new if build_session
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class AllMatcher < Base
|
|
5
|
+
def initialize(matchers)
|
|
6
|
+
super()
|
|
7
|
+
|
|
8
|
+
@matchers = matchers
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
attr_reader :matchers
|
|
12
|
+
|
|
13
|
+
def *(matcher)
|
|
14
|
+
matcher = Matcher.cache(matcher)
|
|
15
|
+
|
|
16
|
+
if matcher.is_a?(AllMatcher)
|
|
17
|
+
AllMatcher.new(@matchers + matcher.matchers)
|
|
18
|
+
else
|
|
19
|
+
AllMatcher.new(@matchers + [matcher])
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def negate
|
|
24
|
+
AnyMatcher.new(@matchers.map(&:~))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def validate(state)
|
|
28
|
+
@matchers.each do |matcher|
|
|
29
|
+
state.errors << yield(matcher)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def to_s
|
|
34
|
+
"all(#{@matchers.map(&:to_s).join(', ')})"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
module MatcherBuilding
|
|
39
|
+
##
|
|
40
|
+
# Matches all matchers
|
|
41
|
+
# @example
|
|
42
|
+
# # matches 12 but not 9 or 13
|
|
43
|
+
# all(_ > 10, _.even?)
|
|
44
|
+
# # alternatively:
|
|
45
|
+
# of(_ > 10) * of(_.even?)
|
|
46
|
+
# @param matchers [Array<Base>]
|
|
47
|
+
# @return [AllMatcher]
|
|
48
|
+
# @see Base#*
|
|
49
|
+
def all(*matchers)
|
|
50
|
+
case matchers.length
|
|
51
|
+
when 0
|
|
52
|
+
AlwaysMatcher.instance
|
|
53
|
+
when 1
|
|
54
|
+
matcher_of(matchers[0])
|
|
55
|
+
else
|
|
56
|
+
AllMatcher.new(matchers.map { matcher_of(_1) })
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class AlwaysMatcher < Base
|
|
5
|
+
include Singleton
|
|
6
|
+
|
|
7
|
+
def ~
|
|
8
|
+
NeverMatcher.instance
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def validate(_state) end
|
|
12
|
+
|
|
13
|
+
def to_s
|
|
14
|
+
'always'
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
module MatcherBuilding
|
|
19
|
+
##
|
|
20
|
+
# Matches always
|
|
21
|
+
# @example
|
|
22
|
+
# { foo: always }
|
|
23
|
+
# @return [AlwaysMatcher]
|
|
24
|
+
def always
|
|
25
|
+
AlwaysMatcher.instance
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class AnyMatcher < Base
|
|
5
|
+
def initialize(matchers)
|
|
6
|
+
super()
|
|
7
|
+
|
|
8
|
+
@matchers = matchers
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
attr_reader :matchers
|
|
12
|
+
|
|
13
|
+
def +(matcher)
|
|
14
|
+
matcher = Matcher.cache(matcher)
|
|
15
|
+
|
|
16
|
+
if matcher.is_a?(AnyMatcher)
|
|
17
|
+
AnyMatcher.new(@matchers + matcher.matchers)
|
|
18
|
+
else
|
|
19
|
+
AnyMatcher.new(@matchers + [matcher])
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def negate
|
|
24
|
+
AllMatcher.new(@matchers.map(&:~))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def validate(state)
|
|
28
|
+
if @matchers.empty?
|
|
29
|
+
state.errors << state.report.exist
|
|
30
|
+
return
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
sub_errors = @matchers.map do |matcher|
|
|
34
|
+
sub_error = yield matcher
|
|
35
|
+
|
|
36
|
+
return if sub_error.valid?
|
|
37
|
+
|
|
38
|
+
sub_error
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
state.errors << OrError.from(sub_errors)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def to_s
|
|
45
|
+
"any(#{@matchers.map(&:to_s).join(', ')})"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
module MatcherBuilding
|
|
50
|
+
##
|
|
51
|
+
# Matches any matcher
|
|
52
|
+
# @example
|
|
53
|
+
# # matches "foo" and 1 but not 1.5
|
|
54
|
+
# any(String, Integer)
|
|
55
|
+
# # alternatively:
|
|
56
|
+
# of(String) + of(Integer)
|
|
57
|
+
# @param matchers [Array<Base>]
|
|
58
|
+
# @return [AnyMatcher]
|
|
59
|
+
def any(*matchers)
|
|
60
|
+
case matchers.length
|
|
61
|
+
when 0
|
|
62
|
+
NeverMatcher.instance
|
|
63
|
+
when 1
|
|
64
|
+
matcher_of(matchers[0])
|
|
65
|
+
else
|
|
66
|
+
AnyMatcher.new(matchers.map { matcher_of(_1) })
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class ArrayMatcher < Base
|
|
5
|
+
def initialize(array)
|
|
6
|
+
super()
|
|
7
|
+
|
|
8
|
+
@array = array
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def negate
|
|
12
|
+
NegatedArrayMatcher.new(@array)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def validate(state)
|
|
16
|
+
actual = state.actual
|
|
17
|
+
errors = state.errors
|
|
18
|
+
|
|
19
|
+
unless actual.is_a?(Array)
|
|
20
|
+
errors << state.expected.kind_of(Array)
|
|
21
|
+
return
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
errors << state.expected.length_of(@array.length, actual.length) if @array.length != actual.length
|
|
25
|
+
|
|
26
|
+
[@array.length, actual.length].min.times do |i|
|
|
27
|
+
errors[i] << yield(@array[i], actual[i], index: i, parent: actual)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def to_s
|
|
32
|
+
@array.to_s
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|