Skip to content

Instantly share code, notes, and snippets.

@barbuza
Created May 23, 2012 09:42
Show Gist options
  • Save barbuza/2774280 to your computer and use it in GitHub Desktop.
Save barbuza/2774280 to your computer and use it in GitHub Desktop.

HashMethods allow python-like method calling with named args

Usage

class Foo
  extends HashMethods
  def bar(a, b=1)
    [a, b]
  end
  instance_hash_method :bar
  define_hash_method :spam, :a => 1, :b => 2 do |a, b|
    [a, b]
  end
end

Foo.new.bar(:a => 1)            #=> [1, 1]
Foo.new.bar(:a => 1, :b => 2)   #=> [1, 2]
Foo.new.spam                    #=> [1, 2]
Foo.new.spam(:b => 1)           #=> [1, 1]
Foo.new.spam(:a => 2, :b => 1)  #=> [2, 1]
module HashMethods
def instance_hash_method(method_name)
required_args = []
optional_args = []
original_method = instance_method(method_name)
original_method.parameters.each do |type, name|
if type == :req
required_args.push name
elsif type == :opt
optional_args.push name
else
raise ArgumentError.new("rest arguments are not supported with `hash_instance_method`")
end
end
allowed_args = required_args + optional_args
define_method(method_name) do |params={}|
call_args = []
params.each_key do |name|
raise ArgumentError.new("#{name} is not allowed arg for this method") unless allowed_args.include? name
end
required_args.each do |name|
raise ArgumentError.new("#{name} is required") unless params.include? name
call_args.push params.delete(name)
end
optional_args.each do |name|
break unless params.include? name
call_args.push params.delete(name)
end
raise ArgumentError.new("params #{params.keys.join ', '} can not be applied due to optional args order") unless params.empty?
original_method.bind(self).call *call_args
end
end
def define_hash_method(method_name, defaults={}, &blk)
method_args = []
blk.parameters.each do |type, name|
if type == :rest
raise ArgumentError.new("rest arguments are not supported with `hash_method`")
else
method_args.push name
end
end
defaults.each_key do |name|
raise ArgumentError.new("default value specified for unused argument #{name}") unless method_args.include? name
end
define_method(method_name) do |params={}|
params = params.clone
call_args = []
method_args.each do |name|
if params.include? name
call_args.push params.delete(name)
elsif defaults.include? name
call_args.push defaults[name]
else
raise ArgumentError.new("#{name} is required")
end
end
raise ArgumentError.new("#{params.keys.first} is not allowed arg for this method") unless params.empty?
instance_exec *call_args, &blk
end
end
end
class HashStruct
def self.new(*args)
required_args = []
optional_args = {}
for arg in args
if arg.is_a? Symbol
required_args.push arg
elsif arg.is_a? Hash
optional_args.merge! arg
end
end
raise ArgumentError.new("there are intersections between required and optional params") unless (required_args & optional_args.keys).empty?
Class.new do |m|
(required_args + optional_args.keys).each do |name|
attr_reader name
end
define_method :initialize do |params={}|
required = required_args.clone
required_count = required.size
optional = optional_args.keys
allowed_params = required + optional
until params.empty?
name, value = params.shift
if required.include? name
required.delete name
elsif optional.include? name
optional.delete name
else
raise ArgumentError.new("unknown param #{name}, allowed params are: #{allowed_params.join ', '}")
end
instance_variable_set "@#{name}", value
end
raise ArgumentError.new "missing required params: #{required.join ', '}" unless required.empty?
optional.each do |name|
instance_variable_set "@#{name}", optional_args[name]
end
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment