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}
|
January 30th, 2008 at 12:48 AM That is really really cool... Wierd nobody else have leaved a message. I will bookmark this right away :)
February 25th, 2008 at 11:49 PM Cool!
Bookmark it!
March 20th, 2008 at 02:01 PM 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.
April 8th, 2008 at 09:35 AM 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
August 6th, 2008 at 12:40 PM Any suggestions on how to extend this to work on nested hashes?
August 6th, 2008 at 02:01 PM
@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):
hashit = {:support_email => "test@test.com",:allow_comments => 0,
:some_random_hash => {:name => "PullMonkey",
:skills => {:ruby => 5,
:rails => 6}}}
We can modify the Hashit initialize() method just a tiny bit to have it turn all hashes in to classes:
The line I added was this one:
The result is this:
#<Hashit:0xf7f8f0b4 @allow_comments=0, @support_email="test@test.com", @some_random_hash=#<Hashit:0xf7f8edbc @skills=#<Hashit:0xf7f8ec04 @rails=6, @ruby=5>, @name="PullMonkey">>Hope that helps.
August 13th, 2008 at 12:20 PM 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.
August 13th, 2008 at 03:44 PM OpenStruct is interesting, and does seem to cover the initial iteration of Hashit here, but unfortunately doesn't handle nested hashes.