Rails params method: Why can it be accessed like a hash? - ruby-on-rails

Viewing this code:
params[:id]
Params is considered to be a method. Correct me if I'm wrong there. But that's like reading from a hash. So, I'm currently confused.
If params is a method: How does the shown code-example work?

You are correct that params is a method, but here the params method returns an instance of ActionController::Parameters and we call hash accessor method #[] on it.
This is a common pattern in ruby to call methods on the returned object. Let's see it by a simple example:
def params
{
id: 101,
key: 'value',
foo: 'bar'
}
end
params[:id] # => 101
params[:foo] # => 'bar'
As you can see in the example, method params returns a hash object and we call hash accessor method #[] on the returned object.
Reference to rails params method: https://github.com/rails/rails/blob/5e1a039a1dd63ab70300a1340226eab690444cea/actionpack/lib/action_controller/metal/strong_parameters.rb#L1215-L1225
def params
#_params ||= begin
context = {
controller: self.class.name,
action: action_name,
request: request,
params: request.filtered_parameters
}
Parameters.new(request.parameters, context)
end
end
Note for ruby beginners: In ruby, we can call methods without parenthesis. So, above call is equivalent to params()[:id].

Those are known as square bracket accessors and you can add them to any object by implementing the [] and []= methods.
class Store
def initialize(**kwargs)
kwargs.each { |k,v| instance_variable_set("##{k}", v) }
end
def [](key)
instance_variable_get("##{key}")
end
def []=(key, value)
instance_variable_set("##{key}", value)
end
end
store = Store.new(foo: 1, bar: 2, baz: 3)
store[:foo] # 1
store[:foo] = 100
store[:foo] # 100
Also when you call params[:id] - the params method will be called first so you're calling [] on an instance of ActionController::Parameters just like in this simplefied example:
def foo
Store.new(bar: 1)
end
foo[:bar] # 1
Since parens are optional its equivilent to calling params()[:id].

In the context of a Controller, params is indeed a method. Let's say we have an OrganizationsController that is exposing the #index action in a restful endpoint. I will add a breakpoint using the pry gem so that we can better understand what params is:
class OrganizationsController < ApplicationController
def index
binding.pry # Runtime will stop here
render json: Organization.all
end
end
And let's visit the following URL:
http://localhost:3000/organizations.json?foo=bar
We can actually verify that params is a method by explicitly calling it with ():
> params()
=> #<ActionController::Parameters {"foo"=>"bar", "controller"=>"organizations", "action"=>"index", "format"=>"json"} permitted: false>
or by actually asking Ruby where that method is defined:
> method(:params).source_location
=> ["/home/myuser/.rvm/gems/ruby-3.0.2#myproject/gems/actionpack-6.1.4.1/lib/action_controller/metal/strong_parameters.rb", 1186]
The object returned by calling params is not a Hash, but an ActionController::Parameters instead:
> params.class
=> ActionController::Parameters
However, we can call the method :[] on it because it is actually defined in the ActionController::Parameters class (see code)
This makes it look like it's actually a Hash, but it is not, actually. For example, we cannot call the Hash method invert on params, as it is not defined:
> params.invert
NoMethodError: undefined method `invert' for #<ActionController::Parameters {"foo"=>"bar", "controller"=>"organizations", "action"=>"index", "format"=>"json"} permitted: false>

Related

Rails - strong params removing empty fields from nested attributes

I have been working on this line of code for three days.
I have the following strong params:
def location_params
params.require(:location).permit(:country, {:ads_attributes => [:remote, :days]})
end
The method param_clean will delete from the location_params the empty fields, but it will not work with the nested :ads_attributes.
The main reason is that param_clean can only be called on location_params that has class ActiveController::Parameters. I can not call on v the method param_clean
def param_clean
location_params.delete_if{ |k, v| v.empty? or v.instance_of?(ActionController::Parameters) && v.param_clean.empty? }
end
I receive the following error
undefined method `param_clean' for #<ActionController::Parameters:0x007f..>
This is the value of location_params
<ActionController::Parameters {"country"=>"", "ads_attributes"=><ActionController::Parameters {"remote"=>"0", "days"=>""} permitted: true>} permitted: true>
This is the value of v variable when the error is triggered
<ActionController::Parameters {"remote"=>"0", "days"=>""} permitted: true>
v.class => ActionController::Parameters
The method does not work with the nested parameters.
Thanks a lot for your help
Best Regards
Fabrizio
The problem is that the param_clean method is not defined in ActionController::Parameters class. So, you have to change your approach, by either:
Extending ActionController::Parameters to include the method (not that I would recommend it).
Refactor the method. One way to do it would be as follows:
def param_clean(_params)
_params.delete_if do |k, v|
if v.instance_of?(ActionController::Parameters)
param_clean(v)
end
v.empty?
end
end
# how to use it
param_clean(location_params)

Ruby ArgumentError: unknown keyword, but not sure why

I'm trying to make a simple Ruby on Rails plugin. When the redcarpetable function is called with a hash for render_opts, I get "ArgumentError: unknown keyword: render_opts." The code for the function:
def redcarpetable(*fields, renderer: :default, as: [nil], prefix: "rendered", render_opts: {})
fields.each do |field|
if fields.count > 1
define_method "#{prefix}_#{field}" do
Carpet::Rendering.render(read_attribute(field), renderer_opts: render_opts, rc_renderer: renderer).html_safe
end # End defining the method dynamically.
else
if as[0]
as.each do |method_name|
define_method "#{method_name}" do
Carpet::Rendering.render(read_attribute(field), render_opts: render_opts, rc_renderer: renderer).html_safe
end # End defining the method dynamically.
end
else
define_method "rendered_#{field}" do
Carpet::Rendering.render(read_attribute(field), render_opts: render_opts, rc_renderer: renderer).html_safe
end # End defining the method dynamically.
end
end
end # End the fields loop.
end # End the redcarpet method.
How the function is called:
redcarpetable :name, renderer: :simple_parser, as: [:cool_name, :rendered_name], render_opts: {:generate_toc_data: true}
In order to allow for a hash of render options, what must be done to the function declaration? The full code (not documented well or refactored yet) is here.
You call the Carpet::Rendering like this:
Carpet::Rendering.render(read_attribute(field),
render_opts: render_opts, rc_renderer: renderer
).html_safe
But the option is actually called renderer_opts. Just change it to:
Carpet::Rendering.render(read_attribute(field),
renderer_opts: render_opts, rc_renderer: renderer
).html_safe
You might also want to change it in the methods' signature too.
I think your issue might be caused by putting the *fields splat before the other arguments.
Though I'm not specifically sure what's causing your error, you can get an options hash using the following approach:
def redcarpetable(options={}, *fields)
defaults = {
foo: "bar",
bar: "foo"
}
options= defaults.merge(options)
puts options[:foo] # => "bar"
end
This way you can set defaults and override them when you call the method.
In your method body, you're going to have to reference the variables via the options hash,
i.e. options[:foo] and not just foo.
When you call the method, unless your passing nothing to *fields you're going to
have to include the braces in your options argument.
For example:
redcarpetable({foo: bar}, ["field1" "field2"]
And not:
redcarpetable(foo: bar, ["field1, "field2"]
Also, if you're passing any fields but not passing any options, you'll have to include
empty braces:
redcarpetable({}, ["field1", "field2"])

is that a ruby on rails strange behaviour with overwrite params? Or do I just dont aderstand ruby, again?

pre annotation: I have a solution, I want to understand what happens here, and if this behaviour is intended
edit a try for a better readable shortcut:
if you have the following code in Rails Controller:
def get_page
prepare_anythig params
if is_it_monday?
params=monday_default_paramms
end
finish_any_other_thing params
end
this works only on monday
Following functioning little controller function, not very intersting, I know
class SvgTestController < SiteController
def get_the_page
require "base64"
#main_width="auto"
params[:ci]||=['default']
puts "? params:",params
generate_drawing(params, false)
render ...
end
end
the console shows me how expected:
? params:
{"ci"=>"not default", "controller"=>"svg_test", "action"=>"get_the_page"}
Then I made a small (ok, erroneous or not valid as I now know - or think) change, I extended my get_the_page with 'get params via base64 encode json'
class SvgTestController < SiteController
def get_the_page
require "base64"
#main_width="auto"
params[:ci]||=['default']
# add here
puts "? params:",params
json=params[:json]
puts "json?",json.inspect
if json
plain = Base64.decode64(json)
puts "we are in here:", plain
params=JSON.parse(plain).with_indifferent_access
puts "? params now:",params
end
# end
puts "? params:",params
generate_drawing(params, false)
render ...
end
end
Solution working fine and the output like this:
? params:
{"json"=>"eyJjaSI6eyIwMDAwMDAwMDAyMDQ4MDgiOnsic3J2IjoxfX19", "controller"=>"svg_test", "action"=>"get_the_page", "ci"=>["default"]}
json?
"eyJjaSI6eyIwMDAwMDAwMDAyMDQ4MDgiOnsic3J2IjoxfX19"
we are in here:
{"ci":{"000000000204808":{"srv":1}}}
? params now:
{"ci"=>{"000000000204808"=>{"srv"=>1}}}
? params:
{"ci"=>{"000000000204808"=>{"srv"=>1}}}
later I got, working not with JSON-logic
NoMethodError in SvgTestController#get_the_page
undefined method `[]' for nil:NilClass
and my console shows me:
? params:
{"ci"=>"10.203.192.83", "controller"=>"svg_test", "action"=>"get_the_page"}
json?
nil
? params:
_(nothing to read here)_
So ruby overwrites my params (ok its a method, my fault) even if not in if ... end?
Again I ask: Is this wanted? And if, how to prevent such errors without knowing all and all the time about whats behind words like params?
edit
My solution, but not the answer to my question
...
params_used=params
json=params[:json]
if json
plain = Base64.decode64(json)
params_used=JSON.parse(plain).with_indifferent_access
end
puts "? params:",params_used
generate_drawing(params_used, false)
I think the "error" is because you're actually creating a variable. Annotation of your code:
def get_the_page
require "base64"
#main_width="auto"
params[:ci]||=['default'] # params method
# you modified #params, a mutable hash
# add here
puts "? params:",params # params method
json=params[:json] # params method
# you accessed #params[:json]
puts "json?",json.inspect
if json
plain = Base64.decode64(json)
puts "we are in here:", plain
params=JSON.parse(plain).with_indifferent_access # params variable
puts "? params now:",params # params variable
end
# end
puts "? params:",params # params variable
generate_drawing(params, false) # params variable
render ...
end
What's happening, I'd wager, is that the Ruby interpreter picks up the fact that a variable named params continues to be used after if block, so proceeds to initialize it (to nil) immediately before your if block irrespective of whether the block is visited or not.

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

Rails Method Ignoring Default Param - WHY?

I am at a loss as to why this is happening. I have the following function:
def as_json(options = {})
json = {
:id => id,
# ... more unimportant code
}
unless options[:simple]
# ... more unimportant code
end
json
end
It works most of the time, but in one particular partial where I call this:
window.JSONdata = <%= #day.to_json.html_safe %>
I get the following error:
ActionView::Template::Error (You have a nil object when you didn't expect it!
You might have expected an instance of Array.
The error occurred while evaluating nil.[]):
Pointing to the line "unless options[:simple]". As far as I can tell, the options hash is nil - thus the method is ignoring the default param assignment. WHY? I can fix this by changing the method to:
def as_json(options)
options ||= {}
json = {
:id => id,
# ... more unimportant code
}
unless options[:simple]
# ... more unimportant code
end
json
end
Does this make any sense to anyone!? Most appreciative for your help.
This is because you're using to_json, which has a default options of nil. to_json will eventually call as_json and pass the nil as options.
Here's where it happens on the Rails source code. First, to_json is defined with the default options of nil.
# https://github.com/rails/rails/blob/v3.0.7/activesupport/lib/active_support/core_ext/object/to_json.rb#L15
def to_json(options = nil)
ActiveSupport::JSON.encode(self, options)
end
Eventually it will arrive here.
# https://github.com/rails/rails/blob/v3.0.7/activesupport/lib/active_support/json/encoding.rb#L41
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
As you see, as_json is called with value.as_json(options_for(value)) and options_for(value) will return the default value of to_json, which is nil.

Resources