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
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,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 *(other)
|
|
14
|
+
other = Matcher.cache(other)
|
|
15
|
+
|
|
16
|
+
if other.is_a?(AllMatcher)
|
|
17
|
+
AllMatcher.new(@matchers + other.matchers)
|
|
18
|
+
else
|
|
19
|
+
AllMatcher.new(@matchers + [other])
|
|
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.join(', ')})"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
module MatcherDsl
|
|
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,34 @@
|
|
|
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 MatcherDsl
|
|
19
|
+
##
|
|
20
|
+
# Matches always
|
|
21
|
+
#
|
|
22
|
+
# Many matchers accept child matchers, for instance the HashMatcher. But
|
|
23
|
+
# before they invoke a child matcher they often perform implicit checks. And
|
|
24
|
+
# sometimes, we are only interested in those implicit checks and don't care
|
|
25
|
+
# about having a child matcher.
|
|
26
|
+
#
|
|
27
|
+
# @example
|
|
28
|
+
# { foo: always }
|
|
29
|
+
# @return [AlwaysMatcher]
|
|
30
|
+
def always
|
|
31
|
+
AlwaysMatcher.instance
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
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 +(other)
|
|
14
|
+
other = Matcher.cache(other)
|
|
15
|
+
|
|
16
|
+
if other.is_a?(AnyMatcher)
|
|
17
|
+
AnyMatcher.new(@matchers + other.matchers)
|
|
18
|
+
else
|
|
19
|
+
AnyMatcher.new(@matchers + [other])
|
|
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 nil 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.join(', ')})"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
module MatcherDsl
|
|
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,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
##
|
|
5
|
+
# == Basic array matching
|
|
6
|
+
#
|
|
7
|
+
# m = Matcher.build { [1, 2, 3] }
|
|
8
|
+
#
|
|
9
|
+
# m.match?([1, 2, 3]) # => true
|
|
10
|
+
# m.match([1, 2]) # > root: expected length of 3 but was 2
|
|
11
|
+
# m.match([1, 2, 3, 4]) # > root: expected length of 3 but was 4
|
|
12
|
+
#
|
|
13
|
+
# m.match([3, 2, 1])
|
|
14
|
+
# # > root[0]: expected 1 but got 3
|
|
15
|
+
# # > root[2]: expected 3 but got 1
|
|
16
|
+
#
|
|
17
|
+
# m = Match.build { [Integer, String] }
|
|
18
|
+
# m.match?([1, "foo"]) # => true
|
|
19
|
+
#
|
|
20
|
+
# == Values passed to element matchers
|
|
21
|
+
# +ArrayMatcher+ passes +index+ and +parent+ to its element matchers.
|
|
22
|
+
#
|
|
23
|
+
# # index or i
|
|
24
|
+
# m = Matcher.build { [_ == i] }
|
|
25
|
+
# m.match?([0]) # => true
|
|
26
|
+
# m.match([1]) # > root[0]: expected actual == index but got 1 == 0
|
|
27
|
+
#
|
|
28
|
+
# # parent
|
|
29
|
+
# m = Matcher.build do
|
|
30
|
+
# in_order = imply(i > 0, parent[i - 1] <= _)
|
|
31
|
+
# [in_order, in_order, in_order]
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
# m.match?([1, 2, 3])
|
|
35
|
+
# # => true
|
|
36
|
+
# m.match([1, 2, 0])
|
|
37
|
+
# # > root[2]: expected actual >= parent[index - 1] but got 0 >= 2, where
|
|
38
|
+
# # parent = [1, 2, 0], index = 2
|
|
39
|
+
class ArrayMatcher < Base
|
|
40
|
+
def initialize(array)
|
|
41
|
+
super()
|
|
42
|
+
|
|
43
|
+
@array = array
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def negate
|
|
47
|
+
NegatedArrayMatcher.new(@array)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def validate(state)
|
|
51
|
+
actual = state.actual
|
|
52
|
+
errors = state.errors
|
|
53
|
+
|
|
54
|
+
unless actual.is_a?(Array)
|
|
55
|
+
errors << state.expected.kind_of(Array)
|
|
56
|
+
return
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
if @array.length != actual.length
|
|
60
|
+
errors << state.expected.length_of(@array.length, actual.length)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
[@array.length, actual.length].min.times do |i|
|
|
64
|
+
errors[i] << yield(@array[i], actual[i], index: i, parent: actual)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def to_s
|
|
69
|
+
@array.to_s
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class BlockMatcher < Base
|
|
5
|
+
def initialize(block, description = nil, negated: false)
|
|
6
|
+
super()
|
|
7
|
+
|
|
8
|
+
@block = block
|
|
9
|
+
@description = description
|
|
10
|
+
@negated = negated
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def negate
|
|
14
|
+
BlockMatcher.new(@block, @description, negated: !@negated)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def validate(state)
|
|
18
|
+
result = Utils.call_block(@block, state.values)
|
|
19
|
+
|
|
20
|
+
state.errors << build_message(state) if @negated ^ !result
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def to_s
|
|
24
|
+
string = @description || "-> { #{block_location} }"
|
|
25
|
+
|
|
26
|
+
if @negated
|
|
27
|
+
"neg(#{string})"
|
|
28
|
+
else
|
|
29
|
+
string
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def build_message(state)
|
|
36
|
+
if @description
|
|
37
|
+
state.expected.not_if(@negated)
|
|
38
|
+
.described_by(@description)
|
|
39
|
+
else
|
|
40
|
+
state.expected.namespace(:block).not_if(@negated)
|
|
41
|
+
.satisfied(block_location)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def block_location
|
|
46
|
+
Utils.block_location(@block)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
module MatcherDsl
|
|
51
|
+
##
|
|
52
|
+
# Matches when block returns truthy
|
|
53
|
+
# @example
|
|
54
|
+
# satisfy('an even number') { |actual:| actual.even? }
|
|
55
|
+
# @param message [String, nil] optional description for error reporting
|
|
56
|
+
# @return [BlockMatcher]
|
|
57
|
+
def satisfy(message = nil, &block)
|
|
58
|
+
BlockMatcher.new(block, message)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class BooleanMatcher < Base
|
|
5
|
+
BOOLEAN = [false, true].freeze
|
|
6
|
+
|
|
7
|
+
def initialize(negated: false)
|
|
8
|
+
super()
|
|
9
|
+
|
|
10
|
+
@negated = negated
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def negate
|
|
14
|
+
BooleanMatcher.new(negated: !@negated)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def validate(state)
|
|
18
|
+
state.errors << state.expected.not_if(@negated).in(BOOLEAN) if
|
|
19
|
+
@negated == BOOLEAN.include?(state.actual)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_s
|
|
23
|
+
"#{'~' if @negated}boolean"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
module MatcherDsl
|
|
28
|
+
##
|
|
29
|
+
# Matches +true+ and +false+
|
|
30
|
+
# @example
|
|
31
|
+
# { available: boolean }
|
|
32
|
+
# @return [BooleanMatcher]
|
|
33
|
+
def boolean
|
|
34
|
+
@boolean ||= BooleanMatcher.new
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class DigMatcher < Base
|
|
5
|
+
def initialize(keys, matcher, optional: false, negated: false)
|
|
6
|
+
super()
|
|
7
|
+
|
|
8
|
+
@keys = keys
|
|
9
|
+
@matcher = negated ? ~matcher : matcher
|
|
10
|
+
@original_matcher = matcher
|
|
11
|
+
@optional = optional
|
|
12
|
+
@negated = negated
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def negate
|
|
16
|
+
DigMatcher.new(
|
|
17
|
+
@keys, @original_matcher, optional: @optional, negated: !@negated
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def validate(state, &)
|
|
22
|
+
return validate_negated(state, &) if @negated
|
|
23
|
+
|
|
24
|
+
cur = state.actual
|
|
25
|
+
errors = state.errors
|
|
26
|
+
|
|
27
|
+
@keys.each do |key|
|
|
28
|
+
key = key.evaluate(state.values) if key.is_a?(Expression)
|
|
29
|
+
is_array = cur.is_a?(Array)
|
|
30
|
+
|
|
31
|
+
if is_array
|
|
32
|
+
unless key.is_a?(Integer)
|
|
33
|
+
errors << state.expected(cur).kind_of(Hash)
|
|
34
|
+
return nil
|
|
35
|
+
end
|
|
36
|
+
elsif !cur.is_a?(Hash)
|
|
37
|
+
or_error = state.new_collector.or!
|
|
38
|
+
or_error << state.expected(cur).kind_of(Hash)
|
|
39
|
+
or_error << state.expected(cur).kind_of(Array)
|
|
40
|
+
errors << or_error.error
|
|
41
|
+
|
|
42
|
+
return nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
prev = cur
|
|
46
|
+
cur = cur[key]
|
|
47
|
+
|
|
48
|
+
if cur.nil?
|
|
49
|
+
if @optional
|
|
50
|
+
return nil unless is_array ? index?(prev, key) : prev.key?(key)
|
|
51
|
+
elsif is_array
|
|
52
|
+
unless index?(prev, key)
|
|
53
|
+
errors << state.expected(prev).having_index(key)
|
|
54
|
+
return nil
|
|
55
|
+
end
|
|
56
|
+
else
|
|
57
|
+
unless prev.key?(key)
|
|
58
|
+
errors << state.expected(prev).having_key(key)
|
|
59
|
+
return nil
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
errors = errors[key]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
errors << yield(@matcher, cur)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def to_s
|
|
71
|
+
helper = "#{'optional_' if @optional}dig"
|
|
72
|
+
keys = @keys.map(&:inspect).join(", ")
|
|
73
|
+
matcher = Matcher.parenthesize(@original_matcher)
|
|
74
|
+
|
|
75
|
+
"#{'~' if @negated}#{helper}(#{keys}) ^ #{matcher}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def index?(array, index)
|
|
81
|
+
index.between?(-array.length, array.length - 1)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def validate_negated(state)
|
|
85
|
+
cur = state.actual
|
|
86
|
+
errors = state.errors
|
|
87
|
+
|
|
88
|
+
@keys.each do |key|
|
|
89
|
+
key = key.evaluate(state.values) if key.is_a?(Expression)
|
|
90
|
+
is_array = cur.is_a?(Array)
|
|
91
|
+
|
|
92
|
+
return nil if is_array ? !key.is_a?(Integer) : !cur.is_a?(Hash)
|
|
93
|
+
|
|
94
|
+
prev = cur
|
|
95
|
+
cur = cur[key]
|
|
96
|
+
|
|
97
|
+
if cur.nil?
|
|
98
|
+
if is_array
|
|
99
|
+
unless index?(prev, key)
|
|
100
|
+
errors << state.expected(prev).having_index(key) if @optional
|
|
101
|
+
return nil
|
|
102
|
+
end
|
|
103
|
+
else
|
|
104
|
+
unless prev.key?(key)
|
|
105
|
+
errors << state.expected(prev).having_key(key) if @optional
|
|
106
|
+
return nil
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
errors = errors[key]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
errors << yield(@matcher, cur)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
module MatcherDsl
|
|
119
|
+
##
|
|
120
|
+
# Matches deeply nested values
|
|
121
|
+
# @example
|
|
122
|
+
# # matches [0, { a: { "B" => 42 } }] where b: "B", but not []
|
|
123
|
+
# dig(1, :a, vars[:b]) ^ Integer
|
|
124
|
+
# @param path [Array<Expression>]
|
|
125
|
+
# @param optional [true, false] matches if path doesn't exist when +true+
|
|
126
|
+
# @return [Chain<DigMatcher>]
|
|
127
|
+
def dig(*path, optional: false)
|
|
128
|
+
path.each_with_index do |key, i|
|
|
129
|
+
path[i] = expression_or_value(key)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
Chain.new do |matcher|
|
|
133
|
+
DigMatcher.new(path, matcher, optional:)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
##
|
|
138
|
+
# Matches deeply nested value only if path exists
|
|
139
|
+
# @example
|
|
140
|
+
# # matches { a: { b: 1 } } and { a: {} } but
|
|
141
|
+
# # neither { a: { b: nil } } nor { a: nil }
|
|
142
|
+
# optional_dig(:a, :b) ^ 1
|
|
143
|
+
# @param path [Array<Expression>]
|
|
144
|
+
# @return [Chain<DigMatcher>]
|
|
145
|
+
def optional_dig(*path)
|
|
146
|
+
dig(*path, optional: true)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
##
|
|
5
|
+
# Match each item.
|
|
6
|
+
# @example
|
|
7
|
+
# m = Matcher.build { each(Integer) }
|
|
8
|
+
#
|
|
9
|
+
# m.match?([1, 2, 3])
|
|
10
|
+
# # => true
|
|
11
|
+
# m.match([1, "foo"])
|
|
12
|
+
# # > root[1]: expected a kind of Integer but got "foo"
|
|
13
|
+
#
|
|
14
|
+
# # "each" passes index and parent to its item matcher:
|
|
15
|
+
# m = Matcher.build { each(_ == i) }
|
|
16
|
+
# m.match?([0, 1]) # => true
|
|
17
|
+
# m.match?([0, 2]) # => false
|
|
18
|
+
#
|
|
19
|
+
# m = Matcher.build { each(_ < parent.length) }
|
|
20
|
+
# m.match?([1, 0, 2]) # => true
|
|
21
|
+
# m.match?([1, 2, 3]) # => false
|
|
22
|
+
class EachMatcher < Base
|
|
23
|
+
def initialize(matcher)
|
|
24
|
+
super()
|
|
25
|
+
|
|
26
|
+
@matcher = matcher
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def negate
|
|
30
|
+
NegatedEachMatcher.new(@matcher)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def validate(state)
|
|
34
|
+
unless state.actual.respond_to?(:each)
|
|
35
|
+
state.errors << state.expected.responding_to(:each)
|
|
36
|
+
return
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
i = 0
|
|
40
|
+
state.actual.each do |item|
|
|
41
|
+
state.errors[i] << yield(@matcher, item, index: i, parent: state.actual)
|
|
42
|
+
i += 1
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def to_s
|
|
47
|
+
"each(#{@matcher})"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
module MatcherDsl
|
|
52
|
+
##
|
|
53
|
+
# Matches each item with matcher
|
|
54
|
+
#
|
|
55
|
+
# == +matcher+ values
|
|
56
|
+
#
|
|
57
|
+
# Passes +index+
|
|
58
|
+
#
|
|
59
|
+
# m = Matcher.build { each(_ == i) }
|
|
60
|
+
# m.match?([0, 1]) # => true
|
|
61
|
+
# m.match?([0, 2]) # => false
|
|
62
|
+
#
|
|
63
|
+
# Passes +parent+
|
|
64
|
+
#
|
|
65
|
+
# m = Matcher.build { each(_ < parent.length) }
|
|
66
|
+
# m.match?([1, 0, 2]) # => true
|
|
67
|
+
# m.match?([1, 2, 3]) # => false
|
|
68
|
+
#
|
|
69
|
+
# @example
|
|
70
|
+
# # matches [1, 2] but not [1, "foo"]
|
|
71
|
+
# each(Integer)
|
|
72
|
+
# # alternatively:
|
|
73
|
+
# each ^ Integer
|
|
74
|
+
# @overload each(matcher)
|
|
75
|
+
# @param matcher [Base]
|
|
76
|
+
# @return [EachMatcher]
|
|
77
|
+
# @overload each
|
|
78
|
+
# @return [Chain<EachMatcher>]
|
|
79
|
+
def each(matcher = UNDEFINED)
|
|
80
|
+
return Chain.new { each(_1) } if Matcher.undefined?(matcher)
|
|
81
|
+
|
|
82
|
+
EachMatcher.new(matcher_of(matcher))
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|