Naming getter method with 'get_' when attribute is already present - ruby-on-rails

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

Related

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

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/

Does Rails have an instance method on ActiveRecord models that combines "respond_to?" and "send"?

I have done some googling on it but to no avail.
My scenario is that I have a helper method that takes some options and then renders a commonly used graphical element on the page through a partial, tweaked by the sent in options. Much of these options have different overridable defaults set by the helper, based on the first two required arguments - object and context.
def my_helper(object, context, options = {})
defaults = { ... }
defaults[:foo] = "bar" if object.is_a?(SomeObject)
defaults[:ping] = "pong" if context.eql?(:some_context)
...
render partial: '/path/to/partial', locals: defaults.merge(options)
end
While the context is all nice and dandy, I have decided to move away from looking at the object class and, where plausible, use respond_to? rather. What I want to avoid though is having multiple if object.respond_to?(:foo?) && object.foo?, and just use something like if object.respond_to_and_send(:foo?), which would return nil if the object cannot respond to the method.
Update
I forgot to mention that this is a Rails 3.2 application, which is a shame since the updated try method in Rails 4, as mentioned in Holger Just's answer, is exactly what I need.
You can use try like this:
object.try(:foo?)
It will check if object responds to foo? and if so, calling the method. If object does not respond to the method, try will return nil.
See the documentation for details.
The answer to your question is No. UPD: object.try(:foo?) works in Rails 4, as Holger Just suggests above.
There are workarounds though. One of them is to create a module with the method you need and extend your context object with it.
module SmartResponder
def respond_to_and_send(method, *args, &block)
public_send(method, *args, &block) if respond_to?(method)
end
end
context = [1,2,3]
context.extend(SmartResponder)
context.respond_to_and_send(:to_s)
# => "[1, 2, 3]"
context.respond_to_and_send(:to_sssss)
# => nil
context.respond_to_and_send(:inject, 100) { |x,y| x+y }
# => 106
object.send(:foo) rescue nil
This should do the trick. Be careful of introducing subtle bugs using this technique. (for example if the method call itself raises an error, you will get nil and the actual exception will be hidden)

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'

Add http(s) to URL if it's not there?

I'm using this regex in my model to validate an URL submitted by the user. I don't want to force the user to type the http part, but would like to add it myself if it's not there.
validates :url, :format => { :with => /^((http|https):\/\/)?[a-z0-9]+([-.]{1}[a-z0-9]+).[a-z]{2,5}(:[0-9]{1,5})?(\/.)?$/ix, :message => " is not valid" }
Any idea how I could do that? I have very little experience with validation and regex..
Use a before filter to add it if it is not there:
before_validation :smart_add_url_protocol
protected
def smart_add_url_protocol
unless url[/\Ahttp:\/\//] || url[/\Ahttps:\/\//]
self.url = "http://#{url}"
end
end
Leave the validation you have in, that way if they make a typo they can correct the protocol.
Don't do this with a regex, use URI.parse to pull it apart and then see if there is a scheme on the URL:
u = URI.parse('/pancakes')
if(!u.scheme)
# prepend http:// and try again
elsif(%w{http https}.include?(u.scheme))
# you're okay
else
# you've been give some other kind of
# URL and might want to complain about it
end
Using the URI library for this also makes it easy to clean up any stray nonsense (such as userinfo) that someone might try to put into a URL.
The accepted answer is quite okay.
But if the field (url) is optional, it may raise an error such as undefined method + for nil class.
The following should resolve that:
def smart_add_url_protocol
if self.url && !url_protocol_present?
self.url = "http://#{self.url}"
end
end
def url_protocol_present?
self.url[/\Ahttp:\/\//] || self.url[/\Ahttps:\/\//]
end
Preface, justification and how it should be done
I hate it when people change model in a before_validation hook. Then when someday it happens that for some reason models need to be persisted with save(validate: false), then some filter that was suppose to be always run on assigned fields does not get run. Sure, having invalid data is usually something you want to avoid, but there would be no need for such option if it wasn't used. Another problem with it is that every time you ask from a model is it valid these modifications also take place. The fact that simply asking if a model is valid may result in the model getting modified is just unexpected, perhaps even unwanted. There for if I'd have to choose a hook I'd go for before_save hook. However, that won't do it for me since we provide preview views for our models and that would break the URIs in the preview view since the hook would never get called. There for, I decided it's best to separate the concept in to a module or concern and provide a nice way for one to apply a "monkey patch" ensuring that changing the fields value always runs through a filter that adds a default protocol if it is missing.
The module
#app/models/helpers/uri_field.rb
module Helpers::URIField
def ensure_valid_protocol_in_uri(field, default_protocol = "http", protocols_matcher="https?")
alias_method "original_#{field}=", "#{field}="
define_method "#{field}=" do |new_uri|
if "#{field}_changed?"
if new_uri.present? and not new_uri =~ /^#{protocols_matcher}:\/\//
new_uri = "#{default_protocol}://#{new_uri}"
end
self.send("original_#{field}=", new_uri)
end
end
end
end
In your model
extend Helpers::URIField
ensure_valid_protocol_in_uri :url
#Should you wish to default to https or support other protocols e.g. ftp, it is
#easy to extend this solution to cover those cases as well
#e.g. with something like this
#ensure_valid_protocol_in_uri :url, "https", "https?|ftp"
As a concern
If for some reason, you'd rather use the Rails Concern pattern it is easy to convert the above module to a concern module (it is used in an exactly similar way, except you use include Concerns::URIField:
#app/models/concerns/uri_field.rb
module Concerns::URIField
extend ActiveSupport::Concern
included do
def self.ensure_valid_protocol_in_uri(field, default_protocol = "http", protocols_matcher="https?")
alias_method "original_#{field}=", "#{field}="
define_method "#{field}=" do |new_uri|
if "#{field}_changed?"
if new_uri.present? and not new_uri =~ /^#{protocols_matcher}:\/\//
new_uri = "#{default_protocol}://#{new_uri}"
end
self.send("original_#{field}=", new_uri)
end
end
end
end
end
P.S. The above approaches were tested with Rails 3 and Mongoid 2.
P.P.S If you find this method redefinition and aliasing too magical you could opt not to override the method, but rather use the virtual field pattern, much like password (virtual, mass assignable) and encrypted_password (gets persisted, non mass assignable) and use a sanitize_url (virtual, mass assignable) and url (gets persisted, non mass assignable).
Based on mu's answer, here's the code I'm using in my model. This runs when :link is saved without the need for model filters. Super is required to call the default save method.
def link=(_link)
u=URI.parse(_link)
if (!u.scheme)
link = "http://" + _link
else
link = _link
end
super(link)
end
Using some of the aforementioned regexps, here is a handy method for overriding the default url on a model (If your ActiveRecord model has an 'url' column, for instance)
def url
_url = read_attribute(:url).try(:downcase)
if(_url.present?)
unless _url[/\Ahttp:\/\//] || _url[/\Ahttps:\/\//]
_url = "http://#{_url}"
end
end
_url
end
I had to do it for multiple columns on the same model.
before_validation :add_url_protocol
def add_url_protocol
[
:facebook_url, :instagram_url, :linkedin_url,
:tiktok_url, :youtube_url, :twitter_url, :twitch_url
].each do |url_method|
url = self.send(url_method)
if url.present? && !(%w{http https}.include?(URI.parse(url).scheme))
self.send("#{url_method.to_s}=", 'https://'.concat(url))
end
end
end
I wouldn't try to do that in the validation, since it's not really part of the validation.
Have the validation optionally check for it; if they screw it up it'll be a validation error, which is good.
Consider using a callback (after_create, after_validation, whatever) to prepend a protocol if there isn't one there already.
(I voted up the other answers; I think they're both better than mine. But here's another option :)

Resources