tags:

views:

183

answers:

3

I have a class and a hash. How can I get the members of the hash to dynamically become methods on the class with the key as the method name?

class User
  def initialize
    @attributes = {"sn" => "Doe", "givenName" => "John"}
  end
end

For example, I would like to be able to have the following output Doe:

u = User.new
puts u.sn

I'm sure this is covered in many references and tutorials, but I can not seem to find the answer because I don't know the real names for anything.

Thanks in advance!

+4  A: 
def method_missing(name, *args, &blk)
  if args.empty? && blk.nil? && @attributes.has_key?(name)
    @attributes[name]
  else
    super
  end
end

Explanation: If you call a method, which does not exist, method_missing is invoked with the name of the method as the first parameter, followed by the arguments given to the method and the block if one was given.

In the above we say that if a method, which was not defined, is called without arguments and without a block and the hash has an entry with the method name as key, it will return the value of that entry. Otherwise it will just proceed as usual.

sepp2k
I had to add to change it to: name.to_s in both places, but it got me where I wanted! Thanks :)
Michael
Oh, right, I didn't notice you were using strings as hash keys.
sepp2k
+1  A: 

The solution by sepp2k is the way to go. However, if your @attributes never change after the initialization and you need speed, then you could do it in this way:

class User
  def initialize
    @attributes = {"sn" => "Doe", "givenName" => "John"}
    @attributes.each do |k,v|
      self.class.send :define_method, k do v end
    end
  end
end

User.new.givenName # => "John"

This generates all the methods in advance...

severin
A: 

Actually severin have a better idea, just because usage of method_missing is a bad practice, not all the time, but most of it.

One problem with that code provided by severin: it returns value that have been passed to initializer, so you cannot change it. I suggest you a little different approach:

class User < Hash
  def initialize(attrs)
    attrs.each do |k, v|
      self[k] = v
    end
  end

  def []=(k, v)
    unless respond_to?(k)
      self.class.send :define_method, k do
        self[k]
      end
    end

    super
  end
end

Lets check it:

u = User.new(:name => 'John')
p u.name
u[:name] = 'Maria'
p u.name

And also you can do it with Struct:

attrs = {:name => 'John', :age => 22, :position => 'developer'}
keys = attrs.keys

user = Struct.new(*keys).new(*keys.map { |k| attrs[k] })

Lets test it:

p user
p user.name
user[:name] = 'Maria'
p user.name
user.name = 'Vlad'
p user[:name]

Or even OpenStruct, but be careful it will not create method if it already have it in instance methods, you can look for that by using OpenStruct.instance_methods (because of type is used, I'm now using second approach):

attrs = {:name => 'John', :age => 22, :position => 'developer'}
user = OpenStruct.new(attrs)

Yep, so easy:

user.name
user[:name] # will give you an error, because OpenStruct isn't a Enumerable or Hash
Dmitry Polushkin