Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added class methods support #4

Merged
merged 2 commits into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions delivered.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ Gem::Specification.new do |s|
s.summary = 'Simple runtime type checking for Ruby method signatures'
s.required_ruby_version = '>= 3.2'

a.metadata['homepage_uri'] = spec.homepage
a.metadata['source_code_uri'] = spec.homepage
a.metadata['changelog_uri'] = "#{spec.homepage}/releases"
s.metadata['homepage_uri'] = s.homepage
s.metadata['source_code_uri'] = s.homepage
s.metadata['changelog_uri'] = "#{s.homepage}/releases"

s.files = Dir.glob('{bin/*,lib/**/*,[A-Z]*}')
s.platform = Gem::Platform::RUBY
Expand Down
44 changes: 30 additions & 14 deletions lib/delivered/signature.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,44 @@ module Signature

def sig(*sig_args, returns: NULL, **sig_kwargs)
meta = class << self; self; end
sig_check = ->(klass, name, *args, **kwargs, &block) {
sig_args.each.with_index do |arg, i|
args[i] => ^arg
end

kwargs.each do |key, value|
value => ^(sig_kwargs[key])
end

result = if block
klass.send(:"__#{name}", *args, **kwargs, &block)
else
klass.send(:"__#{name}", *args, **kwargs)
end

result => ^returns if returns != NULL

result
}
meta.send :define_method, :method_added do |name|
meta.send :remove_method, :method_added
meta.send :remove_method, :singleton_method_added

alias_method :"__#{name}", name
define_method name do |*args, **kwargs, &block|
sig_args.each.with_index do |arg, i|
args[i] => ^arg
end

kwargs.each do |key, value|
value => ^(sig_kwargs[key])
end
sig_check.call(self, name, *args, **kwargs, &block)
end
end

result = if block
send(:"__#{name}", *args, **kwargs, &block)
else
send(:"__#{name}", *args, **kwargs)
end
meta.send :define_method, :singleton_method_added do |name|
next if name == :singleton_method_added
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't found this documented anywhere in ruby, but looks like defining singleton_method_added also calls itself. Tried isolating this on a simple example just in case define_method or send where causing it somehow, but it also happened on isolation


result => ^returns if returns != NULL
meta.send :remove_method, :singleton_method_added
meta.send :remove_method, :method_added
Comment on lines 28 to +42
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, we wait for either a singleton method or an instance method, whatever happens first will remove both method_added callbacks


result
meta.alias_method :"__#{name}", name
define_singleton_method name do |*args, **kwargs, &block|
sig_check.call(self, name, *args, **kwargs, &block)
end
end
end
Expand Down
37 changes: 37 additions & 0 deletions test/delivered/signature.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ def age = @age.to_s

sig Integer, returns: Integer
attr_writer :age

sig String, age: Integer, returns: Array
def self.where(_name, age: nil)
[]
end

sig String, returns: Array
def find_by_name(name)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo? should be a class method I think.

User.new(name)
end
end

it 'supports positional args' do
Expand Down Expand Up @@ -66,4 +76,31 @@ def age = @age.to_s
it 'raises on incorrect kwarg type' do
expect { User.new('Joel', town: 1) }.to raise_exception NoMatchingPatternError
end

with 'class methods' do
it 'supports class method signatures' do
users = User.where('Joel')
expect(users.length).to be == 0
end

it 'raises on incorrect return type' do
expect { User.find_by_name('Hugo') }.to raise_exception NoMatchingPatternError
end

it 'raises on missing args' do
expect { User.where }.to raise_exception NoMatchingPatternError
end

it 'raises on incorrect arg type' do
expect { User.where(1) }.to raise_exception NoMatchingPatternError
end

it 'supports keyword args' do
expect(User.where('hugo', age: 27)).to be == []
end

it 'raises on incorrect kwarg type' do
expect { User.where(1, age: 'twentyseven') }.to raise_exception NoMatchingPatternError
end
end
end