Signed, Sealed, Delivered 🎹
Delivered gives you the ability to define method signatures in Ruby, and have them checked at runtime. This is useful for ensuring that your methods are being called with the correct arguments, and for providing better error messages when they are not. It also serves as a nice way of documenting your methods using actual code instead of comments.
Simply define a method signature using the sig
method directly before the method to be checked,
and Delivered will check that the method is being called with the correct arguments and types.
class User
extend Delivered::Signature
sig String, age: Integer
def create(name, age:)
"User #{name} created with age #{age}"
end
end
If an invalid argument is given to User#create
, for example, if age
is a String
instead of
the required Integer
, a Delivered::ArgumentError
exception will be raised.
You can use single and double splats in your method signatures, and Delivered will pass them through without checking, while still checking the other named positional and keyword arguments.
sig String
def create(name, *args, foo:, **kwargs); end
You can also check the return value of the method by passing a Hash with an Array as the key, and the value as the return type to check.
sig [String, age: Integer] => String
def create(name, age:)
"User #{name} created with age #{age}"
end
Or by placing the return type in a block to sig
.
sig(String, age: Integer) { String }
def create(name, age:)
"User #{name} created with age #{age}"
end
As well as Ruby's native types (ie. String
, Integer
, etc.), Delivered provides a couple of
extra types in Delivered::Types
.
You can call these directly with Delivered::Types.Boolean
, or for brevity, assign
Delivered::Types
to T
in your classes:
class User
extend Delivered::Signature
T = Delivered::Types
end
The following examples all use the T
alias, and assumes the above.
Value MUST be true
or false
. Does not support "truthy" or "falsy" values.
sig validate: T.Boolean
def create(validate:); end
Value MUST be respond to the given method(s).
sig name: T.RespondTo(:to_s)
def create(name:); end
Value MUST be a Range of the given type
sig name: T.RangeOf(Integer)
def create(name: 1...2); end
Value MUST be an Array of the given type
sig names: T.ArrayOf(String)
def create(names: ['Joel', 'Ash']); end
Value MUST be an Enumerable of the optional given type
sig users: T.Enumerable
def create(users: [1, 2]); end
sig users: T.Enumerable(User)
def create(users: User.all); end
Value MUST be any of the given list of values, that is, the value must be one of the given list.
sig T.Any(:male, :female)
def create(gender); end
If no type is given, the value CAN be any type or value.
sig save: T.Any
def create(save: nil); end
You can also pass nil
to allow a nil value alongside any other types you provide.
sig T.Any(String, nil)
def create(save = nil); end
When a type is given, the value MUST be nil OR of the given type.
sig save: T.Nilable(String)
def create(save: nil); end
sig T.Nilable(String)
def update(name = nil); end
If no type is given, the value CAN be nil. This essentially allows any value, including nil.
sig save: T.Nilable
def create(save: nil); end
You may notice that Nilable
is interchangeable with Any
. The following are equivilent:
sig save: T.Nilable
def create(save: nil); end
sig save: T.Any
def create(save: nil); end
As are these:
sig T.Nilable(String)
def update(name = nil); end
sig T.Any(String, nil)
def update(name = nil); end