matchi 4.0.0 → 4.1.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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +76 -15
  3. data/lib/matchi.rb +300 -0
  4. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e8454c54085d68ded31ba34e1ff0ab03cdc3dab452316ef82a15967f3de3ee4c
4
- data.tar.gz: ac11c5ab16b7e93f16189b1fa62f0d0502e0d94e67627f81dfab40cd7db48d61
3
+ metadata.gz: 730c0baf16a64842af123077150d70aee64e664ef6d5626dadf4730c1ee3425d
4
+ data.tar.gz: a934654b282119e28d4fb5c4079298770f86a834d58621240737f2473f067500
5
5
  SHA512:
6
- metadata.gz: b984f3a46f4eb2f0743b4407518261ca0539efd780b28ab0b0f476a687925bedeec2d88249160d7c148617809080593d9472957dc8571bf92295007dfaf941b7
7
- data.tar.gz: 96f609b1bc8af4ddf6a5b7b839ed0b981018d7aac53f1d26b5ea05c1629318f3cdbef64e9776dd2224c3fdd48b56e148e8e3a94df4de5d07e93164941da0fa25
6
+ metadata.gz: 75884ba2533b155d3f1e4490f9b8f6071a95aad0fca637dcdb0eee2bf60e1a3dc6bd7cab35ab93a3a7c18b680b9d789ee71bfbd7d524e6ca5ea8573520a3deec
7
+ data.tar.gz: d421e42619fc45d1f1aab95ccde2a24d6a1226f4163a4afbde02fc8e693343997fa5a9a0081d72734955b2b6a25b3f9f735766702860d2184c44f403e169f442
data/README.md CHANGED
@@ -63,6 +63,67 @@ A **Matchi** matcher is a simple Ruby object that follows these requirements:
63
63
  2. Optionally, it may implement:
64
64
  - `to_s`: Returns a human-readable description of the match criteria
65
65
 
66
+ ### Using Matchers
67
+
68
+ There are two main ways to use Matchi matchers:
69
+
70
+ #### 1. Direct Class Instantiation
71
+
72
+ You can create matchers directly from their classes:
73
+
74
+ ```ruby
75
+ matcher = Matchi::Eq.new("foo")
76
+ matcher.match? { "foo" } # => true
77
+
78
+ matcher = Matchi::BeWithin.new(0.5).of(3.0)
79
+ matcher.match? { 3.2 } # => true
80
+ ```
81
+
82
+ #### 2. Helper Methods via Module Inclusion
83
+
84
+ For a more expressive and readable syntax, you can include or extend the `Matchi` module to get access to helper methods:
85
+
86
+ ```ruby
87
+ # Including in a class for instance methods
88
+ class MyTestFramework
89
+ include Matchi
90
+
91
+ def test_equality
92
+ # Helper methods available as instance methods
93
+ matcher = eq("foo")
94
+ assert(matcher.match? { "foo" })
95
+
96
+ # Method chaining works too
97
+ assert(change(@array, :length).by(1).match? { @array << 1 })
98
+ end
99
+ end
100
+
101
+ # Extending a class for class methods
102
+ class MyAssertions
103
+ extend Matchi
104
+
105
+ def self.assert_equals(expected, actual)
106
+ eq(expected).match? { actual }
107
+ end
108
+
109
+ def self.assert_within_range(expected, delta, actual)
110
+ be_within(delta).of(expected).match? { actual }
111
+ end
112
+ end
113
+ ```
114
+
115
+ Available helper methods correspond to the built-in matchers:
116
+ - `eq` / `eql` - For equivalence matching
117
+ - `be` / `equal` - For identity matching
118
+ - `be_within` - For delta comparisons
119
+ - `match` - For regular expression matching
120
+ - `change` - For state changes
121
+ - `be_true`, `be_false`, `be_nil` - For state verification
122
+ - `be_an_instance_of` - For exact type matching
123
+ - `be_a_kind_of` - For type hierarchy matching
124
+ - `satisfy` - For custom block-based matching
125
+ - Dynamic predicate matchers (`be_*` and `have_*`)
126
+
66
127
  ### Built-in matchers
67
128
 
68
129
  Here is the collection of generic matchers.
@@ -72,15 +133,15 @@ Here is the collection of generic matchers.
72
133
  ##### `Be`
73
134
  Checks for object identity using Ruby's `equal?` method.
74
135
  ```ruby
75
- Matchi::Be.new(:foo).match? { :foo } # => true (same object)
76
- Matchi::Be.new("test").match? { "test" } # => false (different objects)
136
+ Matchi::Be.new(:foo).match? { :foo } # => true (same object)
137
+ Matchi::Be.new("test").match? { "test" } # => false (different objects)
77
138
  ```
78
139
 
79
140
  ##### `Eq`
80
141
  Verifies object equivalence using Ruby's `eql?` method.
81
142
  ```ruby
82
- Matchi::Eq.new("foo").match? { "foo" } # => true (equivalent content)
83
- Matchi::Eq.new([1, 2]).match? { [1, 2] } # => true (equivalent arrays)
143
+ Matchi::Eq.new("foo").match? { "foo" } # => true (equivalent content)
144
+ Matchi::Eq.new([1, 2]).match? { [1, 2] } # => true (equivalent arrays)
84
145
  ```
85
146
 
86
147
  #### Type and Class Matchers
@@ -113,8 +174,8 @@ Matchi::Match.new(/^foo/).match? { "barfoo" } # => false
113
174
  ##### `Satisfy`
114
175
  Provides custom matching through a block.
115
176
  ```ruby
116
- Matchi::Satisfy.new { |x| x > 0 && x < 10 }.match? { 5 } # => true
117
- Matchi::Satisfy.new { |x| x.start_with?("test") }.match? { "test_file" } # => true
177
+ Matchi::Satisfy.new { |x| x.positive? && x < 10 }.match? { 5 } # => true
178
+ Matchi::Satisfy.new { |x| x.start_with?("test") }.match? { "test_file" } # => true
118
179
  ```
119
180
 
120
181
  #### State Change Matchers
@@ -125,31 +186,31 @@ Verifies state changes in objects with multiple variation methods:
125
186
  ###### Basic Change
126
187
  ```ruby
127
188
  array = []
128
- Matchi::Change.new(array, :length).by(2).match? { array.push(1, 2) } # => true
189
+ Matchi::Change.new(array, :length).by(2).match? { array.push(1, 2) } # => true
129
190
  ```
130
191
 
131
192
  ###### Minimum Change
132
193
  ```ruby
133
194
  counter = 0
134
- Matchi::Change.new(counter, :to_i).by_at_least(2).match? { counter += 3 } # => true
195
+ Matchi::Change.new(counter, :to_i).by_at_least(2).match? { counter += 3 } # => true
135
196
  ```
136
197
 
137
198
  ###### Maximum Change
138
199
  ```ruby
139
200
  value = 10
140
- Matchi::Change.new(value, :to_i).by_at_most(5).match? { value += 3 } # => true
201
+ Matchi::Change.new(value, :to_i).by_at_most(5).match? { value += 3 } # => true
141
202
  ```
142
203
 
143
204
  ###### From-To Change
144
205
  ```ruby
145
206
  string = "hello"
146
- Matchi::Change.new(string, :upcase).from("hello").to("HELLO").match? { string.upcase! } # => true
207
+ Matchi::Change.new(string, :upcase).from("hello").to("HELLO").match? { string.upcase! } # => true
147
208
  ```
148
209
 
149
210
  ###### To-Only Change
150
211
  ```ruby
151
212
  number = 1
152
- Matchi::Change.new(number, :to_i).to(5).match? { number = 5 } # => true
213
+ Matchi::Change.new(number, :to_i).to(5).match? { number = 5 } # => true
153
214
  ```
154
215
 
155
216
  #### Numeric Matchers
@@ -166,8 +227,8 @@ Matchi::BeWithin.new(5).of(100).match? { 98 } # => true
166
227
  ##### `RaiseException`
167
228
  Verifies that code raises specific exceptions.
168
229
  ```ruby
169
- Matchi::RaiseException.new(ArgumentError).match? { raise ArgumentError } # => true
170
- Matchi::RaiseException.new(NameError).match? { undefined_variable } # => true
230
+ Matchi::RaiseException.new(ArgumentError).match? { raise ArgumentError } # => true
231
+ Matchi::RaiseException.new(NameError).match? { undefined_variable } # => true
171
232
  ```
172
233
 
173
234
  ##### `Predicate`
@@ -181,7 +242,7 @@ Matchi::Predicate.new(:be_nil).match? { nil } # => true (calls nil?)
181
242
 
182
243
  ###### Using `have_` prefix
183
244
  ```ruby
184
- Matchi::Predicate.new(:have_key, :foo).match? { { foo: 42 } } # => true (calls has_key?)
245
+ Matchi::Predicate.new(:have_key, :foo).match? { { foo: 42 } } # => true (calls has_key?)
185
246
  ```
186
247
 
187
248
  ### Custom matchers
@@ -288,7 +349,7 @@ This is why Matchi's built-in matchers are implemented with this security consid
288
349
  ```ruby
289
350
  # Implementation in Matchi::Eq
290
351
  def match?
291
- @expected.eql?(yield) # Expected value controls the comparison
352
+ @expected.eql?(yield) # Expected value controls the comparison
292
353
  end
293
354
  ```
294
355
 
data/lib/matchi.rb CHANGED
@@ -2,8 +2,308 @@
2
2
 
3
3
  # A collection of damn simple expectation matchers.
4
4
  #
5
+ # The Matchi module provides two ways to create and use matchers:
6
+ #
7
+ # 1. Direct instantiation through classes:
8
+ # ```ruby
9
+ # matcher = Matchi::Eq.new("foo")
10
+ # matcher.match? { "foo" } # => true
11
+ # ```
12
+ #
13
+ # 2. Helper methods by including/extending the Matchi module:
14
+ # ```ruby
15
+ # # Via include in a class
16
+ # class MyTestFramework
17
+ # include Matchi
18
+ #
19
+ # def test_something
20
+ # # Helpers are now available as instance methods
21
+ # matcher = eq("foo")
22
+ # matcher.match? { "foo" } # => true
23
+ #
24
+ # # Multiple helpers can be chained
25
+ # change(@array, :length).by(1).match? { @array << 1 }
26
+ # end
27
+ # end
28
+ #
29
+ # # Or via extend for direct usage
30
+ # class MyOtherFramework
31
+ # extend Matchi
32
+ #
33
+ # def self.assert_equals(expected, actual)
34
+ # eq(expected).match? { actual }
35
+ # end
36
+ # end
37
+ # ```
38
+ #
39
+ # The helper method approach provides a more readable and fluent interface,
40
+ # making tests easier to write and understand. It's particularly useful when
41
+ # integrating with testing frameworks or creating custom assertions.
42
+ #
43
+ # Available helpers include:
44
+ # - eq/eql : Equivalence matching
45
+ # - be/equal : Identity matching
46
+ # - be_within : Delta comparison
47
+ # - match : Regular expression matching
48
+ # - change : State change verification
49
+ # - be_true/be_false/be_nil : State verification
50
+ # - be_an_instance_of : Exact type matching
51
+ # - be_a_kind_of : Type hierarchy matching
52
+ # - satisfy : Custom block-based matching
53
+ # - be_* & have_* : Dynamic predicate matching
54
+ #
5
55
  # @api public
6
56
  module Matchi
57
+ # Equivalence matcher
58
+ #
59
+ # @example
60
+ # matcher = eq("foo")
61
+ # matcher.match? { "foo" } # => true
62
+ # matcher.match? { "bar" } # => false
63
+ #
64
+ # @param expected [#eql?] An expected equivalent object.
65
+ #
66
+ # @return [#match?] An equivalence matcher.
67
+ #
68
+ # @api public
69
+ def eq(expected)
70
+ ::Matchi::Eq.new(expected)
71
+ end
72
+
73
+ alias eql eq
74
+
75
+ # Identity matcher
76
+ #
77
+ # @example
78
+ # object = "foo"
79
+ # matcher = be(object)
80
+ # matcher.match? { object } # => true
81
+ # matcher.match? { "foo" } # => false
82
+ #
83
+ # @param expected [#equal?] The expected identical object.
84
+ #
85
+ # @return [#match?] An identity matcher.
86
+ #
87
+ # @api public
88
+ def be(expected)
89
+ ::Matchi::Be.new(expected)
90
+ end
91
+
92
+ alias equal be
93
+
94
+ # Comparisons matcher
95
+ #
96
+ # @example
97
+ # matcher = be_within(1).of(41)
98
+ # matcher.match? { 42 } # => true
99
+ # matcher.match? { 43 } # => false
100
+ #
101
+ # @param delta [Numeric] A numeric value.
102
+ #
103
+ # @return [#match?] A comparison matcher.
104
+ #
105
+ # @api public
106
+ def be_within(delta)
107
+ ::Matchi::BeWithin.new(delta)
108
+ end
109
+
110
+ # Regular expressions matcher
111
+ #
112
+ # @example
113
+ # matcher = match(/^foo$/)
114
+ # matcher.match? { "foo" } # => true
115
+ # matcher.match? { "bar" } # => false
116
+ #
117
+ # @param expected [#match] A regular expression.
118
+ #
119
+ # @return [#match?] A regular expression matcher.
120
+ #
121
+ # @api public
122
+ def match(expected)
123
+ ::Matchi::Match.new(expected)
124
+ end
125
+
126
+ # Expecting errors matcher
127
+ #
128
+ # @example
129
+ # matcher = raise_exception(NameError)
130
+ # matcher.match? { Boom } # => true
131
+ # matcher.match? { true } # => false
132
+ #
133
+ # @param expected [Exception, #to_s] The expected exception name.
134
+ #
135
+ # @return [#match?] An error matcher.
136
+ #
137
+ # @api public
138
+ def raise_exception(expected)
139
+ ::Matchi::RaiseException.new(expected)
140
+ end
141
+
142
+ # True matcher
143
+ #
144
+ # @example
145
+ # matcher = be_true
146
+ # matcher.match? { true } # => true
147
+ # matcher.match? { false } # => false
148
+ # matcher.match? { nil } # => false
149
+ # matcher.match? { 4 } # => false
150
+ #
151
+ # @return [#match?] A `true` matcher.
152
+ #
153
+ # @api public
154
+ def be_true
155
+ be(true)
156
+ end
157
+
158
+ # False matcher
159
+ #
160
+ # @example
161
+ # matcher = be_false
162
+ # matcher.match? { false } # => true
163
+ # matcher.match? { true } # => false
164
+ # matcher.match? { nil } # => false
165
+ # matcher.match? { 4 } # => false
166
+ #
167
+ # @return [#match?] A `false` matcher.
168
+ #
169
+ # @api public
170
+ def be_false
171
+ be(false)
172
+ end
173
+
174
+ # Nil matcher
175
+ #
176
+ # @example
177
+ # matcher = be_nil
178
+ # matcher.match? { nil } # => true
179
+ # matcher.match? { false } # => false
180
+ # matcher.match? { true } # => false
181
+ # matcher.match? { 4 } # => false
182
+ #
183
+ # @return [#match?] A `nil` matcher.
184
+ #
185
+ # @api public
186
+ def be_nil
187
+ be(nil)
188
+ end
189
+
190
+ # Type/class matcher
191
+ #
192
+ # Verifies exact class matching (no inheritance).
193
+ #
194
+ # @example
195
+ # matcher = be_an_instance_of(String)
196
+ # matcher.match? { "foo" } # => true
197
+ # matcher.match? { 4 } # => false
198
+ #
199
+ # @param expected [Class, #to_s] The expected class name.
200
+ #
201
+ # @return [#match?] A type/class matcher.
202
+ #
203
+ # @api public
204
+ def be_an_instance_of(expected)
205
+ ::Matchi::BeAnInstanceOf.new(expected)
206
+ end
207
+
208
+ # Type/class matcher
209
+ #
210
+ # Verifies class inheritance and module inclusion.
211
+ #
212
+ # @example
213
+ # matcher = be_a_kind_of(Numeric)
214
+ # matcher.match? { 42 } # => true (Integer inherits from Numeric)
215
+ # matcher.match? { 42.0 } # => true (Float inherits from Numeric)
216
+ #
217
+ # @param expected [Class, #to_s] The expected class name.
218
+ #
219
+ # @return [#match?] A type/class matcher.
220
+ #
221
+ # @api public
222
+ def be_a_kind_of(expected)
223
+ ::Matchi::BeAKindOf.new(expected)
224
+ end
225
+
226
+ # Change matcher
227
+ #
228
+ # @example
229
+ # object = []
230
+ # matcher = change(object, :length).by(1)
231
+ # matcher.match? { object << 1 } # => true
232
+ #
233
+ # object = []
234
+ # matcher = change(object, :length).by_at_least(1)
235
+ # matcher.match? { object << 1 } # => true
236
+ #
237
+ # object = []
238
+ # matcher = change(object, :length).by_at_most(1)
239
+ # matcher.match? { object << 1 } # => true
240
+ #
241
+ # object = "foo"
242
+ # matcher = change(object, :to_s).from("foo").to("FOO")
243
+ # matcher.match? { object.upcase! } # => true
244
+ #
245
+ # object = "foo"
246
+ # matcher = change(object, :to_s).to("FOO")
247
+ # matcher.match? { object.upcase! } # => true
248
+ #
249
+ # @param object [#object_id] An object.
250
+ # @param method [Symbol] The name of a method.
251
+ #
252
+ # @return [#match?] A change matcher.
253
+ #
254
+ # @api public
255
+ def change(object, method, ...)
256
+ ::Matchi::Change.new(object, method, ...)
257
+ end
258
+
259
+ # Satisfy matcher
260
+ #
261
+ # @example
262
+ # matcher = satisfy { |value| value == 42 }
263
+ # matcher.match? { 42 } # => true
264
+ #
265
+ # @yield [value] A block that defines the satisfaction criteria
266
+ # @yieldparam value The value to test
267
+ # @yieldreturn [Boolean] true if the value satisfies the criteria
268
+ #
269
+ # @return [#match?] A satisfy matcher.
270
+ #
271
+ # @api public
272
+ def satisfy(&)
273
+ ::Matchi::Satisfy.new(&)
274
+ end
275
+
276
+ private
277
+
278
+ # Predicate matcher, or default method missing behavior.
279
+ #
280
+ # @example Empty predicate matcher
281
+ # matcher = be_empty
282
+ # matcher.match? { [] } # => true
283
+ # matcher.match? { [4] } # => false
284
+ def method_missing(name, ...)
285
+ return super unless predicate_matcher_name?(name)
286
+
287
+ ::Matchi::Predicate.new(name, ...)
288
+ end
289
+
290
+ # :nocov:
291
+
292
+ # Hook method to return whether the obj can respond to id method or not.
293
+ def respond_to_missing?(name, include_private = false)
294
+ predicate_matcher_name?(name) || super
295
+ end
296
+
297
+ # :nocov:
298
+
299
+ # Predicate matcher name detector.
300
+ #
301
+ # @param name [Array, Symbol] The name of a potential predicate matcher.
302
+ #
303
+ # @return [Boolean] Indicates if it is a predicate matcher name or not.
304
+ def predicate_matcher_name?(name)
305
+ name.start_with?("be_", "have_") && !name.end_with?("!", "?")
306
+ end
7
307
  end
8
308
 
9
309
  Dir[File.join(File.dirname(__FILE__), "matchi", "*.rb")].each do |fname|
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: matchi
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.0
4
+ version: 4.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyril Kato
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2024-12-29 00:00:00.000000000 Z
10
+ date: 2024-12-31 00:00:00.000000000 Z
11
11
  dependencies: []
12
12
  description: "Collection of expectation matchers for Rubyists \U0001F939"
13
13
  email: contact@cyril.email