John Feminella John Feminella - 6 months ago 11
Ruby Question

Metaprogrammatically defining Ruby methods that take keyword arguments?

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:


  • it requires keyword arguments with no default value;

  • the keywords are given as an array of symbols in
    attributes
    ; and

  • the
    initialize
    method assigns them to instance variables of the same name


Answer

I 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