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.
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matchi
4
+ # Validation of the numbers given to matchers: deltas and expected values
5
+ # must be finite and real, so that a `NaN`, an infinity or a `Complex`
6
+ # cannot build a matcher that never, or always, passes.
7
+ #
8
+ # @api private
9
+ module Number
10
+ # @param value [Object]
11
+ # @return [Boolean] whether value is a finite real Numeric
12
+ def self.finite_real?(value)
13
+ ::Numeric === value && value.real? && value.finite?
14
+ end
15
+
16
+ # @param value [Object]
17
+ # @param role [String] the argument name, for the error message
18
+ # @return [Numeric] value
19
+ # @raise [ArgumentError] when value is not a finite real Numeric
20
+ def self.finite_real!(value, role)
21
+ raise ::ArgumentError, "#{role} must be a finite real Numeric (got: #{value.inspect})" unless finite_real?(value)
22
+
23
+ value
24
+ end
25
+
26
+ # @param value [Object]
27
+ # @param role [String] the argument name, for the error message
28
+ # @return [Numeric] value
29
+ # @raise [ArgumentError] when value is not a finite real non-negative Numeric
30
+ def self.non_negative!(value, role)
31
+ finite_real!(value, role)
32
+ raise ::ArgumentError, "#{role} must be non-negative (got: #{value.inspect})" if value.negative?
33
+
34
+ value
35
+ end
36
+ end
37
+ end
@@ -1,172 +1,73 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "constant"
4
+
3
5
  module Matchi
4
- # Predicate matcher that checks if an object responds to a predicate method with a truthy value.
5
- #
6
- # This matcher converts a predicate name (starting with 'be_' or 'have_') into a method call
7
- # ending with '?' and verifies that calling this method returns a boolean value. It's useful
8
- # for testing state-checking methods and collection properties. The matcher supports two types
9
- # of predicate formats: 'be_*' which converts to '*?' and 'have_*' which converts to 'has_*?'.
10
- #
11
- # @example Basic empty check
12
- # matcher = Matchi::Predicate.new(:be_empty)
13
- # matcher.match? { [] } # => true
14
- # matcher.match? { [1, 2] } # => false
15
- #
16
- # @example Object property check with arguments
17
- # matcher = Matchi::Predicate.new(:have_key, :name)
18
- # matcher.match? { { name: "Alice" } } # => true
19
- # matcher.match? { { age: 30 } } # => false
20
- #
21
- # @example Using keyword arguments
22
- # class Record
23
- # def complete?(status: nil)
24
- # status.nil? || status == :validated
25
- # end
26
- # end
6
+ # Predicate matcher: passes when a predicate method of the actual value
7
+ # returns `true`. The matcher name `be_xxx` calls `xxx?`, and `have_xxx`
8
+ # calls `has_xxx?`, with `Kernel#public_send` bound to the actual value.
9
+ # The predicate must return `true` or `false`; any other value is an error,
10
+ # not a truthy pass.
27
11
  #
28
- # matcher = Matchi::Predicate.new(:be_complete, status: :validated)
29
- # matcher.match? { Record.new } # => true
12
+ # Since the actual value answers the call, this matcher depends on the
13
+ # honesty of the object under test. Use it for the object's own documented
14
+ # predicates.
30
15
  #
31
- # @example With block arguments
32
- # class List
33
- # def all?(&block)
34
- # block ? super : empty?
35
- # end
36
- # end
37
- #
38
- # matcher = Matchi::Predicate.new(:be_all) { |x| x.positive? }
39
- # matcher.match? { [1, 2, 3] } # => true
40
- # matcher.match? { [-1, 2, 3] } # => false
41
- #
42
- # @see https://ruby-doc.org/core/Object.html#method-i-respond_to-3F
16
+ # @example
17
+ # Matchi::Predicate.new(:be_empty).match? { [] } # => true
18
+ # Matchi::Predicate.new(:have_key, :a).match? { { a: 1 } } # => true
43
19
  class Predicate
44
- # Mapping of predicate prefixes to their method name transformations.
45
- # Each entry defines how a prefix should be converted to its method form.
46
- #
47
- # @api private
48
- PREFIXES = {
49
- "be_" => ->(name) { "#{name.gsub(/\A(?:be_)/, "")}?" },
50
- "have_" => ->(name) { "#{name.gsub(/\A(?:have_)/, "has_")}?" }
51
- }.freeze
20
+ # A matcher name: `be_` or `have_`, then a letter, then letters, digits
21
+ # or underscores, in any script. It never ends with `?` or `!`.
22
+ NAME = /\A(be|have)_\p{Alpha}[\p{Alnum}_]*\z/
52
23
 
53
- # Initialize the matcher with a predicate name and optional arguments.
54
- #
55
- # @api public
56
- #
57
- # @param name [#to_s] A matcher name starting with 'be_' or 'have_'
58
- # @param args [Array] Optional positional arguments to pass to the predicate method
59
- # @param kwargs [Hash] Optional keyword arguments to pass to the predicate method
60
- # @param block [Proc] Optional block to pass to the predicate method
61
- #
62
- # @raise [ArgumentError] if the predicate name format is invalid
63
- #
64
- # @return [Predicate] a new instance of the matcher
65
- #
66
- # @example With simple predicate
67
- # Predicate.new(:be_empty) # Empty check
68
- #
69
- # @example With arguments
70
- # Predicate.new(:have_key, :id) # Key presence check
71
- #
72
- # @example With keyword arguments
73
- # Predicate.new(:be_valid, status: true) # Conditional validation
24
+ # @param name [Symbol, String] `be_xxx` or `have_xxx`
25
+ # @param args [Array] positional arguments for the predicate
26
+ # @param kwargs [Hash] keyword arguments for the predicate
27
+ # @param block [Proc, nil] a block for the predicate
28
+ # @raise [ArgumentError] when the name does not start with be_ or have_
74
29
  def initialize(name, *args, **kwargs, &block)
75
30
  @name = String(name)
76
- raise ::ArgumentError, "invalid predicate name format" unless valid_name?
31
+ raise ::ArgumentError, "name must match #{NAME.inspect} (got: #{name.inspect})" unless NAME.match?(@name)
77
32
 
33
+ @method_name = method_name
78
34
  @args = args
79
35
  @kwargs = kwargs
80
36
  @block = block
81
37
  end
82
38
 
83
- # Checks if the yielded object responds to and returns true for the predicate.
84
- #
85
- # This method converts the predicate name into a method name according to the prefix
86
- # mapping and calls it on the yielded object with any provided arguments. The method
87
- # must return a boolean value, or a TypeError will be raised.
88
- #
89
- # @api public
90
- #
91
- # @yield [] Block that returns the object to check
92
- # @yieldreturn [Object] The object to call the predicate method on
93
- #
94
- # @return [Boolean] true if the predicate method returns true
95
- #
96
- # @raise [ArgumentError] if no block is provided
97
- # @raise [TypeError] if predicate method returns non-boolean value
98
- #
99
- # @example Basic usage
100
- # matcher = Predicate.new(:be_empty)
101
- # matcher.match? { [] } # => true
102
- # matcher.match? { [1] } # => false
103
- #
104
- # @example With arguments
105
- # matcher = Predicate.new(:have_key, :id)
106
- # matcher.match? { { id: 1 } } # => true
39
+ # @yieldreturn [Object] the actual value
40
+ # @return [Boolean]
41
+ # @raise [ArgumentError] when no block is given
42
+ # @raise [TypeError] when the predicate returns something other than true or false
43
+ # @raise [NoMethodError] when the actual value has no such public predicate
107
44
  def match?
108
45
  raise ::ArgumentError, "a block must be provided" unless block_given?
109
46
 
110
- value = yield.send(method_name, *@args, **@kwargs, &@block)
111
- return value if [false, true].include?(value)
47
+ result = ::Kernel.instance_method(:public_send).bind_call(yield, @method_name, *@args, **@kwargs, &@block)
48
+ return result if true.equal?(result) || false.equal?(result)
112
49
 
113
- raise ::TypeError, "Boolean expected, but #{value.class} instance returned."
50
+ raise ::TypeError, "#{@method_name} must return true or false (got a #{Constant.label(result_class(result))})"
114
51
  end
115
52
 
116
- # Returns a human-readable description of the matcher.
117
- #
118
- # @api public
119
- #
120
- # @return [String] A string describing what this matcher verifies
121
- #
122
- # @example Simple predicate
123
- # Predicate.new(:be_empty).to_s # => "be empty"
124
- #
125
- # @example With arguments
126
- # Predicate.new(:have_key, :id).to_s # => "have key :id"
127
- #
128
- # @example With keyword arguments
129
- # Predicate.new(:be_valid, active: true).to_s # => "be valid active: true"
53
+ # @return [String]
130
54
  def to_s
131
- (
132
- "#{@name.tr("_", " ")} " + [
133
- @args.map(&:inspect).join(", "),
134
- @kwargs.map { |k, v| "#{k}: #{v.inspect}" }.join(", "),
135
- (@block.nil? ? "" : "&block")
136
- ].reject { |i| i.eql?("") }.join(", ")
137
- ).strip
55
+ arguments = @args.map(&:inspect) + @kwargs.map { |key, value| "#{key}: #{value.inspect}" }
56
+ arguments << "&block" if @block
57
+
58
+ [@name.tr("_", " "), arguments.join(", ")].reject(&:empty?).join(" ")
138
59
  end
139
60
 
140
61
  private
141
62
 
142
- # Converts the predicate name into the actual method name to call.
143
- #
144
- # @api private
145
- #
146
- # @return [Symbol] The method name to call on the object
147
- # @raise [ArgumentError] if the predicate prefix is unknown
148
- #
149
- # @example
150
- # # With be_ prefix
151
- # method_name # => :empty? (from be_empty)
152
- # # With have_ prefix
153
- # method_name # => :has_key? (from have_key)
154
63
  def method_name
155
- _, transform = PREFIXES.find { |prefix, _| @name.start_with?(prefix) }
156
- return transform.call(@name) if transform
64
+ return "#{@name.delete_prefix("be_")}?" if @name.start_with?("be_")
157
65
 
158
- raise ::ArgumentError, "unknown prefix in predicate name: #{@name}"
66
+ "has_#{@name.delete_prefix("have_")}?"
159
67
  end
160
68
 
161
- # Verifies that the predicate name follows the required format.
162
- #
163
- # @api private
164
- #
165
- # @return [Boolean] true if the name follows the required format
166
- def valid_name?
167
- return false if @name.match?(/[?!]\z/)
168
-
169
- PREFIXES.keys.any? { |prefix| @name.start_with?(prefix) }
69
+ def result_class(result)
70
+ ::Kernel.instance_method(:class).bind_call(result)
170
71
  end
171
72
  end
172
73
  end
@@ -1,133 +1,101 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "constant"
4
+
3
5
  module Matchi
4
- # Exception matcher that verifies if a block of code raises a specific exception.
5
- #
6
- # This matcher ensures that code raises an expected exception by executing it within
7
- # a controlled environment. It supports matching against specific exception classes
8
- # and their subclasses, providing a reliable way to test error handling.
9
- #
10
- # @example Basic usage with standard exceptions
11
- # matcher = Matchi::RaiseException.new(ArgumentError)
12
- # matcher.match? { raise ArgumentError } # => true
13
- # matcher.match? { raise RuntimeError } # => false
14
- # matcher.match? { "no error" } # => false
15
- #
16
- # @example Working with inheritance hierarchy
17
- # class CustomError < StandardError; end
18
- # class SpecificError < CustomError; end
19
- #
20
- # matcher = Matchi::RaiseException.new(CustomError)
21
- # matcher.match? { raise CustomError } # => true
22
- # matcher.match? { raise SpecificError } # => true
23
- # matcher.match? { raise StandardError } # => false
6
+ # Exception matcher: passes when evaluating the block raises an exception
7
+ # of the expected class or of one of its descendants (`Module#===`, so the
8
+ # exception cannot lie about its class), optionally with an expected
9
+ # message: a String the message must be `eql?` to, or a Regexp it must
10
+ # match.
24
11
  #
25
- # @example Using string class names
26
- # matcher = Matchi::RaiseException.new("ArgumentError")
27
- # matcher.match? { raise ArgumentError } # => true
12
+ # Every `Exception` is caught, including `SystemExit` and `Interrupt`: a
13
+ # specification must be able to assert on any of them, so an unexpected
14
+ # `exit` or Ctrl-C inside the block is reported as a failure, not
15
+ # propagated. The expected class can be given directly, or by name; see
16
+ # {Matchi::Constant}.
28
17
  #
29
- # @example With custom exception hierarchies
30
- # module Api
31
- # class Error < StandardError; end
32
- # class AuthenticationError < Error; end
33
- # end
34
- #
35
- # matcher = Matchi::RaiseException.new("Api::Error")
36
- # matcher.match? { raise Api::AuthenticationError } # => true
37
- #
38
- # @see https://ruby-doc.org/core/Exception.html
39
- # @see https://ruby-doc.org/core/StandardError.html
18
+ # @example
19
+ # Matchi::RaiseException.new(ZeroDivisionError).match? { 1 / 0 } # => true
20
+ # Matchi::RaiseException.new(:StandardError).match? { 1 / 0 } # => true
21
+ # Matchi::RaiseException.new(ZeroDivisionError, /by 0/).match? { 1 / 0 } # => true
22
+ # Matchi::RaiseException.new(ZeroDivisionError, "nope").match? { 1 / 0 } # => false
23
+ # Matchi::RaiseException.new(IOError).match? { 1 / 0 } # => false
40
24
  class RaiseException
41
- # Initialize the matcher with an exception class.
42
- #
43
- # @api public
44
- #
45
- # @param expected [Exception, #to_s] The expected exception class or its name
46
- # Can be provided as a Class, String, or Symbol
47
- #
48
- # @raise [ArgumentError] if the class name doesn't start with an uppercase letter
49
- #
50
- # @return [RaiseException] a new instance of the matcher
51
- #
52
- # @example
53
- # RaiseException.new(ArgumentError) # Using class
54
- # RaiseException.new("ArgumentError") # Using string
55
- # RaiseException.new(:ArgumentError) # Using symbol
56
- # RaiseException.new("Api::CustomError") # Using namespaced class
57
- def initialize(expected)
58
- @expected = String(expected)
59
- return if /\A[A-Z]/.match?(@expected)
60
-
61
- raise ::ArgumentError,
62
- "expected must start with an uppercase letter (got: #{@expected})"
25
+ # @param expected [Class, String, Symbol] the expected exception class, or its name
26
+ # @param message [String, Regexp, nil] the expected message, exact or as a pattern
27
+ # @raise [ArgumentError] when expected is neither an Exception class nor a constant name,
28
+ # or message is neither a String nor a Regexp
29
+ def initialize(expected, message = nil)
30
+ @expected = Constant.normalize(expected, ::Class)
31
+ @message = message
32
+ validate_class!
33
+ validate_message!
63
34
  end
64
35
 
65
- # Checks if the yielded block raises the expected exception.
66
- #
67
- # This method executes the provided block within a begin/rescue clause and verifies
68
- # that it raises an exception of the expected type. It also handles inheritance,
69
- # allowing subclasses of the expected exception to match.
70
- #
71
- # @api public
72
- #
73
- # @yield [] Block that should raise an exception
74
- # @yieldreturn [Object] The result of the block (if it doesn't raise)
75
- #
76
- # @return [Boolean] true if the block raises the expected exception
77
- #
78
- # @raise [ArgumentError] if no block is provided
79
- # @raise [ArgumentError] if expected exception class doesn't inherit from Exception
80
- # @raise [NameError] if the expected exception class cannot be found
81
- #
82
- # @example Standard usage
83
- # matcher = RaiseException.new(ArgumentError)
84
- # matcher.match? { raise ArgumentError } # => true
85
- #
86
- # @example With inheritance
87
- # matcher = RaiseException.new(StandardError)
88
- # matcher.match? { raise ArgumentError } # => true
89
- #
90
- # @example Without exception
91
- # matcher = RaiseException.new(StandardError)
92
- # matcher.match? { "no error" } # => false
36
+ # @yieldreturn [Object] ignored, the block is evaluated for its exception
37
+ # @return [Boolean]
38
+ # @raise [ArgumentError] when no block is given
39
+ # @raise [NameError] when the name is not defined
40
+ # @raise [TypeError] when the name does not resolve to an Exception class
93
41
  def match?
94
42
  raise ::ArgumentError, "a block must be provided" unless block_given?
95
43
 
44
+ # Resolved before the block, so that a NameError from the name is not
45
+ # mistaken for an exception raised by the block.
96
46
  klass = expected_class
97
- raise ::ArgumentError, "expected exception class must inherit from Exception" unless klass <= ::Exception
98
47
 
99
48
  begin
100
49
  yield
101
50
  false
102
51
  rescue ::Exception => e # rubocop:disable Lint/RescueException
103
- e.class <= klass
52
+ klass === e && message?(e)
104
53
  end
105
54
  end
106
55
 
107
- # Returns a human-readable description of the matcher.
108
- #
109
- # @api public
110
- #
111
- # @return [String] A string describing what this matcher verifies
112
- #
113
- # @example
114
- # RaiseException.new(ArgumentError).to_s # => "raise exception ArgumentError"
56
+ # @return [String]
115
57
  def to_s
116
- "raise exception #{@expected}"
58
+ return "raise exception #{Constant.label(@expected)}" if @message.nil?
59
+
60
+ "raise exception #{Constant.label(@expected)} with message #{@message.inspect}"
117
61
  end
118
62
 
119
63
  private
120
64
 
121
- # Resolves the expected class name to an actual Exception class.
122
- #
123
- # @api private
124
- #
125
- # @return [Class] The resolved exception class
126
- # @raise [NameError] If the class name cannot be resolved to an actual class
127
- #
128
- # @note This method handles both string and symbol class names through constant resolution
65
+ def validate_class!
66
+ return unless ::Class === @expected
67
+ return if @expected <= ::Exception
68
+
69
+ raise ::ArgumentError, "expected must inherit from Exception (got: #{@expected.inspect})"
70
+ end
71
+
72
+ def validate_message!
73
+ return if @message.nil? || ::String === @message || ::Regexp === @message
74
+
75
+ raise ::ArgumentError, "message must be a String or a Regexp (got: #{@message.inspect})"
76
+ end
77
+
129
78
  def expected_class
130
- ::Object.const_get(@expected)
79
+ return @expected if ::Class === @expected
80
+
81
+ klass = Constant.resolve(@expected, ::Class)
82
+ raise ::TypeError, "#{@expected} is not an Exception class" unless klass <= ::Exception
83
+
84
+ klass
85
+ end
86
+
87
+ # The message is whatever the exception answers to `message`, which is
88
+ # its own data, as with any exception; it must be a valid String. The
89
+ # expected String or Regexp receives the comparison.
90
+ def message?(exception)
91
+ return true if @message.nil?
92
+
93
+ message = exception.message
94
+ return false unless ::String === message && message.valid_encoding?
95
+
96
+ ::String === @message ? @message.eql?(message) : @message.match?(message)
97
+ rescue ::Encoding::CompatibilityError
98
+ false
131
99
  end
132
100
  end
133
101
  end
@@ -1,114 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "constant"
4
+
3
5
  module Matchi
4
- # Custom predicate matcher that validates values against an arbitrary condition.
5
- #
6
- # This matcher provides a flexible way to test values against custom conditions
7
- # defined in a block. Unlike specific matchers that test for predetermined
8
- # conditions, Satisfy allows you to define any custom validation logic at runtime.
9
- # This makes it particularly useful for complex or composite conditions that
10
- # aren't covered by other matchers.
11
- #
12
- # @example Basic numeric validation
13
- # matcher = Matchi::Satisfy.new { |n| n.positive? && n.even? }
14
- # matcher.match? { 2 } # => true
15
- # matcher.match? { -2 } # => false
16
- # matcher.match? { 3 } # => false
17
- #
18
- # @example String pattern validation
19
- # matcher = Matchi::Satisfy.new { |s| s.start_with?("test") && s.length < 10 }
20
- # matcher.match? { "test_123" } # => true
21
- # matcher.match? { "test_12345" } # => false
22
- # matcher.match? { "other" } # => false
6
+ # Custom matcher: passes when the given block, called with the actual value,
7
+ # returns `true`. The block must return `true` or `false`; any other value
8
+ # is an error, not a truthy pass.
23
9
  #
24
- # @example Complex object validation
25
- # class User
26
- # attr_reader :age, :name
27
- # def initialize(name, age)
28
- # @name, @age = name, age
29
- # end
30
- # end
31
- #
32
- # matcher = Matchi::Satisfy.new { |user|
33
- # user.age >= 18 && user.name.length >= 2
34
- # }
35
- # matcher.match? { User.new("Alice", 25) } # => true
36
- # matcher.match? { User.new("B", 20) } # => false
37
- #
38
- # @example Using with collections
39
- # matcher = Matchi::Satisfy.new { |arr|
40
- # arr.all? { |x| x.is_a?(Integer) } && arr.sum.even?
41
- # }
42
- # matcher.match? { [2, 4, 6] } # => true
43
- # matcher.match? { [1, 3, 5] } # => false
44
- # matcher.match? { [1, "2", 3] } # => false
45
- #
46
- # @see https://ruby-doc.org/core/Proc.html
47
- # @see Matchi::Predicate For testing existing predicate methods
10
+ # @example
11
+ # Matchi::Satisfy.new { |value| value > 41 }.match? { 42 } # => true
48
12
  class Satisfy
49
- # Initialize the matcher with a validation block.
50
- #
51
- # @api public
52
- #
53
- # @yield [Object] Block that defines the validation condition
54
- # @yieldparam value The value to validate
55
- # @yieldreturn [Boolean] true if the value meets the condition
56
- #
57
- # @raise [ArgumentError] if no block is provided
58
- #
59
- # @return [Satisfy] a new instance of the matcher
60
- #
61
- # @example Simple numeric validation
62
- # Satisfy.new { |n| n > 0 }
63
- #
64
- # @example Complex condition
65
- # Satisfy.new { |obj|
66
- # obj.respond_to?(:length) && obj.length.between?(2, 10)
67
- # }
13
+ # @yieldparam actual [Object] the actual value
14
+ # @yieldreturn [Boolean]
15
+ # @raise [ArgumentError] when no block is given
68
16
  def initialize(&block)
69
17
  raise ::ArgumentError, "a block must be provided" unless block_given?
70
18
 
71
- @expected = block
19
+ @predicate = block
72
20
  end
73
21
 
74
- # Checks if the yielded value satisfies the validation block.
75
- #
76
- # This method passes the value returned by the provided block to the
77
- # validation block defined at initialization. The matcher succeeds if
78
- # the validation block returns a truthy value.
79
- #
80
- # @api public
81
- #
82
- # @yield [] Block that returns the value to validate
83
- # @yieldreturn [Object] The value to check against the condition
84
- #
85
- # @return [Boolean] true if the value satisfies the condition
86
- #
87
- # @raise [ArgumentError] if no block is provided
88
- #
89
- # @example Using with direct values
90
- # matcher = Satisfy.new { |n| n.positive? }
91
- # matcher.match? { 42 } # => true
92
- # matcher.match? { -1 } # => false
93
- #
94
- # @example Using with computed values
95
- # matcher = Satisfy.new { |s| s.length.even? }
96
- # matcher.match? { "test".upcase } # => true
97
- # matcher.match? { "a" * 3 } # => false
22
+ # @yieldreturn [Object] the actual value
23
+ # @return [Boolean]
24
+ # @raise [ArgumentError] when no block is given
25
+ # @raise [TypeError] when the predicate returns something other than true or false
98
26
  def match?
99
27
  raise ::ArgumentError, "a block must be provided" unless block_given?
100
28
 
101
- @expected.call(yield)
29
+ result = @predicate.call(yield)
30
+ return result if true.equal?(result) || false.equal?(result)
31
+
32
+ klass = ::Kernel.instance_method(:class).bind_call(result)
33
+ raise ::TypeError, "the block must return true or false (got a #{Constant.label(klass)})"
102
34
  end
103
35
 
104
- # Returns a human-readable description of the matcher.
105
- #
106
- # @api public
107
- #
108
- # @return [String] A string describing what this matcher verifies
109
- #
110
- # @example
111
- # Satisfy.new { |n| n > 0 }.to_s # => "satisfy &block"
36
+ # @return [String]
112
37
  def to_s
113
38
  "satisfy &block"
114
39
  end