Implement method_missing in Rails - ruby-on-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?

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").

Related

Naming getter method with 'get_' when attribute is already present

I'm attempting to restrict an API's content type in a RoR application, with a method that gets inherited by all controllers.
CONTENT_TYPE = 'application/vnd.api+json'
def restrict_content_Type
return if request.content_type = CONTENT_TYPE
render_content_type_error
end
this works fine, but now I must use a different content type for a single endpoint and controller, and I'd like to just change the content of the CONTENT_TYPE constant while reusing the code I already have. To use a different constant I must use a reader method that looks up the constant in the current controller.
I refactored the code into:
def get_content_type
self::CONTENT_TYPE
end
def restrict_content_type
return if request.content_type == get_content_type
...
end
The reason I've used a get_* reader is that self.content_type returns the Request's content type: https://api.rubyonrails.org/classes/ActionDispatch/Response.html#method-i-content_type
At this point Rubocop is complaining because of the name I used, get_*readers are not idiomatic Ruby.
I can surely override this behaviour in rubocop but I'd like to hear what are my other options and if there are other solutions, because I don't like the name of the method either.
Any idea?
You can use some other names that reveals the purpose of this this method, e.g. current_content_type, restricted_content_type, disabled_content_type - whatever suits you best.
About the naming it could be nice to have a method called invalid_content_type? which returns a Boolean.
For e.g :
def invalid_content_type?(content_type)
request.content_type == content_type
end
def restrict_content_type
return if invalid_content_type(self::CONTENT_TYPE)
...
end

Preferred Workflow for Ruby-YAML interaction

I have just started working on a project where there is a lot of interaction between Ruby and 5-6 levels deep YAML files. Since Ruby will respond with NoMethodError: undefined method '[]' for nil:NilClass when you are trying to access a key that doesn't exist there are lots of methods with the following setup:
def retrieve_som_data(key1, key2)
results = []
if data(key1, key2)
if data_set_2(key, key2)["my_key"]
results = data_set_2(key, key2)["my_other_key"]
end
end
return results.clone
end
This looks horrible, so I am looking at a way to refactor it. I have tried working on a version where I would replace a method like this:
def data(key1, key2)
if data = names_data(key1)
return data[key2]
end
end
with this instead:
def data(key1, key2)
names_data(key1).fetch(key2)
end
This raises a more specific error KeyError which can than be rescued and acted on in any method calling .data(), but this also doesn't seem like a good solution readability wise.
I'd love to get some input on how you are handling situations where you are trying to access YAML_DATA[key][key1][key2][key3][key4] and take into account that any of the provided keys could hit something thats nil.
What are your preferred workflows for this?
If you're using rails, they've added a method, try to Object. For nil objects this will always return nil, rather than throwing so you could do something along the lines of this:
def get_yaml_obj(yaml_data, key1, key2, key3)
yaml_data.try(:[], key1).try(:[], key2).try(:[], key3)
end
Or if you have an arbitary number of keys:
def get_data(yaml_data, keys)
keys.each do |key|
yaml_data = yaml_data.try(:[], key)
end
yaml_data
end

Storing json api calls in Rails

I have built an app that consumes a json api. I removed active record from my app because the data in the api can theoretically change and I don't want to wipe the database each time.
Right now I have a method called self.all for each class that loops through the json creating ruby objects. I then call that method in various functions in order to work with the data finding sums and percentages. This all works fine, but seems a bit slow. I was wondering if there is somewhere I should be storing my .all call rather than instantiating new objects for each method that works with the data.
...response was assign above using HTTParty...
def self.all
puppies = []
if response.success?
response['puppies'].each do |puppy|
accounts << new(puppy['name'],
puppy['price'].to_money,
puppy['DOB'])
end
else
raise response.response
end
accounts
end
# the methods below only accept arguments to allow testing with Factories
# puppies is passed in as Puppy.all
def self.sum(puppies)
# returns money object
sum = Money.new(0, 'USD')
puppies.each do |puppy|
sum += puppy.price
end
sum
end
def self.prices(puppies)
prices = puppies.map { |puppy| puppy.price }
end
def self.names(puppies)
names = puppies.map { |puppy| puppy.name }
end
....many more methods that take an argument of Puppy.all in the controller....
Should I use cacheing? should I bring back active record? or is how I'm doing it fine? Should I store Puppy.all somewhere rather than calling the method each time?
What I guess is happening is that you are making a request with HTTParty every time you call any class method. What you can consider is creating a class variable for the response and a class variable called expires_at. Then you can do some basic caching.
##expires_at = Time.zone.now
##http_response
def make_http_call
renew_http_response if ##expires_at.past?
end
def renew_http_response
# make HTTParty request here
##http_response = # HTTParty response
##expires_at = 30.minutes.from_now
end
# And in your code, change response to ##response
# ie response.success? to ##response.success?
This is all in memory and you lose everything if you restart your server. If you want more robust caching, the better thing to do would probably to look into rails low-level caching

write a lib file to add correlation_guid with every json response

Whenever I render json or publish it to some queue I want to attach a correlation_guid so I could follow it along my stack of services that use and push the data along.
The correlation_guid will either be given as a header, or not exist, in which case I'd make it.
Both of these parts are easy. The tough part is actually sticking it in my responses. I was thinking of altering the method to_json, so that whenever that method is called, it does something of the following:
#should override other to_jsons
def to_json
unless self[:correlation_id]
self[:correlation_id] = header['CORRELATION-ID'] || SecureRandom.uuid
end
super
end
However, how would I catch all the to_jsons? I know Array, Hash, ActiveRecord, and probably more have that. Further, I'm pretty sure super as above wouldn't work, but the idea is to then use the to_json of whatever the original object is.
I think you want to override the as_json in your base model instead. as_json is invoked by to_json:
# File activesupport/lib/active_support/core_ext/object/to_json.rb, line 15
def to_json(options = nil)
ActiveSupport::JSON.encode(self, options)
end
# File activesupport/lib/active_support/json/encoding.rb, line 48
def encode(value, use_options = true)
check_for_circular_references(value) do
jsonified = use_options ? value.as_json(options_for(value)) : value.as_json
jsonified.encode_json(self)
end
end
Here's also a nice article about the difference between as_json and to_json: http://jonathanjulian.com/2010/04/rails-to_json-or-as_json/

How to transform a string into a variable/field?

I'm new to Ruby and I would like to find out what the best way of doing things is.
Assume the following scenario:
I have a text field where the user can input strings. Based on what the user inputs (after validation) I would like to access different fields of an instance variable.
Example: #zoo is an instance variable. The user inputs "monkey" and I would like to access #zoo.monkey. How can I do that in Ruby?
One idea that crossed my mind is to have a hash:
zoo_hash = { "monkey" => #zoo.monkey, ... }
but I was wondering if there is a better way to do this?
Thanks!
#zoo.attributes gives you a hash of the object attributes. So you can access them like
#zoo.attributes['monkey']
This will give nil if the attribute is not present. Calling a method which doesn't exist will throw NoMethodError
In your controller you could use the public_send (or even send) method like this:
def your_action
#zoo.public_send(params[:your_field])
end
Obviously this is no good, since someone can post somehing like delete_all as the method name, so you must sanitize the value you get from the form. As a simple example:
ALLOWED_METHODS = [:monkey, :tiger]
def your_action
raise unless ALLOWED_METHODS.include?(params[:your_field])
#zoo.public_send(params[:your_field])
end
There is much better way to do this - you should use Object#send or (even better, because it raises error if you try to call private or protected method) Object#public_send, like this:
message = 'monkey'
#zoo.public_send( message )
You could implement method_missing in your class and have it interrogate #zoo for a matching method. Documentation: http://ruby-doc.org/core-1.9.3/BasicObject.html#method-i-method_missing
require 'ostruct' # only necessary for my example
class ZooKeeper
def initialize
#zoo = OpenStruct.new(monkey: 'chimp')
end
def method_missing(method, *args)
if #zoo.respond_to?(method)
return #zoo.send(method)
else
super
end
end
end
keeper = ZooKeeper.new
keeper.monkey #=> "chimp"
keeper.lion #=> NoMethodError: undefined method `lion'

Resources