matchi 4.1.1 → 4.2.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/README.md +148 -219
- data/lib/matchi/be.rb +58 -16
- data/lib/matchi/be_a_kind_of.rb +87 -34
- data/lib/matchi/be_an_instance_of.rb +74 -62
- data/lib/matchi/be_within.rb +125 -14
- data/lib/matchi/change/by.rb +99 -22
- data/lib/matchi/change/by_at_least.rb +118 -21
- data/lib/matchi/change/by_at_most.rb +132 -22
- data/lib/matchi/change/from/to.rb +92 -25
- data/lib/matchi/change/from.rb +31 -1
- data/lib/matchi/change/to.rb +72 -23
- data/lib/matchi/change.rb +119 -45
- data/lib/matchi/eq.rb +58 -16
- data/lib/matchi/match.rb +56 -16
- data/lib/matchi/predicate.rb +122 -33
- data/lib/matchi/raise_exception.rb +89 -22
- data/lib/matchi/satisfy.rb +87 -16
- data/lib/matchi.rb +51 -297
- metadata +12 -5
- data/lib/matchi/be_within/of.rb +0 -51
data/lib/matchi/be_a_kind_of.rb
CHANGED
@@ -1,68 +1,118 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Matchi
|
4
|
-
#
|
4
|
+
# Type matcher that checks if an object is an instance of a class or one of its subclasses.
|
5
5
|
#
|
6
|
-
# This matcher provides a
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
# @example Basic usage
|
11
|
-
# require "matchi/be_a_kind_of"
|
6
|
+
# This matcher provides a reliable way to verify object types while respecting Ruby's
|
7
|
+
# inheritance hierarchy. It uses the case equality operator (===) which is Ruby's
|
8
|
+
# built-in mechanism for type checking, ensuring consistent behavior with Ruby's
|
9
|
+
# own type system.
|
12
10
|
#
|
11
|
+
# @example Basic usage with simple types
|
13
12
|
# matcher = Matchi::BeAKindOf.new(Numeric)
|
14
13
|
# matcher.match? { 42 } # => true
|
15
14
|
# matcher.match? { 42.0 } # => true
|
16
15
|
# matcher.match? { "42" } # => false
|
16
|
+
#
|
17
|
+
# @example Working with inheritance hierarchies
|
18
|
+
# class Animal; end
|
19
|
+
# class Dog < Animal; end
|
20
|
+
# class GermanShepherd < Dog; end
|
21
|
+
#
|
22
|
+
# matcher = Matchi::BeAKindOf.new(Animal)
|
23
|
+
# matcher.match? { Dog.new } # => true
|
24
|
+
# matcher.match? { GermanShepherd.new } # => true
|
25
|
+
# matcher.match? { Object.new } # => false
|
26
|
+
#
|
27
|
+
# @example Using with modules and interfaces
|
28
|
+
# module Swimmable
|
29
|
+
# def swim; end
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# class Duck
|
33
|
+
# include Swimmable
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# matcher = Matchi::BeAKindOf.new(Swimmable)
|
37
|
+
# matcher.match? { Duck.new } # => true
|
38
|
+
# matcher.match? { Object.new } # => false
|
39
|
+
#
|
40
|
+
# @example Different ways to specify the class
|
41
|
+
# # Using class directly
|
42
|
+
# Matchi::BeAKindOf.new(String)
|
43
|
+
#
|
44
|
+
# # Using class name as string
|
45
|
+
# Matchi::BeAKindOf.new("String")
|
46
|
+
#
|
47
|
+
# # Using class name as symbol
|
48
|
+
# Matchi::BeAKindOf.new(:String)
|
49
|
+
#
|
50
|
+
# # Using namespaced class
|
51
|
+
# Matchi::BeAKindOf.new("MyModule::MyClass")
|
52
|
+
#
|
53
|
+
# @see Matchi::BeAnInstanceOf
|
54
|
+
# @see https://ruby-doc.org/core/Module.html#method-i-3D-3D-3D
|
17
55
|
class BeAKindOf
|
18
|
-
#
|
56
|
+
# Creates a new type matcher for the specified class.
|
19
57
|
#
|
20
|
-
# @
|
21
|
-
# require "matchi/be_a_kind_of"
|
58
|
+
# @api public
|
22
59
|
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
# Matchi::BeAKindOf.new(:String)
|
60
|
+
# @param expected [Class, #to_s] The expected class or its name
|
61
|
+
# Can be provided as a Class object, String, or Symbol
|
26
62
|
#
|
27
|
-
# @param expected [Class, #to_s] The expected class name
|
28
63
|
# @raise [ArgumentError] if the class name doesn't start with an uppercase letter
|
64
|
+
#
|
65
|
+
# @return [BeAKindOf] a new instance of the matcher
|
66
|
+
#
|
67
|
+
# @example
|
68
|
+
# BeAKindOf.new(String) # Using class
|
69
|
+
# BeAKindOf.new("String") # Using string
|
70
|
+
# BeAKindOf.new(:String) # Using symbol
|
29
71
|
def initialize(expected)
|
30
72
|
@expected = String(expected)
|
31
73
|
return if /\A[A-Z]/.match?(@expected)
|
32
74
|
|
33
|
-
raise
|
75
|
+
raise ArgumentError,
|
34
76
|
"expected must start with an uppercase letter (got: #{@expected})"
|
35
77
|
end
|
36
78
|
|
37
|
-
# Checks if the yielded object is an instance of the expected class
|
38
|
-
# or one of its subclasses.
|
79
|
+
# Checks if the yielded object is an instance of the expected class or its subclasses.
|
39
80
|
#
|
40
|
-
# This method
|
41
|
-
# way to check class hierarchy relationships
|
42
|
-
#
|
43
|
-
# class or one of its subclasses.
|
81
|
+
# This method leverages Ruby's case equality operator (===) which provides a reliable
|
82
|
+
# way to check class hierarchy relationships. When a class is the receiver of ===,
|
83
|
+
# it returns true if the argument is an instance of that class or one of its subclasses.
|
44
84
|
#
|
45
|
-
# @
|
46
|
-
# class Animal; end
|
47
|
-
# class Dog < Animal; end
|
85
|
+
# @api public
|
48
86
|
#
|
49
|
-
#
|
50
|
-
#
|
51
|
-
# matcher.match? { Animal.new } # => true
|
52
|
-
# matcher.match? { Object.new } # => false
|
87
|
+
# @yield [] Block that returns the object to check
|
88
|
+
# @yieldreturn [Object] The object to verify the type of
|
53
89
|
#
|
54
|
-
# @yieldreturn [Object] the actual value to check
|
55
90
|
# @return [Boolean] true if the object is an instance of the expected class or one of its subclasses
|
91
|
+
#
|
56
92
|
# @raise [ArgumentError] if no block is provided
|
93
|
+
# @raise [NameError] if the expected class cannot be found
|
94
|
+
#
|
95
|
+
# @example Simple type check
|
96
|
+
# matcher = BeAKindOf.new(Numeric)
|
97
|
+
# matcher.match? { 42 } # => true
|
98
|
+
#
|
99
|
+
# @example With inheritance
|
100
|
+
# matcher = BeAKindOf.new(Animal)
|
101
|
+
# matcher.match? { Dog.new } # => true
|
57
102
|
def match?
|
58
103
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
59
104
|
|
60
105
|
expected_class === yield # rubocop:disable Style/CaseEquality
|
61
106
|
end
|
62
107
|
|
63
|
-
# Returns a
|
108
|
+
# Returns a human-readable description of the matcher.
|
64
109
|
#
|
65
|
-
# @
|
110
|
+
# @api public
|
111
|
+
#
|
112
|
+
# @return [String] A string describing what this matcher verifies
|
113
|
+
#
|
114
|
+
# @example
|
115
|
+
# BeAKindOf.new(String).to_s # => "be a kind of String"
|
66
116
|
def to_s
|
67
117
|
"be a kind of #{@expected}"
|
68
118
|
end
|
@@ -70,10 +120,13 @@ module Matchi
|
|
70
120
|
private
|
71
121
|
|
72
122
|
# Resolves the expected class name to an actual Class object.
|
73
|
-
# This method handles both string and symbol class names through constant resolution.
|
74
123
|
#
|
75
|
-
# @
|
76
|
-
#
|
124
|
+
# @api private
|
125
|
+
#
|
126
|
+
# @return [Class] The resolved class object
|
127
|
+
# @raise [NameError] If the class name cannot be resolved to an actual class
|
128
|
+
#
|
129
|
+
# @note This method handles both string and symbol class names through constant resolution
|
77
130
|
def expected_class
|
78
131
|
::Object.const_get(@expected)
|
79
132
|
end
|
@@ -1,62 +1,71 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Matchi
|
4
|
-
#
|
4
|
+
# Type matcher that checks if an object is an exact instance of a specific class.
|
5
5
|
#
|
6
|
-
# This matcher
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
6
|
+
# This matcher provides a secure way to verify an object's exact type, ensuring it
|
7
|
+
# matches a specific class without including subclasses. It uses Ruby's method binding
|
8
|
+
# mechanism to bypass potential method overrides, providing better protection against
|
9
|
+
# type check spoofing than standard instance_of? checks.
|
10
10
|
#
|
11
11
|
# @example Basic usage
|
12
|
-
# require "matchi/be_an_instance_of"
|
13
|
-
#
|
14
12
|
# matcher = Matchi::BeAnInstanceOf.new(String)
|
15
|
-
# matcher.match? { "
|
16
|
-
# matcher.match? { :
|
17
|
-
#
|
18
|
-
# @example Enhanced class checking in practice
|
19
|
-
# # Consider a class that attempts to masquerade as String by overriding
|
20
|
-
# # common type checking methods:
|
21
|
-
# class MaliciousString
|
22
|
-
# def class
|
23
|
-
# ::String
|
24
|
-
# end
|
13
|
+
# matcher.match? { "test" } # => true
|
14
|
+
# matcher.match? { :test } # => false
|
25
15
|
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
#
|
16
|
+
# @example Inheritance behavior
|
17
|
+
# class Animal; end
|
18
|
+
# class Dog < Animal; end
|
29
19
|
#
|
30
|
-
#
|
31
|
-
#
|
32
|
-
#
|
20
|
+
# matcher = Matchi::BeAnInstanceOf.new(Animal)
|
21
|
+
# matcher.match? { Animal.new } # => true
|
22
|
+
# matcher.match? { Dog.new } # => false # Subclass doesn't match
|
33
23
|
#
|
34
|
-
#
|
35
|
-
#
|
36
|
-
#
|
24
|
+
# @example Secure type checking
|
25
|
+
# # Consider a class that attempts to masquerade as String:
|
26
|
+
# class MaliciousString
|
27
|
+
# def class; String; end
|
28
|
+
# def instance_of?(klass); true; end
|
37
29
|
# end
|
38
30
|
#
|
39
31
|
# obj = MaliciousString.new
|
40
|
-
# obj.
|
41
|
-
# obj.is_a?(String) # => true
|
42
|
-
# obj.kind_of?(String) # => true
|
43
|
-
# obj.instance_of?(String) # => true
|
32
|
+
# obj.instance_of?(String) # => true (spoofed)
|
44
33
|
#
|
45
|
-
# # Using our enhanced checking approach:
|
46
34
|
# matcher = Matchi::BeAnInstanceOf.new(String)
|
47
|
-
# matcher.match? { obj }
|
35
|
+
# matcher.match? { obj } # => false (secure)
|
36
|
+
#
|
37
|
+
# @example Different ways to specify the class
|
38
|
+
# # Using class directly
|
39
|
+
# Matchi::BeAnInstanceOf.new(String)
|
40
|
+
#
|
41
|
+
# # Using class name as string
|
42
|
+
# Matchi::BeAnInstanceOf.new("String")
|
43
|
+
#
|
44
|
+
# # Using class name as symbol
|
45
|
+
# Matchi::BeAnInstanceOf.new(:String)
|
46
|
+
#
|
47
|
+
# # Using namespaced class
|
48
|
+
# Matchi::BeAnInstanceOf.new("MyModule::MyClass")
|
49
|
+
#
|
50
|
+
# @see Matchi::BeAKindOf
|
51
|
+
# @see https://ruby-doc.org/core/Object.html#method-i-instance_of-3F
|
52
|
+
# @see https://ruby-doc.org/core/Module.html#method-i-bind_call
|
48
53
|
class BeAnInstanceOf
|
49
54
|
# Initialize the matcher with (the name of) a class or module.
|
50
55
|
#
|
51
|
-
# @
|
52
|
-
# require "matchi/be_an_instance_of"
|
56
|
+
# @api public
|
53
57
|
#
|
54
|
-
#
|
55
|
-
#
|
56
|
-
# Matchi::BeAnInstanceOf.new(:String)
|
58
|
+
# @param expected [Class, #to_s] The expected class or its name
|
59
|
+
# Can be provided as a Class object, String, or Symbol
|
57
60
|
#
|
58
|
-
# @param expected [Class, #to_s] The expected class name
|
59
61
|
# @raise [ArgumentError] if the class name doesn't start with an uppercase letter
|
62
|
+
#
|
63
|
+
# @return [BeAnInstanceOf] a new instance of the matcher
|
64
|
+
#
|
65
|
+
# @example
|
66
|
+
# BeAnInstanceOf.new(String) # Using class
|
67
|
+
# BeAnInstanceOf.new("String") # Using string
|
68
|
+
# BeAnInstanceOf.new(:String) # Using symbol
|
60
69
|
def initialize(expected)
|
61
70
|
@expected = String(expected)
|
62
71
|
return if /\A[A-Z]/.match?(@expected)
|
@@ -67,30 +76,25 @@ module Matchi
|
|
67
76
|
|
68
77
|
# Securely checks if the yielded object is an instance of the expected class.
|
69
78
|
#
|
70
|
-
# This method uses
|
71
|
-
#
|
79
|
+
# This method uses Ruby's method binding mechanism to get the true class of an object,
|
80
|
+
# bypassing potential method overrides. While not completely foolproof, it provides
|
81
|
+
# better protection against type check spoofing than using regular method calls which
|
82
|
+
# can be overridden.
|
72
83
|
#
|
73
|
-
#
|
74
|
-
# method from the Object class
|
75
|
-
# 2. .bind_call(obj) binds this original method to our object and calls it,
|
76
|
-
# ensuring we get the real class regardless of method overrides
|
84
|
+
# @api public
|
77
85
|
#
|
78
|
-
#
|
79
|
-
#
|
80
|
-
# completely foolproof, it provides better protection against type check spoofing
|
81
|
-
# than using regular method calls which can be overridden.
|
86
|
+
# @yield [] Block that returns the object to check
|
87
|
+
# @yieldreturn [Object] The object to verify the type of
|
82
88
|
#
|
83
|
-
# @example Basic class check
|
84
|
-
# matcher = Matchi::BeAnInstanceOf.new(String)
|
85
|
-
# matcher.match? { "test" } # => true
|
86
|
-
# matcher.match? { StringIO.new } # => false
|
87
|
-
#
|
88
|
-
# @see https://ruby-doc.org/core/Method.html#method-i-bind_call
|
89
|
-
# @see https://ruby-doc.org/core/UnboundMethod.html
|
90
|
-
#
|
91
|
-
# @yieldreturn [Object] the actual value to check
|
92
89
|
# @return [Boolean] true if the object's actual class is exactly the expected class
|
90
|
+
#
|
93
91
|
# @raise [ArgumentError] if no block is provided
|
92
|
+
# @raise [NameError] if the expected class cannot be found
|
93
|
+
#
|
94
|
+
# @example Simple type check
|
95
|
+
# matcher = BeAnInstanceOf.new(String)
|
96
|
+
# matcher.match? { "test" } # => true
|
97
|
+
# matcher.match? { StringIO.new } # => false
|
94
98
|
def match?
|
95
99
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
96
100
|
|
@@ -98,9 +102,14 @@ module Matchi
|
|
98
102
|
expected_class == actual_class
|
99
103
|
end
|
100
104
|
|
101
|
-
# Returns a
|
105
|
+
# Returns a human-readable description of the matcher.
|
106
|
+
#
|
107
|
+
# @api public
|
108
|
+
#
|
109
|
+
# @return [String] A string describing what this matcher verifies
|
102
110
|
#
|
103
|
-
# @
|
111
|
+
# @example
|
112
|
+
# BeAnInstanceOf.new(String).to_s # => "be an instance of String"
|
104
113
|
def to_s
|
105
114
|
"be an instance of #{@expected}"
|
106
115
|
end
|
@@ -108,10 +117,13 @@ module Matchi
|
|
108
117
|
private
|
109
118
|
|
110
119
|
# Resolves the expected class name to an actual Class object.
|
111
|
-
# This method handles both string and symbol class names through constant resolution.
|
112
120
|
#
|
113
|
-
# @
|
114
|
-
#
|
121
|
+
# @api private
|
122
|
+
#
|
123
|
+
# @return [Class] The resolved class object
|
124
|
+
# @raise [NameError] If the class name cannot be resolved to an actual class
|
125
|
+
#
|
126
|
+
# @note This method handles both string and symbol class names through constant resolution
|
115
127
|
def expected_class
|
116
128
|
::Object.const_get(@expected)
|
117
129
|
end
|
data/lib/matchi/be_within.rb
CHANGED
@@ -1,18 +1,47 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative File.join("be_within", "of")
|
4
|
-
|
5
3
|
module Matchi
|
6
|
-
#
|
4
|
+
# Delta comparison matcher that checks if a numeric value is within a specified range.
|
5
|
+
#
|
6
|
+
# This matcher verifies that a numeric value falls within a certain distance (delta)
|
7
|
+
# of an expected value. It's particularly useful for floating-point comparisons or
|
8
|
+
# when checking if a value is approximately equal to another within a given tolerance.
|
9
|
+
#
|
10
|
+
# @example Basic usage with integers
|
11
|
+
# matcher = Matchi::BeWithin.new(1).of(10)
|
12
|
+
# matcher.match? { 9 } # => true
|
13
|
+
# matcher.match? { 10 } # => true
|
14
|
+
# matcher.match? { 11 } # => true
|
15
|
+
# matcher.match? { 12 } # => false
|
16
|
+
#
|
17
|
+
# @example Floating point comparisons
|
18
|
+
# matcher = Matchi::BeWithin.new(0.1).of(3.14)
|
19
|
+
# matcher.match? { 3.1 } # => true
|
20
|
+
# matcher.match? { 3.2 } # => true
|
21
|
+
# matcher.match? { 3.0 } # => false
|
22
|
+
#
|
23
|
+
# @example Working with percentages
|
24
|
+
# matcher = Matchi::BeWithin.new(5).of(100) # 5% margin
|
25
|
+
# matcher.match? { 95 } # => true
|
26
|
+
# matcher.match? { 105 } # => true
|
27
|
+
# matcher.match? { 110 } # => false
|
28
|
+
#
|
29
|
+
# @see https://ruby-doc.org/core/Numeric.html#method-i-abs
|
7
30
|
class BeWithin
|
8
|
-
# Initialize
|
31
|
+
# Initialize the matcher with a delta value.
|
9
32
|
#
|
10
|
-
# @
|
11
|
-
#
|
33
|
+
# @api public
|
34
|
+
#
|
35
|
+
# @param delta [Numeric] The maximum allowed difference from the expected value
|
12
36
|
#
|
13
|
-
#
|
37
|
+
# @raise [ArgumentError] if delta is not a Numeric
|
38
|
+
# @raise [ArgumentError] if delta is negative
|
14
39
|
#
|
15
|
-
# @
|
40
|
+
# @return [BeWithin] a new instance of the matcher
|
41
|
+
#
|
42
|
+
# @example
|
43
|
+
# BeWithin.new(0.5) # Using float
|
44
|
+
# BeWithin.new(2) # Using integer
|
16
45
|
def initialize(delta)
|
17
46
|
raise ::ArgumentError, "delta must be a Numeric" unless delta.is_a?(::Numeric)
|
18
47
|
raise ::ArgumentError, "delta must be non-negative" if delta.negative?
|
@@ -20,19 +49,101 @@ module Matchi
|
|
20
49
|
@delta = delta
|
21
50
|
end
|
22
51
|
|
23
|
-
#
|
52
|
+
# Raises NotImplementedError as this is not a complete matcher.
|
53
|
+
#
|
54
|
+
# This class acts as a builder for the actual matcher, which is created
|
55
|
+
# by calling the #of method. Direct use of #match? is not supported.
|
56
|
+
#
|
57
|
+
# @api public
|
58
|
+
#
|
59
|
+
# @raise [NotImplementedError] always, as this method should not be used
|
24
60
|
#
|
25
61
|
# @example
|
26
|
-
#
|
62
|
+
# # Don't do this:
|
63
|
+
# BeWithin.new(0.5).match? { 42 } # Raises NotImplementedError
|
64
|
+
#
|
65
|
+
# # Do this instead:
|
66
|
+
# BeWithin.new(0.5).of(42).match? { 41.8 } # Works correctly
|
67
|
+
def match?
|
68
|
+
raise ::NotImplementedError, "BeWithin is not a complete matcher. Use BeWithin#of to create a valid matcher."
|
69
|
+
end
|
70
|
+
|
71
|
+
# Specifies the expected reference value.
|
27
72
|
#
|
28
|
-
#
|
29
|
-
# be_within_wrapper.of(41)
|
73
|
+
# @api public
|
30
74
|
#
|
31
|
-
# @param expected [Numeric] The
|
75
|
+
# @param expected [Numeric] The reference value to compare against
|
32
76
|
#
|
33
|
-
# @
|
77
|
+
# @raise [ArgumentError] if expected is not a Numeric
|
78
|
+
#
|
79
|
+
# @return [#match?] A matcher that checks if a value is within the specified range
|
80
|
+
#
|
81
|
+
# @example
|
82
|
+
# be_within_wrapper = BeWithin.new(0.5)
|
83
|
+
# be_within_wrapper.of(3.14) # Creates matcher for values in range 2.64..3.64
|
34
84
|
def of(expected)
|
35
85
|
Of.new(@delta, expected)
|
36
86
|
end
|
87
|
+
|
88
|
+
# Nested class that performs the actual comparison.
|
89
|
+
#
|
90
|
+
# This class implements the actual matching logic, comparing the provided value
|
91
|
+
# against the expected value using the specified delta.
|
92
|
+
class Of
|
93
|
+
# Initialize the matcher with a delta and an expected value.
|
94
|
+
#
|
95
|
+
# @api private
|
96
|
+
#
|
97
|
+
# @param delta [Numeric] The maximum allowed difference from the expected value
|
98
|
+
# @param expected [Numeric] The reference value to compare against
|
99
|
+
#
|
100
|
+
# @raise [ArgumentError] if delta is not a Numeric
|
101
|
+
# @raise [ArgumentError] if expected is not a Numeric
|
102
|
+
# @raise [ArgumentError] if delta is negative
|
103
|
+
def initialize(delta, expected)
|
104
|
+
raise ::ArgumentError, "delta must be a Numeric" unless delta.is_a?(::Numeric)
|
105
|
+
raise ::ArgumentError, "expected must be a Numeric" unless expected.is_a?(::Numeric)
|
106
|
+
raise ::ArgumentError, "delta must be non-negative" if delta.negative?
|
107
|
+
|
108
|
+
@delta = delta
|
109
|
+
@expected = expected
|
110
|
+
end
|
111
|
+
|
112
|
+
# Checks if the yielded value is within the accepted range.
|
113
|
+
#
|
114
|
+
# The value is considered within range if its absolute difference from the
|
115
|
+
# expected value is less than or equal to the specified delta.
|
116
|
+
#
|
117
|
+
# @api public
|
118
|
+
#
|
119
|
+
# @yield [] Block that returns the value to check
|
120
|
+
# @yieldreturn [Numeric] The value to verify
|
121
|
+
#
|
122
|
+
# @return [Boolean] true if the value is within the accepted range
|
123
|
+
#
|
124
|
+
# @raise [ArgumentError] if no block is provided
|
125
|
+
#
|
126
|
+
# @example
|
127
|
+
# matcher = BeWithin.new(0.5).of(3.14)
|
128
|
+
# matcher.match? { 3.0 } # => true
|
129
|
+
# matcher.match? { 4.0 } # => false
|
130
|
+
def match?
|
131
|
+
raise ::ArgumentError, "a block must be provided" unless block_given?
|
132
|
+
|
133
|
+
(@expected - yield).abs <= @delta
|
134
|
+
end
|
135
|
+
|
136
|
+
# Returns a human-readable description of the matcher.
|
137
|
+
#
|
138
|
+
# @api public
|
139
|
+
#
|
140
|
+
# @return [String] A string describing what this matcher verifies
|
141
|
+
#
|
142
|
+
# @example
|
143
|
+
# BeWithin.new(0.5).of(3.14).to_s # => "be within 0.5 of 3.14"
|
144
|
+
def to_s
|
145
|
+
"be within #{@delta} of #{@expected}"
|
146
|
+
end
|
147
|
+
end
|
37
148
|
end
|
38
149
|
end
|
data/lib/matchi/change/by.rb
CHANGED
@@ -2,43 +2,113 @@
|
|
2
2
|
|
3
3
|
module Matchi
|
4
4
|
class Change
|
5
|
-
#
|
5
|
+
# Exact delta matcher that verifies precise numeric changes in object state.
|
6
|
+
#
|
7
|
+
# This matcher ensures that a numeric value changes by exactly the specified amount
|
8
|
+
# after executing a block of code. It's particularly useful for testing operations
|
9
|
+
# that should produce precise, predictable changes in numeric attributes, such as
|
10
|
+
# counters, quantities, or calculated values.
|
11
|
+
#
|
12
|
+
# @example Testing collection size changes
|
13
|
+
# array = []
|
14
|
+
# matcher = Matchi::Change::By.new(2) { array.size }
|
15
|
+
# matcher.match? { array.concat([1, 2]) } # => true
|
16
|
+
# matcher.match? { array.push(3) } # => false # Changed by 1
|
17
|
+
# matcher.match? { array.push(3, 4, 5) } # => false # Changed by 3
|
18
|
+
#
|
19
|
+
# @example Verifying numeric calculations
|
20
|
+
# counter = 100
|
21
|
+
# matcher = Matchi::Change::By.new(-10) { counter }
|
22
|
+
# matcher.match? { counter -= 10 } # => true
|
23
|
+
# matcher.match? { counter -= 15 } # => false # Changed too much
|
24
|
+
#
|
25
|
+
# @example Working with custom numeric attributes
|
26
|
+
# class Account
|
27
|
+
# attr_reader :balance
|
28
|
+
#
|
29
|
+
# def initialize(initial_balance)
|
30
|
+
# @balance = initial_balance
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# def deposit(amount)
|
34
|
+
# @balance += amount
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# def withdraw(amount)
|
38
|
+
# @balance -= amount
|
39
|
+
# end
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# account = Account.new(1000)
|
43
|
+
# matcher = Matchi::Change::By.new(50) { account.balance }
|
44
|
+
# matcher.match? { account.deposit(50) } # => true
|
45
|
+
# matcher.match? { account.deposit(75) } # => false # Changed by 75
|
46
|
+
#
|
47
|
+
# @example Handling floating point values
|
48
|
+
# temperature = 20.0
|
49
|
+
# matcher = Matchi::Change::By.new(1.5) { temperature }
|
50
|
+
# matcher.match? { temperature += 1.5 } # => true
|
51
|
+
# matcher.match? { temperature += 1.6 } # => false # Not exact match
|
52
|
+
#
|
53
|
+
# @note This matcher checks for exact changes, use ByAtLeast or ByAtMost for
|
54
|
+
# more flexible delta comparisons
|
55
|
+
#
|
56
|
+
# @see Matchi::Change::ByAtLeast For minimum change validation
|
57
|
+
# @see Matchi::Change::ByAtMost For maximum change validation
|
58
|
+
# @see Matchi::Change::From::To For exact value transition validation
|
6
59
|
class By
|
7
|
-
# Initialize the matcher with an
|
60
|
+
# Initialize the matcher with an expected delta and a state block.
|
8
61
|
#
|
9
|
-
# @
|
10
|
-
#
|
62
|
+
# @api public
|
63
|
+
#
|
64
|
+
# @param expected [Numeric] The exact amount by which the value should change
|
65
|
+
# @param state [Proc] Block that retrieves the current value
|
66
|
+
#
|
67
|
+
# @raise [ArgumentError] if expected is not a Numeric
|
68
|
+
# @raise [ArgumentError] if no state block is provided
|
11
69
|
#
|
12
|
-
#
|
70
|
+
# @return [By] a new instance of the matcher
|
13
71
|
#
|
14
|
-
#
|
72
|
+
# @example With positive delta
|
73
|
+
# By.new(5) { counter.value }
|
15
74
|
#
|
16
|
-
# @
|
17
|
-
#
|
18
|
-
#
|
75
|
+
# @example With negative delta
|
76
|
+
# By.new(-3) { stock_level.quantity }
|
77
|
+
#
|
78
|
+
# @example With floating point delta
|
79
|
+
# By.new(0.5) { temperature.celsius }
|
19
80
|
def initialize(expected, &state)
|
20
81
|
raise ::ArgumentError, "expected must be a Numeric" unless expected.is_a?(::Numeric)
|
21
82
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
22
83
|
|
23
84
|
@expected = expected
|
24
|
-
@state
|
85
|
+
@state = state
|
25
86
|
end
|
26
87
|
|
27
|
-
#
|
28
|
-
# before and after the code execution.
|
88
|
+
# Verifies that the value changes by exactly the expected amount.
|
29
89
|
#
|
30
|
-
#
|
31
|
-
#
|
90
|
+
# This method compares the value before and after executing the provided block,
|
91
|
+
# ensuring that the difference matches the expected delta exactly. It's useful
|
92
|
+
# for cases where precision is important and approximate changes are not acceptable.
|
32
93
|
#
|
33
|
-
#
|
94
|
+
# @api public
|
34
95
|
#
|
35
|
-
#
|
36
|
-
#
|
96
|
+
# @yield [] Block that should cause the state change
|
97
|
+
# @yieldreturn [Object] The result of the block (not used)
|
37
98
|
#
|
38
|
-
# @
|
99
|
+
# @return [Boolean] true if the value changed by exactly the expected amount
|
39
100
|
#
|
40
|
-
# @
|
41
|
-
#
|
101
|
+
# @raise [ArgumentError] if no block is provided
|
102
|
+
#
|
103
|
+
# @example Basic usage
|
104
|
+
# counter = 0
|
105
|
+
# matcher = By.new(5) { counter }
|
106
|
+
# matcher.match? { counter += 5 } # => true
|
107
|
+
#
|
108
|
+
# @example Failed match
|
109
|
+
# items = []
|
110
|
+
# matcher = By.new(2) { items.size }
|
111
|
+
# matcher.match? { items.push(1) } # => false # Changed by 1
|
42
112
|
def match?
|
43
113
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
44
114
|
|
@@ -49,9 +119,16 @@ module Matchi
|
|
49
119
|
@expected == (value_after - value_before)
|
50
120
|
end
|
51
121
|
|
52
|
-
# Returns a
|
122
|
+
# Returns a human-readable description of the matcher.
|
123
|
+
#
|
124
|
+
# @api public
|
53
125
|
#
|
54
|
-
# @return [String]
|
126
|
+
# @return [String] A string describing what this matcher verifies
|
127
|
+
#
|
128
|
+
# @example
|
129
|
+
# By.new(5).to_s # => "change by 5"
|
130
|
+
# By.new(-3).to_s # => "change by -3"
|
131
|
+
# By.new(2.5).to_s # => "change by 2.5"
|
55
132
|
def to_s
|
56
133
|
"change by #{@expected.inspect}"
|
57
134
|
end
|