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,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class ParseIso8601Matcher < Base
|
|
5
|
+
extend OnceBefore
|
|
6
|
+
|
|
7
|
+
def initialize(matcher, negated: false)
|
|
8
|
+
super()
|
|
9
|
+
|
|
10
|
+
@matcher = negated ? ~matcher : matcher
|
|
11
|
+
@original_matcher = matcher
|
|
12
|
+
@negated = negated
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
once_before :initialize do
|
|
16
|
+
require 'time'
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def negate
|
|
20
|
+
ParseIso8601Matcher.new(@original_matcher, negated: !@negated)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def validate(state, &)
|
|
24
|
+
actual = state.actual
|
|
25
|
+
|
|
26
|
+
unless actual.is_a?(String)
|
|
27
|
+
state.errors << state.expected.kind_of(String) unless @negated
|
|
28
|
+
return
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
value = Time.iso8601(actual)
|
|
32
|
+
|
|
33
|
+
if @matcher.is_a?(NeverMatcher)
|
|
34
|
+
state.errors << state.expected.not.valid_format(:iso8601)
|
|
35
|
+
return
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
result = yield(@matcher, value)
|
|
39
|
+
|
|
40
|
+
return if result.valid?
|
|
41
|
+
|
|
42
|
+
time_of = Call.new(Constant.new(Time), :iso8601, [Variable.actual])
|
|
43
|
+
state.errors[time_of] << result
|
|
44
|
+
rescue ArgumentError
|
|
45
|
+
state.errors << state.expected.valid_format(:iso8601) unless @negated
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def to_s
|
|
49
|
+
prefix = @negated ? '~' : ''
|
|
50
|
+
|
|
51
|
+
return "#{prefix}iso8601_format" if @original_matcher.is_a?(AlwaysMatcher)
|
|
52
|
+
|
|
53
|
+
"#{prefix}parse_iso8601(#{@original_matcher})"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
module MatcherBuilding
|
|
58
|
+
##
|
|
59
|
+
# Parses ISO 8601 time and matches with given matcher
|
|
60
|
+
# @example
|
|
61
|
+
# # matches "1999-12-31T23:59:00+01:00"
|
|
62
|
+
# parse_iso8601(_ < expr { Time.now })
|
|
63
|
+
# # alternatively:
|
|
64
|
+
# parse_iso8601 ^ (_ < expr { Time.now })
|
|
65
|
+
# # without matcher matches any valid ISO 8601 string
|
|
66
|
+
# parse_iso8601
|
|
67
|
+
# @overload parse_iso8601(matcher)
|
|
68
|
+
# @param matcher [Base]
|
|
69
|
+
# @return [ParseIso8601Matcher]
|
|
70
|
+
# @overload parse_iso8601
|
|
71
|
+
# @return [OptionalChain<ParseIso8601Matcher>]
|
|
72
|
+
# @see #iso8601_format
|
|
73
|
+
def parse_iso8601(matcher = UNDEFINED)
|
|
74
|
+
return Chain.new { parse_iso8601(_1) }.optional if
|
|
75
|
+
Matcher.undefined?(matcher)
|
|
76
|
+
|
|
77
|
+
matcher = matcher_of(matcher)
|
|
78
|
+
|
|
79
|
+
ParseIso8601Matcher.new(matcher)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
##
|
|
83
|
+
# Matches valid ISO 8601 strings
|
|
84
|
+
# @example
|
|
85
|
+
# # matches { timestamp: "2025-11-16T21:13:33+01:00" }
|
|
86
|
+
# { timestamp: iso8601_format }
|
|
87
|
+
# @return [ParseIso8601Matcher]
|
|
88
|
+
def iso8601_format
|
|
89
|
+
@iso8601_format ||= ParseIso8601Matcher.new(AlwaysMatcher.instance)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class ParseJsonMatcher < Base
|
|
5
|
+
extend OnceBefore
|
|
6
|
+
|
|
7
|
+
def initialize(matcher, json_options: nil, negated: false)
|
|
8
|
+
super()
|
|
9
|
+
|
|
10
|
+
@matcher = negated ? ~matcher : matcher
|
|
11
|
+
@original_matcher = matcher
|
|
12
|
+
@json_options = json_options
|
|
13
|
+
@negated = negated
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
once_before :initialize do
|
|
17
|
+
require 'json'
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def negate
|
|
21
|
+
ParseJsonMatcher.new(@original_matcher, json_options: @json_options, negated: !@negated)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def validate(state)
|
|
25
|
+
actual = state.actual
|
|
26
|
+
|
|
27
|
+
unless actual.is_a?(String)
|
|
28
|
+
state.errors << state.expected.kind_of(String) unless @negated
|
|
29
|
+
return
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
value = JSON.parse(actual, **@json_options)
|
|
33
|
+
|
|
34
|
+
if @matcher.is_a?(NeverMatcher)
|
|
35
|
+
state.errors << state.expected.not.valid_format(:json)
|
|
36
|
+
return
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
result = yield(@matcher, value)
|
|
40
|
+
|
|
41
|
+
return if result.valid?
|
|
42
|
+
|
|
43
|
+
parse_json = Call.new(Constant.new(JSON), :parse, [Variable.actual])
|
|
44
|
+
state.errors[parse_json] << result
|
|
45
|
+
rescue JSON::ParserError
|
|
46
|
+
state.errors << state.expected.valid_format(:json) unless @negated
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def to_s
|
|
50
|
+
prefix = @negated ? '~' : ''
|
|
51
|
+
|
|
52
|
+
return "#{prefix}json_format" if @original_matcher.is_a?(AlwaysMatcher)
|
|
53
|
+
|
|
54
|
+
"#{prefix}parse_json(#{@original_matcher})"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
module MatcherBuilding
|
|
59
|
+
##
|
|
60
|
+
# Parses JSON and matches with given matcher
|
|
61
|
+
# @example
|
|
62
|
+
# # matches '{"foo":42}'
|
|
63
|
+
# parse_json({ "foo" => Integer })
|
|
64
|
+
# # alternatively:
|
|
65
|
+
# parse_json ^ { "foo" => Integer }
|
|
66
|
+
# # without matcher matches any valid JSON string
|
|
67
|
+
# parse_json
|
|
68
|
+
# @overload parse_json(matcher, **json_options)
|
|
69
|
+
# @param matcher [Base]
|
|
70
|
+
# @return [ParseJsonMatcher]
|
|
71
|
+
# @overload parse_json(**json_options)
|
|
72
|
+
# @return [OptionalChain<ParseJsonMatcher>]
|
|
73
|
+
# @see #json_format
|
|
74
|
+
def parse_json(matcher = UNDEFINED, **)
|
|
75
|
+
return Chain.new { parse_json(_1, **) }.optional if
|
|
76
|
+
Matcher.undefined?(matcher)
|
|
77
|
+
|
|
78
|
+
matcher = matcher_of(matcher)
|
|
79
|
+
json_options = {}.merge(**)
|
|
80
|
+
json_options = nil if json_options.empty?
|
|
81
|
+
|
|
82
|
+
ParseJsonMatcher.new(matcher, json_options:)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
##
|
|
86
|
+
# Matches valid JSON strings
|
|
87
|
+
# @example
|
|
88
|
+
# # matches { payload: '{"foo":42}' }
|
|
89
|
+
# { payload: json_format }
|
|
90
|
+
# @return [ParseJsonMatcher]
|
|
91
|
+
def json_format
|
|
92
|
+
@json_format ||= ParseJsonMatcher.new(AlwaysMatcher.instance)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
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 MatcherBuilding
|
|
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
|
+
raise 'cannot mix project(expression) ^ matcher and project(expression => matcher)' if
|
|
54
|
+
!Matcher.undefined?(expression) && !projections.empty?
|
|
55
|
+
|
|
56
|
+
return Chain.new { project(expression => _1) } unless Matcher.undefined?(expression)
|
|
57
|
+
|
|
58
|
+
project_matchers = projections.map do |e, m|
|
|
59
|
+
e = expression_of(e)
|
|
60
|
+
m = matcher_of(m)
|
|
61
|
+
|
|
62
|
+
ProjectMatcher.new(e, m)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
all(*project_matchers)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
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).raising(@expression, @rescue_exception, given)
|
|
36
|
+
rescue @rescue_exception => e
|
|
37
|
+
state.errors[rescue_last_error] << yield(@matcher, unwrap_exception(e))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def to_s
|
|
41
|
+
"#{'~' if @negated}raises(#{@expression}, #{@original_matcher})"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def rescue_last_error
|
|
47
|
+
@rescue_last_error ||= RescueLastErrorExpression.new(@expression)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def unwrap_exception(e)
|
|
51
|
+
case e
|
|
52
|
+
when CallError
|
|
53
|
+
e.cause
|
|
54
|
+
else
|
|
55
|
+
e
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
module MatcherBuilding
|
|
61
|
+
##
|
|
62
|
+
# Matches raised error
|
|
63
|
+
# @example
|
|
64
|
+
# # matches {} (because fetch raises KeyError)
|
|
65
|
+
# raises(_.fetch(:foo), KeyError)
|
|
66
|
+
# # alternatively:
|
|
67
|
+
# raises(_.fetch(:foo)) ^ KeyError
|
|
68
|
+
# # match error message
|
|
69
|
+
# raises(_.call, message: /something went wrong/)
|
|
70
|
+
# # pass block instead of expression
|
|
71
|
+
# raises(NoMethodError) { |x| x.foo }
|
|
72
|
+
# # rescue non-standard exceptions
|
|
73
|
+
# raises(_.call, rescue: Exception)
|
|
74
|
+
# @overload raises(expression, matcher, message: UNDEFINED, rescue: StandardError)
|
|
75
|
+
# @param expression [Expression]
|
|
76
|
+
# @param matcher [Base] matcher for error
|
|
77
|
+
# @param message [Base] matcher for message
|
|
78
|
+
# @param rescue [Class] exception class to rescue
|
|
79
|
+
# @return [RaisesMatcher]
|
|
80
|
+
# @overload raises(matcher, message: UNDEFINED, rescue: StandardError)
|
|
81
|
+
# @param matcher [Base] matcher for error
|
|
82
|
+
# @param message [Base] matcher for message
|
|
83
|
+
# @param rescue [Class] exception class to rescue
|
|
84
|
+
# @yield actual
|
|
85
|
+
# @return [OptionalChain<RaisesMatcher>]
|
|
86
|
+
def raises(
|
|
87
|
+
expression_or_matcher = UNDEFINED,
|
|
88
|
+
matcher = UNDEFINED,
|
|
89
|
+
message: UNDEFINED,
|
|
90
|
+
rescue: StandardError,
|
|
91
|
+
&block
|
|
92
|
+
)
|
|
93
|
+
no_arg1 = Matcher.undefined?(expression_or_matcher)
|
|
94
|
+
no_arg2 = Matcher.undefined?(matcher)
|
|
95
|
+
|
|
96
|
+
if no_arg1 == no_arg2 && no_arg1 ^ block_given?
|
|
97
|
+
raise ArgumentError, 'both expression and block given' unless no_arg1
|
|
98
|
+
|
|
99
|
+
raise ArgumentError, 'neither expression nor block given'
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
if block_given?
|
|
103
|
+
expression = ProcExpression.new(block)
|
|
104
|
+
matcher = expression_or_matcher
|
|
105
|
+
else
|
|
106
|
+
expression = expression_of(expression_or_matcher)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
return Chain.new { raises(expression, _1, message:, rescue:) }.optional if
|
|
110
|
+
Matcher.undefined?(matcher)
|
|
111
|
+
|
|
112
|
+
matcher = matcher_of(matcher)
|
|
113
|
+
|
|
114
|
+
unless Matcher.undefined?(message)
|
|
115
|
+
@raises_message_call ||= expression_of(Call.new(Variable.actual, :message))
|
|
116
|
+
message_matcher = matcher_of(message)
|
|
117
|
+
|
|
118
|
+
matcher &= ProjectMatcher.new(@raises_message_call, message_matcher)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
RaisesMatcher.new(expression, matcher, rescue_exception: { rescue: }[:rescue])
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
state.errors << state.expected.comparable_to(@range.begin) unless @negated
|
|
27
|
+
return
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
return if @range.include?(state.actual) ^ @negated
|
|
31
|
+
|
|
32
|
+
state.errors << state.expected.not_if(@negated).between(
|
|
33
|
+
@range.begin,
|
|
34
|
+
@range.end,
|
|
35
|
+
exclude_end: @range.exclude_end?,
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def to_s
|
|
40
|
+
if @negated
|
|
41
|
+
"neg(#{@range})"
|
|
42
|
+
else
|
|
43
|
+
@range.to_s
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class ReferenceMatcher < Base
|
|
5
|
+
Settings = Struct.new(:target, :cache)
|
|
6
|
+
|
|
7
|
+
def initialize(key, settings, cyclic: nil, negated: false, session_key: object_id)
|
|
8
|
+
super()
|
|
9
|
+
|
|
10
|
+
@key = key
|
|
11
|
+
@settings = settings
|
|
12
|
+
@cyclic = cyclic
|
|
13
|
+
@negated = negated
|
|
14
|
+
@session_key = session_key
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def negate
|
|
18
|
+
ReferenceMatcher.new(@key, @settings, cyclic: @cyclic, negated: !@negated, session_key: @session_key)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def validate(state)
|
|
22
|
+
actual = state.actual
|
|
23
|
+
sess = class_session
|
|
24
|
+
depth = sess[:depth]
|
|
25
|
+
depth = depth ? depth + 1 : 1
|
|
26
|
+
sess[:depth] = depth
|
|
27
|
+
|
|
28
|
+
if depth > Matcher.max_reference_depth
|
|
29
|
+
state.errors << "match level too deep: #{depth}"
|
|
30
|
+
return
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
unless visited.add?(actual.object_id)
|
|
34
|
+
state.errors << state.report.namespace(:reference).cyclic if @negated == @cyclic
|
|
35
|
+
|
|
36
|
+
return
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
unless cache?
|
|
40
|
+
state.errors << yield(target)
|
|
41
|
+
return
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
cache = (sess[:cache] ||= {})
|
|
45
|
+
cache_key = [@key, actual.object_id]
|
|
46
|
+
cached_result = cache[cache_key]
|
|
47
|
+
|
|
48
|
+
if cached_result.nil?
|
|
49
|
+
# If @cyclic then call #match instead of yield. We disallow passing
|
|
50
|
+
# previous values for cyclic reference matchers. #match will create a
|
|
51
|
+
# new values stack.
|
|
52
|
+
target_errors = @cyclic ? target.match(actual) : yield(target)
|
|
53
|
+
cache[cache_key] = @negated ^ target_errors.valid?
|
|
54
|
+
|
|
55
|
+
state.errors << target_errors
|
|
56
|
+
elsif @negated == cached_result
|
|
57
|
+
state.errors << state.report.namespace(:reference).failed_from_cache
|
|
58
|
+
end
|
|
59
|
+
ensure
|
|
60
|
+
sess[:depth] = depth - 1
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def to_s
|
|
64
|
+
"#{'~' if @negated}refs[#{@key.inspect}]"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def cache?
|
|
70
|
+
return @cache if defined? @cache
|
|
71
|
+
|
|
72
|
+
@cache = @settings[@key].cache
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def visited
|
|
76
|
+
session(@session_key)[:visited] ||= Set.new
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def target
|
|
80
|
+
return @target if defined? @target
|
|
81
|
+
|
|
82
|
+
pair = @settings[@key].target
|
|
83
|
+
|
|
84
|
+
raise "No target for #{@key.inspect}" unless pair
|
|
85
|
+
|
|
86
|
+
@target = if @negated
|
|
87
|
+
pair[1] ||= ~pair[0]
|
|
88
|
+
else
|
|
89
|
+
pair[0]
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
module MatcherBuilding
|
|
95
|
+
def refs?
|
|
96
|
+
!@refs.nil?
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
##
|
|
100
|
+
# Returns the reference matcher collection for recursive matchers
|
|
101
|
+
# @example
|
|
102
|
+
# refs[:list] = {
|
|
103
|
+
# head: Integer,
|
|
104
|
+
# tail: optional(refs[:list]),
|
|
105
|
+
# }
|
|
106
|
+
# @return [ReferenceMatcherCollection]
|
|
107
|
+
def refs
|
|
108
|
+
@refs ||= ReferenceMatcherCollection.new(self)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
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(cache: true) { |_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,84 @@
|
|
|
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 MatcherBuilding
|
|
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
|
+
# @overload regexp(pattern, matcher)
|
|
71
|
+
# @param pattern [Regexp]
|
|
72
|
+
# @param matcher [Base]
|
|
73
|
+
# @return [RegexpMatcher]
|
|
74
|
+
# @overload regexp(pattern)
|
|
75
|
+
# @param pattern [Regexp]
|
|
76
|
+
# @return [OptionalChain<RegexpMatcher>]
|
|
77
|
+
def regexp(pattern, matcher = UNDEFINED)
|
|
78
|
+
return Chain.new { regexp(pattern, _1) }.optional if
|
|
79
|
+
Matcher.undefined?(matcher)
|
|
80
|
+
|
|
81
|
+
RegexpMatcher.new(pattern, matcher_of(matcher))
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|