ruby: mass initializing instance variables - ruby-on-rails

Is there an easy way to bulk assign instance variables
def initialize(title: nil, label_left: nil, label_right: nil, color_set: nil)
#title = title
#label_left = label_left
#label_right = label_right
#color_set = color_set
end
Can I loop/iterate through the initialize arguments and assign automatically?

If you want specific variables, then not really. Using reflection here, even if it was available, would be trouble.
What you've got here is the most trivial case. Often you'll see code that looks more like this:
def initialize(title: nil, label: nil, label_width: nil, color_set: nil)
#title = title.to_s
#label = label_left.to_s
#label_width = label_width.to_i
#color_set = MyRGBConverter.to_rgb(color_set)
end
The initializer is a place where you can do any necessary conversion and validation. If some of those arguments are required to be certain values you'll have tests for that, raise an exception on an error and so forth. The repetitive code you have here in the minimal case often gets expanded and augmented on so there's no general purpose solution possible.
That leads to code like this:
def initialize(title: nil, label: nil, label_width: nil, color_set: nil)
#title = title.to_s
unless (#title.match(/\S/))
raise "Title not specified"
end
#label = label_left.to_s
unless (#label.match(/\A\w+\z/))
raise "Invalid label #{#label.inspect}"
end
#label_width = label_width.to_i
if (#label_width <= 0)
raise "Label width must be > 0, #{#label_width} specified."
end
#color_set = MyRGBConverter.to_rgb(color_set)
end
If you really don't care about which arguments you're taking in then you can do this in Ruby 2.3 with the new keyword-arguments specifier **:
def initialize(**options)
options.each do |key, value|
instance_variable_set("##{key}", value)
end
end
Note that's potentially dangerous if those values are user supplied, so you might want to white-list them somehow:
VALID_OPTIONS = [ :title, :label_left, :label_right, :color_set ]
def initialize(**options)
options.each do |key, value|
raise "Unknown option #{key.inspect}" unless (VALID_OPTIONS.include?(key))
instance_variable_set("##{key}", value)
end
end
Where you're seeing a lot of these arguments being passed in on a regular basis you might want to evaluate if creating some kind of struct-like object and passing that through might be a better plan. It depends on how your code works.

#tadman provided already an excellent answer to this, but here is one more: If you are willing to dispense with named parameters, you could do it like this:
def initialize(*args)
#title, #label_left, #label_right, #color_set, *nada = args
fail "Too many arguments" unless nada.empty?
end
[UPDATE: Fixed the code, according to the comment given by #sawa].

Related

Rails merge (and manual assignment) assigning nil

I am trying to create a model in a controller using strong params in Rails 5.1 (some things changed from previous for strong_params). However, when I inspect the params, the merged ones are NOT present and I am getting an ForbiddenAttributesError tracing back to the Model.new line below. The only thing in the Model is verify presence for all the attributes.
class ModelController < ApplicationController
before_action :application_controller_action
def create
#model = Model.new(strong_params)
if #model.valid?
result = #model.save
else
render html: 'MODEL NOT VALID'
end
render html: 'DONE'
end
private
def strong_params
# attr_1 and attr_2 are set in the application controller and are available here.
params.require(:model).permit(:name, :attribute_1, :attribute_2).merge(attribute_1: #attr_1, attribute_2: #attr_2)
# Inserting the following two lines causes a ForbiddenAttributesError
puts params.inspect # DOES NOT INCLUDE #attr_1 and/or #attr_2
return params
end
I may be doing something wrong though because I've even tried putting the strong params into a model with the attributes (which I can inspect just before) and it still fails because the validation for attr_1 and attr_2 fail in the Model.
def create
puts #user.inspect (not nil)
#model = Model.new(name: strong_params[:name], attribute_1: #attr_1, attribute_2: #attr_2)
UPDATE:
OK, I'm getting some weird errors from my troubleshooting. It seems the merge is not working correctly, though I'm sure it was at one point.
The first thing I checked was #attr_1 and #attr_2, they are definitely getting set.
For troubleshooting purposes, I've reduced the application before_action to this:
def application_before_action
#attr_1 = Model.first
#attr_2 = Model.last
With the code above, inspecting the params object and then returning it after the require().permit(), I am getting a ForbiddenAttributesError (no indication of what). If I remove those lines, I get a missing attributes error from the model indicating that #attr_1 and #attr_2 are missing.
UPDATE 2
Changed the title of the question, because I probably got confused during troubleshooting. I think the issue is just that the merge is assigning nil... but strangely so is the manual assignment suggested by (myself originally) and another answer here. The attributes keys are there, but they're getting assigned nil. Also, noticed my example was using a single Model, when there are actually two Models, Model1 and Model2. I am assigning the values from Model1 to Model2.
Here is a better demonstration of the error:
def create
puts '0:'
puts #model1.inspect
puts '1:'
puts strong_params.inspect
#model2 = Model2.new(strong_params) do |m|
m.user_id = #attr_1
m.account_number = #attr_2
end
puts '3:'
puts #model2.inspect
if #model2.valid?
result = #model2.save
render html: 'SUCCESS' and return
else
render html: #model2.errors.full_messages and return
end
end
Outputs in console:
0:
#<Model1 id: 29, attribute_1: 'test_value_1', attribute_2: 'test_value_2', created_at: "2018-08-15 03:55:08", updated_at: "2018-08-15 04:05:01">
1:
<ActionController::Parameters {"name"=>"test_name", "attribute_1"=>nil, "attribute_2"=>nil} permitted: true>
3:
#<Model2 id: nil, name: 'test_name', attribute_1: nil, attribute_2: nil, created_at: nil, updated_at: nil>
Obviously the nil id and timestamps are because the model has not been saved yet.
The html model2.errors.full_messages are: ["attribute_1 can't be blank", "attribute_2 can't be blank"]
SOLUTION
Coming from a pure ruby environment previously, I was mistaken about ActiveRecord default accessors for models. Removing the accessors seems to have resolved the problem.
Instead of mucking about with the params hash you can just assign the odd values one by one:
class ModelController < ApplicationController
before_action :application_controller_action
def create
#model = Model.new(strong_params) do |m|
m.attribute_1 = #attr_1
m.attribute_2 = #attr_2
end
if #model.valid?
result = #model.save
else
render html: 'MODEL NOT VALID'
end
# don't do this it will just give a double render error
render html: 'DONE'
end
private
private
def strong_params
params.require(:model).permit(:name, :attribute_1, :attribute_2)
end
end
In general this is a much more readable way to merge params with values from the session for example.
The reason your strong parameters method does not work is its just plain broken in every possible way. The main point is that you're not returning the whitelisted and merged params hash. You're returning the whole shebang.
You also seem under the faulty impression that .require, .permit and .merge alter the orginal hash - they don't - they return a new hash (well actually an ActionContoller::Parameters instance to be specific).
def strong_params
# attr_1 and attr_2 are set in the application controller and are available here.
permitted = params.require(:model).permit(:name, :attribute_1, :attribute_2)
.merge(attribute_1: #attr_1, attribute_2: #attr_2)
puts permitted.inspect
permitted # return is implicit
end
Or just:
def strong_params
# attr_1 and attr_2 are set in the application controller and are available here.
params.require(:model).permit(:name, :attribute_1, :attribute_2)
.merge(attribute_1: #attr_1, attribute_2: #attr_2)
end
You could convert to hash before merge
params.require(:model).permit(:name).to_h.merge(attribute_1: #attr_1, attribute_2: #attr_2)
You would have to be very sure that you are assigning non-user input though otherwise you are negating the purpose of strong parameters.

Interpolating an attribute's key before save

I'm using Rails 4 and have an Article model that has answer, side_effects, and benefits as attributes.
I am trying to create a before_save method that automatically looks at the side effects and benefits and creates links corresponding to another article on the site.
Instead of writing two virtually identical methods, one for side effects and one for benefits, I would like to use the same method and check to assure the attribute does not equal answer.
So far I have something like this:
before_save :link_to_article
private
def link_to_article
self.attributes.each do |key, value|
unless key == "answer"
linked_attrs = []
self.key.split(';').each do |i|
a = Article.where('lower(specific) = ?', i.downcase.strip).first
if a && a.approved?
linked_attrs.push("<a href='/questions/#{a.slug}' target=_blank>#{i.strip}</a>")
else
linked_attrs.push(i.strip)
end
end
self.key = linked_attrs.join('; ')
end
end
end
but chaining on the key like that gives me an undefined method 'key'.
How can I go about interpolating in the attribute?
in this bit: self.key you are asking for it to literally call a method called key, but what you want, is to call the method-name that is stored in the variable key.
you can use: self.send(key) instead, but it can be a little dangerous.
If somebody hacks up a new form on their browser to send you the attribute called delete! you don't want it accidentally called using send, so it might be better to use read_attribute and write_attribute.
Example below:
def link_to_article
self.attributes.each do |key, value|
unless key == "answer"
linked_attrs = []
self.read_attribute(key).split(';').each do |i|
a = Article.where('lower(specific) = ?', i.downcase.strip).first
if a && a.approved?
linked_attrs.push("<a href='/questions/#{a.slug}' target=_blank>#{i.strip}</a>")
else
linked_attrs.push(i.strip)
end
end
self.write_attribute(key, linked_attrs.join('; '))
end
end
end
I'd also recommend using strong attributes in the controller to make sure you're only permitting the allowed set of attributes.
OLD (before I knew this was to be used on all attributes)
That said... why do you go through every single attribute and only do something if the attribute is called answer? why not just not bother with going through the attributes and look directly at answer?
eg:
def link_to_article
linked_attrs = []
self.answer.split(';').each do |i|
a = Article.where('lower(specific) = ?', i.downcase.strip).first
if a && a.approved?
linked_attrs.push("<a href='/questions/#{a.slug}' target=_blank>#{i.strip}</a>")
else
linked_attrs.push(i.strip)
end
end
self.answer = linked_attrs.join('; ')
end

Missing keywords error on initializing a constructor

I have a Source model and ArticlesController. When user clicks scrape button, the control is passed to below mentioned ArticlesController#Scrape. The scrape then calls Source model where the sources are being initialised and the list of articles are returned in a form of hash to articles inside Scrape.
Source Model -
class Source
attr_accessor :source_name, :source_url, :source_type, :source_key, :tag_name
def self.all_instances
##array
end
# Default constructor
def initialize
end
def initialize(source_name:, source_url:, source_type:, source_key:, tag_name:)
#source_name = source_name
#source_url = source_url
#source_type = source_type
#source_key = source_key
#tag_name = tag_name
##array << self
end
def init
self.new('The Age',
'http://www.theage.com.au/rssheadlines/victoria/article/rss.xml',
'RSS',
'',
'Victoria News')
end
def import
init()
//returns hash of articles back
end
end
class ArticlesController < ApplicationController
def scrape
#get_Articles = Source.new
articles = #get_Articles.import
//stores articles in article model
//redirect to article path
end
end
I am getting ArgumentError in ArticlesController#scrape on #get_Articles = Source.new
Inside Source class the constructor def initialize(source_name:, source_url:, source_type:, source_key:, tag_name:) is being called. To rectify the issue I created a default constructor also, so that the parameterized constructor doesn't get called. However, I am not sure how to fix this problem. Could somebody please help?
I think you are doing it wrong with the def initialize method. You don't want parameterized constructor just removed it.
if you want this as well then you need to handle this for null values also.
Just creating a default constructor will not solve the issue because it will be override with other one.
You can try like this
def initialize(options ={})
#source_name = options[:source_name] if options[:source_name].present?
#handle and assign other keys and values similer to this
##array << self
end
now you can use this as
#get_Articles = Source.new
or
#get_Articles = Source.new(source_name: "abc")
First of all, the way you are trying to overload initialize method is incorrect. In ruby if you define the same method again in same class then the most latest interpretation will take preceding(based on when it get interpreted). So here initialize with parameter taking preceding.
There are many ways to overload a method based on parameters
Approach one: define method with default value assignment like below
def initialize(source_name = nil, source_url = nil, source_type = nil, source_key= nil, tag_name = nil)
end
In this approach the sequence of arguments does matter when invoking. i.e we can not invoke method with only tag_name the other values should also be passed as some value or nil
like Source.new nil, nil, nil, nil, 'tag_name_value'
Approach two: Using Hash as arguments (mentioned by #Prakash): This is the most popular and generic. In this we need to explicitly check for required argument name and need to assign default values to them if needed. This is mostly done by hash merging
def initialize(options ={})
options = {source_name: nil, source_url: nil, source_type: nil, source_key: nil, tag_name: nil}.merge(options)
end
# calling method
Source.new source_name: 'somevalue' #or so one
The disadvantage of this approach is there can be many keys in hash passed to method and to handle that you need to do extra check on input hash
Source.new source_name: 'somevalue', unexpected_key: 'unexpectedvalue'
Approach three
Ruby 2.0 has introduced the keyword arguments (also named argument in ruby 1.9) where you can provide a name to parameters like you were trying, the only thing you should keep in mind is to assign a default value to every parameter.
def initialize(source_name: nil, source_url: nil, source_type: nil, source_key: nil, tag_name: nil)
end
now you can invoke like
Source.new
Source.new source_url: 'somevalue'
Source.new source_name: 'somevalue'
Source.new source_type: 'somevalue', source_name: 'somevalue'
Source.new tag_name: 'somevalue'
# or any combination of arguments in any sequence
# but not the following, this give you error 'unknown keyword: unexpected_key'
Source.new tag_name: 'somevalue', unexpected_key: 'unexpectedvalue'

Is there a way to access method arguments in Ruby?

New to Ruby and ROR and loving it each day, so here is my question since I have not idea how to google it (and I have tried :) )
we have method
def foo(first_name, last_name, age, sex, is_plumber)
# some code
# error happens here
logger.error "Method has failed, here are all method arguments #{SOMETHING}"
end
So what I am looking for way to get all arguments passed to method, without listing each one. Since this is Ruby I assume there is a way :) if it was java I would just list them :)
Output would be:
Method has failed, here are all method arguments {"Mario", "Super", 40, true, true}
In Ruby 1.9.2 and later you can use the parameters method on a method to get the list of parameters for that method. This will return a list of pairs indicating the name of the parameter and whether it is required.
e.g.
If you do
def foo(x, y)
end
then
method(:foo).parameters # => [[:req, :x], [:req, :y]]
You can use the special variable __method__ to get the name of the current method. So within a method the names of its parameters can be obtained via
args = method(__method__).parameters.map { |arg| arg[1].to_s }
You could then display the name and value of each parameter with
logger.error "Method failed with " + args.map { |arg| "#{arg} = #{eval arg}" }.join(', ')
Note: since this answer was originally written, in current versions of Ruby eval can no longer be called with a symbol. To address this, an explicit to_s has been added when building the list of parameter names i.e. parameters.map { |arg| arg[1].to_s }
Since Ruby 2.1 you can use binding.local_variable_get to read value of any local variable, including method parameters (arguments). Thanks to that you can improve the accepted answer to avoid evil eval.
def foo(x, y)
method(__method__).parameters.map do |_, name|
binding.local_variable_get(name)
end
end
foo(1, 2) # => 1, 2
One way to handle this is:
def foo(*args)
first_name, last_name, age, sex, is_plumber = *args
# some code
# error happens here
logger.error "Method has failed, here are all method arguments #{args.inspect}"
end
This is an interesting question. Maybe using local_variables? But there must be a way other than using eval. I'm looking in Kernel doc
class Test
def method(first, last)
local_variables.each do |var|
puts eval var.to_s
end
end
end
Test.new().method("aaa", 1) # outputs "aaa", 1
If you need arguments as a Hash, and you don't want to pollute method's body with tricky extraction of parameters, use this:
def mymethod(firstarg, kw_arg1:, kw_arg2: :default)
args = MethodArguments.(binding) # All arguments are in `args` hash now
...
end
Just add this class to your project:
class MethodArguments
def self.call(ext_binding)
raise ArgumentError, "Binding expected, #{ext_binding.class.name} given" unless ext_binding.is_a?(Binding)
method_name = ext_binding.eval("__method__")
ext_binding.receiver.method(method_name).parameters.map do |_, name|
[name, ext_binding.local_variable_get(name)]
end.to_h
end
end
This may be helpful...
def foo(x, y)
args(binding)
end
def args(callers_binding)
callers_name = caller[0][/`.*'/][1..-2]
parameters = method(callers_name).parameters
parameters.map { |_, arg_name|
callers_binding.local_variable_get(arg_name)
}
end
You can define a constant such as:
ARGS_TO_HASH = "method(__method__).parameters.map { |arg| arg[1].to_s }.map { |arg| { arg.to_sym => eval(arg) } }.reduce Hash.new, :merge"
And use it in your code like:
args = eval(ARGS_TO_HASH)
another_method_that_takes_the_same_arguments(**args)
If the function is inside some class then you can do something like this:
class Car
def drive(speed)
end
end
car = Car.new
method = car.method(:drive)
p method.parameters #=> [[:req, :speed]]
If you would change the method signature, you can do something like this:
def foo(*args)
# some code
# error happens here
logger.error "Method has failed, here are all method arguments #{args}"
end
Or:
def foo(opts={})
# some code
# error happens here
logger.error "Method has failed, here are all method arguments #{opts.values}"
end
In this case, interpolated args or opts.values will be an array, but you can join if on comma. Cheers
It seems like what this question is trying to accomplish could be done with a gem I just released, https://github.com/ericbeland/exception_details. It will list local variables and vlaues (and instance variables) from rescued exceptions. Might be worth a look...
Before I go further, you're passing too many arguments into foo. It looks like all of those arguments are attributes on a Model, correct? You should really be passing the object itself. End of speech.
You could use a "splat" argument. It shoves everything into an array. It would look like:
def foo(*bar)
...
log.error "Error with arguments #{bar.joins(', ')}"
end

deserialize database object

I have a statistic model
class Statistic < ActiveRecord::Base
serialize :value
end
The model contains a value attribute containing a Goals object. I want to deserialize the goals object
when I do
goals = Statistic.all
goals.each do |goal|
test = goal.value
end
I get an error
value was supposed to be a Goals, but was a String
In the debugger I see that goal.value is of type String and contains the goal data
--- !ruby/object:Goals \ngoals: {}\n\ngoals_against: 1\ngoals_for: 0\nversion: 1\n
When I add serialize :value, Goals I get following error when deserialzing
ActiveRecord::SerializationTypeMismatch in ClubsController#new
value was supposed to be a Goals, but was a String
The Goals class
class Goals
attr_accessor :goals
attr_accessor :goals_for
attr_accessor :goals_against
attr_accessor :goals_own
attr_accessor :penalty_for
attr_accessor :penalty_against
def initialize(goals = nil, goals_against = nil, goals_own = nil, penalty_for = nil, penalty_against = nil)
#version = 1
if goals.nil?
#goals = {}
else
#goals = goals
end
#goals_against = goals_against.to_i
#goals_own = goals_own.to_i unless goals_own.nil?
unless penalty_for.nil?
#penalty_for = penalty_for.to_i
#penalty_against = penalty_against.to_i
end
set_goals_for()
end
private
def set_goals_for
#goals_for = 0
#goals.each_value {|value| #goals_for += value.to_i }
#goals_for += #goals_own unless #goals_own.nil?
end
end
Someone knows how I can make rails know that its an goals object and not a string?
Thanks
Most of my experience with serialization problems comes from Rails 1 era, but I recall two usual reasons of serialization failures:
silently ignored exceptions
class reloading
Looking at the file ./activerecord/lib/active_record/base.rb (tag v3.0.7 from git) I see that there is a 'rescue' clause:
def object_from_yaml(string)
return string unless string.is_a?(String) && string =~ /^---/
YAML::load(string) rescue string
end
You may try to investigate what exception is thrown by YAML::load. I usually change this method into something like this:
begin
YAML::load(string)
rescue => e
Rails.logger.warn "YAML exception (ignored): #{e.inspect}"
string
end
About the class reloading, is your problem also visible in test mode? I was registering my classes in YAML, and noticed that the class definition was gone in each second request, since the class object was recreated, and the registered one was taken away from the class chains. I don't think this is your problem, but I am signalling it anyway - maybe this will be helpful?

Resources