Collect KeyErrors from Ruby hash into array - ruby-on-rails

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

Related

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

Ruby - collection of methods

I have inherited a bunch of methods that are not wrapped by any classes or modules, and are just listed in an .rb file. This was made possible due to the file being used inside a Cucumber test suite. I want to take a collection of all these methods and iterate over each method call, doing some work on each one as they are called.
EX:
def call_all_methods
method1
method2
method3(true)
method3(false)
method4('Y', true)
method4('N', true)
method4('Y', false)
method4('N', false)
end
What i want to be able to do is wrap these all in an array and call them individually with a begin/rescue block around them
$all_methods.each do |method|
begin
method.call
rescue Exception1
handle_exception1
rescue Exception2
handle_exception2
end
end
I've tried putting them all in an array using %w
call_all_methods = %w(...)
and that works but it makes the methods ugly to look at in the IDE
I've tried doing a readlines on the file, but the methods get executed while the file is being read.
I could create methods to wrap each call, but then I have a method to call another method (one line) which isn't right either.
I have looked at Ruby: methods as array elements - how do they work? but neither of those solutions seemed like good solutions for what I'm trying to do, as it would dirty the code
If I understand what you're asking correctly, you could just wrap those methods in a class.
class MyMethods
# all those methods that you have in that file
end
You could then list them all by doing
all_methods = MyMethods.instance_methods(false)
To execute them, you can do all_methods.each {|m| MyMethods.new.send(m)}
You could do something like this:
def execute_several(arr)
arr.each do |method, *args|
begin
v = send(method, *args)
puts "for method '#{method}': #{v}"
rescue ArgumentError => e
puts "for method '#{method}': #{e.message}"
end
end
end
arr = [
[:class],
[:rand, 20],
[:Integer, "20"],
[:Integer, 'cat']
]
execute_several(arr)
# for method 'class': Object
# for method 'rand': 17
# for method 'Integer': 20
# for method 'Integer': invalid value for Integer(): "cat"
Here's an example of how that would be done within a class:
class Array
def execute_several(arr)
arr.each do |method, args|
begin
v = args ? send(method, args) : send(method)
puts "for method '#{method}': #{v}"
rescue TypeError => e
puts "for method '#{method}': #{e.message}"
end
end
end
end
arr = [
[:reverse],
['first'],
[:&, [2,3,4]],
[:|, 'cat']
]
[1,2,3].execute_several(arr)
# for method 'reverse': [3, 2, 1]
# for method 'first': 1
# for method '&': [2, 3]
# for method '|': no implicit conversion of String into Array
I ended up making an array of procs

ruby pass method as a parameter

Im trying to pass a method as a parameter to method_2, execute this one and return the result:
def method_2( method_p, param )
res = method(method_p).call(param)
return res
end
def method_1
klass = MyKlass.instance
return method_2( klass.foo, "test" )
end
this's MyKlass file:
class MyKlass
def foo(param)
param+param
end
end
All I got is an error
wrong number of arguments (0 for 1)
You can use symbols to refer to methods:
def method_2(method_symbol, *args)
send method_symbol, *args
end
However, since you're calling the method on a specific object, you would either have to pass that in as an additional argument, or use a proc or a lambda, which is like a block wrapped in an object:
def method_2(proc, *args)
proc.call(*args)
end
method_2(->(param){ klass.foo(param) }, "test")
It's more common to just use blocks to do this:
def method_2(receiver, *args, &block)
yield receiver, *args
end
method_2(klass, "test") do |receiver, param|
receiver.foo(param)
end
All of these are fairly contrived examples; is there a specific problem you're trying to solve?
When you:
return method_2( klass.foo, "test" )
klass.foo requires one arg, that might be what's causing your error.

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