How do I convert a string to a class method? - ruby-on-rails

This is how to convert a string to a class in Rails/Ruby:
p = "Post"
Kernel.const_get(p)
eval(p)
p.constantize
But what if I am retrieving a method from an array/active record object like:
Post.description
but it could be
Post.anything
where anything is a string like anything = "description".
This is helpful since I want to refactor a very large class and reduce lines of code and repetition. How can I make it work?

Post.send(anything)

While eval can be a useful tool for this sort of thing, and those from other backgrounds may take to using it as often as one might a can opener, it's actually dangerous to use so casually. Eval implies that anything can happen if you're not careful.
A safer method is this:
on_class = "Post"
on_class.constantize.send("method_name")
on_class.constantize.send("method_name", arg1)
Object#send will call whatever method you want. You can send either a Symbol or a String and provided the method isn't private or protected, should work.

Since this is taged as a Ruby on Rails question, I'll elaborate just a little.
In Rails 3, assuming title is the name of a field on an ActiveRecord object, then the following is also valid:
#post = Post.new
method = "title"
#post.send(method) # => #post.title
#post.send("#{method}=","New Name") # => #post.title = "New Name"

Try this:
class Test
def method_missing(id, *args)
puts "#{id} - get your method name"
puts "#{args} - get values"
end
end
a = Test.new
a.name('123')
So the general syntax would be a.<anything>(<any argument>).

Related

How can i refactor this Ruby, Rails code?

I'm new at Rails... Is there a better way to refactor this code:
def get_product_price_minimum
Product.minimum(:price).to_i
end
def get_product_price_maximum
Product.maximum(:price).to_i
end
You can define something like "prices" (a vague method name, to avoid using get_ or set_ prefixes) to expect an argument, which would be the maximum or minimum for which to query your model:
def prices(what)
Product.public_send(what, :price).to_i
end
Then you can use it by passing the minimum or maximum as a symbol or a string.
I'm not sure your function works
but you can try (if price is column and price is a int not a string)
def get_product_price_minimum
Product.order('price').last
end

Using .constantize to invoke both a class and method from separate strings

I'm trying to call a class and method which are both stored as strings in the DB.
I would be looking to create an object from these 2 strings:
class_name = 'Some::Class'
method_name = 'a_method'
params = {foo: 'bar'}
class_name.new(params).method_name
I have used constantize before for a similar approach, but I'm not sure exactly what the best solution is here?
You can use const_get:
Class.const_get(class_name).new(params).public_send(method_name)
class_name.constantize.new(params).send(method_name) should work?
If you want to send args with the method, you can use, for example, .send(method_name, params)
If you're getting the data from external sources, I'm not sure how secure constantize is, but it works smoothly and is nicely readable. You can also use classify if the format of the class name could vary.
You tagged your question with ruby-on-rails, therefore I suggest using safe_constantize (available in Rails only) and public_send (plain Ruby):
class_name = 'Some::Class'
method_name = 'a_method'
params = {foo: 'bar'}
class_name.safe_constantize.new(params).public_send(method_name)
If the use of Kernel#eval is safe you could do the following.
module Some
class Klass
def initialize(params)
#params = params
end
def a_method
puts "keys = #{#params.keys}"
end
end
end
class_name = 'Some::Klass'
params = { foo: 'bar', oof: 'rab' }
method_name = 'a_method'
str = "%s.new(%s).%s" % [class_name, params, method_name]
#=> "Some::Klass.new({:foo=>\"bar\", :oof=>\"rab\"}).a_method"
eval str
keys = [:foo, :oof]
This approach obviously has wide application.

save multiple parameters in a loop, rails controller

I've created a form with about 40 fields available to edit, I'm trying to save them to a database using the controller. I currently have this code:
c = Form.find(params[:id])
if c
params.each do |k,v|
c.k = params[:v]
end
Which doesn't work, I get this error: undefined method 'k='
if I was going to write them all out manually it would look like this:
c = Form.find(params[:id])
if c
c.title = params[:title]
c.reference = params[:reference]
....
etc.
Assuming that you're trying to update the attributes on your Form record based on what gets passed into params, try this as a basic outline:
c = Form.find_by_id(params[:id])
if c
params.each do |k, v|
c[k] = v
end
c.save!
end
Your original code's use of params[:v] was probably not doing what you were intending, and you really meant for it to be params[:k] instead. However there's actually no need to look up the value for that key inside the loop like that because you already have the value at hand in v.
Here's a quick rundown on the ways of interacting with ActiveRecord attributes: http://www.davidverhasselt.com/2011/06/28/5-ways-to-set-attributes-in-activerecord/
i dont know what you are trying todo but your code seems to be very odd. Solution is as follow
c.send "#{k}=", params[:v]
What about
c = Form.find(params[:id])
c.update_attributes(params[:form])
Note that I guessed the [:form] part in the second line, it depends on your form. check your html source, and see if your fields are something like this:
<input name="form[field_name]" ...
As you see, name contains an "array like" form. Check your HTML source and adapt (so if its name="foo[field_name]", you need to use c.update_attributes(params[:foo]))

If string is empty then return some default value

Often I need to check if some value is blank and write that "No data present" like that:
#user.address.blank? ? "We don't know user's address" : #user.address
And when we have got about 20-30 fields that we need to process this way it becomes ugly.
What I've made is extended String class with or method
class String
def or(what)
self.strip.blank? ? what : self
end
end
#user.address.or("We don't know user's address")
Now it is looking better. But it is still raw and rough
How it would be better to solve my problem. Maybe it would be better to extend ActiveSupport class or use helper method or mixins or anything else. What ruby idealogy, your experience and best practices can tell to me.
ActiveSupport adds a presence method to all objects that returns its receiver if present? (the opposite of blank?), and nil otherwise.
Example:
host = config[:host].presence || 'localhost'
Phrogz sort of gave me the idea in PofMagicfingers comment, but what about overriding | instead?
class String
def |(what)
self.strip.blank? ? what : self
end
end
#user.address | "We don't know user's address"
Since you're doing this in Ruby on Rails, it looks like you're working with a model. If you wanted a reasonable default value everywhere in your app, you could (for example) override the address method for your User model.
I don't know ActiveRecord well enough to provide good code for this; in Sequel it would be something like:
class User < Sequel::Model
def address
if (val=self[:address]).empty?
"We don't know user's address"
else
val
end
end
end
...but for the example above this seems like you'd be mixing view logic into your model, which is not a good idea.
Your or method might have some unwanted side-effects, since the alternative (default) value is always evaluated, even if the string is not empty.
For example
#user.address.or User.make_a_long_and_painful_SQL_query_here
would make extra work even if address is not empty. Maybe you could update that a bit (sorry about confusing one-liner, trying to keep it short):
class String
def or what = ""
self.strip.empty? ? block_given? ? yield : what : self
end
end
#user.address.or "We don't know user's address"
#user.address.or { User.make_a_long_and_painful_SQL_query_here }
It is probably better to extend ActiveRecord or individual models instead of String.
In your view, you might prefer a more explicit pattern like
#user.attr_or_default :address, "We don't know the user's address"
Ruby:
unless my_str.empty? then my_str else 'default' end
RoR:
unless my_str.blank? then my_str else 'default' end
I recommend to use options.fetch(:myOption, defaultValue) because it works great with boolean flags like the ones mentioned above and therefore seems better to use in general.
Examples
value = {}
puts !!(value.fetch(:condition, true)) # Print true
value = {}
value[:condition] = false
puts !!(value.fetch(:condition, true)) # Print false
value = {}
value[:condition] = true
puts !!(value.fetch(:condition, true)) # Print true
value = {}
value[:condition] = nil
puts !!(value.fetch(:condition, true)) # Print false

Rails, using time_select on a non active record model

I am trying to use a time_select to input a time into a model that will then perform some calculations.
the time_select helper prepares the params that is return so that it can be used in a multi-parameter assignment to an Active Record object.
Something like the following
Parameters: {"commit"=>"Calculate", "authenticity_token"=>"eQ/wixLHfrboPd/Ol5IkhQ4lENpt9vc4j0PcIw0Iy/M=", "calculator"=>{"time(2i)"=>"6", "time(3i)"=>"10", "time(4i)"=>"17", "time(5i)"=>"15", "time(1i)"=>"2009"}}
My question is, what is the best way to use this format in a non-active record model. Also on a side note. What is the meaning of the (5i), (4i) etc.? (Other than the obvious reason to distinguish the different time values, basically why it was named this way)
Thank you
You can create a method in the non active record model as follows
# This will return a Time object from provided hash
def parse_calculator_time(hash)
Time.parse("#{hash['time1i']}-#{hash['time2i']}-#{hash['time3i']} #{hash['time4i']}:#{hash['time5i']}")
end
You can then call the method from the controller action as follows
time_object = YourModel.parse_calculator_time(params[:calculator])
It may not be the best solution, but it is simple to use.
Cheers :)
The letter after the number stands for the type to which you wish it to be cast. In this case, integer. It could also be f for float or s for string.
I just did this myself and the easiest way that I could find was to basically copy/paste the Rails code into my base module (or abstract object).
I copied the following functions verbatim from ActiveRecord::Base
assign_multiparameter_attributes(pairs)
extract_callstack_for_multiparameter_attributes(pairs)
type_cast_attribute_value(multiparameter_name, value)
find_parameter_position(multiparameter_name)
I also have the following methods which call/use them:
def setup_parameters(params = {})
new_params = {}
multi_parameter_attributes = []
params.each do |k,v|
if k.to_s.include?("(")
multi_parameter_attributes << [ k.to_s, v ]
else
new_params[k.to_s] = v
end
end
new_params.merge(assign_multiparameter_attributes(multi_parameter_attributes))
end
# Very simplified version of the ActiveRecord::Base method that handles only dates/times
def execute_callstack_for_multiparameter_attributes(callstack)
attributes = {}
callstack.each do |name, values|
if values.empty?
send(name + '=', nil)
else
value = case values.size
when 2 then t = Time.new; Time.local(t.year, t.month, t.day, values[0], values[min], 0, 0)
when 5 then t = Time.time_with_datetime_fallback(:local, *values)
when 3 then Date.new(*values)
else nil
end
attributes[name.to_s] = value
end
end
attributes
end
If you find a better solution, please let me know :-)

Resources