Matt posted a really elegant piece of code today that generates Graphviz files (suitable for importing into OmniGraffle) of your Rails ActiveRecord relationships. It’s pretty neat, and certainly handy for getting to know foreign codebases.
There’s one neat trick in there, though, that I wanted to expand on, as Matt breifly chatted to me about the problem earlier today over IM – namely, how you get the actual class object so that you can call reflect_on_all_associations
on it.
In Ruby, it’s easy to dynamically call methods – you can put the name of the method into a string, and then simply run Object.send(methodname)
. Getting the actual Class Object for a particular object – that’s much trickier.
There’s the obvious solution of using eval
. So, to get all the methods on your classname:
classname = 'Integer' eval classname + '.methods'
but that, of course, is pretty nasty and kludgy. This is Ruby, after all; there’s got to be a better solution, right?
There is. If you look in the Pickaxe, you’ll find that Class Names Are Constants:
All the built-in classes, along with the classes you define, have a corresponding global constant with the same name as the class.
So this means that by passing the class name to the const_get
method on the Kernel
module, we’ll confirm if a class exists with that name (eg Integer
). Then, because that constant is really a reference to an object of the same name, by sending a message (the method calal) to the constant, it will be passed on to the object and run (which is the best way I’ve got of explaining this). Job done! To use the previous example:
classname = 'Integer' Kernel.const_get(classname).methods
Which is, you must admit, a bit more elegant and maintainable than the evil that is eval
.
Simon Willison | 3 Aug 2006
How does that work with classes with the same name as other classes, but in different modules? I never really figured out classes and modules when I was playing with Ruby so this is probably a dumb question.
ctran | 3 Aug 2006
There’s a shortcut in Rails for this, ‘Integer’.constantize.methods will do the same.
bobo | 6 Aug 2006
Re: classes with same name? In a way you’ve stumbled on the very purpose of ruby modules, as I understand it: it’s a mechanism for disambiguating classes, for avoiding class name clashes. A module provides a http://en.wikipedia.org/wiki/Namespace
Anyway, to answer your question:
ModuleName.const_get(“MyClass”)
Tom | 6 Aug 2006
Simon and I have now discussed this over IM. As Bobo points out, we discussed namespacing, and how classes not inside a module end up as part of Kernel. (And, of course, that Kernel is a module that Class includes).
mexxx | 11 Aug 2006
Very nice. I have another question in a way related to this.
Let’s say that I have a class BigTree and I have a string ‘big_tree’ how do you get the string translated to ‘BigTree’ so that I can use this translated string the way you showed?
Tom | 11 Aug 2006
No problems, mexxx. Have a look at this little blog post for some tips.
Once you’ve done that, you can then use what I describe in this post.
Zargony | 6 Sep 2006
mexxx, in Rails there’s the camelcase method:
“big_tree”.camelcase #=> ‘BigTree’
Gert | 2 Nov 2006
About constantize as alternative, doesn’t that use eval, which was precisely what this post wanted to avoid?
http://dev.rubyonrails.org/ticket/2856
Daniel | 6 Nov 2006
How would you do this if you don’t know your module and class in advance. For example I want to get an instance of ‘MyModule::MyClass’ it works with eval but neighter with constantize nor with Kernel. Thanks, Daniel
Joey | 1 Mar 2007
In (late) response to Daniel’s question:
Object.const_get(‘MyModule’).const_get(‘MyClass’)
=> MyModule::MyClass
Nate Klaiber | 26 Sep 2007
I recently had the need to do the same thing as discussed here. Since I used it in several places I just extended String to have a ‘to_class’ method that simply said:
def.to_class
Kernel.const_get(self)
end
So I could use something like “Product”.to_class which would then allow me to use all methods on Product.
This could even be extended more by using ‘classify’ or the like before you return the constant.
Nate Klaiber | 26 Sep 2007
Oops.
def to_class
…
end
Mirage | 9 Jun 2008
A nice one-liner to handle the case with classes nested in an arbitrary number of modules is the following:
class_name = “MyModule::MyClass”
class_name.split(‘::’).inject(Kernel) {|scope, const_name| scope.const_get(const_name)}
Which as Nate pointed out could be put in a String.to_class method, or Kernel.get_class.
Chris Hoeppner | 1 Dec 2008
Time to dig into #inject. It’s a method I have not yet understood.
xprdr | 3 Dec 2008
inject is not used for the purpose you are trying to accomplish here. plus there are at least two types of injects:
http://www.java2s.com/Code/Ruby/Range/Injectarange.htm
and
http://www.java2s.com/Code/Ruby/Hash/injectwithregularexpression.htm
Michael A | 15 Dec 2008
Well done Mirage. I’ve never used inject for any real work but it makes short work of this problem.
btower | 3 Jan 2009
What makes the non-eval version more maintainable?
petef | 17 Nov 2009
And of course FWIW you can go the other way to define classes from Strings:
# Declare all the classes up front to avoid forward references
%w(Tom Dick Harry George).each do |c|
Tunnel.const_set(c, Class.new)
end
Frank | 19 Dec 2009
Thanks petef. Just needed that answer!
Jaydeep Dave | 8 Feb 2010
This is the answer needed maaan……. you have no idea, what you have helped me to build.
DSimon | 11 Jun 2010
This is lovely! It’s especially nice because it is much more secure than eval when handling user input. I don’t have to worry about some sneaky user trying to get me to load a type named ” exec ‘rm -rf /’ “.
Of course, I still have to make sure that the class name specified isn’t something unexpected which would have disastrous side effects, but that’s way easier than trying to somehow sanitize a call to exec; all I have to do is make sure the input class includes the base type of the classes I expect to deserialize in its ancestors.
Josh Carter | 26 Jul 2010
@btower you really don’t want to be eval’ing strings that could come from an untrusted source, lest someone get a class name into your system like “system(‘rm -rf /’)”. ;)
Thanks for the tips here. I had figured out the issue with creating classes under nested modules, but failed to realize that I could condense my several lines of code using inject (thanks @Mirage). FWIW more recent versions of Ruby have an alias “reduce” that I think is closer to the intent of the method than “inject.”
16 Nov 2006
Trackback: Generic actions for Rails subclasses
26 Apr 2009
Trackback: Andrew’s Blog » Getting Reference to a Class with a String Class Name in Ruby