Calin
Calin

Reputation: 6847

Using def_delegate with a hash

I know how Forwardable#def_delegate works with methods on objects, but is there a similar way to forward methods names to hash keys. Like:

hash = { some_value: 42, other_value: 31415 }
def_delegate :hash, :some_value, :other_value

Calling object.some_value should return 42

PS: def and class eval is a way, but is there a nicer way?

Upvotes: 7

Views: 3814

Answers (3)

Jordan Running
Jordan Running

Reputation: 106027

Not directly, no. One option is to use OpenStruct from Ruby's standard library.

require "ostruct"

class Foo
  extend Forwardable
  delegate :@data, :some_value, :other_value

  def initialize(hash)
    @data = OpenStruct.new(hash)
  end
end

hash = { some_value: 42, other_value: 31415 }
foo = Foo.new(hash)
foo.some_value # => 42

A simpler option is to just delegate the :[] method, but it's not as pretty:

class Foo
  extend Forwardable
  delegate :@data, :[]

  def initialize(hash)
    @data = hash
  end
end

hash = { some_value: 42, other_value: 31415 }
foo = Foo.new(hash)
foo[:some_value] # => 42

Barring that, there's always define_method:

[ :some_value, :other_value ].each do |meth|
  define_method(meth) { @data[meth] }
end

Or method_missing:

def method_missing(meth, *args, &block)
  return @data[meth] if @data.key?(meth)
  super
end

def respond_to_missing?(meth, *args)
  @data.key?(meth) || super
end

Upvotes: 8

Kristján
Kristján

Reputation: 18803

This is a good job for OpenStruct, which basically wraps a Hash in an object.

2.2.1 :001 > require 'ostruct'
 => true
2.2.1 :002 > s = OpenStruct.new(a: 1, b: 2)
 => #<OpenStruct a=1, b=2>
2.2.1 :003 > s.a
 => 1
2.2.1 :004 > s.c = 3
 => 3

If you want to be strict about the methods available, Struct lets you create little, dynamic classes.

2.2.1 :001 > hash = {a: 1, b: 2}
 => {:a=>1, :b=>2}
2.2.1 :002 > struct = Struct.new(*hash.keys)
 => #<Class:0x007fd104b32888>
2.2.1 :003 > instance = struct.new(*hash.values)
 => #<struct a=1, b=2>
2.2.1 :004 > instance.a = 3
 => 3
2.2.1 :005 > instance.c
NoMethodError: undefined method `c' for #<struct a=3, b=2>

Upvotes: 8

Adrian
Adrian

Reputation: 15171

The best way to implement this probably won't involve Forwardable, and will depend on your specific use case. Here is an example of one way you could do this without eval:

class C
  class << self
    attr_accessor :hash

    def def_hash_delegate(key)
      define_method(key) do
        C.hash[key]
      end
    end
  end

  @hash = { some_value: 42, other_value: 31415 }

  def_hash_delegate :some_value
  def_hash_delegate :other_value
  def_hash_delegate :value_3
end

c = C.new

puts c.some_value
puts c.other_value
C.hash[:value_3] = 3948
puts c.value_3

Upvotes: 3

Related Questions