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.
@@ -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