PullMonkey Blog

06 Jan

Convert a Ruby hash into a class object


I first saw the need to convert a hash object to a class when answering this post.

In the post, the user wanted to load a YAML object into his hash and then present the data from the hash in a form. Needless to say it was not very DRY the way it had to be implemented. So I started looking into it, I found this. This solution was a great starting point for where I ended up, but it was not general enough, it was hard coded, plus it was missing the getters and setters. So it turns out that in ruby it wasn't too much trouble to convert a hash into a class object. So let's get started:

I have implemented this for use in Rails, so let's start with the model that does all the magic:

1
2
3
4
5
6
7
8
9
10
11

class Hashit
  def initialize(hash)
    hash.each do |k,v|
      self.instance_variable_set("@#{k}", v)  ## create and initialize an instance variable for this key/value pair
      self.class.send(:define_method, k, proc{self.instance_variable_get("@#{k}")})  ## create the getter that returns the instance variable
      self.class.send(:define_method, "#{k}=", proc{|v| self.instance_variable_set("@#{k}", v)})  ## create the setter that sets the instance variable
    end
  end
end

Notice the self.class.send(:define_method ...) rather than self.define_method, this is a hack to overcome the fact that define_method() is private. I had come across this when trying to figure out the post mentioned above. Found the information to solve this here.

Ok, so on to the Controller that creates the Hashit object:

1
2
3
4
5
6
7
8
9

class TestItController < ApplicationController
  def index
    hashit = {:support_email  => "test@test.com",
              :allow_comments => 0}
    @hashit  = Hashit.new(hashit)
  end
end

Well that is easy, pass in a hash and get an object. Here is what @hashit looks like at this point:

1
2

#<Hashit:0xb6a65110 @allow_comments=0, @support_email="test@test.com">

And of course, now the view, what we wanted to clean up and make more elegant. Here is what the user started with:

Note: In this example @hashit is an actual hash, not a class object.

1
2
3
4
5
6

<% form_tag :action => 'config', :method => :post do %>
  <%= text_field 'settings',  'support_email', :size => 20, :value => @hashit['support_email']%>
  ...
<% end %>

And here is what our view code looks like now:

1
2
3
4
5

<% form_for :hashit, :url => {:action => 'index'} do |f| %>
  <%= f.text_field :support_email %>
<% end %>

Much simpler, DRYer :)

Well that is pretty much it, I suppose the next step would be to have a save method that updates the hash? This way we can do @hashit.save() and it will return a new hash that you can use. Well actually, that probably isn't too hard, lets see if I can do it real quick, class object back to hash ...

Well, I am back and I was able to figure it out, here is the new class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

class Hashit
  def initialize(hash)
    hash.each do |k,v|
      self.instance_variable_set("@#{k}", v)
      self.class.send(:define_method, k, proc{self.instance_variable_get("@#{k}")})
      self.class.send(:define_method, "#{k}=", proc{|v| self.instance_variable_set("@#{k}", v)})
    end
  end

  def save
    hash_to_return = {}
    self.instance_variables.each do |var|
      hash_to_return[var.gsub("@","")] = self.instance_variable_get(var)
    end
    return hash_to_return
  end
end

Just added the save() method, that takes all the instance variables and sets them as keys in our new hash. So here is the outcome of our save():

1
2

{"support_email"=>"new@some_email", "allow_comments"=>0}


Filed under: Home, development, rails, ruby Tags:


22 Responses to “Convert a Ruby hash into a class object”

  1. By Mikkel Riber on Jan 6, 2008 | Reply

    That is really really cool… Wierd nobody else have leaved a message. I will bookmark this right away :)

  2. By Winteen on Jan 6, 2008 | Reply

    Cool!
    Bookmark it!

  3. By NMZ on Jan 6, 2008 | Reply

    At first I didn’t realize what you were trying to do, since I never thought of even doing it. Then the light bulb hit and I was just…wow.

    Definitely a bookmark.

  4. By Dr_J on Jan 6, 2008 | Reply

    Excellent … as a Ruby novice, I bookmarked this one quickly, too.

    In my case, I needed something slightly different. I have a model object that contains a hash, and wanted to easily present/modify the contents of the hash. So, as a method on my model object, I used:

    def build_get_set(hash)
    hash.each do |k,v|
    self.class.send(:define_method, k, proc { @values[k] } ) ## create the getter that returns the hashmap value
    self.class.send(:define_method, "#{k}=", proc { |v| @values[k]=v } ) ## create the setter that sets the hashmap value
    end
    end

    This doesn’t create the attributes, but creates accessors/mutators that act on the hash itself. The only catch is that loading the object from somewhere (in my case, from my session), you have to ensure that this method is invoked (which I haven’t figured out how to do quite yet, so it’s very manual for now — not good).

    Thanks!!

    j

  5. By Richard Luck on Jan 6, 2008 | Reply

    Any suggestions on how to extend this to work on nested hashes?

  6. By charlie on Jan 6, 2008 | Reply

    @Richard - Good question. Here is what I have came up with. Let’s say that you have a a hash like this (using the example in this article):<filter:code attributes=lang="ruby">hashit = {:support_email => "test@test.com",
    :allow_comments => 0,
    :some_random_hash => {:name => "PullMonkey",
    :skills => {:ruby => 5,
    :rails => 6}}}</filter:code>We can modify the Hashit initialize() method just a tiny bit to have it turn all hashes in to classes:<filter:code attributes=lang="ruby">class Hashit
    def initialize(hash)
    hash.each do |k,v|
    v = Hashit.new(v) if v.is_a?(Hash)
    self.instance_variable_set("@#{k}", v) ## create and initialize an instance variable for this key/value pair
    self.class.send(:define_method, k, proc{self.instance_variable_get("@#{k}")}) ## create the getter that returns the instance variable
    self.class.send(:define_method, "#{k}=", proc{|v| self.instance_variable_set("@#{k}", v)}) ## create the setter that sets the instance variable
    end
    end
    end</filter:code>The line I added was this one:<filter:code attributes=lang="ruby">v = Hashit.new(v) if v.is_a?(Hash)</filter:code>The result is this:<filter:code attributes=lang="ruby">#<Hashit:0xf7f8f0b4 @allow_comments=0, @support_email="test@test.com", @some_random_hash=#<Hashit:0xf7f8edbc @skills=#<Hashit:0xf7f8ec04 @rails=6, @ruby=5>, @name="PullMonkey">></filter:code>Hope that helps.

  7. By sark on Jan 6, 2008 | Reply

    You might also want to check out the ruby OpenStruct object. It seems to accomplish everything you want and it was included with Ruby a while ago.

  8. By stuart on Jan 6, 2008 | Reply

    OpenStruct is interesting, and does seem to cover the initial iteration of Hashit here, but unfortunately doesn’t handle nested hashes.

  9. By Mike on Jan 6, 2008 | Reply

    It is also possible to support Array members of Hashit - just add the following line to initialize method:

    v = v.map { |item| Hashit.new(item) } if v.is_a?(Array)

    Thank you for this excellent article.

  10. By Kirk Bushell on Jan 6, 2008 | Reply

    Stuart, very true that OpenStruct doesn’t iterate over child hashes, but that is easily implemented within your own class :)
    Great job!

  11. By Yuriy on May 21, 2010 | Reply

    Great job, exactly what I was looking for!

  12. By Emmanuel on Jul 21, 2010 | Reply

    how about:

    require ‘ostruct’
    class Hashit < OpenStruct
    def save
    self.class.new(@table)
    end
    end

    Although it has the annoying property of returning nil to all methods that are not defined. You could “fix” that like this:

    require ‘ostruct’

    class Hashit < OpenStruct
    module DummyMethodMissing
    def method_missing(*args, &block)
    raise NoMethodError
    end
    end

    def initialize(*args)
    super(*args)
    extend DummyMethodMissing
    end
    end

  13. By taobao on Sep 8, 2010 | Reply

    class Hashit < OpenStruct
    module DummyMethodMissing
    def method_missing(*args, &block)
    raise NoMethodError
    end
    end

  14. By zx12r on Oct 22, 2010 | Reply

    made a small change in the save method for address nested ones too

    def save(object = self)
    hash_to_return = {}
    object.instance_variables.each do |var|
    instance_variable_value = object.instance_variable_get(var)
    if instance_variable_value.is_a?(HashToObject)
    hash_value = get_hash(instance_variable_value)
    hash_to_return[var.gsub("@","")] = hash_value
    else
    hash_to_return[var.gsub("@","")] = instance_variable_value
    end
    end
    return hash_to_return
    end

  15. By Avram on Dec 17, 2010 | Reply

    Hello,

    I didn’t read this in too much detail because I found another solution. Perhaps this misses the point, if so I apologize.

    ActiveResource can actually do this for you. This is essentially what ARes already does when it gets a hash back from a ReST interface. You just need a dummy ARes-based model (or you can use a legit one if you have it). All you do is call #new on it, and pas the hash in. self.site is a required setting for ARes classes, but it can be empty.

    >> class Foo > Foo.new(:a=>1,:b=>2,:c=>{:d=>3,:e=>4})
    => #1, “b”=>2, “c”=>#3, “e”=>4}>}>
    >> f.c.d
    => 3
    >>

  16. By Avram on Dec 17, 2010 | Reply

    Somehow that code snippet got frotzed. Here it is again, without the prompts, and with < instead of a less-than sign for for class inheritance, in opes that the server doesn’t digest it:

    class Foo < ActiveResource::Base
    self.site = ”
    end
    f = Foo.new(:a=>1,:b=>2,:c=>{:d=>3,:e=>4})
    (returns ruby object)
    f.c.d
    (returns 3)

  17. By Sheldon Hearn on Sep 27, 2011 | Reply

    I’m keen to steal your idea of combining option splatting with accessor definition, into magic_options:

    http://github.com/sheldonh/magic_options

    So instead of

    class Cow
    include MagicOptions
    magic_initialize :only => respond_to?
    attr_accessor :name, :color, :gender
    end

    we could support

    class Cow
    include MagicOptions
    magic_initialize :accessors => [:name, :color, :gender]
    end

  18. By Simone on Oct 27, 2011 | Reply

    Hello,
    this is how I changed initialize() method to make it recurse on value that are hash themselves:

    class HashIt
    def initialize(hash)
    hash.each do |k,v|
    case v
    when Hash; self.instance_variable_set(”@#{k}”, HashIt.new(v))
    else; self.instance_variable_set(”@#{k}”, v)
    end
    self.class.send(:define_method, k, proc{ self.instance_variable_get(”@#{k}”) })
    self.class.send(:define_method, “#{k}=”, proc{ |v| self.instance_variable_set(”@#{k}”, v)})
    end
    end
    end

    thank you for your post :-)

  19. By .jpg on Feb 17, 2012 | Reply

    Can this be accomplished with ActiveModel? It looks like you’re already in rails.

    Thanks so much for these ideas! I took them and modified them a little to make them slightly more extendible/useable.

    https://gist.github.com/1855631

    Thoughts? Please comment on the gist…

    .jpg

  20. By Tom Cocca on May 3, 2012 | Reply

    I found a much faster way to accomplish the same thing as your initialize method. I was seeing very slow times initializing a set of objects so I re-wrote the #initialize method as follows:

    def initialize(hash)
    hash.each do |key, value|
    self.instance_variable_set(”@#{key}”, value)
    self.class.send(:attr_reader, key)
    end
    end

    That creates attr_reader methods to access the instance variables. If you want to be able to read and write to those methods just use an attr_accessor:

    def initialize(hash)
    hash.each do |key, value|
    self.instance_variable_set(”@#{key}”, value)
    self.class.send(:attr_accessor, key)
    end
    end

    I found this code to run much faster than the:
    self.class.send(:define_method, k, proc{self.instance_variable_get(”@#{k}”)})
    self.class.send(:define_method, “#{k}=”, proc{|v| self.instance_variable_set(”@#{k}”, v)})

    Thanks,
    ~ Tom

  21. By zx12r on May 25, 2012 | Reply

    Found a class inside hashie gem called “mash” which does exactly this

    https://github.com/intridea/hashie#mash

  1. 1 Trackback(s)

  2. Convert your Hash keys to object properties in Ruby « Gooder Code - web development blog, php, java, asp.net, html, javascript

Sorry, comments for this entry are closed at this time.