matchi 3.3.2 → 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.
@@ -16,53 +16,42 @@ module Matchi
16
16
  # @param block [Proc] A block of code.
17
17
  def initialize(name, *args, **kwargs, &block)
18
18
  @name = String(name)
19
-
20
- raise ::ArgumentError unless valid_name?
19
+ raise ::ArgumentError, "invalid predicate name format" unless valid_name?
21
20
 
22
21
  @args = args
23
22
  @kwargs = kwargs
24
23
  @block = block
25
24
  end
26
25
 
27
- # @return [Array] The method name with any arguments to send to the subject.
28
- def expected
29
- [method_name, @args, @kwargs, @block]
30
- end
31
-
32
26
  # Boolean comparison between the actual value and the expected value.
33
27
  #
34
28
  # @example
35
29
  # require "matchi/predicate"
36
30
  #
37
31
  # matcher = Matchi::Predicate.new(:be_empty)
38
- #
39
- # matcher.expected # => [:empty?, [], {}, nil]
40
- # matcher.matches? { [] } # => true
32
+ # matcher.match? { [] } # => true
41
33
  #
42
34
  # @example
43
35
  # require "matchi/predicate"
44
36
  #
45
37
  # matcher = Matchi::Predicate.new(:have_key, :foo)
46
- #
47
- # matcher.expected # => [:has_key?, [:foo], {}, nil]
48
- # matcher.matches? { { foo: 42 } } # => true
38
+ # matcher.match? { { foo: 42 } } # => true
49
39
  #
50
40
  # @yieldreturn [#object_id] The actual value to receive the method request.
51
41
  #
52
42
  # @return [Boolean] A boolean returned by the actual value being tested.
53
- def matches?
43
+ def match?
44
+ raise ::ArgumentError, "a block must be provided" unless block_given?
45
+
54
46
  value = yield.send(method_name, *@args, **@kwargs, &@block)
55
47
  return value if [false, true].include?(value)
56
48
 
57
49
  raise ::TypeError, "Boolean expected, but #{value.class} instance returned."
58
50
  end
59
51
 
60
- # A string containing a human-readable representation of the matcher.
61
- def inspect
62
- "#{self.class}(#{@name}, *#{@args.inspect}, **#{@kwargs.inspect}, &#{@block.inspect})"
63
- end
64
-
65
52
  # Returns a string representing the matcher.
53
+ #
54
+ # @return [String] a human-readable description of the matcher
66
55
  def to_s
67
56
  (
68
57
  "#{@name.tr("_", " ")} " + [
@@ -3,9 +3,6 @@
3
3
  module Matchi
4
4
  # *Expecting errors* matcher.
5
5
  class RaiseException
6
- # @return [String] The expected exception name.
7
- attr_reader :expected
8
-
9
6
  # Initialize the matcher with a descendant of class Exception.
10
7
  #
11
8
  # @example
@@ -16,6 +13,10 @@ module Matchi
16
13
  # @param expected [Exception, #to_s] The expected exception name.
17
14
  def initialize(expected)
18
15
  @expected = String(expected)
16
+ return if /\A[A-Z]/.match?(@expected)
17
+
18
+ raise ::ArgumentError,
19
+ "expected must start with an uppercase letter (got: #{@expected})"
19
20
  end
20
21
 
21
22
  # Boolean comparison between the actual value and the expected value.
@@ -24,30 +25,42 @@ module Matchi
24
25
  # require "matchi/raise_exception"
25
26
  #
26
27
  # matcher = Matchi::RaiseException.new(NameError)
27
- #
28
- # matcher.expected # => "NameError"
29
- # matcher.matches? { Boom } # => true
28
+ # matcher.match? { Boom } # => true
30
29
  #
31
30
  # @yieldreturn [#object_id] The actual value to compare to the expected
32
31
  # one.
33
32
  #
34
33
  # @return [Boolean] Comparison between actual and expected values.
35
- def matches?
36
- yield
37
- rescue self.class.const_get(expected) => _e
38
- true
39
- else
40
- false
41
- end
34
+ def match?
35
+ raise ::ArgumentError, "a block must be provided" unless block_given?
36
+
37
+ klass = expected_class
38
+ raise ::ArgumentError, "expected exception class must inherit from Exception" unless klass <= ::Exception
42
39
 
43
- # A string containing a human-readable representation of the matcher.
44
- def inspect
45
- "#{self.class}(#{expected})"
40
+ begin
41
+ yield
42
+ false
43
+ rescue Exception => e # rubocop:disable Lint/RescueException
44
+ e.class <= klass # Checks if the class of the thrown exception is klass or one of its subclasses
45
+ end
46
46
  end
47
47
 
48
48
  # Returns a string representing the matcher.
49
+ #
50
+ # @return [String] a human-readable description of the matcher
49
51
  def to_s
50
- "raise exception #{expected}"
52
+ "raise exception #{@expected}"
53
+ end
54
+
55
+ private
56
+
57
+ # Resolves the expected class name to an actual Class object.
58
+ # This method handles both string and symbol class names through constant resolution.
59
+ #
60
+ # @return [Class] the resolved class
61
+ # @raise [NameError] if the class doesn't exist
62
+ def expected_class
63
+ ::Object.const_get(@expected)
51
64
  end
52
65
  end
53
66
  end
@@ -3,9 +3,6 @@
3
3
  module Matchi
4
4
  # *Satisfy* matcher.
5
5
  class Satisfy
6
- # @return [Proc] A block of code.
7
- attr_reader :expected
8
-
9
6
  # Initialize the matcher with a block.
10
7
  #
11
8
  # @example
@@ -15,6 +12,8 @@ module Matchi
15
12
  #
16
13
  # @param block [Proc] A block of code.
17
14
  def initialize(&block)
15
+ raise ::ArgumentError, "a block must be provided" unless block_given?
16
+
18
17
  @expected = block
19
18
  end
20
19
 
@@ -24,24 +23,21 @@ module Matchi
24
23
  # require "matchi/satisfy"
25
24
  #
26
25
  # matcher = Matchi::Satisfy.new { |value| value == 42 }
27
- #
28
- # matcher.expected # => #<Proc:0x00007fbaafc65540>
29
- # matcher.matches? { 42 } # => true
26
+ # matcher.match? { 42 } # => true
30
27
  #
31
28
  # @yieldreturn [#object_id] The actual value to compare to the expected
32
29
  # one.
33
30
  #
34
31
  # @return [Boolean] Comparison between actual and expected values.
35
- def matches?
36
- expected.call(yield)
37
- end
32
+ def match?
33
+ raise ::ArgumentError, "a block must be provided" unless block_given?
38
34
 
39
- # A string containing a human-readable representation of the matcher.
40
- def inspect
41
- "#{self.class}(&block)"
35
+ @expected.call(yield)
42
36
  end
43
37
 
44
38
  # Returns a string representing the matcher.
39
+ #
40
+ # @return [String] a human-readable description of the matcher
45
41
  def to_s
46
42
  "satisfy &block"
47
43
  end
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,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: matchi
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.3.2
4
+ version: 4.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyril Kato
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-01-25 00:00:00.000000000 Z
10
+ date: 2024-12-31 00:00:00.000000000 Z
12
11
  dependencies: []
13
12
  description: "Collection of expectation matchers for Rubyists \U0001F939"
14
13
  email: contact@cyril.email
@@ -20,6 +19,7 @@ files:
20
19
  - README.md
21
20
  - lib/matchi.rb
22
21
  - lib/matchi/be.rb
22
+ - lib/matchi/be_a_kind_of.rb
23
23
  - lib/matchi/be_an_instance_of.rb
24
24
  - lib/matchi/be_within.rb
25
25
  - lib/matchi/be_within/of.rb
@@ -40,7 +40,6 @@ licenses:
40
40
  - MIT
41
41
  metadata:
42
42
  rubygems_mfa_required: 'true'
43
- post_install_message:
44
43
  rdoc_options: []
45
44
  require_paths:
46
45
  - lib
@@ -55,8 +54,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
55
54
  - !ruby/object:Gem::Version
56
55
  version: '0'
57
56
  requirements: []
58
- rubygems_version: 3.4.19
59
- signing_key:
57
+ rubygems_version: 3.6.2
60
58
  specification_version: 4
61
59
  summary: "Collection of expectation matchers for Rubyists \U0001F939"
62
60
  test_files: []