matchi 3.3.2 → 4.1.0

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