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.

Files changed (123) hide show
  1. checksums.yaml +7 -0
  2. data/lib/matcher/assertions.rb +21 -0
  3. data/lib/matcher/base.rb +189 -0
  4. data/lib/matcher/builder.rb +74 -0
  5. data/lib/matcher/chain.rb +60 -0
  6. data/lib/matcher/debug.rb +48 -0
  7. data/lib/matcher/errors/and_error.rb +86 -0
  8. data/lib/matcher/errors/boolean_collector.rb +51 -0
  9. data/lib/matcher/errors/element_error.rb +24 -0
  10. data/lib/matcher/errors/empty_error.rb +23 -0
  11. data/lib/matcher/errors/error.rb +39 -0
  12. data/lib/matcher/errors/error_collector.rb +99 -0
  13. data/lib/matcher/errors/nested_error.rb +96 -0
  14. data/lib/matcher/errors/or_error.rb +86 -0
  15. data/lib/matcher/expression_cache.rb +57 -0
  16. data/lib/matcher/expression_labeler.rb +91 -0
  17. data/lib/matcher/expressions/array_expression.rb +45 -0
  18. data/lib/matcher/expressions/block.rb +153 -0
  19. data/lib/matcher/expressions/call.rb +338 -0
  20. data/lib/matcher/expressions/call_error.rb +45 -0
  21. data/lib/matcher/expressions/constant.rb +53 -0
  22. data/lib/matcher/expressions/expression.rb +147 -0
  23. data/lib/matcher/expressions/expression_building.rb +258 -0
  24. data/lib/matcher/expressions/expression_walker.rb +73 -0
  25. data/lib/matcher/expressions/hash_expression.rb +59 -0
  26. data/lib/matcher/expressions/proc_expression.rb +92 -0
  27. data/lib/matcher/expressions/range_expression.rb +58 -0
  28. data/lib/matcher/expressions/recorder.rb +86 -0
  29. data/lib/matcher/expressions/rescue_last_error_expression.rb +44 -0
  30. data/lib/matcher/expressions/set_expression.rb +45 -0
  31. data/lib/matcher/expressions/string_expression.rb +53 -0
  32. data/lib/matcher/expressions/symbol_proc.rb +53 -0
  33. data/lib/matcher/expressions/variable.rb +85 -0
  34. data/lib/matcher/hash_stack.rb +53 -0
  35. data/lib/matcher/list.rb +102 -0
  36. data/lib/matcher/markers/optional.rb +80 -0
  37. data/lib/matcher/markers/others.rb +28 -0
  38. data/lib/matcher/matcher_cache.rb +18 -0
  39. data/lib/matcher/matchers/all_matcher.rb +60 -0
  40. data/lib/matcher/matchers/always_matcher.rb +28 -0
  41. data/lib/matcher/matchers/any_matcher.rb +70 -0
  42. data/lib/matcher/matchers/array_matcher.rb +35 -0
  43. data/lib/matcher/matchers/block_matcher.rb +59 -0
  44. data/lib/matcher/matchers/boolean_matcher.rb +35 -0
  45. data/lib/matcher/matchers/dig_matcher.rb +146 -0
  46. data/lib/matcher/matchers/each_matcher.rb +52 -0
  47. data/lib/matcher/matchers/each_pair_matcher.rb +119 -0
  48. data/lib/matcher/matchers/equal_matcher.rb +197 -0
  49. data/lib/matcher/matchers/equal_set_matcher.rb +99 -0
  50. data/lib/matcher/matchers/expression_matcher.rb +73 -0
  51. data/lib/matcher/matchers/filter_matcher.rb +111 -0
  52. data/lib/matcher/matchers/hash_matcher.rb +223 -0
  53. data/lib/matcher/matchers/imply_matcher.rb +81 -0
  54. data/lib/matcher/matchers/imply_some_matcher.rb +112 -0
  55. data/lib/matcher/matchers/index_by_matcher.rb +175 -0
  56. data/lib/matcher/matchers/inline_matcher.rb +99 -0
  57. data/lib/matcher/matchers/keys_matcher.rb +121 -0
  58. data/lib/matcher/matchers/kind_of_matcher.rb +35 -0
  59. data/lib/matcher/matchers/lazy_all_matcher.rb +68 -0
  60. data/lib/matcher/matchers/lazy_any_matcher.rb +68 -0
  61. data/lib/matcher/matchers/let_matcher.rb +73 -0
  62. data/lib/matcher/matchers/map_matcher.rb +129 -0
  63. data/lib/matcher/matchers/matcher_building.rb +5 -0
  64. data/lib/matcher/matchers/negated_array_matcher.rb +38 -0
  65. data/lib/matcher/matchers/negated_each_matcher.rb +36 -0
  66. data/lib/matcher/matchers/negated_each_pair_matcher.rb +38 -0
  67. data/lib/matcher/matchers/negated_imply_some_matcher.rb +46 -0
  68. data/lib/matcher/matchers/negated_matcher.rb +23 -0
  69. data/lib/matcher/matchers/negated_project_matcher.rb +31 -0
  70. data/lib/matcher/matchers/never_matcher.rb +29 -0
  71. data/lib/matcher/matchers/one_matcher.rb +70 -0
  72. data/lib/matcher/matchers/optional_matcher.rb +38 -0
  73. data/lib/matcher/matchers/parse_float_matcher.rb +86 -0
  74. data/lib/matcher/matchers/parse_integer_matcher.rb +98 -0
  75. data/lib/matcher/matchers/parse_iso8601_matcher.rb +92 -0
  76. data/lib/matcher/matchers/parse_json_matcher.rb +95 -0
  77. data/lib/matcher/matchers/project_matcher.rb +68 -0
  78. data/lib/matcher/matchers/raises_matcher.rb +124 -0
  79. data/lib/matcher/matchers/range_matcher.rb +47 -0
  80. data/lib/matcher/matchers/reference_matcher.rb +111 -0
  81. data/lib/matcher/matchers/reference_matcher_collection.rb +57 -0
  82. data/lib/matcher/matchers/regexp_matcher.rb +84 -0
  83. data/lib/matcher/messages/expected_phrasing.rb +342 -0
  84. data/lib/matcher/messages/message.rb +102 -0
  85. data/lib/matcher/messages/message_builder.rb +35 -0
  86. data/lib/matcher/messages/message_rules.rb +223 -0
  87. data/lib/matcher/messages/namespaced_message_builder.rb +19 -0
  88. data/lib/matcher/messages/phrasing.rb +57 -0
  89. data/lib/matcher/messages/standard_message_builder.rb +105 -0
  90. data/lib/matcher/once_before.rb +18 -0
  91. data/lib/matcher/optional_chain.rb +24 -0
  92. data/lib/matcher/patterns/ast_mapping.rb +42 -0
  93. data/lib/matcher/patterns/capture_hole.rb +33 -0
  94. data/lib/matcher/patterns/constant_hole.rb +14 -0
  95. data/lib/matcher/patterns/hole.rb +30 -0
  96. data/lib/matcher/patterns/method_hole.rb +58 -0
  97. data/lib/matcher/patterns/pattern.rb +92 -0
  98. data/lib/matcher/patterns/pattern_building.rb +39 -0
  99. data/lib/matcher/patterns/pattern_capture.rb +11 -0
  100. data/lib/matcher/patterns/pattern_match.rb +29 -0
  101. data/lib/matcher/patterns/variable_hole.rb +14 -0
  102. data/lib/matcher/reporter.rb +98 -0
  103. data/lib/matcher/rules/message_factory.rb +25 -0
  104. data/lib/matcher/rules/message_rule.rb +18 -0
  105. data/lib/matcher/rules/message_rule_context.rb +24 -0
  106. data/lib/matcher/rules/rule_builder.rb +29 -0
  107. data/lib/matcher/rules/rule_set.rb +57 -0
  108. data/lib/matcher/rules/transform_builder.rb +24 -0
  109. data/lib/matcher/rules/transform_mapping.rb +5 -0
  110. data/lib/matcher/rules/transform_rule.rb +21 -0
  111. data/lib/matcher/state.rb +40 -0
  112. data/lib/matcher/testing/error_builder.rb +62 -0
  113. data/lib/matcher/testing/error_checker.rb +496 -0
  114. data/lib/matcher/testing/error_testing.rb +37 -0
  115. data/lib/matcher/testing/pattern_testing.rb +11 -0
  116. data/lib/matcher/testing/pattern_testing_scope.rb +34 -0
  117. data/lib/matcher/testing.rb +102 -0
  118. data/lib/matcher/undefined.rb +10 -0
  119. data/lib/matcher/utils/mapping_utils.rb +61 -0
  120. data/lib/matcher/utils.rb +72 -0
  121. data/lib/matcher/version.rb +5 -0
  122. data/lib/matcher.rb +337 -0
  123. metadata +167 -0
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class MapMatcher < Base
5
+ include MappingUtils
6
+
7
+ def initialize(projection, matcher, negated: false)
8
+ super()
9
+
10
+ @projection = projection
11
+ @matcher = negated ? ~matcher : matcher
12
+ @original_matcher = matcher
13
+ @negated = negated
14
+ end
15
+
16
+ def negate
17
+ MapMatcher.new(@projection, @original_matcher, negated: !@negated)
18
+ end
19
+
20
+ def validate(state, &)
21
+ return validate_negated(state, &) if @negated
22
+
23
+ actual = state.actual
24
+ values = state.values
25
+
26
+ unless actual.respond_to?(:each)
27
+ state.errors << state.expected.responding_to(:each)
28
+ return
29
+ end
30
+
31
+ i = 0
32
+ mapped = []
33
+ mapping_failed = false
34
+
35
+ actual.each do |act|
36
+ mapped << @projection.evaluate(
37
+ values.merge(actual: act, index: i, original: actual),
38
+ )
39
+ rescue CallError => e
40
+ state.errors[i] << e.message_for_errors(act)
41
+ mapping_failed = true
42
+ ensure
43
+ i += 1
44
+ end
45
+
46
+ return if mapping_failed
47
+
48
+ errors = yield @matcher, mapped, original: actual
49
+ errors = map_errors2(errors) unless state.boolean?
50
+
51
+ state.errors << errors
52
+ end
53
+
54
+ def to_s
55
+ "#{'~' if @negated}map(#{@projection}, #{@original_matcher})"
56
+ end
57
+
58
+ private
59
+
60
+ def validate_negated(state)
61
+ actual = state.actual
62
+ values = state.values
63
+
64
+ return unless actual.respond_to?(:map)
65
+
66
+ mapped = []
67
+
68
+ actual.map.with_index do |item, i|
69
+ mapped << @projection.evaluate(
70
+ values.merge(actual: item, index: i, original: actual),
71
+ )
72
+ rescue CallError
73
+ return if @negated
74
+ end
75
+
76
+ mapped_errors = yield @matcher, mapped, original: actual
77
+
78
+ state.errors << map_errors2(mapped_errors)
79
+ end
80
+
81
+ def map_errors2(errors)
82
+ map_errors(errors) do |nested_error|
83
+ key = nested_error.key
84
+
85
+ next unless index_call?(key)
86
+
87
+ NestedError.new(
88
+ key,
89
+ NestedError.new(@projection, nested_error.child),
90
+ )
91
+ end
92
+ end
93
+
94
+ def mapped_base
95
+ @mapped_base ||= map_base(:map, @projection)
96
+ end
97
+ end
98
+
99
+ module MatcherBuilding
100
+ ##
101
+ # Maps items to another value before matching
102
+ # == +expression+ values
103
+ # - actual
104
+ # - index
105
+ # - original
106
+ # == +matcher+ values
107
+ # - original
108
+ # @example
109
+ # # matches ["1", "2"]
110
+ # map(_.to_i, [1, 2])
111
+ # # alternatively:
112
+ # map(_.to_i) ^ [1, 2]
113
+ # @overload map(expression, matcher)
114
+ # @param expression [Expression]
115
+ # @param matcher [Base]
116
+ # @return [MapMatcher]
117
+ # @overload map(expression)
118
+ # @param expression [Expression]
119
+ # @return [Chain<MapMatcher>]
120
+ def map(expression, matcher = UNDEFINED)
121
+ return Chain.new { map(expression, _1) } if Matcher.undefined?(matcher)
122
+
123
+ expression = expression_of(expression)
124
+ matcher = matcher_of(matcher)
125
+
126
+ MapMatcher.new(expression, matcher)
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ module MatcherBuilding end
5
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class NegatedArrayMatcher < Base
5
+ def initialize(array)
6
+ super()
7
+
8
+ @array = array
9
+ @neg_array = @array.map(&:~)
10
+ end
11
+
12
+ def negate
13
+ ArrayMatcher.new(@array)
14
+ end
15
+
16
+ def validate(state)
17
+ actual = state.actual
18
+
19
+ return if !actual.is_a?(Array) || @array.length != actual.length
20
+
21
+ collector = state.new_collector.or!
22
+
23
+ @array.length.times do |i|
24
+ result = yield @neg_array[i], actual[i], index: i, parent: actual
25
+
26
+ return if result.valid?
27
+
28
+ collector[i] << result
29
+ end
30
+
31
+ state.errors << collector.error
32
+ end
33
+
34
+ def to_s
35
+ "neg(#{@array})"
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class NegatedEachMatcher < Base
5
+ def initialize(matcher)
6
+ super()
7
+
8
+ @matcher = matcher
9
+ @neg_matcher = ~matcher
10
+ end
11
+
12
+ def negate
13
+ EachMatcher.new(@matcher)
14
+ end
15
+
16
+ def validate(state)
17
+ return unless state.actual.respond_to?(:each)
18
+
19
+ collector = state.new_collector.or!
20
+
21
+ state.actual.each.with_index do |item, i|
22
+ result = yield @neg_matcher, item, index: i, parent: state.actual
23
+
24
+ return if result.valid?
25
+
26
+ collector[i] << result
27
+ end
28
+
29
+ state.errors << collector.error
30
+ end
31
+
32
+ def to_s
33
+ "~each(#{@matcher})"
34
+ end
35
+ end
36
+ end
@@ -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 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,23 @@
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
+ state.errors << state.expected.namespace(:negated).not.valid(@matcher) if yield(@matcher).valid?
17
+ end
18
+
19
+ def to_s
20
+ "neg(#{@matcher})"
21
+ end
22
+ end
23
+ 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,29 @@
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 MatcherBuilding
21
+ ##
22
+ # Never matches. Opposite of {#always}
23
+ # @return [NeverMatcher]
24
+ # @see #always
25
+ def never
26
+ NeverMatcher.instance
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,70 @@
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
+ else
33
+ if valid_matchers.length == 0
34
+ state.errors << OrError.from(invalid_errors)
35
+ elsif valid_matchers.length > 1
36
+ negated_matchers = valid_matchers.map(&:~)
37
+ any_matcher = AnyMatcher.new(negated_matchers)
38
+
39
+ state.errors << yield(any_matcher)
40
+ end
41
+ end
42
+ end
43
+
44
+ def to_s
45
+ "#{'~' if @negated}one(#{@matchers.map(&:to_s).join(', ')})"
46
+ end
47
+ end
48
+
49
+ module MatcherBuilding
50
+ ##
51
+ # Matches exactly one matcher
52
+ # @example
53
+ # # matches [1] and [2] but not [] or [1, 2]
54
+ # one(_.include?(1), _.include?(2))
55
+ # @param matchers [Array<Base>]
56
+ # @return [OneMatcher]
57
+ def one(*matchers)
58
+ matchers = matchers.map { matcher_of(_1) }
59
+
60
+ case matchers.count
61
+ when 0
62
+ NeverMatcher.instance
63
+ when 1
64
+ matchers[0]
65
+ else
66
+ OneMatcher.new(matchers)
67
+ end
68
+ end
69
+ end
70
+ 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 MatcherBuilding
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,98 @@
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(@original_matcher, base: @base, negated: !@negated)
16
+ end
17
+
18
+ def validate(state, &)
19
+ actual = state.actual
20
+
21
+ unless actual.is_a?(String)
22
+ state.errors << state.expected.kind_of(String) unless @negated
23
+ return
24
+ end
25
+
26
+ value = Integer(actual)
27
+
28
+ if @matcher.is_a?(NeverMatcher)
29
+ state.errors << state.expected.not.valid_format(:integer)
30
+ return
31
+ end
32
+
33
+ result = yield(@matcher, value)
34
+
35
+ return if result.valid?
36
+
37
+ integer_of = Call.new(Constant.new(Kernel), :Integer, [Variable.actual])
38
+ state.errors[integer_of] << result
39
+ rescue ArgumentError
40
+ state.errors << state.expected.valid_format(:integer) unless @negated
41
+ end
42
+
43
+ def to_s
44
+ prefix = @negated ? '~' : ''
45
+
46
+ if @original_matcher.is_a?(AlwaysMatcher)
47
+ args = @base == 0 ? '' : "(base: #{@base})"
48
+ return "#{prefix}integer_format#{args}"
49
+ end
50
+
51
+ base_arg = @base == 0 ? '' : ", base: #{@base}"
52
+
53
+ "#{'~' if @negated}parse_integer(#{@original_matcher}#{base_arg})"
54
+ end
55
+ end
56
+
57
+ module MatcherBuilding
58
+ ##
59
+ # Parses integer and matches with given matcher
60
+ # @example
61
+ # # matches "7"
62
+ # parse_integer(_.odd?)
63
+ # # alternatively:
64
+ # parse_integer ^ (_.odd?)
65
+ # # without matcher matches any integer string
66
+ # parse_integer
67
+ # @overload parse_integer(matcher, base: 0)
68
+ # @param matcher [Base]
69
+ # @param base [Integer]
70
+ # @return [ParseIntegerMatcher]
71
+ # @overload parse_integer(base: 0)
72
+ # @return [OptionalChain<ParseIntegerMatcher>]
73
+ # @see #integer_format
74
+ def parse_integer(matcher = UNDEFINED, base: 0)
75
+ return Chain.new { parse_integer(_1, base:) }.optional if
76
+ Matcher.undefined?(matcher)
77
+
78
+ matcher = matcher_of(matcher)
79
+
80
+ ParseIntegerMatcher.new(matcher, base:)
81
+ end
82
+
83
+ ##
84
+ # Matches integer strings
85
+ # @example
86
+ # # matches { payload: "42" }
87
+ # { payload: integer_format }
88
+ # @param base [Integer]
89
+ # @return [ParseIntegerMatcher]
90
+ def integer_format(base: 0)
91
+ if base == 0
92
+ @integer_format ||= ParseIntegerMatcher.new(AlwaysMatcher.instance, base:)
93
+ else
94
+ ParseIntegerMatcher.new(AlwaysMatcher.instance, base:)
95
+ end
96
+ end
97
+ end
98
+ end