matchi 4.2.0 → 5.0.0
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 +4 -4
- data/LICENSE.md +1 -1
- data/README.md +91 -227
- data/lib/matchi/be.rb +10 -65
- data/lib/matchi/be_a_kind_of.rb +21 -116
- data/lib/matchi/be_an_instance_of.rb +24 -113
- data/lib/matchi/be_within/of.rb +57 -0
- data/lib/matchi/be_within.rb +17 -134
- data/lib/matchi/change/delta.rb +56 -0
- data/lib/matchi/change/from.rb +21 -61
- data/lib/matchi/change/from_to.rb +46 -0
- data/lib/matchi/change/to.rb +17 -81
- data/lib/matchi/change.rb +73 -148
- data/lib/matchi/constant.rb +55 -0
- data/lib/matchi/eq.rb +24 -65
- data/lib/matchi/match.rb +28 -64
- data/lib/matchi/number.rb +37 -0
- data/lib/matchi/predicate.rb +40 -139
- data/lib/matchi/raise_exception.rb +72 -104
- data/lib/matchi/satisfy.rb +21 -96
- data/lib/matchi.rb +22 -60
- metadata +17 -17
- data/lib/matchi/change/by.rb +0 -137
- data/lib/matchi/change/by_at_least.rb +0 -158
- data/lib/matchi/change/by_at_most.rb +0 -171
- data/lib/matchi/change/from/to.rb +0 -132
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matchi
|
|
4
|
+
# Validation of the numbers given to matchers: deltas and expected values
|
|
5
|
+
# must be finite and real, so that a `NaN`, an infinity or a `Complex`
|
|
6
|
+
# cannot build a matcher that never, or always, passes.
|
|
7
|
+
#
|
|
8
|
+
# @api private
|
|
9
|
+
module Number
|
|
10
|
+
# @param value [Object]
|
|
11
|
+
# @return [Boolean] whether value is a finite real Numeric
|
|
12
|
+
def self.finite_real?(value)
|
|
13
|
+
::Numeric === value && value.real? && value.finite?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @param value [Object]
|
|
17
|
+
# @param role [String] the argument name, for the error message
|
|
18
|
+
# @return [Numeric] value
|
|
19
|
+
# @raise [ArgumentError] when value is not a finite real Numeric
|
|
20
|
+
def self.finite_real!(value, role)
|
|
21
|
+
raise ::ArgumentError, "#{role} must be a finite real Numeric (got: #{value.inspect})" unless finite_real?(value)
|
|
22
|
+
|
|
23
|
+
value
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @param value [Object]
|
|
27
|
+
# @param role [String] the argument name, for the error message
|
|
28
|
+
# @return [Numeric] value
|
|
29
|
+
# @raise [ArgumentError] when value is not a finite real non-negative Numeric
|
|
30
|
+
def self.non_negative!(value, role)
|
|
31
|
+
finite_real!(value, role)
|
|
32
|
+
raise ::ArgumentError, "#{role} must be non-negative (got: #{value.inspect})" if value.negative?
|
|
33
|
+
|
|
34
|
+
value
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/matchi/predicate.rb
CHANGED
|
@@ -1,172 +1,73 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "constant"
|
|
4
|
+
|
|
3
5
|
module Matchi
|
|
4
|
-
# Predicate matcher
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# of predicate formats: 'be_*' which converts to '*?' and 'have_*' which converts to 'has_*?'.
|
|
10
|
-
#
|
|
11
|
-
# @example Basic empty check
|
|
12
|
-
# matcher = Matchi::Predicate.new(:be_empty)
|
|
13
|
-
# matcher.match? { [] } # => true
|
|
14
|
-
# matcher.match? { [1, 2] } # => false
|
|
15
|
-
#
|
|
16
|
-
# @example Object property check with arguments
|
|
17
|
-
# matcher = Matchi::Predicate.new(:have_key, :name)
|
|
18
|
-
# matcher.match? { { name: "Alice" } } # => true
|
|
19
|
-
# matcher.match? { { age: 30 } } # => false
|
|
20
|
-
#
|
|
21
|
-
# @example Using keyword arguments
|
|
22
|
-
# class Record
|
|
23
|
-
# def complete?(status: nil)
|
|
24
|
-
# status.nil? || status == :validated
|
|
25
|
-
# end
|
|
26
|
-
# end
|
|
6
|
+
# Predicate matcher: passes when a predicate method of the actual value
|
|
7
|
+
# returns `true`. The matcher name `be_xxx` calls `xxx?`, and `have_xxx`
|
|
8
|
+
# calls `has_xxx?`, with `Kernel#public_send` bound to the actual value.
|
|
9
|
+
# The predicate must return `true` or `false`; any other value is an error,
|
|
10
|
+
# not a truthy pass.
|
|
27
11
|
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
12
|
+
# Since the actual value answers the call, this matcher depends on the
|
|
13
|
+
# honesty of the object under test. Use it for the object's own documented
|
|
14
|
+
# predicates.
|
|
30
15
|
#
|
|
31
|
-
# @example
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
# block ? super : empty?
|
|
35
|
-
# end
|
|
36
|
-
# end
|
|
37
|
-
#
|
|
38
|
-
# matcher = Matchi::Predicate.new(:be_all) { |x| x.positive? }
|
|
39
|
-
# matcher.match? { [1, 2, 3] } # => true
|
|
40
|
-
# matcher.match? { [-1, 2, 3] } # => false
|
|
41
|
-
#
|
|
42
|
-
# @see https://ruby-doc.org/core/Object.html#method-i-respond_to-3F
|
|
16
|
+
# @example
|
|
17
|
+
# Matchi::Predicate.new(:be_empty).match? { [] } # => true
|
|
18
|
+
# Matchi::Predicate.new(:have_key, :a).match? { { a: 1 } } # => true
|
|
43
19
|
class Predicate
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
|
|
47
|
-
# @api private
|
|
48
|
-
PREFIXES = {
|
|
49
|
-
"be_" => ->(name) { "#{name.gsub(/\A(?:be_)/, "")}?" },
|
|
50
|
-
"have_" => ->(name) { "#{name.gsub(/\A(?:have_)/, "has_")}?" }
|
|
51
|
-
}.freeze
|
|
20
|
+
# A matcher name: `be_` or `have_`, then a letter, then letters, digits
|
|
21
|
+
# or underscores, in any script. It never ends with `?` or `!`.
|
|
22
|
+
NAME = /\A(be|have)_\p{Alpha}[\p{Alnum}_]*\z/
|
|
52
23
|
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
# @
|
|
56
|
-
#
|
|
57
|
-
# @
|
|
58
|
-
# @param args [Array] Optional positional arguments to pass to the predicate method
|
|
59
|
-
# @param kwargs [Hash] Optional keyword arguments to pass to the predicate method
|
|
60
|
-
# @param block [Proc] Optional block to pass to the predicate method
|
|
61
|
-
#
|
|
62
|
-
# @raise [ArgumentError] if the predicate name format is invalid
|
|
63
|
-
#
|
|
64
|
-
# @return [Predicate] a new instance of the matcher
|
|
65
|
-
#
|
|
66
|
-
# @example With simple predicate
|
|
67
|
-
# Predicate.new(:be_empty) # Empty check
|
|
68
|
-
#
|
|
69
|
-
# @example With arguments
|
|
70
|
-
# Predicate.new(:have_key, :id) # Key presence check
|
|
71
|
-
#
|
|
72
|
-
# @example With keyword arguments
|
|
73
|
-
# Predicate.new(:be_valid, status: true) # Conditional validation
|
|
24
|
+
# @param name [Symbol, String] `be_xxx` or `have_xxx`
|
|
25
|
+
# @param args [Array] positional arguments for the predicate
|
|
26
|
+
# @param kwargs [Hash] keyword arguments for the predicate
|
|
27
|
+
# @param block [Proc, nil] a block for the predicate
|
|
28
|
+
# @raise [ArgumentError] when the name does not start with be_ or have_
|
|
74
29
|
def initialize(name, *args, **kwargs, &block)
|
|
75
30
|
@name = String(name)
|
|
76
|
-
raise ::ArgumentError, "
|
|
31
|
+
raise ::ArgumentError, "name must match #{NAME.inspect} (got: #{name.inspect})" unless NAME.match?(@name)
|
|
77
32
|
|
|
33
|
+
@method_name = method_name
|
|
78
34
|
@args = args
|
|
79
35
|
@kwargs = kwargs
|
|
80
36
|
@block = block
|
|
81
37
|
end
|
|
82
38
|
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
#
|
|
88
|
-
#
|
|
89
|
-
# @api public
|
|
90
|
-
#
|
|
91
|
-
# @yield [] Block that returns the object to check
|
|
92
|
-
# @yieldreturn [Object] The object to call the predicate method on
|
|
93
|
-
#
|
|
94
|
-
# @return [Boolean] true if the predicate method returns true
|
|
95
|
-
#
|
|
96
|
-
# @raise [ArgumentError] if no block is provided
|
|
97
|
-
# @raise [TypeError] if predicate method returns non-boolean value
|
|
98
|
-
#
|
|
99
|
-
# @example Basic usage
|
|
100
|
-
# matcher = Predicate.new(:be_empty)
|
|
101
|
-
# matcher.match? { [] } # => true
|
|
102
|
-
# matcher.match? { [1] } # => false
|
|
103
|
-
#
|
|
104
|
-
# @example With arguments
|
|
105
|
-
# matcher = Predicate.new(:have_key, :id)
|
|
106
|
-
# matcher.match? { { id: 1 } } # => true
|
|
39
|
+
# @yieldreturn [Object] the actual value
|
|
40
|
+
# @return [Boolean]
|
|
41
|
+
# @raise [ArgumentError] when no block is given
|
|
42
|
+
# @raise [TypeError] when the predicate returns something other than true or false
|
|
43
|
+
# @raise [NoMethodError] when the actual value has no such public predicate
|
|
107
44
|
def match?
|
|
108
45
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
|
109
46
|
|
|
110
|
-
|
|
111
|
-
return
|
|
47
|
+
result = ::Kernel.instance_method(:public_send).bind_call(yield, @method_name, *@args, **@kwargs, &@block)
|
|
48
|
+
return result if true.equal?(result) || false.equal?(result)
|
|
112
49
|
|
|
113
|
-
raise ::TypeError, "
|
|
50
|
+
raise ::TypeError, "#{@method_name} must return true or false (got a #{Constant.label(result_class(result))})"
|
|
114
51
|
end
|
|
115
52
|
|
|
116
|
-
#
|
|
117
|
-
#
|
|
118
|
-
# @api public
|
|
119
|
-
#
|
|
120
|
-
# @return [String] A string describing what this matcher verifies
|
|
121
|
-
#
|
|
122
|
-
# @example Simple predicate
|
|
123
|
-
# Predicate.new(:be_empty).to_s # => "be empty"
|
|
124
|
-
#
|
|
125
|
-
# @example With arguments
|
|
126
|
-
# Predicate.new(:have_key, :id).to_s # => "have key :id"
|
|
127
|
-
#
|
|
128
|
-
# @example With keyword arguments
|
|
129
|
-
# Predicate.new(:be_valid, active: true).to_s # => "be valid active: true"
|
|
53
|
+
# @return [String]
|
|
130
54
|
def to_s
|
|
131
|
-
(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
(@block.nil? ? "" : "&block")
|
|
136
|
-
].reject { |i| i.eql?("") }.join(", ")
|
|
137
|
-
).strip
|
|
55
|
+
arguments = @args.map(&:inspect) + @kwargs.map { |key, value| "#{key}: #{value.inspect}" }
|
|
56
|
+
arguments << "&block" if @block
|
|
57
|
+
|
|
58
|
+
[@name.tr("_", " "), arguments.join(", ")].reject(&:empty?).join(" ")
|
|
138
59
|
end
|
|
139
60
|
|
|
140
61
|
private
|
|
141
62
|
|
|
142
|
-
# Converts the predicate name into the actual method name to call.
|
|
143
|
-
#
|
|
144
|
-
# @api private
|
|
145
|
-
#
|
|
146
|
-
# @return [Symbol] The method name to call on the object
|
|
147
|
-
# @raise [ArgumentError] if the predicate prefix is unknown
|
|
148
|
-
#
|
|
149
|
-
# @example
|
|
150
|
-
# # With be_ prefix
|
|
151
|
-
# method_name # => :empty? (from be_empty)
|
|
152
|
-
# # With have_ prefix
|
|
153
|
-
# method_name # => :has_key? (from have_key)
|
|
154
63
|
def method_name
|
|
155
|
-
|
|
156
|
-
return transform.call(@name) if transform
|
|
64
|
+
return "#{@name.delete_prefix("be_")}?" if @name.start_with?("be_")
|
|
157
65
|
|
|
158
|
-
|
|
66
|
+
"has_#{@name.delete_prefix("have_")}?"
|
|
159
67
|
end
|
|
160
68
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
# @api private
|
|
164
|
-
#
|
|
165
|
-
# @return [Boolean] true if the name follows the required format
|
|
166
|
-
def valid_name?
|
|
167
|
-
return false if @name.match?(/[?!]\z/)
|
|
168
|
-
|
|
169
|
-
PREFIXES.keys.any? { |prefix| @name.start_with?(prefix) }
|
|
69
|
+
def result_class(result)
|
|
70
|
+
::Kernel.instance_method(:class).bind_call(result)
|
|
170
71
|
end
|
|
171
72
|
end
|
|
172
73
|
end
|
|
@@ -1,133 +1,101 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "constant"
|
|
4
|
+
|
|
3
5
|
module Matchi
|
|
4
|
-
# Exception matcher
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# a
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# @example Basic usage with standard exceptions
|
|
11
|
-
# matcher = Matchi::RaiseException.new(ArgumentError)
|
|
12
|
-
# matcher.match? { raise ArgumentError } # => true
|
|
13
|
-
# matcher.match? { raise RuntimeError } # => false
|
|
14
|
-
# matcher.match? { "no error" } # => false
|
|
15
|
-
#
|
|
16
|
-
# @example Working with inheritance hierarchy
|
|
17
|
-
# class CustomError < StandardError; end
|
|
18
|
-
# class SpecificError < CustomError; end
|
|
19
|
-
#
|
|
20
|
-
# matcher = Matchi::RaiseException.new(CustomError)
|
|
21
|
-
# matcher.match? { raise CustomError } # => true
|
|
22
|
-
# matcher.match? { raise SpecificError } # => true
|
|
23
|
-
# matcher.match? { raise StandardError } # => false
|
|
6
|
+
# Exception matcher: passes when evaluating the block raises an exception
|
|
7
|
+
# of the expected class or of one of its descendants (`Module#===`, so the
|
|
8
|
+
# exception cannot lie about its class), optionally with an expected
|
|
9
|
+
# message: a String the message must be `eql?` to, or a Regexp it must
|
|
10
|
+
# match.
|
|
24
11
|
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
12
|
+
# Every `Exception` is caught, including `SystemExit` and `Interrupt`: a
|
|
13
|
+
# specification must be able to assert on any of them, so an unexpected
|
|
14
|
+
# `exit` or Ctrl-C inside the block is reported as a failure, not
|
|
15
|
+
# propagated. The expected class can be given directly, or by name; see
|
|
16
|
+
# {Matchi::Constant}.
|
|
28
17
|
#
|
|
29
|
-
# @example
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
# matcher = Matchi::RaiseException.new("Api::Error")
|
|
36
|
-
# matcher.match? { raise Api::AuthenticationError } # => true
|
|
37
|
-
#
|
|
38
|
-
# @see https://ruby-doc.org/core/Exception.html
|
|
39
|
-
# @see https://ruby-doc.org/core/StandardError.html
|
|
18
|
+
# @example
|
|
19
|
+
# Matchi::RaiseException.new(ZeroDivisionError).match? { 1 / 0 } # => true
|
|
20
|
+
# Matchi::RaiseException.new(:StandardError).match? { 1 / 0 } # => true
|
|
21
|
+
# Matchi::RaiseException.new(ZeroDivisionError, /by 0/).match? { 1 / 0 } # => true
|
|
22
|
+
# Matchi::RaiseException.new(ZeroDivisionError, "nope").match? { 1 / 0 } # => false
|
|
23
|
+
# Matchi::RaiseException.new(IOError).match? { 1 / 0 } # => false
|
|
40
24
|
class RaiseException
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
# @
|
|
44
|
-
#
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
# @return [RaiseException] a new instance of the matcher
|
|
51
|
-
#
|
|
52
|
-
# @example
|
|
53
|
-
# RaiseException.new(ArgumentError) # Using class
|
|
54
|
-
# RaiseException.new("ArgumentError") # Using string
|
|
55
|
-
# RaiseException.new(:ArgumentError) # Using symbol
|
|
56
|
-
# RaiseException.new("Api::CustomError") # Using namespaced class
|
|
57
|
-
def initialize(expected)
|
|
58
|
-
@expected = String(expected)
|
|
59
|
-
return if /\A[A-Z]/.match?(@expected)
|
|
60
|
-
|
|
61
|
-
raise ::ArgumentError,
|
|
62
|
-
"expected must start with an uppercase letter (got: #{@expected})"
|
|
25
|
+
# @param expected [Class, String, Symbol] the expected exception class, or its name
|
|
26
|
+
# @param message [String, Regexp, nil] the expected message, exact or as a pattern
|
|
27
|
+
# @raise [ArgumentError] when expected is neither an Exception class nor a constant name,
|
|
28
|
+
# or message is neither a String nor a Regexp
|
|
29
|
+
def initialize(expected, message = nil)
|
|
30
|
+
@expected = Constant.normalize(expected, ::Class)
|
|
31
|
+
@message = message
|
|
32
|
+
validate_class!
|
|
33
|
+
validate_message!
|
|
63
34
|
end
|
|
64
35
|
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
# @api public
|
|
72
|
-
#
|
|
73
|
-
# @yield [] Block that should raise an exception
|
|
74
|
-
# @yieldreturn [Object] The result of the block (if it doesn't raise)
|
|
75
|
-
#
|
|
76
|
-
# @return [Boolean] true if the block raises the expected exception
|
|
77
|
-
#
|
|
78
|
-
# @raise [ArgumentError] if no block is provided
|
|
79
|
-
# @raise [ArgumentError] if expected exception class doesn't inherit from Exception
|
|
80
|
-
# @raise [NameError] if the expected exception class cannot be found
|
|
81
|
-
#
|
|
82
|
-
# @example Standard usage
|
|
83
|
-
# matcher = RaiseException.new(ArgumentError)
|
|
84
|
-
# matcher.match? { raise ArgumentError } # => true
|
|
85
|
-
#
|
|
86
|
-
# @example With inheritance
|
|
87
|
-
# matcher = RaiseException.new(StandardError)
|
|
88
|
-
# matcher.match? { raise ArgumentError } # => true
|
|
89
|
-
#
|
|
90
|
-
# @example Without exception
|
|
91
|
-
# matcher = RaiseException.new(StandardError)
|
|
92
|
-
# matcher.match? { "no error" } # => false
|
|
36
|
+
# @yieldreturn [Object] ignored, the block is evaluated for its exception
|
|
37
|
+
# @return [Boolean]
|
|
38
|
+
# @raise [ArgumentError] when no block is given
|
|
39
|
+
# @raise [NameError] when the name is not defined
|
|
40
|
+
# @raise [TypeError] when the name does not resolve to an Exception class
|
|
93
41
|
def match?
|
|
94
42
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
|
95
43
|
|
|
44
|
+
# Resolved before the block, so that a NameError from the name is not
|
|
45
|
+
# mistaken for an exception raised by the block.
|
|
96
46
|
klass = expected_class
|
|
97
|
-
raise ::ArgumentError, "expected exception class must inherit from Exception" unless klass <= ::Exception
|
|
98
47
|
|
|
99
48
|
begin
|
|
100
49
|
yield
|
|
101
50
|
false
|
|
102
51
|
rescue ::Exception => e # rubocop:disable Lint/RescueException
|
|
103
|
-
e
|
|
52
|
+
klass === e && message?(e)
|
|
104
53
|
end
|
|
105
54
|
end
|
|
106
55
|
|
|
107
|
-
#
|
|
108
|
-
#
|
|
109
|
-
# @api public
|
|
110
|
-
#
|
|
111
|
-
# @return [String] A string describing what this matcher verifies
|
|
112
|
-
#
|
|
113
|
-
# @example
|
|
114
|
-
# RaiseException.new(ArgumentError).to_s # => "raise exception ArgumentError"
|
|
56
|
+
# @return [String]
|
|
115
57
|
def to_s
|
|
116
|
-
"raise exception #{@expected}"
|
|
58
|
+
return "raise exception #{Constant.label(@expected)}" if @message.nil?
|
|
59
|
+
|
|
60
|
+
"raise exception #{Constant.label(@expected)} with message #{@message.inspect}"
|
|
117
61
|
end
|
|
118
62
|
|
|
119
63
|
private
|
|
120
64
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
65
|
+
def validate_class!
|
|
66
|
+
return unless ::Class === @expected
|
|
67
|
+
return if @expected <= ::Exception
|
|
68
|
+
|
|
69
|
+
raise ::ArgumentError, "expected must inherit from Exception (got: #{@expected.inspect})"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def validate_message!
|
|
73
|
+
return if @message.nil? || ::String === @message || ::Regexp === @message
|
|
74
|
+
|
|
75
|
+
raise ::ArgumentError, "message must be a String or a Regexp (got: #{@message.inspect})"
|
|
76
|
+
end
|
|
77
|
+
|
|
129
78
|
def expected_class
|
|
130
|
-
::
|
|
79
|
+
return @expected if ::Class === @expected
|
|
80
|
+
|
|
81
|
+
klass = Constant.resolve(@expected, ::Class)
|
|
82
|
+
raise ::TypeError, "#{@expected} is not an Exception class" unless klass <= ::Exception
|
|
83
|
+
|
|
84
|
+
klass
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# The message is whatever the exception answers to `message`, which is
|
|
88
|
+
# its own data, as with any exception; it must be a valid String. The
|
|
89
|
+
# expected String or Regexp receives the comparison.
|
|
90
|
+
def message?(exception)
|
|
91
|
+
return true if @message.nil?
|
|
92
|
+
|
|
93
|
+
message = exception.message
|
|
94
|
+
return false unless ::String === message && message.valid_encoding?
|
|
95
|
+
|
|
96
|
+
::String === @message ? @message.eql?(message) : @message.match?(message)
|
|
97
|
+
rescue ::Encoding::CompatibilityError
|
|
98
|
+
false
|
|
131
99
|
end
|
|
132
100
|
end
|
|
133
101
|
end
|
data/lib/matchi/satisfy.rb
CHANGED
|
@@ -1,114 +1,39 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "constant"
|
|
4
|
+
|
|
3
5
|
module Matchi
|
|
4
|
-
# Custom
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# defined in a block. Unlike specific matchers that test for predetermined
|
|
8
|
-
# conditions, Satisfy allows you to define any custom validation logic at runtime.
|
|
9
|
-
# This makes it particularly useful for complex or composite conditions that
|
|
10
|
-
# aren't covered by other matchers.
|
|
11
|
-
#
|
|
12
|
-
# @example Basic numeric validation
|
|
13
|
-
# matcher = Matchi::Satisfy.new { |n| n.positive? && n.even? }
|
|
14
|
-
# matcher.match? { 2 } # => true
|
|
15
|
-
# matcher.match? { -2 } # => false
|
|
16
|
-
# matcher.match? { 3 } # => false
|
|
17
|
-
#
|
|
18
|
-
# @example String pattern validation
|
|
19
|
-
# matcher = Matchi::Satisfy.new { |s| s.start_with?("test") && s.length < 10 }
|
|
20
|
-
# matcher.match? { "test_123" } # => true
|
|
21
|
-
# matcher.match? { "test_12345" } # => false
|
|
22
|
-
# matcher.match? { "other" } # => false
|
|
6
|
+
# Custom matcher: passes when the given block, called with the actual value,
|
|
7
|
+
# returns `true`. The block must return `true` or `false`; any other value
|
|
8
|
+
# is an error, not a truthy pass.
|
|
23
9
|
#
|
|
24
|
-
# @example
|
|
25
|
-
#
|
|
26
|
-
# attr_reader :age, :name
|
|
27
|
-
# def initialize(name, age)
|
|
28
|
-
# @name, @age = name, age
|
|
29
|
-
# end
|
|
30
|
-
# end
|
|
31
|
-
#
|
|
32
|
-
# matcher = Matchi::Satisfy.new { |user|
|
|
33
|
-
# user.age >= 18 && user.name.length >= 2
|
|
34
|
-
# }
|
|
35
|
-
# matcher.match? { User.new("Alice", 25) } # => true
|
|
36
|
-
# matcher.match? { User.new("B", 20) } # => false
|
|
37
|
-
#
|
|
38
|
-
# @example Using with collections
|
|
39
|
-
# matcher = Matchi::Satisfy.new { |arr|
|
|
40
|
-
# arr.all? { |x| x.is_a?(Integer) } && arr.sum.even?
|
|
41
|
-
# }
|
|
42
|
-
# matcher.match? { [2, 4, 6] } # => true
|
|
43
|
-
# matcher.match? { [1, 3, 5] } # => false
|
|
44
|
-
# matcher.match? { [1, "2", 3] } # => false
|
|
45
|
-
#
|
|
46
|
-
# @see https://ruby-doc.org/core/Proc.html
|
|
47
|
-
# @see Matchi::Predicate For testing existing predicate methods
|
|
10
|
+
# @example
|
|
11
|
+
# Matchi::Satisfy.new { |value| value > 41 }.match? { 42 } # => true
|
|
48
12
|
class Satisfy
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
# @
|
|
52
|
-
#
|
|
53
|
-
# @yield [Object] Block that defines the validation condition
|
|
54
|
-
# @yieldparam value The value to validate
|
|
55
|
-
# @yieldreturn [Boolean] true if the value meets the condition
|
|
56
|
-
#
|
|
57
|
-
# @raise [ArgumentError] if no block is provided
|
|
58
|
-
#
|
|
59
|
-
# @return [Satisfy] a new instance of the matcher
|
|
60
|
-
#
|
|
61
|
-
# @example Simple numeric validation
|
|
62
|
-
# Satisfy.new { |n| n > 0 }
|
|
63
|
-
#
|
|
64
|
-
# @example Complex condition
|
|
65
|
-
# Satisfy.new { |obj|
|
|
66
|
-
# obj.respond_to?(:length) && obj.length.between?(2, 10)
|
|
67
|
-
# }
|
|
13
|
+
# @yieldparam actual [Object] the actual value
|
|
14
|
+
# @yieldreturn [Boolean]
|
|
15
|
+
# @raise [ArgumentError] when no block is given
|
|
68
16
|
def initialize(&block)
|
|
69
17
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
|
70
18
|
|
|
71
|
-
@
|
|
19
|
+
@predicate = block
|
|
72
20
|
end
|
|
73
21
|
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
# the validation block returns a truthy value.
|
|
79
|
-
#
|
|
80
|
-
# @api public
|
|
81
|
-
#
|
|
82
|
-
# @yield [] Block that returns the value to validate
|
|
83
|
-
# @yieldreturn [Object] The value to check against the condition
|
|
84
|
-
#
|
|
85
|
-
# @return [Boolean] true if the value satisfies the condition
|
|
86
|
-
#
|
|
87
|
-
# @raise [ArgumentError] if no block is provided
|
|
88
|
-
#
|
|
89
|
-
# @example Using with direct values
|
|
90
|
-
# matcher = Satisfy.new { |n| n.positive? }
|
|
91
|
-
# matcher.match? { 42 } # => true
|
|
92
|
-
# matcher.match? { -1 } # => false
|
|
93
|
-
#
|
|
94
|
-
# @example Using with computed values
|
|
95
|
-
# matcher = Satisfy.new { |s| s.length.even? }
|
|
96
|
-
# matcher.match? { "test".upcase } # => true
|
|
97
|
-
# matcher.match? { "a" * 3 } # => false
|
|
22
|
+
# @yieldreturn [Object] the actual value
|
|
23
|
+
# @return [Boolean]
|
|
24
|
+
# @raise [ArgumentError] when no block is given
|
|
25
|
+
# @raise [TypeError] when the predicate returns something other than true or false
|
|
98
26
|
def match?
|
|
99
27
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
|
100
28
|
|
|
101
|
-
@
|
|
29
|
+
result = @predicate.call(yield)
|
|
30
|
+
return result if true.equal?(result) || false.equal?(result)
|
|
31
|
+
|
|
32
|
+
klass = ::Kernel.instance_method(:class).bind_call(result)
|
|
33
|
+
raise ::TypeError, "the block must return true or false (got a #{Constant.label(klass)})"
|
|
102
34
|
end
|
|
103
35
|
|
|
104
|
-
#
|
|
105
|
-
#
|
|
106
|
-
# @api public
|
|
107
|
-
#
|
|
108
|
-
# @return [String] A string describing what this matcher verifies
|
|
109
|
-
#
|
|
110
|
-
# @example
|
|
111
|
-
# Satisfy.new { |n| n > 0 }.to_s # => "satisfy &block"
|
|
36
|
+
# @return [String]
|
|
112
37
|
def to_s
|
|
113
38
|
"satisfy &block"
|
|
114
39
|
end
|