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.
Related
i'm trying (and actually succeded, but i don't understand how it works) to write a custom method for a hash in my model (I'm working on Ruby on Rails 6).
My hash looks like this
my_hash = {
[['name_1', 'slug_1']=>value_1],
[['name_2', 'slug_2']=>value_2],
[['name_1', 'slug_1']=>value_3],
[['name_2', 'slug_2']=>value_4]
}
So basically a hash of arrays. You notice that the 'keys' are arrays that repeat themselves many times, but with different values. What i want to achieve is to write a custom method that "joins" all the keys in only one key, which will have an array of values assigned, so basically i should be able to get:
my_hash = {
['name_1', 'slug_1']=>"values": [value_1, value_3],
['name_2', 'slug_2']=>"values": [value_2, value_4]
}
For that, I have this piece of code, which i use many times:
my_hash.inject({}) do |hash, record|
# each record has the following format => [["unit_name", "axis.slug"]=>average_value(float)]
keys, value = record
# now keys has ["unit_name", "axis.slug"] and values equals average_value
hash[keys.first] ||= {}
hash[keys.first][keys.last] = value.to_f
hash
end
Since I use this many times, i wanted to write a custom method, so i did:
def format_hash_data my_hash
my_hash.inject({}) do |hash, record|
# each record has the following format => [["unit_name", "axis.slug"]=>average_value(float)]
keys, value = record
# now keys has ["unit_name", "axis.slug"] and values equals average_value
hash[keys.first] ||= {}
hash[keys.first][keys.last] = value.to_f
hash
end
end
And used it like: my_hash = format_hash_data(my_hash) with no success(it threw an error saying that 'format_hash_data' was not a valid method for the class).
So I fiddled around and added 'self' to the name of the method, leaving:
def self.format_hash_data my_hash
my_hash.inject({}) do |hash, record|
# each record has the following format => [["unit_name", "axis.slug"]=>average_value(float)]
keys, value = record
# now keys has ["unit_name", "axis.slug"] and values equals average_value
hash[keys.first] ||= {}
hash[keys.first][keys.last] = value.to_f
hash
end
end
Which, to my surprise, worked flawlessly when using my_hash = format_hash_data(my_hash)
I don't really understand why adding 'self' makes my code works, maybe anyone can shed some light? I tried using things like send() or instance_eval first, to just send the piece of code to the actual hash as a method (something like my_hash.instance_eval(my_method)) but I couldn't get it working.
I'm sorry about the long explanation, I hope i was clear enough so any of you who had this same dilemma can understand. Thanks in advance.
Prepending self. to the method name makes it a class method instead of an instance method. If you are not sure of the difference, you should look it up as it is fundamental to properly defining and using classes and methods.
As a class method, you would use it as:
my_hash = MyHash.format_hash_data(my_hash)
Or if you're in scope of the class, simply my_hash = format_hash_data(my_hash), which is why it worked in your case with the self. prepended (class method definition).
If you want to define it as an instance method (a method that is defined for the instance), you would use it like so:
my_hash = my_hash.format_hash_data
And the definition would use the implicit self of the instance:
def format_hash_data
self.inject({}) do |hash, record|
# each record has the following format => [["unit_name", "axis.slug"]=>average_value(float)]
keys, value = record
# now keys has ["unit_name", "axis.slug"] and values equals average_value
hash[keys.first] ||= {}
hash[keys.first][keys.last] = value.to_f
hash
end
end
I am trying to not jsonify a string within a hash. It already has been escaped.
Reading around the way of handling this for PORO is to overwrite as_json. So I wrapped the string in another object. But as I'm dealing with just a string that leads to a stack level too deep when I return the object. When I return the already encoded string, it obviously tries to escape it. activesupport-6.0.3.2/lib/active_support/json/encoding.rb
def jsonify(value)
case value
when String
EscapedString.new(value)
when Numeric, NilClass, TrueClass, FalseClass
value.as_json
when Hash
Hash[value.map { |k, v| [jsonify(k), jsonify(v)] }]
when Array
value.map { |v| jsonify(v) }
else
jsonify value.as_json
end
end
What I could do is monkey patch the above method to accept the class I have wrapped the string in and do nothing.
I guess the other option is to parse the JSON string back into a hash and let it follow the normal procedure. That would impact our performance too much though.
So my question is, Is there are more elegant way of telling Rails to do nothing when encountering a specified object when it tries to jsonify it.?
Edit:
Im using this already jsonified string like:
{foo: MyStringJsonWrapper.new('{"bar":"foobar"}')}.to_json
I was looking around in the file activesupport-6.0.3.2/lib/active_support/json/encoding.rb and noticed you can configure which class is used to perform the json encoding. It is currently JSONGemEncoder which is contained within that class.
So I made my own class:
class MyJSONEncoder < ActiveSupport::JSON::Encoding::JSONGemEncoder
class AlreadyEscapedString < String
def to_json(*)
self
end
end
private
def jsonify(value)
if value.is_a?(AlreadyEncodedStringWrapper)
AlreadyEscapedString.new(value.already_jsonified)
else
super
end
end
end
and
class AlreadyEncodedStringWrapper
attr_accessor :already_jsonified
def initialize already_jsonified
#already_jsonified = already_jsonified
end
def as_json(options = {})
self
end
end
The extra class of AlreadyEscapedString exists to overwrite the behaviour of EscapedString(found in the same encoding class) but do nothing.
Then just set:
ActiveSupport.json_encoder = MyJSONEncoder
Example:
jsonified_hash = {foobar: "barbaz"}.to_json
already_encoded_string = AlreadyEncodedStringWrapper.new(jsonified_hash)
{foo: already_encoded_string}.to_json
=> "{\"foo\": {\"foobar\": \"barbaz\"}}"
To solve my problem, I set fictitious Car Model below:
Car Model has
3 attributes(id, car_name, owner_name) and
2 methods which return integers(riders, passengers).
I want to get ONE HASH which has the values of 2 attributes and 2 methods from all of cars. To solve this problem, my temporary solution is below:
json_format = Car.all.to_json(only: [:car_name, :owner_name], methods: [:riders, :passengers])
final_hash = ActiveSupport::JSON.decode(json_format)
This works, but this is bad because I use 'to_json' method only for its optional function.
Is their any other choice to getting the one hash directly from Car Model via its own optional function?
Use as_json. It's what to_json uses under the hood, and accepts all the same options but returns a Hash.
I feel the same as you that using the json commands for a method unrelated to json is poor practice.
If you don't mind being verbose, you could do something like this:
formatted_cars = Array.new
Car.all.each do |c|
formatted_cars.push( car_name: c.car_name,
owner_name: c.owner_name,
riders: c.riders,
passengers: c.passengers )
end
I find when hitting both attributes and methods it's cleanest just to specify the assignments like this. This is also the technique I would use in passing an object with its virtual attributes to javascript.
I agree that turning it into Json and back is pretty stupid. I would do this like so.
Car.all.collect{|car| hash = {}; [:car_name, :owner_name, :riders, :passengers].each{|key| hash[key] = car.send(key)}; hash}
it would be cleaner to put this into a method in the object
#in Car class
def stats_hash
hash = {}
[:car_name, :owner_name, :riders, :passengers].each do |key|
hash[key] = self.send(key)
end
hash
end
then do
Car.all.collect(&:stats_hash)
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>).
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 :-)