From 4898a9acd918eb165c4eb9d7e18e3d7e3d0b678c Mon Sep 17 00:00:00 2001 From: Joel Moss Date: Thu, 4 Apr 2024 11:07:39 +0100 Subject: [PATCH] feat: Replaced returns kwarg with block or hashrocket - the choice is yours! fixes #5 --- .rubocop.yml | 9 ++++ README.md | 28 +++++++++-- lib/delivered/signature.rb | 29 +++++++++-- test/delivered/signature.rb | 95 +++++++++++++++++++++++++++---------- 4 files changed, 129 insertions(+), 32 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index ab147de..30a5c66 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -5,6 +5,11 @@ AllCops: Style/Documentation: Enabled: false +Style/HashAsLastArrayItem: + Enabled: false + +Layout/LineLength: + Max: 100 Lint/ConstantDefinitionInBlock: Exclude: @@ -14,6 +19,10 @@ Metrics/AbcSize: Enabled: false Metrics/MethodLength: Enabled: false +Metrics/CyclomaticComplexity: + Enabled: false +Metrics/PerceivedComplexity: + Enabled: false Metrics/BlockLength: Exclude: diff --git a/README.md b/README.md index 2859874..dd66505 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 +``` diff --git a/lib/delivered/signature.rb b/lib/delivered/signature.rb index 1866fc1..ff48b34 100644 --- a/lib/delivered/signature.rb +++ b/lib/delivered/signature.rb @@ -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 @@ -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 diff --git a/test/delivered/signature.rb b/test/delivered/signature.rb index 67f3977..22bbf92 100644 --- a/test/delivered/signature.rb +++ b/test/delivered/signature.rb @@ -6,7 +6,10 @@ 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 @@ -14,51 +17,93 @@ def initialize(name, age = nil, town: nil, &block) @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