In my current project, I am having the following repetitive pattern in some classes:
class MyClass
def method1(pars1, ...)
preamble
# implementation method1
rescue StandardError => e
recovery
end
def method2(pars2, ...)
preamble
# implementation method2
rescue StandardError => e
recovery
end
# more methods with the same pattern
end
So, I have been thinking about how to dry that repetive pattern. My goal is to have something like this:
class MyClass
define_operation :method1, pars1, ... do
# implementation method1
end
define_operation :method2, pars2, ... do
# implementation method2
end
# more methods with the same pattern but generated with define_wrapper_method
end
I have tried to implement a kind of metagenerator but I have had problems for forwarding the block that would receive the generator. This is more or less what I have tried:
def define_operation(op_name, *pars, &block)
define_method(op_name.to_s) do |*pars|
preamble
yield # how can I do here for getting the block? <-----
rescue StandardError => e
recovery
end
end
Unfortunately, I cannot find a way for forwarding block to the define_method method. Also, it is very possible that the parameters, whose number is variable, are being passed to define_method in a wrong way.
I would appreciate any clue, help, suggestion.
You do not need metaprogramming to achieve this. Just add a new method that wraps the common logic like below:
class MyClass
def method1(param1)
run_with_recovery(param1) do |param1|
# implementation method1
end
end
def method2(param1, param2)
run_with_recovery(param1, param2) do |param1, param2|
# implementation method2
end
end
private
def run_with_recovery(*params)
preamble
yield(*params)
rescue StandardError => e
recovery
end
end
Test it here: http://rubyfiddle.com/riddles/4b6e2
If you really wanted to do metaprogramming, this will work:
class MyClass
def self.define_operation(op_name)
define_method(op_name.to_s) do |*args|
begin
puts "preamble"
yield(args)
rescue StandardError => e
puts "recovery"
end
end
end
define_operation :method1 do |param1|
puts param1
end
define_operation :method2 do |param1, param2|
puts param1
puts param2
end
end
MyClass.new.method1("hi")
MyClass.new.method2("hi", "there")
Test this here: http://rubyfiddle.com/riddles/81b9d/2
If I understand correctly you are looking for something like:
class Operation
def self.op(name,&block)
define_method(name) do |*args|
op_wrap(block.arity,*args,&block)
end
end
def op_wrap(arity=0,*args)
if arity == args.size || (arrity < 0 && args.size >= arity.abs - 1)
begin
preamble
yield *args
rescue StandardError => e
recovery
end
else
raise ArgumentError, "wrong number of arguments (given #{args.size}, expected #{arity < 0 ? (arity.abs - 1).to_s.+('+') : arity })"
end
end
def preamble
puts __method__
end
def recovery
puts __method__
end
end
So your usage would be
class MyClass < Operation
op :thing1 do |a,b,c|
puts "I got #{a},#{b},#{c}"
end
op :thing2 do |a,b|
raise StandardError
end
def thing3
thing1(1,2,3)
end
end
Additionally this offers you both options presented as you could still do
def thing4(m1,m2,m3)
#m1 = m1
op_wrap(1,'inside_wrapper') do |str|
# no need to yield because the m1,m2,m3 are in scope
# but you could yield other arguments
puts "#{str} in #{__method__}"
end
end
Allowing you to pre-process arguments and decide what to yield to the block
Examples
m = MyClass.new
m.thing1(4,5,6)
# preamble
# I got 4,5,6
#=> nil
m.thing2('A','B')
# preamble
# recovery
#=> nil
m.thing3
# preamble
# I got 1,2,3
#=> nil
m.thing1(12)
#=> #<ArgumentError: wrong number of arguments (given 1, expected 3)>
Related
For example lets say I have the following code
def my_method
do_work_1
5.times do
puts "This is a test"
end
do_work_2 "Hello"
do_work_3 do
puts "Inside block"
do_something_else
end
end
What I would like is to have the following logs:
do_work_1 called with no params
do_work_1 finished
do_work_2 called with param "Hello"
do_work_2 finished
do_work_3 called with a block param
do_work_3 finished
I could create a helper method and do something like the following
def my_method
log_call method(:do_work_1)
5.times do
puts "This is a test"
end
log_call method(:do_work_2) "Hello"
log_call method(:do_work_3) do
puts "Inside block"
do_something_else
end
end
def log_call(m, params)
puts("#{m.name} called with params #{params}")
m.call(params)
puts("#{m.name} finished")
end
But this isn't very pretty and would require us to use log_call on every method we call that we want logging. Is there a better way to go about achieving this goal?
In production code, I would use something explicit, similar to the log_call solution you already have. I would probably use something even more basic. Yes, it's a lot of typing, but everyone on your team will understand it.
In non-production code, or as a temporary measure, we can consider Module.prepend.
require 'minitest/autorun'
class Worker
def do_work_1(*); end
def do_work_3(*); end
def my_method
do_work_1
2.times { puts "This is a test" }
do_work_3 { puts "Inside block" }
end
end
module WorkLogging
%i[do_work_1 do_work_3].each do |m|
define_method(m) do |*args, **kwargs, &block|
puts format('%s called with: %s', m, [args, kwargs, block&.source_location].compact)
super(*args, **kwargs, &block)
puts format('%s finished', m)
end
end
end
Worker.prepend(WorkLogging)
class MyTest < Minitest::Test
def test_1
expected = <<~EOS
do_work_1 called with: [[], {}]
do_work_1 finished
This is a test
This is a test
do_work_3 called with: [[], {}, [\"derp.rb\", 10]]
do_work_3 finished
EOS
assert_output(expected) { Worker.new.my_method }
end
end
The question has been simplified trivially in the interest of brevity. Exact output format is left as an exercise to the reader.
I have an ActiveRecord class like:
class DBGroup < ActiveRecord:Base
# ...
scope :example_method, -> { // does something }
# ...
end
And now I have another method in another class, something like
def method1
do #open block for rescue method
DBGroup
.example_method
.group(:some_column)
.having("COUNT(*) > 5")
rescue StandardError => e
e
end
end
And in RSpec I am testing kind of like:
it "my test, want to test rescue block"
expect_any_instance_of(DBGroup).to receive(:example_method).and_raise(StandardError)
expect{subject.method1}.to raise_error(StandardError)
end
But I am getting the error like:
RSpec::Mocks::MockExpectationError: DBGroup does not implement #example_method
Scopes are not instance methods, they are class methods, so you should not use expect_instance_of, but expect.
Also, you are rescuing the StandardError inside the method, so the method itself does not raise a StandardError. Actually, a StandardError is raised inside the method and it's rescued inside of it aswel and your test is all about what's happening at it's exterior.
I hope you can see the difference:
def method_that_raises_an_error
raise(StandardError)
end
def method_that_returns_an_error
raise(StandardError)
rescue StandardError => e
e
end
As you can see, your code is more like the second. So, to test it you should do:
expect(DBGroup).to(receive(:example_method).and_raise(StandardError)
method1_return = subject.method1
expect(method1_return).to(be_instance_of(StandardError))
I have the following method:
def fetch_something
#fetch_something ||= array_of_names.inject({}) do |results, name|
begin
results[name] = fetch_value!(name)
rescue NoMethodError, RuntimeError
next
end
results
end
end
To its purpose: It fetches a value for a given name that can raise an error, in which case it will ignore the name and try the next one.
While this works fine I get an error from Rubocop that says:
Lint/NextWithoutAccumulator: Use next with an accumulator argument in
a reduce.
Googling that error leads me to http://www.rubydoc.info/gems/rubocop/0.36.0/RuboCop/Cop/Lint/NextWithoutAccumulator where it says to not omit the accumulator, which would result in a method looking like this:
def fetch_something
#fetch_something ||= array_of_names.inject({}) do |results, name|
begin
results[name] = fetch_value!(name)
rescue NoMethodError, RuntimeError
next(name)
end
results
end
end
The problem is, that this change breaks the otherwise working method. Any ideas on how to tackle that?
Update: Demonstrational example:
array_of_names = ['name1','name2','name3']
def fetch_value!(name)
# some code that raises an error if the name doesn't correspond to anything
end
fetch_something
# => {'name1' => {key1: 'value1', ...}, 'name3' => {key3: 'value3', ...}}
# 'name2' is missing since it wasn't found durcing the lookup
Using your example code, it seems that next(results) does fix the issue for me. I've used some test code that throws a KeyError instead of NoMethodError or RuntimeError, but the idea is still the same:
#array_of_names = ['name1','name2','name3']
def fetch_value!(name)
{'name1' => 'n1', 'name3' => 'n3'}.fetch(name)
end
def fetch_something
#fetch_something ||= #array_of_names.inject({}) do |results, name|
begin
results[name] = fetch_value!(name)
rescue NoMethodError, RuntimeError, KeyError
next(results)
end
results
end
end
p fetch_something
This code outputs:
{"name1"=>"n1", "name3"=>"n3"}
Also, I agree with #Алексей Кузнецов that each_with_object is probably the way to go whenever your block code mutates the data structure you're trying to build. So my preferred implementation of fetch_something might be more like this:
def fetch_something
#fetch_something ||= #array_of_names.each_with_object({}) do |name, results|
begin
results[name] = fetch_value!(name)
rescue NoMethodError, RuntimeError, KeyError
# error handling
end
end
end
Note that in my example the begin/end block is outside the assignment to results[name] in contrast to #Алексей Кузнецов's example which will assign nil to the hash every time an exception occurs.
Just use each_with_object
def fetch_something
#fetch_something ||= array_of_names.each_with_object({}) do |name, results|
results[name] ||= begin
fetch_value!(name)
rescue NoMethodError, RuntimeError
end
end
end
In sidekiq_batch.rb,
def sidekiq_status
begin
something
rescue => e
Rails.logger.error("\nRESCUENIL in sidekiq_status #{e.class} #{e.message} in #{e.backtrace}")
# FIXME RESCUENIL
nil
end
end
In checkin.rb,
def attached_receipt_image
begin
something else
rescue => e
Rails.logger.error("\nRESCUENIL in attached_receipt_image #{e.class} #{e.message} in #{e.backtrace}")
# FIXME RESCUENIL
nil
end
end
In barcode.rb,
def receipt_check?
begin
some code
rescue => e
Rails.logger.error("\nRESCUENIL in receipt_check #{e.class} #{e.message} in #{e.backtrace}")
# FIXME RESCUENIL
nil
end
end
Need to DRY up the code. How can I write a common error-logging routine for all of these methods in my models?
You can write an abstraction for that, but you cannot return from there. You can write:
def with_log(name)
begin
yield
rescue => exc
Rails.logger.error("\nRESCUENIL in #{name} #{exc.class} #{exc.message} in #{exc.backtrace}")
false
end
end
with_log(:sidekiq_status) do
something
true # not needed if something returns a boolean with the success status
end or return
This true can also be moved to with_log, it depends on how you plan to use it.
I have multiple model methods and I want to loop and execute through each of them. How would I perform this in rails 2.3.11? Preferably in a begin/rescue.
Edit:
Thanks maprihoda, I used your example and was able to apply it with the begin/rescue:
class MyModel
def method_1
puts 'In method_1'
end
def method_2
puts 'In method_2'
end
def method_3
%w( method_1 method_2).each { |m|
begin
self.send(m)
rescue => e
puts "#{e.message}"
end
}
end
end
Something like this?
class MyModel
def method_1
puts 'In method_1'
end
def method_2
puts 'In method_2'
end
def method_3
%w( method_1 method_2).each { |m| self.send(m) }
end
end
my_model = MyModel.new
my_model.method_3