John Feminella
John Feminella

Reputation: 311526

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:

Upvotes: 8

Views: 930

Answers (4)

Sam Stickland
Sam Stickland

Reputation: 667

I was also hunting around for this, and eventually stumbled across this gem that does exactly this:

https://github.com/etiennebarrie/kwattr

class FooBar
  kwattr :foo, bar: 21
end

foobar = FooBar.new(foo: 42) # => #<FooBar @foo=42, @bar=21>
foobar.foo # => 42
foobar.bar # => 21

instead of

class FooBar
  attr_reader :foo, :bar

  def initialize(foo:, bar: 21)
    @foo = foo
    @bar = bar
  end
end

Upvotes: 0

John Feminella
John Feminella

Reputation: 311526

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

Upvotes: 6

engineersmnky
engineersmnky

Reputation: 29328

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.

Upvotes: 1

the Tin Man
the Tin Man

Reputation: 160551

It sounds like you're looking for Ruby's built-in OpenStruct:

require 'ostruct'

foo = OpenStruct.new(bar: 1, 'baz' => 2)
foo # => #<OpenStruct bar=1, baz=2>

foo.bar # => 1
foo[:bar] # => 1
foo.baz # => 2
foo.baz = 3
foo # => #<OpenStruct bar=1, baz=3>

I think of OpenStruct as candy-coating on a Hash, where we can access and assign to the instance without any real constraints, unlike creating a real class with the normal accessors. We can pretend it's a hash, or a class with methods. It's a dessert topping, no it's a floor-wax, no, it's two things in one!

Upvotes: 0

Related Questions