Fixrb

defi

Prepares a method call, and sends it to any object.

A challenge is a method name with its arguments, as you would write them after a dot. Sent to an object, it brings back what came out of the call: the value the method returned, or the exception it raised. The same challenge can be sent to as many objects as you like, to compare what each one does with it.

Version
4.0.0
Ruby
3.3 or later
Dependencies
None
License
MIT

Install

Add gem "defi" to your Gemfile, or run gem install defi.

Usage

A challenge takes positional arguments, keyword arguments and a block, and passes them as they are. to sends it to an object and returns a value; object is what came back, raised? says whether it was raised, and call returns the object or raises the exception again:

require "defi"

challenge = Defi::Challenge.new(:fetch, :b, 0)
challenge.to_s # => ".fetch(:b, 0)"

value = challenge.to({ b: 2 })
value.raised? # => false
value.object  # => 2
value.call    # => 2

value = Defi::Challenge.new(:fetch, :b).to({})
value.raised?     # => true
value.object      # => #<KeyError: key not found: :b>
value.call rescue $!.class # => KeyError

Defi::Challenge.new(:encode, "US-ASCII", undef: :replace).to("café").object # => "caf?"
Defi::Challenge.new(:map) { |n| n * 2 }.to([1, 2]).object # => [2, 4]

A challenge and a value are frozen; their arguments are the objects given, not copies.

What comes back

The message is sent with public_send: a private method, or a BasicObject, which has no public_send, gives a NoMethodError. A challenge keeps as its value the exceptions a method raises on its own: a StandardError, a ScriptError such as NotImplementedError, a SystemStackError, or the SystemExit of an exit. What is not the method’s doing goes through: an Interrupt, a signal, a NoMemoryError, the exception a Timeout uses to end its block, and any other subclass of Exception.

require "defi"

# What a method raises on its own comes back as the value.
Defi::Challenge.new(:+, 1).to("a").object.class     # => TypeError
Defi::Challenge.new(:secret).to(Object.new).object.class # => NoMethodError

exiting = Object.new
def exiting.call = exit(3)
Defi::Challenge.new(:call).to(exiting).object.status # => 3

# What is not the method's doing goes through.
interrupting = Object.new
def interrupting.call = raise(Interrupt)
outcome = begin
  Defi::Challenge.new(:call).to(interrupting)
rescue Interrupt
  :went_through
end
outcome # => :went_through

Documentation