Skip to content

Commit

Permalink
Refactor and add support for class based signatures
Browse files Browse the repository at this point in the history
Move validation logic into Validation class
Support signatures such as:
  sig(User, {Option => :to_i}, [Integer])
  • Loading branch information
maxjustus committed Jun 9, 2012
1 parent f909082 commit 7df415f
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 24 deletions.
16 changes: 16 additions & 0 deletions Readme.md
Expand Up @@ -5,13 +5,22 @@ If an argument responds to type conversion from signature, it will convert the a
If the argument doesn't respond to the given type conversion, an exception is thrown.
Return values can be converted using block passed to sig method.

class User; end
class Admin < User; end
class Option; end

class Herp
include Str8jacket

sig({:to_sym => :to_i}, :to_i, [:to_i]) {|_| Array(_)}
def mom(options, random_integer_flag, random_array_arg)
options[:something] + random_integer_flag + random_array_arg.reduce(0) {|v, n| v + n}
end

sig(User, {Option => :to_s})
def login(user, options)
#log stuff
end
end

Herp.new.mom({'something' => '1'}, '2', ['3']) == [6]
Expand All @@ -20,8 +29,15 @@ Return values can be converted using block passed to sig method.
Herp.new.mom({['wat'] => '1'}, '2', ['3'])
#=> raises an exception with the details regarding failed conversion

Herp.new.login(Admin.new, {Option.new('thing') => 1})
#=> works as expected

Herp.new.login(1, {'nope' => 'dumb'})
#=> raises an exception with the details regarding failed conversion

TODO
====

* Support private and protected methods
* Support singleton methods
* Proper exceptions for improper arity
75 changes: 55 additions & 20 deletions lib/str8jacket.rb
Expand Up @@ -22,46 +22,81 @@ def method_added(method_name)
return_proc = sig.pop

meth = instance_method(method_name)
orig_arguments = meth.parameters

define_method(method_name) do |*args, &blk|
sig.each_with_index do |conversion_meth, index|
sig.each_with_index do |conversion, index|
required = orig_arguments[index][0] == :req
arg = args[index]
args[index] = if conversion_meth.class == Hash
self.class._validate_hash(arg, conversion_meth, method_name, index)
elsif conversion_meth.class == Array
self.class._validate_array(arg, conversion_meth, method_name, index)
else
self.class._validate_arg_type(arg, conversion_meth, method_name, index)
end
args[index] = Str8jacket::Validator.new(arg, conversion, method_name, index, required).validate
end

result = meth.bind(self).call(*args, &blk)
return_proc ? return_proc.call(result) : result
end
end
end

class Validator
attr_accessor :arg, :conversion, :method_name, :index, :required
def initialize(arg, conversion, method_name, index, required)
@arg = arg
@conversion = conversion
@method_name = method_name
@index = index
@required = required
end

def validate
if arg && required
if conversion.class == Hash
validate_hash
elsif conversion.class == Array
validate_array
else
validate_arg_type
end
end
end

def _validate_hash(hash, conversion_meths, method_name, index)
key_conversion, value_conversion = conversion_meths.to_a.flatten
hash.reduce({}) do |new_h, (k,v)|
converted_key = _validate_arg_type(k, key_conversion, method_name, "#{index} (key in hash)")
converted_val = _validate_arg_type(v, value_conversion, method_name, "#{index} (value in hash)")
def validate_hash
key_conversion, value_conversion = conversion.to_a.flatten

arg.reduce({}) do |new_h, (k,v)|
converted_key = validate_arg_type(k, key_conversion, "(key in hash)")
converted_val = validate_arg_type(v, value_conversion, "(value in hash)")
new_h[converted_key] = converted_val
new_h
end
end

def _validate_array(array, conversion, method_name, index)
array.reduce([]) do |new_a, elem|
new_a.push(_validate_arg_type(elem, conversion.first, method_name, index))
def validate_array
arg.reduce([]) do |new_a, elem|
new_a.push(validate_arg_type(elem, conversion.first))
new_a
end
end

def _validate_arg_type(arg, conversion_meth, method_name, index)
unless arg.respond_to?(conversion_meth)
raise "Argument #{arg.inspect} for #{self.name}##{method_name} at position #{index} does not respond to #{conversion_meth}"
def validate_arg_type(arg = @arg, conversion = @conversion, msg = '')
if [String, Symbol].include?(conversion.class)
unless arg.respond_to?(conversion)
argument_error(arg, conversion, msg)
end
arg.send(conversion)
else
validate_arg_class(arg, conversion, msg)
end
arg.send(conversion_meth)
end

def validate_arg_class(arg = @arg, conversion = @conversion, msg = '')
unless arg.is_a?(conversion)
argument_error(arg, conversion, msg, 'is not an instance of')
end
arg
end

def argument_error(arg, conversion, type_msg, conversion_message = 'does not respond to')
raise "Argument #{arg.inspect} #{type_msg} at position #{index} #{conversion_message} #{conversion}".split(' ').join(' ')
end
end
end
36 changes: 32 additions & 4 deletions spec/str8_spec.rb
Expand Up @@ -2,6 +2,9 @@
require_relative '../lib/str8jacket.rb'

describe Str8jacket do
class User; end
class Admin < User; end

class Testingthing
include Str8jacket

Expand All @@ -22,6 +25,11 @@ def lerp(int)
{int => nil}
end

sig(User, {User => Integer}, [Admin])
def login(user, credentials, array_validation = [Admin])
user
end

sig({:to_sym => :to_s}, [:to_i]) { |_| _.to_a }
def hash_validation(options, list_thing)
[options, list_thing]
Expand All @@ -31,19 +39,21 @@ def hash_validation(options, list_thing)
def mom(options, random_integer_flag, random_array_arg)
options[:something] + random_integer_flag + random_array_arg.reduce(0) {|v, n| v + n}
end

sig()
end

let(:instance) { Testingthing.new }
describe 'sig' do
it 'validates and enforces argument types' do
it 'validates and enforces argument types based on respond_to' do
instance.herp(1, {}, 'LOL?')
-> do
instance.herp('a', 1)
end.should raise_exception('Argument "a" for Testingthing#herp at position 0 does not respond to to_int')
end.should raise_exception('Argument "a" at position 0 does not respond to to_int')

-> do
instance.herp(1, 111)
end.should raise_exception('Argument 111 for Testingthing#herp at position 1 does not respond to to_hash')
end.should raise_exception('Argument 111 at position 1 does not respond to to_hash')

-> do
instance.derp({})
Expand All @@ -54,13 +64,31 @@ def mom(options, random_integer_flag, random_array_arg)
end.should_not raise_exception
end

it 'validates argument types based on class' do
-> do
instance.login(User.new, {})
end.should_not raise_exception

-> do
instance.login(Admin.new, {User.new => 1})
end.should_not raise_exception

-> do
instance.login(1, {})
end.should raise_exception('Argument 1 at position 0 is not an instance of User')

-> do
instance.login(User.new, {User.new => 'stuff'})
end.should raise_exception('Argument "stuff" (value in hash) at position 1 is not an instance of Integer')
end

it 'validates and enforces hash and array argument types' do
instance.hash_validation({'herp' => :derp}, ['1']).should == [{:herp => 'derp'}, [1]]
instance.mom({'something' => '1'}, '2', ['3']).should == [6]

-> do
instance.hash_validation({['herp'] => :derp}, ['1'])
end.should raise_exception('Argument ["herp"] for Testingthing#hash_validation at position 0 (key in hash) does not respond to to_sym')
end.should raise_exception('Argument ["herp"] (key in hash) at position 0 does not respond to to_sym')
end

it 'enforces type of return value' do
Expand Down

0 comments on commit 7df415f

Please sign in to comment.