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)>
In my public method #recalculate, calling the private method1. This method throw exception 'ActiveRecord::StaleObjectError'.
def recalculate
method_1
self.save!
end
private
def method_1
begin
####
####
if self.lock_version == Product.find(self.id).lock_version
Product.where(:id => self.id).update_all(attributes)
else
raise ActiveRecord::StaleObjectError.new(self, "test")
end
rescue ActiveRecord::StaleObjectError => e
if tries < 3
tries += 1
sleep(1 + tries)
self.reload
retry
else
raise Exception.new(timeout.inspect)
end
end
end
Rspec Test case:
it 'if car is updated then ActiveRecord::StaleObjectError should be raised' do
prod_v1 =Product.find(#prod.id)
prod_v2 = Car.find(#prod.id)
prod_v1.recalculate
prod_v1.reload # will make lock_version of prod_v1 to 1
prod_v2.recalculate # howvever lock_version of prod_v2 is still 0.
expect{ prod_v2.send(:method1)}.to raise_error(ActiveRecord::StaleObjectError)
Error:
Failure/Error: expect(prod_v2.send(:method1)).to raise_error(ActiveRecord::StaleObjectError)
expected ActiveRecord::StaleObjectError but nothing was raised
Please suggest how to write the unit test case for an exception which is raised in private method.
I have used send based on the link:
Note: Exception was raised for in the first time because self.lock_version == Product.find(self.id) was false . And in retry self.lock_version == Product.find(self.id) is true so exception is not capture.
Here's a simpler version of what your code is actually doing:
class StaleObjectError < Exception
end
class MyClass
def initialize
#tries = 0
end
def method_1
begin
raise StaleObjectError.new("I'm the more specific exception")
rescue StaleObjectError => e
if #tries < 3
#tries += 1
sleep(1 + #tries)
retry
else
raise Exception.new("I'm the failure case")
end
end
end
end
myObject = MyClass.new
begin
myObject.method_1
rescue Exception => e
# in the error condition, this is always #<Exception: I'm the failure case>
puts e.inspect
end
Which results in
#<Exception: I'm the failure case>
You won't be able to expect the ActiveRecord::StaleObjectError because you mask it with your rescue else- you've converted the StaleObjectError into an Exception
If you want to preserve the StaleObjectError then you can raise e in your rescue else instead. So to use my example code again:
if #tries < 3
#tries += 1
sleep(1 + #tries)
retry
else
raise e
end
Which would result in
#<StaleObjectError: I'm the more specific exception>
Then your rspec example should be able to expect the code to raise the correct exception type.
I'm trying to build my own Exception for tagged logging:
module Exceptions
class GeneralException < StandardError
LOGGER_NAME = 'Base'
def initialize(message)
#logger = ActiveSupport::TaggedLogging.new(Logger.new(STDOUT))
#logger.tagged(get_logger_name) { #logger.error message }
#message = message
end
def get_logger_name
self.class::LOGGER_NAME
end
end
class InvalidDataException < GeneralException; end
class SecurityException < GeneralException
LOGGER_NAME = 'Security'
end
class ElasticSearchException < GeneralException
LOGGER_NAME = 'Elastic'
end
end
I'm expecting to be able to call this new exception with:
raise Exceptions::SecurityException "Something security related happened.
The problem is that when I call this I get:
NoMethodError: undefined method 'SecurityException' for Exceptions:Module
Any idea how to correctly raise this error?
Well, quite easy, you need to raise the instance of the error:
raise Exceptions::SecurityException.new "Something security related happend."
or
raise Exceptions::SecurityException, "Something security related happend."
Sorry for my bad English,This is not my first language.
I tried to create an index on elasticsearch but I want to be transactional.
Means if error occurred on saving process or on creating index both processes rollback.
I have these lines to save an entry
ActiveRecord::Base.transaction do
entries.map do |entry|
entry = entries.where(source_entry_id: entry.entry_id).first_or_initialize
entry.data = feed_entry.to_hash(self)
entry.save!
end
end
then I defined this class on concern to have searchable functionality for some entities
require 'elasticsearch/model'
module Searchable
extend ActiveSupport::Concern
included do
include Elasticsearch::Model
after_save { index_document }
after_destroy { delete_document }
end
module ClassMethods
def create_index!
self.__elasticsearch__.create_index! force: true
end
def search_index(*args)
self.__elasticsearch__.search(*args)
end
end
def index_document
return if Rails.env.test?
self.__elasticsearch__.index_document
rescue StandardError => e
handle_error(e)
end
def delete_document
return if Rails.env.test?
self.__elasticsearch__.delete_document
rescue StandardError => e
handle_error(e)
end
def format_date(date)
date.to_s(:iso8601) if date
end
def handle_error(e)
error = Searchable.parse_error(e.message)
if error['result'] != 'not_found'
Raven.capture_message("Elasticsearch: #{error['result']}", extra: error)
end
end
def self.parse_error(string)
parts = string.match(/\[(\d*)\]\s(.*)/)
return ({ 'result': string }) unless parts
result = JSON.parse(parts[2])
result['status'] = parts[1]
result
end
end
and my model include searchable
class
Entry < ApplicationRecord
include Searchable
...
Problem that I faced is if I rescue the error entry will save into database if I throw exception and not handle the exception process will freeze and not doing for the rest entries. how can I do this correctly?
Elasticsearch is not transactional.
You may want to add a transactional queue system in the middle or just log when something goes wrong and manually deal with inconsistencies.
You can also send errors in a message queue system to process them later.
I've created this code to show specific errors messages to user:
application_controller.rb
class ApplicationController < ActionController::Base
rescue_from Exception do |exception|
message = exception.message
message = "default error message" if exception.message.nil?
render :text => message
end
end
room_controller.rb
class RoomController < ApplicationController
def show
#room = Room.find(params[:room_id]) # Can throw 'ActiveRecord::RecordNotFound'
end
def business_method
# something
raise ValidationErros::BusinessException("You cant do this") if something #message "You cant do this" should be shown for user
#...
end
def business_method_2
Room.find(params[:room_id]).do_something
end
end
room.rb
class Room < ActiveRecord::Base
def do_something
#...
raise ValidationErrors::BusinessException("Invalid state for room") if something #message "Invalid state for room" should be shown for user
#...
end
end
app/models/erros/validation_errors.rb
module ValidationErrors
class BusinessException < RuntimeError
attr :message
def initialize(message = nil)
#message = message
end
end
end
example.js
$.ajax({
url: '/room/show/' + roomId,
success: function(data){
//... do something with data
},
error: function(data){
App.notifyError(data) //show dialog with message
}
});
But I can not use the class BusinessException. When BusinessException should be raised,
the message
uninitialized constant Room::ValidationErrors
is shown to user.
if I change this code:
raise ValidationErrors::BusinessException("Invalid state for room") if something
by this:
raise "Invalid state for room" if something
It works.
What change to this code works with BusinessException with messages. I need this
to create specifics rescue_from methods in ApplicationController.
Edit:
Thank you for comments!
My error is it doesn't know ValidationErrors Module. How to import this Module to my class?
I've tested add theses lines to lines:
require 'app/models/errors/validation_errors.rb'
require 'app/models/errors/validation_errors'
But then raise the error:
cannot load such file -- app/models/errors/validation_errors
Solution:
https://stackoverflow.com/a/3356843/740394
config.autoload_paths += %W(#{config.root}/app/models/errors)
raise ::ValidationErrors::BusinessException("Invalid state for room")