diff --git a/README.md b/README.md index 3385010..8b8bff5 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,16 @@ 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. +### Single and Double Splat Arguments + +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. + +```ruby +sig String +def create(name, *args, foo:, **kwargs); end +``` + ### Return Types You can also check the return value of the method by passing a Hash with an Array as the key, and diff --git a/lib/delivered/signature.rb b/lib/delivered/signature.rb index 3fc89d1..e563316 100644 --- a/lib/delivered/signature.rb +++ b/lib/delivered/signature.rb @@ -3,8 +3,6 @@ module Delivered module Signature def sig(*sig_args, **sig_kwargs, &return_blk) - # ap [sig_args, sig_kwargs, return_blk] - # Block return returns = return_blk&.call @@ -20,15 +18,13 @@ def sig(*sig_args, **sig_kwargs, &return_blk) sig_kwargs = sig_args.pop if sig_args.last.is_a?(Hash) end - # ap [sig_args, sig_kwargs, returns] + # ap(sig_args:, sig_kwargs:) meta = class << self; self; end sig_check = lambda do |klass, class_method, name, *args, **kwargs, &block| # rubocop:disable Metrics/BlockLength - cname = if class_method - "#{klass.name}.#{name}" - else - "#{klass.class.name}##{name}" - end + # ap(args:, kwargs:, params: klass.method(:"__#{name}").parameters) + + cname = class_method ? "#{klass.name}.#{name}" : "#{klass.class.name}##{name}" sig_args.each.with_index do |arg, i| args[i] => ^arg @@ -39,13 +35,15 @@ def sig(*sig_args, **sig_kwargs, &return_blk) caller, cause: e end - kwargs.each do |key, value| - value => ^(sig_kwargs[key]) - rescue NoMatchingPatternError => e - raise Delivered::ArgumentError, - "`#{cname}` expected #{sig_kwargs[key].inspect} as keyword argument :#{key}, " \ - "but received `#{value.inspect}`", - caller, cause: e + unless sig_kwargs.empty? + kwargs.each do |key, value| + value => ^(sig_kwargs[key]) + rescue NoMatchingPatternError => e + raise Delivered::ArgumentError, + "`#{cname}` expected #{sig_kwargs[key].inspect} as keyword argument :#{key}, " \ + "but received `#{value.inspect}`", + caller, cause: e + end end result = if block @@ -66,6 +64,7 @@ def sig(*sig_args, **sig_kwargs, &return_blk) result end + # Instance method redefinition meta.send :define_method, :method_added do |name| meta.send :remove_method, :method_added meta.send :remove_method, :singleton_method_added @@ -76,6 +75,7 @@ def sig(*sig_args, **sig_kwargs, &return_blk) end end + # Class method redefinition meta.send :define_method, :singleton_method_added do |name| next if name == :singleton_method_added diff --git a/test/delivered/signature.rb b/test/delivered/signature.rb index c297d1a..034cfcf 100644 --- a/test/delivered/signature.rb +++ b/test/delivered/signature.rb @@ -92,6 +92,132 @@ def self.find_by_name(name) = User.new(name) expect(user.to_s).to be == 'Joel, 47' end + with 'rest args' do + it 'no args given' do + class Name + extend Delivered::Signature + + sig String + def rest(name, *attributes) + [name, attributes] + end + end + + expect(Name.new.rest('Joel')).to be == ['Joel', []] + end + + it 'args given' do + class Name + extend Delivered::Signature + + sig String + def rest(name, *attributes) + [name, attributes] + end + end + + expect(Name.new.rest('Joel', :foo)).to be == ['Joel', [:foo]] + end + + it 'sig args defined' do + class Name + extend Delivered::Signature + + sig String, String + def rest(name, last_name, *attributes) + [name, last_name, attributes] + end + end + + expect(Name.new.rest('Joel', 'Moss', :foo)).to be == ['Joel', 'Moss', [:foo]] + end + end + + with 'rest kwargs' do + it 'no kwargs given' do + class Name + extend Delivered::Signature + + sig String + def rest(name, **attributes) + [name, attributes] + end + end + + expect(Name.new.rest('Joel')).to be == ['Joel', {}] + end + + it 'kwargs given' do + class Name + extend Delivered::Signature + + sig String + def rest(name, **attributes) + [name, attributes] + end + end + + expect(Name.new.rest('Joel', foo: :bar)).to be == ['Joel', { foo: :bar }] + end + end + + with 'named and rest kwargs' do + it 'named kwarg given' do + class Name + extend Delivered::Signature + + sig String + def rest(name, age:, **attributes) + [name, age:, **attributes] + end + end + + expect(Name.new.rest('Joel', age: 47)).to be == ['Joel', { age: 47 }] + end + + it 'named and rest kwarg given' do + class Name + extend Delivered::Signature + + sig String + def rest(name, age:, **attributes) + [name, age:, **attributes] + end + end + + expect(Name.new.rest('Joel', age: 47, town: 'Chorley')) + .to be == ['Joel', { age: 47, town: 'Chorley' }] + end + end + + with 'rest args and kwargs' do + it 'no args given' do + class Name + extend Delivered::Signature + + sig String + def rest(name, *args, **kwargs) + [name, args, kwargs] + end + end + + expect(Name.new.rest('Joel')).to be == ['Joel', [], {}] + end + + it 'args given' do + class Name + extend Delivered::Signature + + sig String + def rest(name, *args, **kwargs) + [name, args, kwargs] + end + end + + expect(Name.new.rest('Joel', :foo, bar: :foo)).to be == ['Joel', [:foo], { bar: :foo }] + end + end + it 'supports block' do user = User.new('Joel', 47) { 'Hello' } expect(user.blk.call).to be == 'Hello'