ruby-on-railsrubymethod-missing

Implement method_missing in Rails


I'm using the OMDB API to learn about using 3rd Party apis in Rails. I've setup my app so all I have to input is the movie title and 6 other attributes get populated from the OMDB API. All of the method calls to retrieve the data from the api are very similar. The only thing that changes is one word in the method name and one word in the method body. Here is one such call:

app/services/omdb_service.rb

def get_image_by_title(title)
  response = HTTP.get("http://www.omdbapi.com/?t=#{title}&apikey=123456789").to_s
  parsed_response = JSON.parse(response)
  parsed_response['Poster']
end

The things that change are the word after get in the method name and the word in the parsed_response['Poster']. They will change depending on what attribute I'm trying to get back.

I thought I could use method_missing to prevent duplication, but I'm having no success with it. Here is my method_missing call:

app/services/omdb_service.rb

def method_missing(method, *args)
  if method.to_s.end_with?('_by_title')
    define_method(method) do | args |
      response = HTTP.get("http://www.omdbapi.com/?t=#{args[0]}&apikey=123456789").to_s
      parsed_response = JSON.parse(response)
      parsed_response['args[1]']
    end
  end
end

Can anyone see what is wrong with my method_missing call?


Solution

  • First of all, let me stress that this isn't necessarily a good use case for method_missing because there doesn't seem to be a way to get self-explanatory method names, parameters and such. Nevertheless, I'll try to answer your question as best as I can.

    First of all, you need to adopt your method naming to the things that the API gives you to reduce the number of parameters. In the example you've given, you'd want to change the method call to get_poster_by_t because poster is the output and t is the input variable based on the URL and response you've shared.

    Following this logic, you'd have to write method missing like so:

    def method_missing(method, *args)
      if method =~ /\Aget_([^_]+)_by_([^_]+)\z/
        response = HTTP.get("http://www.omdbapi.com/?#{$~[2]}=#{args[0]}&apikey=123456789").to_s
        parsed_response = JSON.parse(response)
        parsed_response[$~[1].capitalize]
      end
    end
    

    Then you should also incorporate Ruby's rules for implementing method_missing, namely calling super when your rule doesn't match and also overriding respond_to_missing?. This then gives you:

    def method_missing(method, *args)
      if method.to_s =~ /\Aget_([^_]+)_by_([^_]+)\z/
        response = HTTP.get("http://www.omdbapi.com/?#{$~[2]}=#{args[0]}&apikey=123456789").to_s
        parsed_response = JSON.parse(response)
        parsed_response[$~[1].capitalize]
      else
        super
      end
    end
    
    def respond_to_missing?(method, *args)
      method.to_s =~ /\Aget_([^_]+)_by_([^_]+)\z/ || super
    end
    

    Also see https://makandracards.com/makandra/9821-when-overriding-method_missing-remember-to-override-respond_to_missing-as-well.


    Personally, I'd not use method_missing here but instead go with an expressive method call – something like this:

    def get_field_by_param(field:, param:, value:)
      response = HTTP.get("http://www.omdbapi.com/?#{param}=#{value}&apikey=123456789").to_s
      parsed_response = JSON.parse(response)
      parsed_response[field]
    end
    

    You can then do things like get_field_by_param(field: "Poster", param: :t, value: "Whatever").