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,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class NegatedEachPairMatcher < Base
|
|
5
|
+
def initialize(matcher)
|
|
6
|
+
super()
|
|
7
|
+
|
|
8
|
+
@matcher = matcher
|
|
9
|
+
@neg_matcher = ~matcher
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def negate
|
|
13
|
+
EachPairMatcher.new(@matcher)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def validate(state)
|
|
17
|
+
actual = state.actual
|
|
18
|
+
|
|
19
|
+
return unless actual.respond_to?(:each_pair)
|
|
20
|
+
|
|
21
|
+
collector = state.new_collector.or!
|
|
22
|
+
|
|
23
|
+
actual.each do |key, value|
|
|
24
|
+
result = yield(@neg_matcher, [key, value], key:, value:, parent: actual)
|
|
25
|
+
|
|
26
|
+
return nil if result.valid?
|
|
27
|
+
|
|
28
|
+
collector[key] << result
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
state.errors << collector.error
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def to_s
|
|
35
|
+
"~each_pair(#{@matcher})"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class NegatedImplySomeMatcher < Base
|
|
5
|
+
def initialize(matchers, else_matcher, count)
|
|
6
|
+
ImplySomeMatcher.check(matchers, else_matcher, count)
|
|
7
|
+
|
|
8
|
+
super()
|
|
9
|
+
|
|
10
|
+
@matchers = matchers
|
|
11
|
+
@neg_matchers = matchers.map(&:~)
|
|
12
|
+
@count = count
|
|
13
|
+
@else_matcher = else_matcher
|
|
14
|
+
@neg_else_matcher = @else_matcher&.~
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def negate
|
|
18
|
+
ImplySomeMatcher.new(@matchers, @else_matcher, @count)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def validate(state)
|
|
22
|
+
matchers = @neg_matchers.filter { yield(_1.condition).valid? }
|
|
23
|
+
|
|
24
|
+
if matchers.empty?
|
|
25
|
+
state.errors << yield(@neg_else_matcher) if @else_matcher
|
|
26
|
+
elsif @count == :any || matchers.length == @count
|
|
27
|
+
state.errors.or!
|
|
28
|
+
|
|
29
|
+
matchers.each do |matcher|
|
|
30
|
+
error = yield(matcher)
|
|
31
|
+
|
|
32
|
+
if error.valid?
|
|
33
|
+
state.errors.clear
|
|
34
|
+
break
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
state.errors << error
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def to_s
|
|
43
|
+
"~#{self.~}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class NegatedMatcher < Base
|
|
5
|
+
def initialize(matcher)
|
|
6
|
+
super()
|
|
7
|
+
|
|
8
|
+
@matcher = matcher
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def negate
|
|
12
|
+
@matcher
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def validate(state)
|
|
16
|
+
return unless yield(@matcher).valid?
|
|
17
|
+
|
|
18
|
+
state.errors << state.expected.namespace(:negated).not.valid(@matcher)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_s
|
|
22
|
+
"neg(#{@matcher})"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class NegatedProjectMatcher < Base
|
|
5
|
+
def initialize(expression, matcher)
|
|
6
|
+
super()
|
|
7
|
+
|
|
8
|
+
@expression = expression
|
|
9
|
+
@matcher = matcher
|
|
10
|
+
@neg_matcher = ~matcher
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def negate
|
|
14
|
+
ProjectMatcher.new(@expression, @matcher)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def validate(state)
|
|
18
|
+
begin
|
|
19
|
+
result = @expression.evaluate(state.values)
|
|
20
|
+
rescue CallError
|
|
21
|
+
return
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
state.errors[@expression] << yield(@neg_matcher, result)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def to_s
|
|
28
|
+
"~project(#{@expression} => #{@matcher})"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class NeverMatcher < Base
|
|
5
|
+
include Singleton
|
|
6
|
+
|
|
7
|
+
def ~
|
|
8
|
+
AlwaysMatcher.instance
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def validate(state)
|
|
12
|
+
state.errors << state.report.exist
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def to_s
|
|
16
|
+
"never"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
module MatcherDsl
|
|
21
|
+
##
|
|
22
|
+
# Never matches. Opposite of {#always}
|
|
23
|
+
#
|
|
24
|
+
# Where is the use-case for +never+? Can't think of one other than it's
|
|
25
|
+
# +~always+, and we really want to be able to negate matchers. Some matchers
|
|
26
|
+
# check whether their child matcher is a NeverMatcher to provide a fitting
|
|
27
|
+
# error message.
|
|
28
|
+
#
|
|
29
|
+
# @return [NeverMatcher]
|
|
30
|
+
# @see #always
|
|
31
|
+
def never
|
|
32
|
+
NeverMatcher.instance
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class OneMatcher < Base
|
|
5
|
+
def initialize(matchers, negated: false)
|
|
6
|
+
super()
|
|
7
|
+
|
|
8
|
+
@matchers = matchers
|
|
9
|
+
@negated = negated
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def negate
|
|
13
|
+
OneMatcher.new(@matchers, negated: !@negated)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def validate(state)
|
|
17
|
+
valid_matchers = []
|
|
18
|
+
invalid_errors = []
|
|
19
|
+
|
|
20
|
+
@matchers.each do |matcher|
|
|
21
|
+
error = yield matcher
|
|
22
|
+
|
|
23
|
+
if error.valid?
|
|
24
|
+
valid_matchers << matcher
|
|
25
|
+
else
|
|
26
|
+
invalid_errors << error
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
if @negated
|
|
31
|
+
state.errors << yield(~valid_matchers[0]) if valid_matchers.length == 1
|
|
32
|
+
elsif valid_matchers.length == 0
|
|
33
|
+
state.errors << OrError.from(invalid_errors)
|
|
34
|
+
elsif valid_matchers.length > 1
|
|
35
|
+
negated_matchers = valid_matchers.map(&:~)
|
|
36
|
+
any_matcher = AnyMatcher.new(negated_matchers)
|
|
37
|
+
|
|
38
|
+
state.errors << yield(any_matcher)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def to_s
|
|
43
|
+
"#{'~' if @negated}one(#{@matchers.join(', ')})"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
module MatcherDsl
|
|
48
|
+
##
|
|
49
|
+
# Matches exactly one matcher
|
|
50
|
+
# @example
|
|
51
|
+
# # matches [1] and [2] but not [] or [1, 2]
|
|
52
|
+
# one(_.include?(1), _.include?(2))
|
|
53
|
+
# @param matchers [Array<Base>]
|
|
54
|
+
# @return [OneMatcher]
|
|
55
|
+
def one(*matchers)
|
|
56
|
+
matchers = matchers.map { matcher_of(_1) }
|
|
57
|
+
|
|
58
|
+
case matchers.count
|
|
59
|
+
when 0
|
|
60
|
+
NeverMatcher.instance
|
|
61
|
+
when 1
|
|
62
|
+
matchers[0]
|
|
63
|
+
else
|
|
64
|
+
OneMatcher.new(matchers)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class OptionalMatcher < Base
|
|
5
|
+
def self.cache(matcher, matcher_cache = MatcherCache.current)
|
|
6
|
+
return new(matcher) unless matcher_cache
|
|
7
|
+
|
|
8
|
+
cache = (matcher_cache.optional_matchers ||= {}.compare_by_identity)
|
|
9
|
+
cache[matcher] ||= new(matcher)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(matcher, negated: false)
|
|
13
|
+
super()
|
|
14
|
+
|
|
15
|
+
@matcher = negated ? ~matcher : matcher
|
|
16
|
+
@original_matcher = matcher
|
|
17
|
+
@negated = negated
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def negate
|
|
21
|
+
OptionalMatcher.new(@original_matcher, negated: !@negated)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def validate(state)
|
|
25
|
+
if state.actual.nil?
|
|
26
|
+
state.errors << state.expected.not.equal(nil) if @negated
|
|
27
|
+
else
|
|
28
|
+
state.errors << yield(@matcher)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def to_s
|
|
33
|
+
"#{'~' if @negated}optional(#{@original_matcher})"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# see Optional for optional helper
|
|
38
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class ParseFloatMatcher < Base
|
|
5
|
+
def initialize(matcher, negated: false)
|
|
6
|
+
super()
|
|
7
|
+
|
|
8
|
+
@matcher = negated ? ~matcher : matcher
|
|
9
|
+
@original_matcher = matcher
|
|
10
|
+
@negated = negated
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def negate
|
|
14
|
+
ParseFloatMatcher.new(@original_matcher, negated: !@negated)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def validate(state, &)
|
|
18
|
+
actual = state.actual
|
|
19
|
+
|
|
20
|
+
unless actual.is_a?(String)
|
|
21
|
+
state.errors << state.expected.kind_of(String) unless @negated
|
|
22
|
+
return
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
value = Float(actual)
|
|
26
|
+
|
|
27
|
+
if @matcher.is_a?(NeverMatcher)
|
|
28
|
+
state.errors << state.expected.not.valid_format(:float)
|
|
29
|
+
return
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
result = yield(@matcher, value)
|
|
33
|
+
|
|
34
|
+
return if result.valid?
|
|
35
|
+
|
|
36
|
+
float_of = Call.new(Constant.new(Kernel), :Float, [Variable.actual])
|
|
37
|
+
state.errors[float_of] << result
|
|
38
|
+
rescue ArgumentError
|
|
39
|
+
state.errors << state.expected.valid_format(:float) unless @negated
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def to_s
|
|
43
|
+
prefix = @negated ? "~" : ""
|
|
44
|
+
|
|
45
|
+
return "#{prefix}float_format" if @original_matcher.is_a?(AlwaysMatcher)
|
|
46
|
+
|
|
47
|
+
"#{prefix}parse_float(#{@original_matcher})"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
module MatcherDsl
|
|
52
|
+
##
|
|
53
|
+
# Parses float and matches with given matcher.
|
|
54
|
+
# @example
|
|
55
|
+
# # matches "1.0"
|
|
56
|
+
# parse_float(_ > 0.0)
|
|
57
|
+
# # alternatively:
|
|
58
|
+
# parse_float ^ (_ > 0.0)
|
|
59
|
+
# # without matcher matches any float string
|
|
60
|
+
# parse_float
|
|
61
|
+
# @overload parse_float(matcher)
|
|
62
|
+
# @param matcher [Base]
|
|
63
|
+
# @return [ParseFloatMatcher]
|
|
64
|
+
# @overload parse_float
|
|
65
|
+
# @return [OptionalChain<ParseFloatMatcher>]
|
|
66
|
+
# @see #float_format
|
|
67
|
+
def parse_float(matcher = UNDEFINED)
|
|
68
|
+
return Chain.new { parse_float(_1) }.optional if
|
|
69
|
+
Matcher.undefined?(matcher)
|
|
70
|
+
|
|
71
|
+
matcher = matcher_of(matcher)
|
|
72
|
+
|
|
73
|
+
ParseFloatMatcher.new(matcher)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
##
|
|
77
|
+
# Matches float strings
|
|
78
|
+
# @example
|
|
79
|
+
# # matches { payload: "1.5" }
|
|
80
|
+
# { payload: float_format }
|
|
81
|
+
# @return [ParseFloatMatcher]
|
|
82
|
+
def float_format
|
|
83
|
+
@float_format ||= ParseFloatMatcher.new(AlwaysMatcher.instance)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class ParseIntegerMatcher < Base
|
|
5
|
+
def initialize(matcher, base: 0, negated: false)
|
|
6
|
+
super()
|
|
7
|
+
|
|
8
|
+
@matcher = negated ? ~matcher : matcher
|
|
9
|
+
@original_matcher = matcher
|
|
10
|
+
@base = base
|
|
11
|
+
@negated = negated
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def negate
|
|
15
|
+
ParseIntegerMatcher.new(
|
|
16
|
+
@original_matcher, base: @base, negated: !@negated
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def validate(state, &)
|
|
21
|
+
actual = state.actual
|
|
22
|
+
|
|
23
|
+
unless actual.is_a?(String)
|
|
24
|
+
state.errors << state.expected.kind_of(String) unless @negated
|
|
25
|
+
return
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
value = Integer(actual)
|
|
29
|
+
|
|
30
|
+
if @matcher.is_a?(NeverMatcher)
|
|
31
|
+
state.errors << state.expected.not.valid_format(:integer)
|
|
32
|
+
return
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
result = yield(@matcher, value)
|
|
36
|
+
|
|
37
|
+
return if result.valid?
|
|
38
|
+
|
|
39
|
+
integer_of = Call.new(Constant.new(Kernel), :Integer, [Variable.actual])
|
|
40
|
+
state.errors[integer_of] << result
|
|
41
|
+
rescue ArgumentError
|
|
42
|
+
state.errors << state.expected.valid_format(:integer) unless @negated
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def to_s
|
|
46
|
+
prefix = @negated ? "~" : ""
|
|
47
|
+
|
|
48
|
+
if @original_matcher.is_a?(AlwaysMatcher)
|
|
49
|
+
args = @base == 0 ? "" : "(base: #{@base})"
|
|
50
|
+
return "#{prefix}integer_format#{args}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
base_arg = @base == 0 ? "" : ", base: #{@base}"
|
|
54
|
+
|
|
55
|
+
"#{'~' if @negated}parse_integer(#{@original_matcher}#{base_arg})"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
module MatcherDsl
|
|
60
|
+
##
|
|
61
|
+
# Parses integer and matches with given matcher
|
|
62
|
+
# @example
|
|
63
|
+
# # matches "7"
|
|
64
|
+
# parse_integer(_.odd?)
|
|
65
|
+
# # alternatively:
|
|
66
|
+
# parse_integer ^ (_.odd?)
|
|
67
|
+
# # without matcher matches any integer string
|
|
68
|
+
# parse_integer
|
|
69
|
+
# @overload parse_integer(matcher, base: 0)
|
|
70
|
+
# @param matcher [Base]
|
|
71
|
+
# @param base [Integer]
|
|
72
|
+
# @return [ParseIntegerMatcher]
|
|
73
|
+
# @overload parse_integer(base: 0)
|
|
74
|
+
# @return [OptionalChain<ParseIntegerMatcher>]
|
|
75
|
+
# @see #integer_format
|
|
76
|
+
def parse_integer(matcher = UNDEFINED, base: 0)
|
|
77
|
+
return Chain.new { parse_integer(_1, base:) }.optional if
|
|
78
|
+
Matcher.undefined?(matcher)
|
|
79
|
+
|
|
80
|
+
matcher = matcher_of(matcher)
|
|
81
|
+
|
|
82
|
+
ParseIntegerMatcher.new(matcher, base:)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
##
|
|
86
|
+
# Matches integer strings
|
|
87
|
+
# @example
|
|
88
|
+
# # matches { payload: "42" }
|
|
89
|
+
# { payload: integer_format }
|
|
90
|
+
# @param base [Integer]
|
|
91
|
+
# @return [ParseIntegerMatcher]
|
|
92
|
+
def integer_format(base: 0)
|
|
93
|
+
if base == 0
|
|
94
|
+
@integer_format ||=
|
|
95
|
+
ParseIntegerMatcher.new(AlwaysMatcher.instance, base:)
|
|
96
|
+
else
|
|
97
|
+
ParseIntegerMatcher.new(AlwaysMatcher.instance, base:)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
autoload :ParseIso8601Matcher, "matcher/matchers/parse_iso8601_matcher"
|
|
5
|
+
|
|
6
|
+
module MatcherDsl
|
|
7
|
+
##
|
|
8
|
+
# Parses ISO 8601 time and matches with given matcher
|
|
9
|
+
# @example
|
|
10
|
+
# # matches "1999-12-31T23:59:00+01:00"
|
|
11
|
+
# parse_iso8601(_ < expr { Time.now })
|
|
12
|
+
# # alternatively:
|
|
13
|
+
# parse_iso8601 ^ (_ < expr { Time.now })
|
|
14
|
+
# # without matcher matches any valid ISO 8601 string
|
|
15
|
+
# parse_iso8601
|
|
16
|
+
# @overload parse_iso8601(matcher)
|
|
17
|
+
# @param matcher [Base]
|
|
18
|
+
# @return [ParseIso8601Matcher]
|
|
19
|
+
# @overload parse_iso8601
|
|
20
|
+
# @return [OptionalChain<ParseIso8601Matcher>]
|
|
21
|
+
# @see #iso8601_format
|
|
22
|
+
def parse_iso8601(matcher = UNDEFINED)
|
|
23
|
+
return Chain.new { parse_iso8601(_1) }.optional if
|
|
24
|
+
Matcher.undefined?(matcher)
|
|
25
|
+
|
|
26
|
+
matcher = matcher_of(matcher)
|
|
27
|
+
|
|
28
|
+
ParseIso8601Matcher.new(matcher)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
##
|
|
32
|
+
# Matches valid ISO 8601 strings
|
|
33
|
+
# @example
|
|
34
|
+
# # matches { timestamp: "2025-11-16T21:13:33+01:00" }
|
|
35
|
+
# { timestamp: iso8601_format }
|
|
36
|
+
# @return [ParseIso8601Matcher]
|
|
37
|
+
def iso8601_format
|
|
38
|
+
@iso8601_format ||= ParseIso8601Matcher.new(AlwaysMatcher.instance)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Matcher
|
|
6
|
+
class ParseIso8601Matcher < Base
|
|
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
|
+
def negate
|
|
16
|
+
ParseIso8601Matcher.new(@original_matcher, negated: !@negated)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def validate(state, &)
|
|
20
|
+
actual = state.actual
|
|
21
|
+
|
|
22
|
+
unless actual.is_a?(String)
|
|
23
|
+
state.errors << state.expected.kind_of(String) unless @negated
|
|
24
|
+
return
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
value = Time.iso8601(actual)
|
|
28
|
+
|
|
29
|
+
if @matcher.is_a?(NeverMatcher)
|
|
30
|
+
state.errors << state.expected.not.valid_format(:iso8601)
|
|
31
|
+
return
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
result = yield(@matcher, value)
|
|
35
|
+
|
|
36
|
+
return if result.valid?
|
|
37
|
+
|
|
38
|
+
time_of = Call.new(Constant.new(Time), :iso8601, [Variable.actual])
|
|
39
|
+
state.errors[time_of] << result
|
|
40
|
+
rescue ArgumentError
|
|
41
|
+
state.errors << state.expected.valid_format(:iso8601) unless @negated
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def to_s
|
|
45
|
+
prefix = @negated ? "~" : ""
|
|
46
|
+
|
|
47
|
+
return "#{prefix}iso8601_format" if @original_matcher.is_a?(AlwaysMatcher)
|
|
48
|
+
|
|
49
|
+
"#{prefix}parse_iso8601(#{@original_matcher})"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
autoload :ParseJsonMatcher, "matcher/matchers/parse_json_matcher"
|
|
5
|
+
|
|
6
|
+
module MatcherDsl
|
|
7
|
+
##
|
|
8
|
+
# Parses JSON and matches with given matcher
|
|
9
|
+
# @example
|
|
10
|
+
# # matches '{"foo":42}'
|
|
11
|
+
# parse_json({ "foo" => Integer })
|
|
12
|
+
# # alternatively:
|
|
13
|
+
# parse_json ^ { "foo" => Integer }
|
|
14
|
+
# # without matcher matches any valid JSON string
|
|
15
|
+
# parse_json
|
|
16
|
+
# @overload parse_json(matcher, **json_options)
|
|
17
|
+
# @param matcher [Base]
|
|
18
|
+
# @return [ParseJsonMatcher]
|
|
19
|
+
# @overload parse_json(**json_options)
|
|
20
|
+
# @return [OptionalChain<ParseJsonMatcher>]
|
|
21
|
+
# @see #json_format
|
|
22
|
+
def parse_json(matcher = UNDEFINED, **)
|
|
23
|
+
return Chain.new { parse_json(_1, **) }.optional if
|
|
24
|
+
Matcher.undefined?(matcher)
|
|
25
|
+
|
|
26
|
+
matcher = matcher_of(matcher)
|
|
27
|
+
json_options = {}.merge(**)
|
|
28
|
+
json_options = Compatibility::NULL_KWARGS if json_options.empty?
|
|
29
|
+
|
|
30
|
+
ParseJsonMatcher.new(matcher, json_options:)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
##
|
|
34
|
+
# Matches valid JSON strings
|
|
35
|
+
# @example
|
|
36
|
+
# # matches { payload: '{"foo":42}' }
|
|
37
|
+
# { payload: json_format }
|
|
38
|
+
# @return [ParseJsonMatcher]
|
|
39
|
+
def json_format
|
|
40
|
+
@json_format ||= ParseJsonMatcher.new(AlwaysMatcher.instance)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Matcher
|
|
6
|
+
class ParseJsonMatcher < Base
|
|
7
|
+
def initialize(
|
|
8
|
+
matcher,
|
|
9
|
+
json_options: Compatibility::NULL_KWARGS,
|
|
10
|
+
negated: false
|
|
11
|
+
)
|
|
12
|
+
super()
|
|
13
|
+
|
|
14
|
+
@matcher = negated ? ~matcher : matcher
|
|
15
|
+
@original_matcher = matcher
|
|
16
|
+
@json_options = json_options
|
|
17
|
+
@negated = negated
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def negate
|
|
21
|
+
ParseJsonMatcher.new(
|
|
22
|
+
@original_matcher, json_options: @json_options, negated: !@negated
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def validate(state)
|
|
27
|
+
actual = state.actual
|
|
28
|
+
|
|
29
|
+
unless actual.is_a?(String)
|
|
30
|
+
state.errors << state.expected.kind_of(String) unless @negated
|
|
31
|
+
return
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
value = JSON.parse(actual, **@json_options)
|
|
35
|
+
|
|
36
|
+
if @matcher.is_a?(NeverMatcher)
|
|
37
|
+
state.errors << state.expected.not.valid_format(:json)
|
|
38
|
+
return
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
result = yield(@matcher, value)
|
|
42
|
+
|
|
43
|
+
return if result.valid?
|
|
44
|
+
|
|
45
|
+
parse_json = Call.new(Constant.new(JSON), :parse, [Variable.actual])
|
|
46
|
+
state.errors[parse_json] << result
|
|
47
|
+
rescue JSON::ParserError
|
|
48
|
+
state.errors << state.expected.valid_format(:json) unless @negated
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def to_s
|
|
52
|
+
prefix = @negated ? "~" : ""
|
|
53
|
+
|
|
54
|
+
return "#{prefix}json_format" if @original_matcher.is_a?(AlwaysMatcher)
|
|
55
|
+
|
|
56
|
+
"#{prefix}parse_json(#{@original_matcher})"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|