I'm trying to write a method for a web service that will find objects
from a user specified table, i.e. find all Widgets added in last 7
days, or find all Whatsits whose name starts with Q.
I could right a separate method for each table calling i.e.
Widget.find() or Whatsit.find(), but what I'd like to do is have a
single interface that lets me call something like
user_specified_table.find() where user_specified_table is a string
equal to either Widget or Whatsit.
How can I do this? Is there an entirely different way of doing this
that I should be looking at?
I'm trying to write a method for a web service that will find objects
from a user specified table, i.e. find all Widgets added in last 7
days, or find all Whatsits whose name starts with Q.
I could right a separate method for each table calling i.e.
Widget.find() or Whatsit.find(), but what I'd like to do is have a
single interface that lets me call something like
user_specified_table.find() where user_specified_table is a string
equal to either Widget or Whatsit.
How can I do this? Is there an entirely different way of doing this
that I should be looking at?
method_missing is your best friend.
Oh, and 'send', too.
Fast attempt at an example:
#!/usr/local/bin/ruby
class Finder
def method_missing( sym, *args )
if sym.to_s =~ /(\w+)_find$/
klass = $1.capitalize
"#{klass}.find"
elsif sym.to_s =~ /(\w+)_find_by_([a-z_]+)/
klass = $1.capitalize
params = $2
"#{klass}.find_by_#{params}"
else
super
end
end
end
if __FILE__ == $0
require 'test/unit'
class TC_FOO < Test::Unit::TestCase
def setup @f = Finder.new
end
def test_00
res = @f.whatsit_find
assert_equal( "Whatsit.find" , res )
end
def test_01
res = @f.widget_find_by_name
assert_equal( "Widget.find_by_name" , res )
end
def test_02
assert_raise( NoMethodError ){ @f.widget_fetch_me_coffee
}
end
end
end
Thanks for the quick reply. I'm just starting with both Ruby and Rails,
so this was very helpful. How would you execute the result of the
method_missing call? What should I do to make sure this is secure?
Thanks for the quick reply. I'm just starting with both Ruby and Rails,
so this was very helpful. How would you execute the result of the
method_missing call? What should I do to make sure this is secure?
Once method_missing has teased apart the request, you have few options.
The example just created a string of code, so you could just pass that to eval(). Not recommend expect perhaps to test some code to see that things are perhaps working up to that point.
Since you decompose the name of the missing method into a class name and a request, you can get a reference to the class then try to invoke request as a class method.
class Finder
def method_missing( sym, *args )
if sym.to_s =~ /(\w+)_find$/
klass = $1.capitalize
cls = Object.const_get( klass )
return cls.find
elsif sym.to_s =~ /(\w+)_find_by_([a-z_]+)/
klass = $1.capitalize
cls = Object.const_get( klass )
params = $2
cls.send( "find_by_#{params}", args )
else
super
end
end
end
If you prefer to create object instances, then first call 'new' on the class reference:
def method_missing( sym, *args )
if sym.to_s =~ /(\w+)_find$/
klass = $1.capitalize
cls = Object.const_get( klass )
obj = cls.new
return obj.find
elsif sym.to_s =~ /(\w+)_find_by_([a-z_]+)/
klass = $1.capitalize
cls = Object.const_get( klass )
params = $2
obj = cls.new
obj.send( "find_by_#{params}", args )
else
super
end
end
Note that all sorts of error handling has been omitted here.
You may want to take more precautions in what classes are created and what methods get invoked. For example you could first check that the extracted class name is contained in an 'allowed objects' list before instantiation anything.
This is very helpful. Thank you very much. Not only have you helped me
with this particular problem, you've sent me scurrying after several
new concepts that have me thinking about other problems in new ways.