Tutorial :Initialize a Ruby class from an arbitrary hash, but only keys with matching accessors



Question:

Is there a simple way to list the accessors/readers that have been set in a Ruby Class?

class Test    attr_reader :one, :two      def initialize      # Do something    end      def three    end  end    Test.new  => [one,two]  

What I'm really trying to do is to allow initialize to accept a Hash with any number of attributes in, but only commit the ones that have readers already defined. Something like:

def initialize(opts)    opts.delete_if{|opt,val| not the_list_of_readers.include?(opt)}.each do |opt,val|      eval("@#{opt} = \"#{val}\"")    end  end  

Any other suggestions?


Solution:1

This is what I use (I call this idiom hash-init).

 def initialize(object_attribute_hash = {})    object_attribute_hash.map { |(k, v)| send("#{k}=", v) }   end  

If you are on Ruby 1.9 you can do it even cleaner (send allows private methods):

 def initialize(object_attribute_hash = {})    object_attribute_hash.map { |(k, v)| public_send("#{k}=", v) }   end  

This will raise a NoMethodError if you try to assign to foo and method "foo=" does not exist. If you want to do it clean (assign attrs for which writers exist) you should do a check

 def initialize(object_attribute_hash = {})    object_attribute_hash.map do |(k, v)|       writer_m = "#{k}="      send(writer_m, v) if respond_to?(writer_m) }    end   end  

however this might lead to situations where you feed your object wrong keys (say from a form) and instead of failing loudly it will just swallow them - painful debugging ahead. So in my book a NoMethodError is a better option (it signifies a contract violation).

If you just want a list of all writers (there is no way to do that for readers) you do

 some_object.methods.grep(/\w=$/)  

which is "get an array of method names and grep it for entries which end with a single equals sign after a word character".

If you do

  eval("@#{opt} = \"#{val}\"")  

and val comes from a web form - congratulations, you just equipped your app with a wide-open exploit.


Solution:2

You could override attr_reader, attr_writer and attr_accessor to provide some kind of tracking mechanism for your class so you can have better reflection capability such as this.

For example:

class Class    alias_method :attr_reader_without_tracking, :attr_reader    def attr_reader(*names)      attr_readers.concat(names)      attr_reader_without_tracking(*names)    end      def attr_readers      @attr_readers ||= [ ]    end      alias_method :attr_writer_without_tracking, :attr_writer    def attr_writer(*names)      attr_writers.concat(names)      attr_writer_without_tracking(*names)    end      def attr_writers      @attr_writers ||= [ ]    end      alias_method :attr_accessor_without_tracking, :attr_accessor    def attr_accessor(*names)      attr_readers.concat(names)      attr_writers.concat(names)      attr_accessor_without_tracking(*names)    end  end  

These can be demonstrated fairly simply:

class Foo    attr_reader :foo, :bar    attr_writer :baz    attr_accessor :foobar  end    puts "Readers: " + Foo.attr_readers.join(', ')  # => Readers: foo, bar, foobar  puts "Writers: " + Foo.attr_writers.join(', ')  # => Writers: baz, foobar  


Solution:3

Try something like this:

class Test    attr_accessor :foo, :bar      def initialize(opts = {})      opts.each do |opt, val|        send("#{opt}=", val) if respond_to? "#{opt}="      end    end  end    test = Test.new(:foo => "a", :bar => "b", :baz => "c")    p test.foo # => nil  p test.bar # => nil  p test.baz # => undefined method `baz' for #<Test:0x1001729f0 @bar="b", @foo="a"> (NoMethodError)  

This is basically what Rails does when you pass in a params hash to new. It will ignore all parameters it doesn't know about, and it will allow you to set things that aren't necessarily defined by attr_accessor, but still have an appropriate setter.

The only downside is that this really requires that you have a setter defined (versus just the accessor) which may not be what you're looking for.


Solution:4

Accessors are just ordinary methods that happen to access some piece of data. Here's code that will do roughly what you want. It checks if there's a method named for the hash key and sets an accompanying instance variable if so:

def initialize(opts)    opts.each do |opt,val|      instance_variable_set("@#{opt}", val.to_s) if respond_to? opt    end  end  

Note that this will get tripped up if a key has the same name as a method but that method isn't a simple instance variable access (e.g., {:object_id => 42}). But not all accessors will necessarily be defined by attr_accessor either, so there's not really a better way to tell. I also changed it to use instance_variable_set, which is so much more efficient and secure it's ridiculous.


Solution:5

There's no built-in way to get such a list. The attr_* functions essentially just add methods, create an instance variable, and nothing else. You could write wrappers for them to do what you want, but that might be overkill. Depending on your particular circumstances, you might be able to make use of Object#instance_variable_defined? and Module#public_method_defined?.

Also, avoid using eval when possible:

def initialize(opts)    opts.delete_if{|opt,val| not the_list_of_readers.include?(opt)}.each do |opt,val|      instance_variable_set "@#{opt}", val    end  end  


Solution:6

You can look to see what methods are defined (with Object#methods), and from those identify the setters (the last character of those is =), but there's no 100% sure way to know that those methods weren't implemented in a non-obvious way that involves different instance variables.

Nevertheless Foo.new.methods.grep(/=$/) will give you a printable list of property setters. Or, since you have a hash already, you can try:

def initialize(opts)    opts.each do |opt,val|      instance_variable_set("@#{opt}", val.to_s) if respond_to? "#{opt}="    end  end  

Note:If u also have question or solution just comment us below or mail us on toontricks1994@gmail.com
Previous
Next Post »