matchi 4.0.0 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
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