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.
Files changed (127) hide show
  1. checksums.yaml +7 -0
  2. data/lib/matcher/assertions.rb +19 -0
  3. data/lib/matcher/autoload.rb +5 -0
  4. data/lib/matcher/base.rb +183 -0
  5. data/lib/matcher/compatibility.rb +34 -0
  6. data/lib/matcher/debug.rb +62 -0
  7. data/lib/matcher/dsl/builder.rb +99 -0
  8. data/lib/matcher/dsl/chain.rb +84 -0
  9. data/lib/matcher/dsl/expression_dsl.rb +306 -0
  10. data/lib/matcher/dsl/matcher_dsl.rb +5 -0
  11. data/lib/matcher/dsl/optional.rb +82 -0
  12. data/lib/matcher/dsl/optional_chain.rb +24 -0
  13. data/lib/matcher/dsl/others.rb +28 -0
  14. data/lib/matcher/errors/and_error.rb +88 -0
  15. data/lib/matcher/errors/boolean_collector.rb +51 -0
  16. data/lib/matcher/errors/element_error.rb +24 -0
  17. data/lib/matcher/errors/empty_error.rb +23 -0
  18. data/lib/matcher/errors/error.rb +39 -0
  19. data/lib/matcher/errors/error_collector.rb +100 -0
  20. data/lib/matcher/errors/nested_error.rb +98 -0
  21. data/lib/matcher/errors/or_error.rb +88 -0
  22. data/lib/matcher/expression_cache.rb +57 -0
  23. data/lib/matcher/expression_labeler.rb +96 -0
  24. data/lib/matcher/expressions/array_expression.rb +45 -0
  25. data/lib/matcher/expressions/block.rb +189 -0
  26. data/lib/matcher/expressions/call.rb +307 -0
  27. data/lib/matcher/expressions/call_error.rb +45 -0
  28. data/lib/matcher/expressions/constant.rb +53 -0
  29. data/lib/matcher/expressions/expression.rb +237 -0
  30. data/lib/matcher/expressions/expression_walker.rb +77 -0
  31. data/lib/matcher/expressions/hash_expression.rb +59 -0
  32. data/lib/matcher/expressions/proc_expression.rb +96 -0
  33. data/lib/matcher/expressions/range_expression.rb +65 -0
  34. data/lib/matcher/expressions/recorder.rb +136 -0
  35. data/lib/matcher/expressions/rescue_last_error_expression.rb +49 -0
  36. data/lib/matcher/expressions/set_expression.rb +45 -0
  37. data/lib/matcher/expressions/string_expression.rb +53 -0
  38. data/lib/matcher/expressions/symbol_proc.rb +53 -0
  39. data/lib/matcher/expressions/variable.rb +87 -0
  40. data/lib/matcher/hash_stack.rb +52 -0
  41. data/lib/matcher/list.rb +102 -0
  42. data/lib/matcher/markers.rb +7 -0
  43. data/lib/matcher/matcher_cache.rb +18 -0
  44. data/lib/matcher/matchers/all_matcher.rb +60 -0
  45. data/lib/matcher/matchers/always_matcher.rb +34 -0
  46. data/lib/matcher/matchers/any_matcher.rb +70 -0
  47. data/lib/matcher/matchers/array_matcher.rb +72 -0
  48. data/lib/matcher/matchers/block_matcher.rb +61 -0
  49. data/lib/matcher/matchers/boolean_matcher.rb +37 -0
  50. data/lib/matcher/matchers/dig_matcher.rb +149 -0
  51. data/lib/matcher/matchers/each_matcher.rb +85 -0
  52. data/lib/matcher/matchers/each_pair_matcher.rb +119 -0
  53. data/lib/matcher/matchers/equal_matcher.rb +198 -0
  54. data/lib/matcher/matchers/equal_set_matcher.rb +112 -0
  55. data/lib/matcher/matchers/expression_matcher.rb +69 -0
  56. data/lib/matcher/matchers/filter_matcher.rb +115 -0
  57. data/lib/matcher/matchers/hash_matcher.rb +315 -0
  58. data/lib/matcher/matchers/imply_matcher.rb +83 -0
  59. data/lib/matcher/matchers/imply_some_matcher.rb +116 -0
  60. data/lib/matcher/matchers/index_by_matcher.rb +177 -0
  61. data/lib/matcher/matchers/inline_matcher.rb +101 -0
  62. data/lib/matcher/matchers/keys_matcher.rb +131 -0
  63. data/lib/matcher/matchers/kind_of_matcher.rb +35 -0
  64. data/lib/matcher/matchers/lazy_all_matcher.rb +69 -0
  65. data/lib/matcher/matchers/lazy_any_matcher.rb +69 -0
  66. data/lib/matcher/matchers/let_matcher.rb +73 -0
  67. data/lib/matcher/matchers/map_matcher.rb +148 -0
  68. data/lib/matcher/matchers/negated_array_matcher.rb +38 -0
  69. data/lib/matcher/matchers/negated_each_matcher.rb +36 -0
  70. data/lib/matcher/matchers/negated_each_pair_matcher.rb +38 -0
  71. data/lib/matcher/matchers/negated_imply_some_matcher.rb +46 -0
  72. data/lib/matcher/matchers/negated_matcher.rb +25 -0
  73. data/lib/matcher/matchers/negated_project_matcher.rb +31 -0
  74. data/lib/matcher/matchers/never_matcher.rb +35 -0
  75. data/lib/matcher/matchers/one_matcher.rb +68 -0
  76. data/lib/matcher/matchers/optional_matcher.rb +38 -0
  77. data/lib/matcher/matchers/parse_float_matcher.rb +86 -0
  78. data/lib/matcher/matchers/parse_integer_matcher.rb +101 -0
  79. data/lib/matcher/matchers/parse_iso8601_helper.rb +41 -0
  80. data/lib/matcher/matchers/parse_iso8601_matcher.rb +52 -0
  81. data/lib/matcher/matchers/parse_json_helper.rb +43 -0
  82. data/lib/matcher/matchers/parse_json_matcher.rb +59 -0
  83. data/lib/matcher/matchers/project_matcher.rb +72 -0
  84. data/lib/matcher/matchers/raises_matcher.rb +131 -0
  85. data/lib/matcher/matchers/range_matcher.rb +50 -0
  86. data/lib/matcher/matchers/reference_matcher.rb +213 -0
  87. data/lib/matcher/matchers/reference_matcher_collection.rb +57 -0
  88. data/lib/matcher/matchers/regexp_matcher.rb +86 -0
  89. data/lib/matcher/messages/expected_phrasing.rb +355 -0
  90. data/lib/matcher/messages/message.rb +104 -0
  91. data/lib/matcher/messages/message_builder.rb +35 -0
  92. data/lib/matcher/messages/message_rules.rb +240 -0
  93. data/lib/matcher/messages/namespaced_message_builder.rb +19 -0
  94. data/lib/matcher/messages/phrasing.rb +59 -0
  95. data/lib/matcher/messages/standard_message_builder.rb +105 -0
  96. data/lib/matcher/patterns/ast_mapping.rb +42 -0
  97. data/lib/matcher/patterns/capture_hole.rb +33 -0
  98. data/lib/matcher/patterns/constant_hole.rb +14 -0
  99. data/lib/matcher/patterns/hole.rb +30 -0
  100. data/lib/matcher/patterns/method_hole.rb +62 -0
  101. data/lib/matcher/patterns/pattern.rb +104 -0
  102. data/lib/matcher/patterns/pattern_building.rb +39 -0
  103. data/lib/matcher/patterns/pattern_capture.rb +11 -0
  104. data/lib/matcher/patterns/pattern_match.rb +29 -0
  105. data/lib/matcher/patterns/variable_hole.rb +14 -0
  106. data/lib/matcher/reporter.rb +103 -0
  107. data/lib/matcher/rules/message_factory.rb +26 -0
  108. data/lib/matcher/rules/message_rule.rb +18 -0
  109. data/lib/matcher/rules/message_rule_context.rb +26 -0
  110. data/lib/matcher/rules/rule_builder.rb +29 -0
  111. data/lib/matcher/rules/rule_set.rb +57 -0
  112. data/lib/matcher/rules/transform_builder.rb +24 -0
  113. data/lib/matcher/rules/transform_mapping.rb +5 -0
  114. data/lib/matcher/rules/transform_rule.rb +21 -0
  115. data/lib/matcher/state.rb +40 -0
  116. data/lib/matcher/testing/error_builder.rb +62 -0
  117. data/lib/matcher/testing/error_checker.rb +514 -0
  118. data/lib/matcher/testing/error_testing.rb +37 -0
  119. data/lib/matcher/testing/pattern_testing.rb +11 -0
  120. data/lib/matcher/testing/pattern_testing_scope.rb +34 -0
  121. data/lib/matcher/testing.rb +107 -0
  122. data/lib/matcher/undefined.rb +10 -0
  123. data/lib/matcher/utils/mapping_utils.rb +61 -0
  124. data/lib/matcher/utils.rb +72 -0
  125. data/lib/matcher/version.rb +5 -0
  126. data/lib/matcher.rb +346 -0
  127. 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