matchi 4.1.0 → 4.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,68 +1,118 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Matchi
4
- # *Type/class* matcher for inheritance-aware type checking.
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 clear way to check if an object is an instance of a
7
- # specific class or one of its subclasses. It leverages Ruby's native === operator
8
- # which reliably handles class hierarchy relationships.
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
- # Initialize the matcher with (the name of) a class or module.
56
+ # Creates a new type matcher for the specified class.
19
57
  #
20
- # @example
21
- # require "matchi/be_a_kind_of"
58
+ # @api public
22
59
  #
23
- # Matchi::BeAKindOf.new(String)
24
- # Matchi::BeAKindOf.new("String")
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 ::ArgumentError,
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 uses the case equality operator (===) which provides a reliable
41
- # way to check class hierarchy relationships in Ruby. When a class is the
42
- # receiver of ===, it returns true if the argument is an instance of that
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
- # @example Class hierarchy check
46
- # class Animal; end
47
- # class Dog < Animal; end
85
+ # @api public
48
86
  #
49
- # matcher = Matchi::BeAKindOf.new(Animal)
50
- # matcher.match? { Dog.new } # => true
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 string representing the matcher.
108
+ # Returns a human-readable description of the matcher.
64
109
  #
65
- # @return [String] a human-readable description of the matcher
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
- # @return [Class] the resolved class
76
- # @raise [NameError] if the class doesn't exist
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
- # *Type/class* matcher with enhanced class checking.
4
+ # Type matcher that checks if an object is an exact instance of a specific class.
5
5
  #
6
- # This matcher aims to provide a more reliable way to check if an object is an exact
7
- # instance of a specific class (not a subclass). While not foolproof, it uses a more
8
- # robust method to get the actual class of an object that helps resist common
9
- # attempts at type checking manipulation.
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? { "foo" } # => true
16
- # matcher.match? { :foo } # => false
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
- # def instance_of?(klass)
27
- # self.class == klass
28
- # end
16
+ # @example Inheritance behavior
17
+ # class Animal; end
18
+ # class Dog < Animal; end
29
19
  #
30
- # def is_a?(klass)
31
- # "".is_a?(klass) # Delegates to a real String
32
- # end
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
- # def kind_of?(klass)
35
- # is_a?(klass) # Maintains Ruby's kind_of? alias for is_a?
36
- # end
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.class # => String
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 } # => false
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
- # @example
52
- # require "matchi/be_an_instance_of"
56
+ # @api public
53
57
  #
54
- # Matchi::BeAnInstanceOf.new(String)
55
- # Matchi::BeAnInstanceOf.new("String")
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 a specific Ruby reflection technique to get the true class of
71
- # an object, bypassing potential method overrides:
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
- # 1. ::Object.instance_method(:class) retrieves the original, unoverridden 'class'
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
- # This approach is more reliable than obj.class because it uses Ruby's method
79
- # binding mechanism to call the original implementation directly. While not
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 string representing the matcher.
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
- # @return [String] a human-readable description of the matcher
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
- # @return [Class] the resolved class
114
- # @raise [NameError] if the class doesn't exist
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
@@ -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
- # Wraps the target of a be_within matcher.
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 a wrapper of the be_within matcher with a numeric value.
31
+ # Initialize the matcher with a delta value.
9
32
  #
10
- # @example
11
- # require "matchi/be_within"
33
+ # @api public
34
+ #
35
+ # @param delta [Numeric] The maximum allowed difference from the expected value
12
36
  #
13
- # Matchi::BeWithin.new(1)
37
+ # @raise [ArgumentError] if delta is not a Numeric
38
+ # @raise [ArgumentError] if delta is negative
14
39
  #
15
- # @param delta [Numeric] A numeric value.
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
- # Specifies an expected numeric value.
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
- # require "matchi/be_within"
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
- # be_within_wrapper = Matchi::BeWithin.new(1)
29
- # be_within_wrapper.of(41)
73
+ # @api public
30
74
  #
31
- # @param expected [Numeric] The expected value.
75
+ # @param expected [Numeric] The reference value to compare against
32
76
  #
33
- # @return [#match?] A *be_within of* matcher.
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
@@ -2,43 +2,113 @@
2
2
 
3
3
  module Matchi
4
4
  class Change
5
- # *Change by* matcher.
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 object and a block.
60
+ # Initialize the matcher with an expected delta and a state block.
8
61
  #
9
- # @example
10
- # require "matchi/change/by"
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
- # object = []
70
+ # @return [By] a new instance of the matcher
13
71
  #
14
- # Matchi::Change::By.new(1) { object.length }
72
+ # @example With positive delta
73
+ # By.new(5) { counter.value }
15
74
  #
16
- # @param expected [#object_id] An expected delta.
17
- # @param state [Proc] A block of code to execute to get the
18
- # state of the object.
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 = state
85
+ @state = state
25
86
  end
26
87
 
27
- # Boolean comparison on the expected change by comparing the value
28
- # before and after the code execution.
88
+ # Verifies that the value changes by exactly the expected amount.
29
89
  #
30
- # @example
31
- # require "matchi/change/by"
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
- # object = []
94
+ # @api public
34
95
  #
35
- # matcher = Matchi::Change::By.new(1) { object.length }
36
- # matcher.match? { object << "foo" } # => true
96
+ # @yield [] Block that should cause the state change
97
+ # @yieldreturn [Object] The result of the block (not used)
37
98
  #
38
- # @yieldreturn [#object_id] The block of code to execute.
99
+ # @return [Boolean] true if the value changed by exactly the expected amount
39
100
  #
40
- # @return [Boolean] Comparison between the value before and after the
41
- # code execution.
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 string representing the matcher.
122
+ # Returns a human-readable description of the matcher.
123
+ #
124
+ # @api public
53
125
  #
54
- # @return [String] a human-readable description of the matcher
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