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,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class ProjectMatcher < Base
|
|
5
|
+
def initialize(expression, matcher)
|
|
6
|
+
super()
|
|
7
|
+
|
|
8
|
+
@expression = expression
|
|
9
|
+
@matcher = matcher
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def negate
|
|
13
|
+
NegatedProjectMatcher.new(@expression, @matcher)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def validate(state)
|
|
17
|
+
begin
|
|
18
|
+
result = @expression.evaluate(state.values)
|
|
19
|
+
rescue CallError => e
|
|
20
|
+
# rescuing here instead of method so we won't catch from yield
|
|
21
|
+
state.errors << e.message_for_errors(state.actual)
|
|
22
|
+
return
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
state.errors[@expression] << yield(@matcher, result)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def to_s
|
|
29
|
+
"project(#{@expression} => #{@matcher})"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
module MatcherDsl
|
|
34
|
+
##
|
|
35
|
+
# Matches the value of an expression
|
|
36
|
+
# @example
|
|
37
|
+
# # matches "5"
|
|
38
|
+
# project(_.to_i => _ < 10)
|
|
39
|
+
# # alternatively:
|
|
40
|
+
# project(_.to_i) ^ (_ < 10)
|
|
41
|
+
# # project multiple expressions
|
|
42
|
+
# project(
|
|
43
|
+
# _.foo => 1,
|
|
44
|
+
# _.bar => 2,
|
|
45
|
+
# )
|
|
46
|
+
# @overload project(expression => matcher)
|
|
47
|
+
# @return [ProjectMatcher]
|
|
48
|
+
# @overload project(expression)
|
|
49
|
+
# @return [Chain<ProjectMatcher>]
|
|
50
|
+
# @overload project(**projections)
|
|
51
|
+
# @return [AllMatcher<ProjectMatcher>]
|
|
52
|
+
def project(expression = UNDEFINED, **projections)
|
|
53
|
+
if !Matcher.undefined?(expression) && !projections.empty?
|
|
54
|
+
raise "cannot mix project(expression) ^ matcher and " \
|
|
55
|
+
"project(expression => matcher)"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
unless Matcher.undefined?(expression)
|
|
59
|
+
return Chain.new { project(expression => _1) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
project_matchers = projections.map do |e, m|
|
|
63
|
+
e = expression_of(e)
|
|
64
|
+
m = matcher_of(m)
|
|
65
|
+
|
|
66
|
+
ProjectMatcher.new(e, m)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
all(*project_matchers)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class RaisesMatcher < Base
|
|
5
|
+
def initialize(
|
|
6
|
+
expression,
|
|
7
|
+
matcher,
|
|
8
|
+
negated: false,
|
|
9
|
+
rescue_exception: StandardError
|
|
10
|
+
)
|
|
11
|
+
super()
|
|
12
|
+
|
|
13
|
+
@expression = expression
|
|
14
|
+
@matcher = negated ? ~matcher : matcher
|
|
15
|
+
@original_matcher = matcher
|
|
16
|
+
@negated = negated
|
|
17
|
+
@rescue_exception = rescue_exception
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def negate
|
|
21
|
+
RaisesMatcher.new(
|
|
22
|
+
@expression,
|
|
23
|
+
@original_matcher,
|
|
24
|
+
negated: !@negated,
|
|
25
|
+
rescue_exception: @rescue_exception,
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def validate(state)
|
|
30
|
+
@expression.evaluate(state.values)
|
|
31
|
+
|
|
32
|
+
return if @negated
|
|
33
|
+
|
|
34
|
+
given = @expression.given_for(state.values)
|
|
35
|
+
state.errors << state.expected.namespace(:expression)
|
|
36
|
+
.raising(@expression, @rescue_exception, given)
|
|
37
|
+
rescue @rescue_exception => e
|
|
38
|
+
state.errors[rescue_last_error] << yield(@matcher, unwrap_exception(e))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def to_s
|
|
42
|
+
"#{'~' if @negated}raises(#{@expression}, #{@original_matcher})"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def rescue_last_error
|
|
48
|
+
@rescue_last_error ||= RescueLastErrorExpression.new(@expression)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def unwrap_exception(error)
|
|
52
|
+
case error
|
|
53
|
+
when CallError
|
|
54
|
+
error.cause
|
|
55
|
+
else
|
|
56
|
+
error
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
module MatcherDsl
|
|
62
|
+
##
|
|
63
|
+
# Matches raised error
|
|
64
|
+
# @example
|
|
65
|
+
# # matches {} (because fetch raises KeyError)
|
|
66
|
+
# raises(_.fetch(:foo), KeyError)
|
|
67
|
+
# # alternatively:
|
|
68
|
+
# raises(_.fetch(:foo)) ^ KeyError
|
|
69
|
+
# # match error message
|
|
70
|
+
# raises(_.call, message: /something went wrong/)
|
|
71
|
+
# # pass block instead of expression
|
|
72
|
+
# raises(NoMethodError) { |x| x.fetch(:foo) }
|
|
73
|
+
# # rescue non-standard exceptions
|
|
74
|
+
# raises(_.call, rescue: Exception)
|
|
75
|
+
# @overload raises(expression, matcher, message: UNDEFINED, rescue: StandardError)
|
|
76
|
+
# Matches error raised from expression.
|
|
77
|
+
# @param expression [Expression]
|
|
78
|
+
# @param matcher [Base] matcher for error
|
|
79
|
+
# @param message [Base] matcher for message
|
|
80
|
+
# @param rescue [Class] exception class to rescue
|
|
81
|
+
# @return [RaisesMatcher]
|
|
82
|
+
# @overload raises(matcher, message: UNDEFINED, rescue: StandardError, &)
|
|
83
|
+
# Matches error raised from block.
|
|
84
|
+
# @param matcher [Base] matcher for error
|
|
85
|
+
# @param message [Base] matcher for message
|
|
86
|
+
# @param rescue [Class] exception class to rescue
|
|
87
|
+
# @yield actual
|
|
88
|
+
# @return [OptionalChain<RaisesMatcher>]
|
|
89
|
+
def raises(
|
|
90
|
+
expression_or_matcher = UNDEFINED,
|
|
91
|
+
matcher = UNDEFINED,
|
|
92
|
+
message: UNDEFINED,
|
|
93
|
+
rescue: StandardError,
|
|
94
|
+
&block
|
|
95
|
+
)
|
|
96
|
+
no_arg1 = Matcher.undefined?(expression_or_matcher)
|
|
97
|
+
no_arg2 = Matcher.undefined?(matcher)
|
|
98
|
+
|
|
99
|
+
if no_arg1 == no_arg2 && no_arg1 ^ block_given?
|
|
100
|
+
raise ArgumentError, "both expression and block given" unless no_arg1
|
|
101
|
+
|
|
102
|
+
raise ArgumentError, "neither expression nor block given"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
if block_given?
|
|
106
|
+
expression = ProcExpression.new(block)
|
|
107
|
+
matcher = expression_or_matcher
|
|
108
|
+
else
|
|
109
|
+
expression = expression_of(expression_or_matcher)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
return Chain.new { raises(expression, _1, message:, rescue:) }.optional if
|
|
113
|
+
Matcher.undefined?(matcher)
|
|
114
|
+
|
|
115
|
+
matcher = matcher_of(matcher)
|
|
116
|
+
|
|
117
|
+
unless Matcher.undefined?(message)
|
|
118
|
+
@raises_message_call ||=
|
|
119
|
+
expression_of(Call.new(Variable.actual, :message))
|
|
120
|
+
|
|
121
|
+
message_matcher = matcher_of(message)
|
|
122
|
+
|
|
123
|
+
matcher &= ProjectMatcher.new(@raises_message_call, message_matcher)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
RaisesMatcher.new(
|
|
127
|
+
expression, matcher, rescue_exception: { rescue: }[:rescue]
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class RangeMatcher < Base
|
|
5
|
+
def self.cache(range, matcher_cache = MatcherCache.current)
|
|
6
|
+
return new(range) unless matcher_cache
|
|
7
|
+
|
|
8
|
+
(matcher_cache.range_matchers ||= {})[range] ||= new(range)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(range, negated: false)
|
|
12
|
+
super()
|
|
13
|
+
|
|
14
|
+
@range = range
|
|
15
|
+
@negated = negated
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def negate
|
|
19
|
+
RangeMatcher.new(@range, negated: !@negated)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def validate(state)
|
|
23
|
+
limit = @range.begin || @range.end
|
|
24
|
+
|
|
25
|
+
if (limit <=> state.actual).nil?
|
|
26
|
+
unless @negated
|
|
27
|
+
state.errors << state.expected.comparable_to(@range.begin)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
return
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
return if @range.include?(state.actual) ^ @negated
|
|
34
|
+
|
|
35
|
+
state.errors << state.expected.not_if(@negated).between(
|
|
36
|
+
@range.begin,
|
|
37
|
+
@range.end,
|
|
38
|
+
exclude_end: @range.exclude_end?,
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def to_s
|
|
43
|
+
if @negated
|
|
44
|
+
"neg(#{@range})"
|
|
45
|
+
else
|
|
46
|
+
@range.to_s
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
##
|
|
5
|
+
# Build recursive matchers.
|
|
6
|
+
# m = Matcher.build do
|
|
7
|
+
# refs[:list] = {
|
|
8
|
+
# head: Integer,
|
|
9
|
+
# tail: optional(refs[:list]),
|
|
10
|
+
# }
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# m.match?({ head: 1, tail: { head: 2, tail: nil } })
|
|
14
|
+
# # => true
|
|
15
|
+
# m.match({ head: 1, tail: { head: "two", tail: nil } })
|
|
16
|
+
# # > root[:tail][:head]: expected a kind of Integer but got "two"
|
|
17
|
+
#
|
|
18
|
+
# == Allow cyclic references
|
|
19
|
+
#
|
|
20
|
+
# By default, reference matchers won't allow visiting the same object twice.
|
|
21
|
+
# However, for structures like graphs we can enable cyclic mode. When the
|
|
22
|
+
# reference matcher revisits an object it will assume a new match would be
|
|
23
|
+
# the same as the first match result and skip traversal.
|
|
24
|
+
#
|
|
25
|
+
# Also note, that when caching is enabled values won't be passed to the target
|
|
26
|
+
# matcher to ensure results are reproducible for the same object.
|
|
27
|
+
#
|
|
28
|
+
# m = Matcher.build do
|
|
29
|
+
# refs[:vertex] = {
|
|
30
|
+
# name: String,
|
|
31
|
+
# edges: each(refs[:edge, cyclic: true]),
|
|
32
|
+
# }
|
|
33
|
+
#
|
|
34
|
+
# refs[:edge] = {
|
|
35
|
+
# weight: Integer,
|
|
36
|
+
# destination: refs[:vertex, cyclic: true],
|
|
37
|
+
# }
|
|
38
|
+
#
|
|
39
|
+
# { vertices: each(refs[:vertex]) }
|
|
40
|
+
# end
|
|
41
|
+
#
|
|
42
|
+
# a = { name: 'a', edges: [] }
|
|
43
|
+
# b = { name: 'b', edges: [] }
|
|
44
|
+
# c = { name: 'c', edges: [] }
|
|
45
|
+
#
|
|
46
|
+
# a[:edges] << { weight: 1, destination: b }
|
|
47
|
+
# b[:edges] << { weight: 2, destination: c }
|
|
48
|
+
# c[:edges] << { weight: "3", destination: a }
|
|
49
|
+
#
|
|
50
|
+
# graph = { vertices: [a, b, c] }
|
|
51
|
+
#
|
|
52
|
+
# m.match(graph)
|
|
53
|
+
# # > root[:vertices][0][:edges][0][:destination][:edges][0][:destination]~
|
|
54
|
+
# # [:edges][0][:weight]: expected a kind of Integer but got "3"
|
|
55
|
+
# # > root[:vertices][1]: actual has already failed before
|
|
56
|
+
# # > root[:vertices][2]: actual has already failed before
|
|
57
|
+
#
|
|
58
|
+
# == Disable cache
|
|
59
|
+
#
|
|
60
|
+
# By default, results are cached for each object during a match session.
|
|
61
|
+
# However, if a result depends not only on the actual value but also on other
|
|
62
|
+
# passed values then the cache may return an incorrect result. See the example
|
|
63
|
+
# below:
|
|
64
|
+
#
|
|
65
|
+
# m = Matcher.build do
|
|
66
|
+
# refs[:foo] = _ == vars[:a]
|
|
67
|
+
#
|
|
68
|
+
# [
|
|
69
|
+
# let(a: 1) ^ refs[:foo],
|
|
70
|
+
# let(a: 2) ^ refs[:foo],
|
|
71
|
+
# ]
|
|
72
|
+
# end
|
|
73
|
+
#
|
|
74
|
+
# # should only match [1, 2] but refs[:foo] caches result for 1 as valid:
|
|
75
|
+
#
|
|
76
|
+
# m.match?([1, 1])
|
|
77
|
+
# # => true, but shouldn't
|
|
78
|
+
#
|
|
79
|
+
# # disable cache when matching result depends on other values than actual:
|
|
80
|
+
#
|
|
81
|
+
# m = Matcher.build do
|
|
82
|
+
# refs[:foo, { cache: false }] = _ == vars[:a]
|
|
83
|
+
#
|
|
84
|
+
# [
|
|
85
|
+
# let(a: 1) ^ refs[:foo],
|
|
86
|
+
# let(a: 2) ^ refs[:foo],
|
|
87
|
+
# ]
|
|
88
|
+
# end
|
|
89
|
+
#
|
|
90
|
+
# m.match([1, 1])
|
|
91
|
+
# # > root[1]: expected actual == a but got 1 == 2
|
|
92
|
+
class ReferenceMatcher < Base
|
|
93
|
+
Settings = Struct.new(:target, :cache)
|
|
94
|
+
|
|
95
|
+
def initialize(
|
|
96
|
+
key,
|
|
97
|
+
settings,
|
|
98
|
+
cyclic: nil,
|
|
99
|
+
negated: false,
|
|
100
|
+
session_key: object_id
|
|
101
|
+
)
|
|
102
|
+
super()
|
|
103
|
+
|
|
104
|
+
@key = key
|
|
105
|
+
@settings = settings
|
|
106
|
+
@cyclic = cyclic
|
|
107
|
+
@negated = negated
|
|
108
|
+
@session_key = session_key
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def negate
|
|
112
|
+
ReferenceMatcher.new(
|
|
113
|
+
@key,
|
|
114
|
+
@settings,
|
|
115
|
+
cyclic: @cyclic,
|
|
116
|
+
negated: !@negated,
|
|
117
|
+
session_key: @session_key,
|
|
118
|
+
)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def validate(state)
|
|
122
|
+
actual = state.actual
|
|
123
|
+
sess = class_session
|
|
124
|
+
depth = sess[:depth]
|
|
125
|
+
depth = depth ? depth + 1 : 1
|
|
126
|
+
sess[:depth] = depth
|
|
127
|
+
|
|
128
|
+
if depth > Matcher.max_reference_depth
|
|
129
|
+
state.errors << "match level too deep: #{depth}"
|
|
130
|
+
return
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
unless visited.add?(actual.object_id)
|
|
134
|
+
if @negated == @cyclic
|
|
135
|
+
state.errors << state.report.namespace(:reference).cyclic
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
return
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
unless cache?
|
|
142
|
+
state.errors << yield(target)
|
|
143
|
+
return
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
cache = (sess[:cache] ||= {})
|
|
147
|
+
cache_key = [@key, actual.object_id]
|
|
148
|
+
cached_result = cache[cache_key]
|
|
149
|
+
|
|
150
|
+
if cached_result.nil?
|
|
151
|
+
# If @cyclic then call #match instead of yield. We disallow passing
|
|
152
|
+
# previous values for cyclic reference matchers. #match will create a
|
|
153
|
+
# new values stack.
|
|
154
|
+
target_errors = @cyclic ? target.match(actual) : yield(target)
|
|
155
|
+
cache[cache_key] = @negated ^ target_errors.valid?
|
|
156
|
+
|
|
157
|
+
state.errors << target_errors
|
|
158
|
+
elsif @negated == cached_result
|
|
159
|
+
state.errors << state.report.namespace(:reference).failed_from_cache
|
|
160
|
+
end
|
|
161
|
+
ensure
|
|
162
|
+
sess[:depth] = depth - 1
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def to_s
|
|
166
|
+
"#{'~' if @negated}refs[#{@key.inspect}]"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
private
|
|
170
|
+
|
|
171
|
+
def cache?
|
|
172
|
+
return @cache if defined? @cache
|
|
173
|
+
|
|
174
|
+
@cache = @settings[@key].cache
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def visited
|
|
178
|
+
session(@session_key)[:visited] ||= Set.new
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def target
|
|
182
|
+
return @target if defined? @target
|
|
183
|
+
|
|
184
|
+
pair = @settings[@key].target
|
|
185
|
+
|
|
186
|
+
raise "No target for #{@key.inspect}" unless pair
|
|
187
|
+
|
|
188
|
+
@target = if @negated
|
|
189
|
+
pair[1] ||= ~pair[0]
|
|
190
|
+
else
|
|
191
|
+
pair[0]
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
module MatcherDsl
|
|
197
|
+
def refs?
|
|
198
|
+
!@refs.nil?
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
##
|
|
202
|
+
# Returns the reference matcher collection for recursive matchers
|
|
203
|
+
# @example
|
|
204
|
+
# refs[:list] = {
|
|
205
|
+
# head: Integer,
|
|
206
|
+
# tail: optional(refs[:list]),
|
|
207
|
+
# }
|
|
208
|
+
# @return [ReferenceMatcherCollection]
|
|
209
|
+
def refs
|
|
210
|
+
@refs ||= ReferenceMatcherCollection.new(self)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class ReferenceMatcherCollection
|
|
5
|
+
include NoMatcher
|
|
6
|
+
include NoExpression
|
|
7
|
+
include NoKey
|
|
8
|
+
|
|
9
|
+
attr_reader :last_object_id, :last_matcher
|
|
10
|
+
|
|
11
|
+
def initialize(builder)
|
|
12
|
+
@settings = {}
|
|
13
|
+
@last_object_id = nil
|
|
14
|
+
@last_matcher = nil
|
|
15
|
+
@used = Set.new
|
|
16
|
+
@builder = builder
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def finalize
|
|
20
|
+
@settings.freeze
|
|
21
|
+
|
|
22
|
+
assigned = @settings.each_key.to_set
|
|
23
|
+
missing = @used - assigned
|
|
24
|
+
unused = assigned - @used
|
|
25
|
+
|
|
26
|
+
raise "undefined ref: #{missing.join(', ')}" unless missing.empty?
|
|
27
|
+
raise "unused ref: #{unused.join(', ')}" unless unused.empty?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def [](key, cyclic: false)
|
|
31
|
+
@used << key
|
|
32
|
+
|
|
33
|
+
ReferenceMatcher.new(key, @settings, cyclic:)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
DEFAULT_OPTIONS = { cache: true }.freeze
|
|
37
|
+
|
|
38
|
+
def []=(key, matcher_or_options, matcher = UNDEFINED)
|
|
39
|
+
raise "Cannot reassign reference: #{key.inspect}" if @settings.key?(key)
|
|
40
|
+
|
|
41
|
+
if Matcher.undefined?(matcher)
|
|
42
|
+
options = DEFAULT_OPTIONS
|
|
43
|
+
matcher = matcher_or_options
|
|
44
|
+
else
|
|
45
|
+
options = matcher_or_options.merge(DEFAULT_OPTIONS) { |_k, l, _r| l }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
@last_object_id = matcher.__id__
|
|
49
|
+
matcher = @builder.matcher_of(matcher)
|
|
50
|
+
@last_matcher = matcher
|
|
51
|
+
|
|
52
|
+
settings = (@settings[key] ||= ReferenceMatcher::Settings.new)
|
|
53
|
+
settings.target = [matcher, nil]
|
|
54
|
+
settings.cache = options[:cache]
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class RegexpMatcher < Base
|
|
5
|
+
def self.cache(pattern, matcher_cache = MatcherCache.current)
|
|
6
|
+
return new(pattern) unless matcher_cache
|
|
7
|
+
|
|
8
|
+
(matcher_cache.regexp_matchers ||= {})[pattern] ||= new(pattern)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(pattern, matcher = AlwaysMatcher.instance, negated: false)
|
|
12
|
+
super()
|
|
13
|
+
|
|
14
|
+
@pattern = pattern
|
|
15
|
+
@matcher = negated ? ~matcher : matcher
|
|
16
|
+
@original_matcher = matcher
|
|
17
|
+
@negated = negated
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def negate
|
|
21
|
+
RegexpMatcher.new(@pattern, @original_matcher, negated: !@negated)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def validate(state)
|
|
25
|
+
unless state.actual.is_a?(String)
|
|
26
|
+
state.errors << state.expected.kind_of(String) unless @negated
|
|
27
|
+
return
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
if @original_matcher == AlwaysMatcher.instance
|
|
31
|
+
state.errors << state.expected.not_if(@negated).matching(@pattern) if
|
|
32
|
+
@pattern.match?(state.actual) == @negated
|
|
33
|
+
else
|
|
34
|
+
match = @pattern.match(state.actual)
|
|
35
|
+
|
|
36
|
+
if match
|
|
37
|
+
state.errors[match_pattern_call] << yield(@matcher, match)
|
|
38
|
+
else
|
|
39
|
+
state.errors << state.expected.matching(@pattern) unless @negated
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def match_pattern_call
|
|
45
|
+
@match_pattern_call ||=
|
|
46
|
+
Call.new(Variable.actual, :match, [Constant.new(@pattern)])
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def to_s
|
|
50
|
+
if @original_matcher == AlwaysMatcher.instance
|
|
51
|
+
if @negated
|
|
52
|
+
"neg(#{@pattern.inspect})"
|
|
53
|
+
else
|
|
54
|
+
@pattern.inspect
|
|
55
|
+
end
|
|
56
|
+
else
|
|
57
|
+
"#{'~' if @negated}regexp(#{@pattern.inspect}, #{@original_matcher})"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
module MatcherDsl
|
|
63
|
+
##
|
|
64
|
+
# Matches regular expression and passes MatchData to matcher
|
|
65
|
+
# @example
|
|
66
|
+
# # matches "x=5" but not "y=5" or "x=20"
|
|
67
|
+
# regexp(/x=(\d+)/, project(_[1].to_i => 0..10))
|
|
68
|
+
# # alternatively:
|
|
69
|
+
# regexp(/x=(\d+)/) ^ project(_[1].to_i => 0..10)
|
|
70
|
+
# # without matcher passes if regular expression matches
|
|
71
|
+
# regexp(/x=\d+/)
|
|
72
|
+
# @overload regexp(pattern, matcher)
|
|
73
|
+
# @param pattern [Regexp]
|
|
74
|
+
# @param matcher [Base]
|
|
75
|
+
# @return [RegexpMatcher]
|
|
76
|
+
# @overload regexp(pattern)
|
|
77
|
+
# @param pattern [Regexp]
|
|
78
|
+
# @return [OptionalChain<RegexpMatcher>]
|
|
79
|
+
def regexp(pattern, matcher = UNDEFINED)
|
|
80
|
+
return Chain.new { regexp(pattern, _1) }.optional if
|
|
81
|
+
Matcher.undefined?(matcher)
|
|
82
|
+
|
|
83
|
+
RegexpMatcher.new(pattern, matcher_of(matcher))
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|