Rails Method Ignoring Default Param - WHY? - ruby-on-rails

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.

Related

Rails params method: Why can it be accessed like a hash?

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>

Collect KeyErrors from Ruby hash into array

I need to extract multiple fields from hash. But I respect my client and I want to gather all missed fields instead of returning it one by one. My idea was to use #fetch, intercept error with KeyError, put error.key into instance variable array and return proper error explanation with full list of missed keys.
Something like that
class Extractor
def initialize hash
#hash = hash
#missed_keys = []
end
def call
extract_values
return "Missed keys: #{#missed_keys.join(', ')}" if #missed_keys.present?
rescue KeyError => e
puts 'Field was missed'
#missed_keys << e.key
return 'Error'
end
private
def extract_values
{
value_1: #hash.fetch(:required_field_1),
value_2: #hash.fetch(:required_field_2),
value_3: #hash.fetch(:required_field_3)
}
end
end
When I try to process hash without required fields I got 'Error' after the first missed field:
pry(main)> Extractor.new(hash: {}).call
Field was missed
=> "Error"
Any clues?
DrySchema and other hash validators are not an option.
An issue with the provided solution is that the extracted values are never returned in the happy path (which presumably is important?). The call method is also stateful / non-idempotent. Subsequent calls to call will duplicate the missing-keys.
Finally - not sure how it's being used, but I don't love a method that returns either a hash or a string.
An alternative that attempts to follow a more functional pattern might look like:
class Extractor
attr_reader :hash, :missed_keys, :required_keys
def initialize hash
#hash = hash
#missed_keys = []
#required_keys = [:required_field_1, :required_field_2, :required_field_3]
end
def call
validate_keys_exist!
extract_values
end
private
def validate_keys_exist!
missed_keys = find_missing_keys
raise MissingKeysError, "Missed keys: #{missed_keys.join(', ')}" if missed_keys.any?
end
def find_missing_keys
required_keys - hash.keys
end
def extract_values
hash.slice(*required_keys)
# not sure if you need to map the keys to new values.
# if so you can iterate over a hash of `from: :to` pairs instead of the
# required_keys array.
end
end
Ok, I got it. The reason is in intercept level and method closures.
In aforementioned implementation Ruby tried to execute call method, got an error and exits.
If we rework it like that:
class Extractor
def initialize hash
#hash = hash
#missed_keys = []
end
def call
extract_values
return "Missed keys: #{#missed_keys.join(', ')}" if #missed_keys.present?
end
private
def extract_values
{
value_1: #hash.fetch(:required_field_1),
value_2: #hash.fetch(:required_field_2),
value_3: #hash.fetch(:required_field_3)
}
rescue KeyError => e
puts 'Field was missed'
#missed_keys << e.key
nil
end
end
it looks better, but still not what we wanted:
pry(main)> Extractor.new(hash: {}).call
Field was missed
=> "Missed keys: required_field_1"
This is because ruby tried to execute extract_values method, encounters first missed value and exits
So the solution as follow:
class Extractor
def initialize hash
#hash = hash
#missed_keys = []
end
def call
extract_values
return "Missed keys: #{#missed_keys.join(', ')}" if #missed_keys.present?
end
private
def extract_values
{
value_1: fetch_value(:required_field_1),
value_2: fetch_value(:required_field_2),
value_3: fetch_value(:required_field_3)
}
end
def fetch_value(key)
#hash.fetch(key)
rescue KeyError => e
puts 'Field was missed'
#missed_keys << e.key
nil
end
end
Extractor.new(hash: {}).call
Field was missed
Field was missed
Field was missed
=> "Missed keys: required_field_1, required_field_2, required_field_3"
Error interception is accomplished on the fetch_value level and Ruby skips required values one by one

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"])

Spree error when using decorator with the original code

Need a little help over here :-)
I'm trying to extend the Order class using a decorator, but I get an error back, even when I use the exactly same code from source. For example:
order_decorator.rb (the method is exactly like the source, I'm just using a decorator)
Spree::Order.class_eval do
def update_from_params(params, permitted_params, request_env = {})
success = false
#updating_params = params
run_callbacks :updating_from_params do
attributes = #updating_params[:order] ? #updating_params[:order].permit(permitted_params).delete_if { |k,v| v.nil? } : {}
# Set existing card after setting permitted parameters because
# rails would slice parameters containg ruby objects, apparently
existing_card_id = #updating_params[:order] ? #updating_params[:order][:existing_card] : nil
if existing_card_id.present?
credit_card = CreditCard.find existing_card_id
if credit_card.user_id != self.user_id || credit_card.user_id.blank?
raise Core::GatewayError.new Spree.t(:invalid_credit_card)
end
credit_card.verification_value = params[:cvc_confirm] if params[:cvc_confirm].present?
attributes[:payments_attributes].first[:source] = credit_card
attributes[:payments_attributes].first[:payment_method_id] = credit_card.payment_method_id
attributes[:payments_attributes].first.delete :source_attributes
end
if attributes[:payments_attributes]
attributes[:payments_attributes].first[:request_env] = request_env
end
success = self.update_attributes(attributes)
set_shipments_cost if self.shipments.any?
end
#updating_params = nil
success
end
end
When I run this code, spree never finds #updating_params[:order][:existing_card], even when I select an existing card. Because of that, I can never complete the transaction using a pre-existent card and bogus gateway(gives me empty blanks errors instead).
I tried to bind the method in order_decorator.rb using pry and noticed that the [:existing_card] is actuality at #updating_params' level and not at #updating_params[:order]'s level.
When I delete the decorator, the original code just works fine.
Could somebody explain to me what is wrong with my code?
Thanks,
The method you want to redefine is not really the method of the Order class. It is the method that are mixed by Checkout module within the Order class.
You can see it here: https://github.com/spree/spree/blob/master/core/app/models/spree/order/checkout.rb
Try to do what you want this way:
Create file app/models/spree/order/checkout.rb with code
Spree::Order::Checkout.class_eval do
def self.included(klass)
super
klass.class_eval do
def update_from_params(params, permitted_params, request_env = {})
...
...
...
end
end
end
end

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

Resources