Skip to content

Commit

Permalink
feat: Replaced returns kwarg with block or hashrocket - the choice is…
Browse files Browse the repository at this point in the history
… yours!

fixes #5
  • Loading branch information
joelmoss committed Apr 4, 2024
1 parent db740ed commit 4898a9a
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 32 deletions.
9 changes: 9 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ AllCops:

Style/Documentation:
Enabled: false
Style/HashAsLastArrayItem:
Enabled: false

Layout/LineLength:
Max: 100

Lint/ConstantDefinitionInBlock:
Exclude:
Expand All @@ -14,6 +19,10 @@ Metrics/AbcSize:
Enabled: false
Metrics/MethodLength:
Enabled: false
Metrics/CyclomaticComplexity:
Enabled: false
Metrics/PerceivedComplexity:
Enabled: false

Metrics/BlockLength:
Exclude:
Expand Down
28 changes: 25 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@ runtime. This is useful for ensuring that your methods are being called with the
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.

## Usage

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. it
can also check the return value of the method if you pass `sig` a `returns` keyword argument.
and Delivered will check that the method is being called with the correct arguments and types.

```ruby
class User
extend Delivered::Signature

sig String, age: Integer, returns: String
sig String, age: Integer
def create(name, age:)
"User #{name} created with age #{age}"
end
Expand All @@ -22,3 +23,24 @@ end

If an invalid argument is given to `User#create`, for example, if `age` is a `String` instead of
the required `Integer`, a `NoMatchingPatternError` exception will be raised.

### Return Types

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.

```ruby
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`.

```ruby
sig(String, age: Integer) { String }
def create(name, age:)
"User #{name} created with age #{age}"
end
```
29 changes: 25 additions & 4 deletions lib/delivered/signature.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,29 @@ module Delivered
module Signature
NULL = Object.new

def sig(*sig_args, returns: NULL, **sig_kwargs)
def sig(*sig_args, **sig_kwargs, &return_blk)
# ap [sig_args, sig_kwargs, return_blk]

# Block return
returns = return_blk&.call

# Hashrocket return
if sig_kwargs.keys[0].is_a?(Array)
unless returns.nil?
raise ArgumentError, 'Cannot mix block and hash for return type. Use one or the other.'
end

returns = sig_kwargs.values[0]
sig_args = sig_kwargs.keys[0]
sig_kwargs = sig_args.pop
end

# ap sig_args
# ap sig_kwargs
# ap returns

meta = class << self; self; end
sig_check = lambda { |klass, name, *args, **kwargs, &block|
sig_check = lambda do |klass, name, *args, **kwargs, &block|
sig_args.each.with_index do |arg, i|
args[i] => ^arg
end
Expand All @@ -21,10 +41,11 @@ def sig(*sig_args, returns: NULL, **sig_kwargs)
klass.send(:"__#{name}", *args, **kwargs)
end

result => ^returns if returns != NULL
result => ^returns unless returns.nil?

result
}
end

meta.send :define_method, :method_added do |name|
meta.send :remove_method, :method_added
meta.send :remove_method, :singleton_method_added
Expand Down
95 changes: 70 additions & 25 deletions test/delivered/signature.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,59 +6,104 @@ class User

attr_reader :town, :blk

sig String, Integer, town: String
sig(Integer) { Integer }
attr_writer :age

sig String, town: String
def initialize(name, age = nil, town: nil, &block)
@name = name
@age = age
@town = town
@blk = block
end

sig returns: String
sig(String, age: Integer) { Integer }
def with_block_return(name, age:) = 1 # rubocop:disable Lint/UnusedMethodArgument

sig(String, age: Integer) { Integer }
def with_incorrect_block_return = 'hello'

sig [String, age: Integer] => Integer
def with_incorrect_hash_return = 'hello'

sig [String, age: Integer] => Integer
def with_hash_return(name, age:) = 1 # rubocop:disable Lint/UnusedMethodArgument

sig [] => String
def to_s = "#{@name}, #{@age}"

sig returns: Integer
sig { Integer }
def age = @age.to_s

sig Integer, returns: Integer
attr_writer :age
sig(String, _age: Integer) { Array }
def self.where(_name, _age: nil) = []

sig [String] => Array
def self.find_by_name(name) = User.new(name)
end

sig String, _age: Integer, returns: Array
def self.where(_name, _age: nil)
[]
with 'return type with hash' do
it 'succeeds' do
user = User.new('Joel')
expect(user.with_hash_return('Joel', age: 47)).to be == 1
end

sig String, returns: Array
def self.find_by_name(name)
User.new(name)
it 'raises on incorrect types' do
user = User.new('Joel')
expect { user.with_hash_return }.to raise_exception NoMatchingPatternError
end

it 'raises on incorrect return type' do
user = User.new('Joel')
expect { user.with_incorrect_hash_return }.to raise_exception NoMatchingPatternError
end
end

it 'supports positional args' do
user = User.new('Joel', 47)
expect(user.to_s).to be == 'Joel, 47'
with 'return type as block' do
it 'succeeds' do
user = User.new('Joel')
expect(user.with_block_return('Joel', age: 47)).to be == 1
end

it 'raises on incorrect types' do
user = User.new('Joel')
expect { user.with_block_return }.to raise_exception NoMatchingPatternError
end

it 'raises on incorrect return type' do
user = User.new('Joel')
expect { user.with_incorrect_block_return }.to raise_exception NoMatchingPatternError
end
end

# it 'supports optional positional args'
it 'raises on mix of returns' do
extend Delivered::Signature

it 'supports block' do
user = User.new('Joel', 47) { 'Hello' }
expect(user.blk.call).to be == 'Hello'
expect do
sig([] => Integer) { Integer }
end.to raise_exception(ArgumentError, message: be =~ /Cannot mix/)
end

it 'checks return type' do
it 'supports positional args' do
user = User.new('Joel', 47)
expect(user.to_s).to be == 'Joel, 47'
end

it 'raises on incorrect return type' do
user = User.new('Joel', 47)
expect { user.age }.to raise_exception NoMatchingPatternError
it 'supports block' do
user = User.new('Joel', 47) { 'Hello' }
expect(user.blk.call).to be == 'Hello'
end

it 'checks return type with args' do
user = User.new('Joel', 47)
expect(user.age = 48).to be == 48
with 'attr_writer' do
it 'succeeds' do
user = User.new('Joel')
expect(user.age = 47).to be == 47
end

it 'raise on incorrect type' do
user = User.new('Joel', 47)
expect { user.age = '47' }.to raise_exception NoMatchingPatternError
end
end

it 'raises on missing args' do
Expand Down

0 comments on commit 4898a9a

Please sign in to comment.