M
Matthew Smillie
Hello all,
I've done a quick little project to see if I can wrap my head around
Ruby metaprogramming, and I'd like to run it by some more-experienced
minds.
I picked the quick little task of coding up the web API for flickr
(http://flickr.com/services/API). Not necessarily for practical
usage (though I may use it myself), but more because it struck me as
a good example since there are a lot of methods with extremely
similar behaviour on the client side (send parameters to this URL).
In any case, here's the code:
require 'pp'
class Flickr
def method_missing(method_id, *params)
# Find the desired class name
class_name = method_id.to_s.capitalize
# Find the corresponding instance variable
ivar = :"@#{method_id}"
# have we already made this particular class before?
unless self.class.const_defined?(class_name)
# new class which inherits from this one.
new_class = self.class.const_set(class_name, Class.new
(self.class))
# new instance variable
instance_variable_set(ivar, new_class.new)
end
# if we have parameters, execute the appropriate method (returning
# what, though?) otherwise return the instance we just made so
# that the next thing can be called correctly.
return instance_variable_get(ivar) unless params.length > 0
the_method = instance_variable_get(ivar).class.name.downcase.gsub
(/::/, '.')
# abstract out the actual API call for the moment.
puts "call: http://flickr.com/services/rest/?method='#
{the_method}'"
puts "with these other params: "
pp *params
end
end
# envisaged usage
flickr = Flickr.new
result = flickr.test.echo({"api_key" => "something long", "foo" =>
"bar"})
The idea is that the usage should mirror how the methods are defined
in the flickr docs.
In general I'm asking (like the subject suggests) if my
metaprogramming underpants are showing? Have I defied any particular
conventions? Does this seem like a sensible approach, and if so, a
sensible implementation? Have I set myself up for some rather
spectacular failures?
Specifically, though, there are three aspects of the code that I'm
particularly curious whether anyone has any alternate approaches:
1. encoding the method name as a class heirarchy. e.g.
'flickr.test.echo' is implicit in the class definition that results
from that call (Flickr::Test::Echo), then the method name gets
reconstructed from that when the eventual call is made. Any other
ways to do this?
2. relying on params.length to determine the 'end' of the call seems
a little funny. On the other hand, every method call takes at least
one parameter. One idea I had was to inherit from Proc, and define
#call, which would let flickr methods get passed around as, well,
methods, though this seems to have its own dangers. (If I were to
implement this as a practical library, I think I'd use flickr's
reflection methods to sort this out, but is there a way to do it that
doesn't require that sort of external oracle?)
3. using method_missing strikes me as a potential pitfall, but I can
re-raise this if/when the flickr API returns its own 'method not
found' error. Are there any other 'gotchas' I should watch out for?
Thanks in advance for any feedback.
matthew smillie.
I've done a quick little project to see if I can wrap my head around
Ruby metaprogramming, and I'd like to run it by some more-experienced
minds.
I picked the quick little task of coding up the web API for flickr
(http://flickr.com/services/API). Not necessarily for practical
usage (though I may use it myself), but more because it struck me as
a good example since there are a lot of methods with extremely
similar behaviour on the client side (send parameters to this URL).
In any case, here's the code:
require 'pp'
class Flickr
def method_missing(method_id, *params)
# Find the desired class name
class_name = method_id.to_s.capitalize
# Find the corresponding instance variable
ivar = :"@#{method_id}"
# have we already made this particular class before?
unless self.class.const_defined?(class_name)
# new class which inherits from this one.
new_class = self.class.const_set(class_name, Class.new
(self.class))
# new instance variable
instance_variable_set(ivar, new_class.new)
end
# if we have parameters, execute the appropriate method (returning
# what, though?) otherwise return the instance we just made so
# that the next thing can be called correctly.
return instance_variable_get(ivar) unless params.length > 0
the_method = instance_variable_get(ivar).class.name.downcase.gsub
(/::/, '.')
# abstract out the actual API call for the moment.
puts "call: http://flickr.com/services/rest/?method='#
{the_method}'"
puts "with these other params: "
pp *params
end
end
# envisaged usage
flickr = Flickr.new
result = flickr.test.echo({"api_key" => "something long", "foo" =>
"bar"})
The idea is that the usage should mirror how the methods are defined
in the flickr docs.
In general I'm asking (like the subject suggests) if my
metaprogramming underpants are showing? Have I defied any particular
conventions? Does this seem like a sensible approach, and if so, a
sensible implementation? Have I set myself up for some rather
spectacular failures?
Specifically, though, there are three aspects of the code that I'm
particularly curious whether anyone has any alternate approaches:
1. encoding the method name as a class heirarchy. e.g.
'flickr.test.echo' is implicit in the class definition that results
from that call (Flickr::Test::Echo), then the method name gets
reconstructed from that when the eventual call is made. Any other
ways to do this?
2. relying on params.length to determine the 'end' of the call seems
a little funny. On the other hand, every method call takes at least
one parameter. One idea I had was to inherit from Proc, and define
#call, which would let flickr methods get passed around as, well,
methods, though this seems to have its own dangers. (If I were to
implement this as a practical library, I think I'd use flickr's
reflection methods to sort this out, but is there a way to do it that
doesn't require that sort of external oracle?)
3. using method_missing strikes me as a potential pitfall, but I can
re-raise this if/when the flickr API returns its own 'method not
found' error. Are there any other 'gotchas' I should watch out for?
Thanks in advance for any feedback.
matthew smillie.