views:

219

answers:

1

I have a Flickr interface that I wrote a while ago and part of it bothers me and I'd like to make it nicer. The way it works is I use method missing to construct the url parameters for the flickr call from the methods called on the flickr object, eg.

@flickr.groups.pools.getPhotos(:user_id => "12656878@N06", :group_id => "99404851@N00")

These 'method calls' construct an api call that looks like this

http://api.flickr.com/services/rest/?method=groups.pools.getPhotos&user_id=1848466274& group_id= 99404851@N00

(I have left off the api key bit) It does this by remembering each 'method' until a method is called with arguments at which time it constructs the url and and makes the call to Flickr.

The reason I took this approach is so the ruby code matches the documentation on the Flickr site, you can copy and paste it and it will mostly work, and hopefully it will make it a bit more resilient to api changes because I have no hard coded methods.

What irritates about this though is that in this example the group id is being passed to the getPhotos method rather than the groups method. I would much rather it looked like this.

@flickr.groups(:group_id => "99404851@N00").pools.getPhotos(:user_id => "12656878@N06")

So my question. Is there any way in Ruby that I can detect that the last method has been called so that I can trigger the call to Flickr?

+1  A: 

Since I can't think of any way to detect the last method call, I'm going to suggest a different approach. It is similar to what ActiveRecord does; a proxy class that builds the options and doesn't fetch the data until you call a method that operates on the data.

class Flickr
  def initialize
    @result = FlickrResult.new
  end

  def method_missing(method, *args, &block)
    if @result.data.respond_to?(method)
      @result.run(method, args, block)
    else
      @result.append(method, args[0])
      return self
    end
  end

  class FlickrResult
    attr_reader :data

    def initialize
      @data = []
      @keys = []
      @options = {}
    end

    def append(key, options)
      @keys << key
      @options.merge!(options) if options
    end

    def run(method, args, block)
      if !@did_run
        fetch_data
      end

      @data.send(method, *args, &block)
    end

    def fetch_data
      puts "Only runs once!"
      @url = @keys.join(".") + "?" + @options.map {|k, v| "#{k}=#{v}" }.join("&")
      # use @url and fetch data..
      @data = ["foo", "bar"]

      @did_run = true
    end
  end
end

@flickr = Flickr.new
@flickr.groups(:group_id => "123").pools.thing.users(:user_id => "456")

@flickr.each {|f| p f }
# => "Only runs once!"
# => "foo"
# => "bar"
p @flickr.map {|f| f.upcase }
# => ["FOO", "BAR"]

It only fetches the data when you each or map it or whatever (any array method).

August Lilleaas
Why "@flickr" in the outer scope? What's the advantage of making it an instance variable of Object versus a local variable?
glenn jackman
No advantage (in this case). Just a brainfart, no special considerations in it :)
August Lilleaas
Yeah, this was an alternative method I had looked at. And using memoization on the attributes. I was hoping there was some funky method I'm not aware of.
Henry
This isn't the answer I was hoping for but it looks like the best way to do what I'm after.
Henry