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.
data/lib/matchi.rb CHANGED
@@ -1,65 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Main namespace for the Matchi gem, providing a collection of expectation matchers.
4
- #
5
- # Matchi is a framework-agnostic Ruby library that offers a comprehensive set of
6
- # matchers for testing and verification purposes. Each matcher follows a consistent
7
- # interface pattern, making them easy to use and extend.
8
- #
9
- # @example Basic usage with equality matcher
10
- # Matchi::Eq.new("hello").match? { "hello" } # => true
11
- # Matchi::Eq.new("hello").match? { "world" } # => false
12
- #
13
- # @example Type checking with inheritance awareness
14
- # Matchi::BeAKindOf.new(Numeric).match? { 42 } # => true
15
- # Matchi::BeAKindOf.new(String).match? { 42 } # => false
16
- #
17
- # @example Verifying state changes
18
- # array = []
19
- # Matchi::Change.new(array, :length).by(2).match? { array.push(1, 2) } # => true
20
- #
21
- # @example Pattern matching with regular expressions
22
- # Matchi::Match.new(/^test/).match? { "test_string" } # => true
23
- #
24
- # Each matcher in the Matchi ecosystem implements a consistent interface:
25
- # - An initializer that sets up the expected values or conditions
26
- # - A #match? method that takes a block and returns a boolean
27
- # - An optional #to_s method that provides a human-readable description
28
- #
29
- # @example Creating a custom matcher
30
- # module Matchi
31
- # class BePositive
32
- # def match?
33
- # raise ArgumentError, "a block must be provided" unless block_given?
34
- # yield.positive?
35
- # end
36
- #
37
- # def to_s
38
- # "be positive"
39
- # end
40
- # end
41
- # end
42
- #
43
- # @see Matchi::Be
44
- # @see Matchi::BeAKindOf
45
- # @see Matchi::BeAnInstanceOf
46
- # @see Matchi::BeWithin
47
- # @see Matchi::Change
48
- # @see Matchi::Eq
49
- # @see Matchi::Match
50
- # @see Matchi::Predicate
51
- # @see Matchi::RaiseException
52
- # @see Matchi::Satisfy
3
+ # Framework-agnostic matchers for Ruby.
4
+ #
5
+ # A matcher is any object that responds to `match?` with a block and to
6
+ # `to_s`. The block is evaluated once and yields the actual value, or, for
7
+ # `Change` and `RaiseException`, produces the effect to observe; the matcher
8
+ # returns `true` or `false`. The expected value controls the comparison
9
+ # wherever a comparison takes place, so a hostile actual value cannot forge
10
+ # a match; `Predicate` and the `Change` refinements by delta, which use
11
+ # values produced by the object under test, depend on its honesty.
12
+ #
13
+ # @example
14
+ # Matchi::Eq.new(42).match? { 42 } # => true
53
15
  module Matchi
54
16
  end
55
17
 
56
- require "matchi/be"
57
- require "matchi/be_a_kind_of"
58
- require "matchi/be_an_instance_of"
59
- require "matchi/be_within"
60
- require "matchi/change"
61
- require "matchi/eq"
62
- require "matchi/match"
63
- require "matchi/predicate"
64
- require "matchi/raise_exception"
65
- require "matchi/satisfy"
18
+ require_relative "matchi/be"
19
+ require_relative "matchi/be_a_kind_of"
20
+ require_relative "matchi/be_an_instance_of"
21
+ require_relative "matchi/be_within"
22
+ require_relative "matchi/change"
23
+ require_relative "matchi/eq"
24
+ require_relative "matchi/match"
25
+ require_relative "matchi/predicate"
26
+ require_relative "matchi/raise_exception"
27
+ require_relative "matchi/satisfy"
metadata CHANGED
@@ -1,22 +1,18 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: matchi
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.2.0
4
+ version: 5.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyril Kato
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-01-05 00:00:00.000000000 Z
11
+ date: 2026-10-18 00:00:00.000000000 Z
12
12
  dependencies: []
13
- description: 'Matchi is a framework-agnostic Ruby library that provides a comprehensive
14
- set of expectation matchers for elegant and secure testing. Its design focuses on
15
- simplicity, security, and extensibility, making it easy to integrate with any testing
16
- framework. The library offers a rich collection of built-in matchers for common
17
- testing scenarios while maintaining a clear, consistent API that follows Ruby best
18
- practices. With minimal setup required and support for custom matchers, Matchi enables
19
- developers to write more reliable and maintainable tests.
13
+ description: 'Matchi provides small, explicit expectation matchers for any Ruby testing
14
+ framework. The expected value always controls the comparison, so the object under
15
+ test cannot forge a match.
20
16
 
21
17
  '
22
18
  email: contact@cyril.email
@@ -31,23 +27,27 @@ files:
31
27
  - lib/matchi/be_a_kind_of.rb
32
28
  - lib/matchi/be_an_instance_of.rb
33
29
  - lib/matchi/be_within.rb
30
+ - lib/matchi/be_within/of.rb
34
31
  - lib/matchi/change.rb
35
- - lib/matchi/change/by.rb
36
- - lib/matchi/change/by_at_least.rb
37
- - lib/matchi/change/by_at_most.rb
32
+ - lib/matchi/change/delta.rb
38
33
  - lib/matchi/change/from.rb
39
- - lib/matchi/change/from/to.rb
34
+ - lib/matchi/change/from_to.rb
40
35
  - lib/matchi/change/to.rb
36
+ - lib/matchi/constant.rb
41
37
  - lib/matchi/eq.rb
42
38
  - lib/matchi/match.rb
39
+ - lib/matchi/number.rb
43
40
  - lib/matchi/predicate.rb
44
41
  - lib/matchi/raise_exception.rb
45
42
  - lib/matchi/satisfy.rb
46
- homepage: https://github.com/fixrb/matchi
43
+ homepage: https://fixrb.dev/gems/matchi/
47
44
  licenses:
48
45
  - MIT
49
46
  metadata:
47
+ bug_tracker_uri: https://github.com/fixrb/matchi/issues
48
+ documentation_uri: https://rubydoc.info/gems/matchi
50
49
  rubygems_mfa_required: 'true'
50
+ source_code_uri: https://github.com/fixrb/matchi
51
51
  post_install_message:
52
52
  rdoc_options: []
53
53
  require_paths:
@@ -56,15 +56,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
56
56
  requirements:
57
57
  - - ">="
58
58
  - !ruby/object:Gem::Version
59
- version: 3.1.0
59
+ version: 3.3.0
60
60
  required_rubygems_version: !ruby/object:Gem::Requirement
61
61
  requirements:
62
62
  - - ">="
63
63
  - !ruby/object:Gem::Version
64
64
  version: '0'
65
65
  requirements: []
66
- rubygems_version: 3.3.27
66
+ rubygems_version: 3.5.22
67
67
  signing_key:
68
68
  specification_version: 4
69
- summary: "Framework-agnostic matchers for secure, elegant Ruby testing \U0001F939"
69
+ summary: Framework-agnostic matchers for secure Ruby testing
70
70
  test_files: []
@@ -1,137 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Matchi
4
- class Change
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
59
- class By
60
- # Initialize the matcher with an expected delta and a state block.
61
- #
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
69
- #
70
- # @return [By] a new instance of the matcher
71
- #
72
- # @example With positive delta
73
- # By.new(5) { counter.value }
74
- #
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 }
80
- def initialize(expected, &state)
81
- raise ::ArgumentError, "expected must be a Numeric" unless expected.is_a?(::Numeric)
82
- raise ::ArgumentError, "a block must be provided" unless block_given?
83
-
84
- @expected = expected
85
- @state = state
86
- end
87
-
88
- # Verifies that the value changes by exactly the expected amount.
89
- #
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.
93
- #
94
- # @api public
95
- #
96
- # @yield [] Block that should cause the state change
97
- # @yieldreturn [Object] The result of the block (not used)
98
- #
99
- # @return [Boolean] true if the value changed by exactly the expected amount
100
- #
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
112
- def match?
113
- raise ::ArgumentError, "a block must be provided" unless block_given?
114
-
115
- value_before = @state.call
116
- yield
117
- value_after = @state.call
118
-
119
- @expected == (value_after - value_before)
120
- end
121
-
122
- # Returns a human-readable description of the matcher.
123
- #
124
- # @api public
125
- #
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"
132
- def to_s
133
- "change by #{@expected.inspect}"
134
- end
135
- end
136
- end
137
- end
@@ -1,158 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Matchi
4
- class Change
5
- # Minimum delta matcher that verifies numeric changes meet or exceed a threshold.
6
- #
7
- # This matcher ensures that a numeric value changes by at least the specified amount
8
- # after executing a block of code. It's particularly useful when testing operations
9
- # where you want to ensure a minimum change occurs but larger changes are acceptable,
10
- # such as performance improvements, resource allocation, or progressive counters.
11
- #
12
- # @example Testing collection growth
13
- # items = []
14
- # matcher = Matchi::Change::ByAtLeast.new(2) { items.size }
15
- # matcher.match? { items.push(1, 2) } # => true # Changed by exactly 2
16
- # matcher.match? { items.push(1, 2, 3) } # => true # Changed by more than 2
17
- # matcher.match? { items.push(1) } # => false # Changed by less than 2
18
- #
19
- # @example Verifying performance improvements
20
- # class Benchmark
21
- # def initialize
22
- # @score = 100
23
- # end
24
- #
25
- # def optimize!
26
- # @score += rand(20..30) # Improvement varies
27
- # end
28
- #
29
- # def score
30
- # @score
31
- # end
32
- # end
33
- #
34
- # benchmark = Benchmark.new
35
- # matcher = Matchi::Change::ByAtLeast.new(20) { benchmark.score }
36
- # matcher.match? { benchmark.optimize! } # => true # Any improvement >= 20 passes
37
- #
38
- # @example Resource allocation
39
- # class Pool
40
- # def initialize
41
- # @capacity = 1000
42
- # end
43
- #
44
- # def allocate(minimum, maximum)
45
- # actual = rand(minimum..maximum)
46
- # @capacity -= actual
47
- # actual
48
- # end
49
- #
50
- # def available
51
- # @capacity
52
- # end
53
- # end
54
- #
55
- # pool = Pool.new
56
- # matcher = Matchi::Change::ByAtLeast.new(50) { -pool.available }
57
- # matcher.match? { pool.allocate(50, 100) } # => true # Allocates at least 50
58
- #
59
- # @example Price threshold monitoring
60
- # class Stock
61
- # attr_reader :price
62
- #
63
- # def initialize(price)
64
- # @price = price
65
- # end
66
- #
67
- # def fluctuate!
68
- # @price += rand(-10.0..20.0)
69
- # end
70
- # end
71
- #
72
- # stock = Stock.new(100.0)
73
- # matcher = Matchi::Change::ByAtLeast.new(10.0) { stock.price }
74
- # matcher.match? { stock.fluctuate! } # => true if price rises by 10.0 or more
75
- #
76
- # @note This matcher verifies minimum changes only. For exact changes, use By,
77
- # and for maximum changes, use ByAtMost.
78
- #
79
- # @see Matchi::Change::By For exact change validation
80
- # @see Matchi::Change::ByAtMost For maximum change validation
81
- # @see Matchi::Change::To For final value validation
82
- class ByAtLeast
83
- # Initialize the matcher with a minimum expected change and a state block.
84
- #
85
- # @api public
86
- #
87
- # @param expected [Numeric] The minimum amount by which the value should change
88
- # @param state [Proc] Block that retrieves the current value
89
- #
90
- # @raise [ArgumentError] if expected is not a Numeric
91
- # @raise [ArgumentError] if expected is negative
92
- # @raise [ArgumentError] if no state block is provided
93
- #
94
- # @return [ByAtLeast] a new instance of the matcher
95
- #
96
- # @example With integer minimum
97
- # ByAtLeast.new(5) { counter.value }
98
- #
99
- # @example With floating point minimum
100
- # ByAtLeast.new(0.5) { temperature.celsius }
101
- def initialize(expected, &state)
102
- raise ::ArgumentError, "expected must be a Numeric" unless expected.is_a?(::Numeric)
103
- raise ::ArgumentError, "a block must be provided" unless block_given?
104
- raise ::ArgumentError, "expected must be non-negative" if expected.negative?
105
-
106
- @expected = expected
107
- @state = state
108
- end
109
-
110
- # Checks if the value changes by at least the expected amount.
111
- #
112
- # This method compares the value before and after executing the provided block,
113
- # ensuring that the difference is greater than or equal to the expected minimum.
114
- # This is useful when you want to verify that a change meets a minimum threshold.
115
- #
116
- # @api public
117
- #
118
- # @yield [] Block that should cause the state change
119
- # @yieldreturn [Object] The result of the block (not used)
120
- #
121
- # @return [Boolean] true if the value changed by at least the expected amount
122
- #
123
- # @raise [ArgumentError] if no block is provided
124
- #
125
- # @example Basic usage
126
- # counter = 0
127
- # matcher = ByAtLeast.new(5) { counter }
128
- # matcher.match? { counter += 7 } # => true # Changed by more than minimum
129
- #
130
- # @example Edge case - exact minimum
131
- # items = []
132
- # matcher = ByAtLeast.new(2) { items.size }
133
- # matcher.match? { items.push(1, 2) } # => true # Changed by exactly minimum
134
- def match?
135
- raise ::ArgumentError, "a block must be provided" unless block_given?
136
-
137
- value_before = @state.call
138
- yield
139
- value_after = @state.call
140
-
141
- @expected <= (value_after - value_before)
142
- end
143
-
144
- # Returns a human-readable description of the matcher.
145
- #
146
- # @api public
147
- #
148
- # @return [String] A string describing what this matcher verifies
149
- #
150
- # @example
151
- # ByAtLeast.new(5).to_s # => "change by at least 5"
152
- # ByAtLeast.new(2.5).to_s # => "change by at least 2.5"
153
- def to_s
154
- "change by at least #{@expected.inspect}"
155
- end
156
- end
157
- end
158
- end
@@ -1,171 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Matchi
4
- class Change
5
- # Maximum delta matcher that verifies numeric changes don't exceed a limit.
6
- #
7
- # This matcher ensures that a numeric value changes by no more than the specified amount
8
- # after executing a block of code. It's particularly useful when testing operations
9
- # where you want to enforce an upper bound on changes, such as rate limiting,
10
- # resource consumption, or controlled increments.
11
- #
12
- # @example Testing controlled collection growth
13
- # items = []
14
- # matcher = Matchi::Change::ByAtMost.new(2) { items.size }
15
- # matcher.match? { items.push(1) } # => true # Changed by less than limit
16
- # matcher.match? { items.push(1, 2) } # => true # Changed by exactly limit
17
- # matcher.match? { items.push(1, 2, 3) } # => false # Changed by more than limit
18
- #
19
- # @example Rate limiting
20
- # class RateLimiter
21
- # def initialize
22
- # @requests = 0
23
- # end
24
- #
25
- # def process_batch(items)
26
- # items.each do |item|
27
- # break if @requests >= 3 # Rate limit
28
- # process_item(item)
29
- # end
30
- # end
31
- #
32
- # private
33
- #
34
- # def process_item(item)
35
- # @requests += 1
36
- # # Processing logic...
37
- # end
38
- #
39
- # def requests
40
- # @requests
41
- # end
42
- # end
43
- #
44
- # limiter = RateLimiter.new
45
- # matcher = Matchi::Change::ByAtMost.new(3) { limiter.requests }
46
- # matcher.match? { limiter.process_batch([1, 2, 3, 4, 5]) } # => true
47
- #
48
- # @example Resource consumption
49
- # class ResourcePool
50
- # attr_reader :used
51
- #
52
- # def initialize
53
- # @used = 0
54
- # end
55
- #
56
- # def allocate(requested)
57
- # available = 5 - @used # Maximum pool size is 5
58
- # granted = [requested, available].min
59
- # @used += granted
60
- # granted
61
- # end
62
- # end
63
- #
64
- # pool = ResourcePool.new
65
- # matcher = Matchi::Change::ByAtMost.new(2) { pool.used }
66
- # matcher.match? { pool.allocate(2) } # => true
67
- # matcher.match? { pool.allocate(3) } # => false
68
- #
69
- # @example Score adjustments
70
- # class GameScore
71
- # attr_reader :value
72
- #
73
- # def initialize
74
- # @value = 100
75
- # end
76
- #
77
- # def apply_penalty(amount)
78
- # max_penalty = 10
79
- # actual_penalty = [amount, max_penalty].min
80
- # @value -= actual_penalty
81
- # end
82
- # end
83
- #
84
- # score = GameScore.new
85
- # matcher = Matchi::Change::ByAtMost.new(10) { -score.value }
86
- # matcher.match? { score.apply_penalty(5) } # => true # Small penalty
87
- # matcher.match? { score.apply_penalty(15) } # => true # Limited to max
88
- #
89
- # @note This matcher verifies maximum changes only. For exact changes, use By,
90
- # and for minimum changes, use ByAtLeast.
91
- #
92
- # @see Matchi::Change::By For exact change validation
93
- # @see Matchi::Change::ByAtLeast For minimum change validation
94
- # @see Matchi::Change::To For final value validation
95
- class ByAtMost
96
- # Initialize the matcher with a maximum allowed change and a state block.
97
- #
98
- # @api public
99
- #
100
- # @param expected [Numeric] The maximum amount by which the value should change
101
- # @param state [Proc] Block that retrieves the current value
102
- #
103
- # @raise [ArgumentError] if expected is not a Numeric
104
- # @raise [ArgumentError] if expected is negative
105
- # @raise [ArgumentError] if no state block is provided
106
- #
107
- # @return [ByAtMost] a new instance of the matcher
108
- #
109
- # @example With integer maximum
110
- # ByAtMost.new(5) { counter.value }
111
- #
112
- # @example With floating point maximum
113
- # ByAtMost.new(0.5) { temperature.celsius }
114
- def initialize(expected, &state)
115
- raise ::ArgumentError, "expected must be a Numeric" unless expected.is_a?(::Numeric)
116
- raise ::ArgumentError, "a block must be provided" unless block_given?
117
- raise ::ArgumentError, "expected must be non-negative" if expected.negative?
118
-
119
- @expected = expected
120
- @state = state
121
- end
122
-
123
- # Checks if the value changes by no more than the expected amount.
124
- #
125
- # This method compares the value before and after executing the provided block,
126
- # ensuring that the absolute difference is less than or equal to the expected maximum.
127
- # This is useful for enforcing upper bounds on state changes.
128
- #
129
- # @api public
130
- #
131
- # @yield [] Block that should cause the state change
132
- # @yieldreturn [Object] The result of the block (not used)
133
- #
134
- # @return [Boolean] true if the value changed by at most the expected amount
135
- #
136
- # @raise [ArgumentError] if no block is provided
137
- #
138
- # @example Basic usage with growth
139
- # users = []
140
- # matcher = ByAtMost.new(2) { users.size }
141
- # matcher.match? { users.push('alice') } # => true
142
- #
143
- # @example With negative changes
144
- # stock = 10
145
- # matcher = ByAtMost.new(3) { stock }
146
- # matcher.match? { stock -= 2 } # => true
147
- def match?
148
- raise ::ArgumentError, "a block must be provided" unless block_given?
149
-
150
- value_before = @state.call
151
- yield
152
- value_after = @state.call
153
-
154
- @expected >= (value_after - value_before)
155
- end
156
-
157
- # Returns a human-readable description of the matcher.
158
- #
159
- # @api public
160
- #
161
- # @return [String] A string describing what this matcher verifies
162
- #
163
- # @example
164
- # ByAtMost.new(5).to_s # => "change by at most 5"
165
- # ByAtMost.new(2.5).to_s # => "change by at most 2.5"
166
- def to_s
167
- "change by at most #{@expected.inspect}"
168
- end
169
- end
170
- end
171
- end