Struct
lets me create a new class that takes arguments and has some nice semantics. However, the arguments aren't required, and their order requires consulting the definition:
Point = Struct.new(:x, :y)
Point.new(111, 222)
#=> <point instance with x = 111, y = 222>
Point.new(111)
#=> <point instance with x = 111, y = nil>
I'd like something similar to a Struct, but which uses keyword arguments instead:
Point = StricterStruct.new(:x, :y)
Point.new(x: 111, y: 222)
#=> <point instance with x = 111, y = 222>
Point.new(x: 111)
#=> ArgumentError
That might look something like this:
module StricterStruct
def self.new(*attributes)
klass = Class.new
klass.instance_eval { ... }
klass
end
end
But what should go in the braces to define an initialize
method on klass
such that:
attributes
; andinitialize
method assigns them to instance variables of the same nameI wound up using a (surprisingly Pythonic) **kwargs
strategy, thanks to the new features in Ruby 2.0+:
module StricterStruct
def self.new(*attribute_names_as_symbols)
c = Class.new
l = attribute_names_as_symbols
c.instance_eval {
define_method(:initialize) do |**kwargs|
unless kwargs.keys.sort == l.sort
extra = kwargs.keys - l
missing = l - kwargs.keys
raise ArgumentError.new <<-MESSAGE
keys do not match expected list:
-- missing keys: #{missing}
-- extra keys: #{extra}
MESSAGE
end
kwargs.map do |k, v|
instance_variable_set "@#{k}", v
end
end
l.each do |sym|
attr_reader sym
end
}
c
end
end
I might be misunderstanding the question but are you looking for something like this?
module StricterStruct
def self.new(*attributes)
klass = Class.new
klass.class_eval do
attributes.map!{|n| n.to_s.downcase.gsub(/[^\s\w\d]/,'').split.join("_")}
define_method("initialize") do |args|
raise ArgumentError unless args.keys.map(&:to_s).sort == attributes.sort
args.each { |var,val| instance_variable_set("@#{var}",val) }
end
attr_accessor *attributes
end
klass
end
end
Then
Point = StricterStruct.new(:x,:y)
#=> Point
p = Point.new(x: 12, y: 77)
#=> #<Point:0x2a89400 @x=12, @y=77>
p2 = Point.new(x: 17)
#=> ArgumentError
p2 = Point.new(y: 12)
#=> ArgumentError
p2 = Point.new(y:17, x: 22)
#=> #<Point:0x28cf308 @y=17, @x=22>
If you want something more please explain as I think this meets your criteria at least my understanding of it. As it defines the methods and can take a "keyword"(Hash
) argument and assign the proper instance variables.
If you want the arguments to be specified in the same order as they were defined just remove the sorts.
Also there might be cleaner implementations.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With