In my Rails 4 app I have a Service object that handles communication with Stripe Payments Processor. I want it as a service object so that multiple Controllers/Models can utilize the methods within it.
However, I also need to be able to trap errors when communicating with the Stripe API which then causes the problem as the errors need to be assigned to a particular object.
Here is a method in my StripeCommunicator.rb class:
def create_customer(token,object)
customer = Stripe::Customer.create(:description => 'Accommodation', :email => object.email, :card => token)
return customer
rescue Stripe::CardError => e
#account.errors.add :base, e.message
false
end
as you can see - the errors are being added to the #account object - which essentially renders it useless when I want to use this method from another controller with a View that refers to another object to display errors.
Any ideas?
Simplest thing is to just pass the #account instance in as another argument. Errors is going to be on any model instance, e.g.
def create_customer(token,object,model_instance)
Stripe::Customer.create(description: 'Accommodation', email: object.email, card: token)
# return customer <- don't need this. whatever is last evaluated will be returned
rescue Stripe::CardError => e
model_instance.errors.add :base, e.message
false
end
If you were doing the error handling in the controller instead of a service object, you could take advantage of rescue_from which can handle exceptions falling out from action methods, e.g. in your controller or ApplicationController, etc., do the following:
rescue_from Stripe::CardError, with: :add_error_message_to_base
def add_error_message_to_base(e)
# this assumes that you set #instance in the controller's action method.
#instance.errors.add :base, e.message
respond_with #instance
end
or more generically:
rescue_from Stripe::CardError, with: :add_error_message_to_base
def add_error_message_to_base(e)
model_class_name = self.class.name.chomp('Controller').split('::').last.singularize
instance_value = instance_variable_get("##{model_class_name}")
instance_value.errors.add :base, e.message if instance_value
respond_with instance_value
end
or in a concern, you could do either of the above, putting the rescue_from into the included block:
module StripeErrorHandling
extend ::ActiveSupport::Concern
included do
rescue_from Stripe::CardError, with: :add_error_message_to_base
end
def add_error_message_to_base(e)
# see comment above...
#instance.errors.add :base, e.message
respond_with #instance
end
end
And you can use config.exceptions_app to handle errors at the Rack-level as José Valim describes here.
You could also inherit the method vs. having a separate service class, or have a concern/module. You might even do through hooks, e.g.:
# not exactly what you were doing but just for example.
# could put in app/controller/concerns among other places.
module ActionsCreateStripeCustomer
extend ::ActiveSupport::Concern
included do
around_action :create_stripe_customer
end
def create_stripe_customer
# this (indirectly) calls the action method, and you will
# set #instance in your action method for this example.
yield
customer = Stripe::Customer.find_or_create_by(description: 'Accommodation', email: object.email, card: token)
# could set customer on #instance here and save if needed, etc.
rescue Stripe::CardError => e
if #instance
#instance.errors.add :base, e.message
respond_with #instance
else
logger.warn("Expected #instance to be set by #{self.class.name}##{params[:action]}")
raise e
end
end
end
Then in the controller:
include ActionsCreateStripeCustomer
There is also before_action, after_action, etc. Also, you can just include modules and when instance methods are called they call on the including class instance first, then the first included module, then the second, etc. if you do super if defined?(super) to call the prior method, and it automatically puts in all the arguments and block.
And, if it were about getting the model class name rather than the instance, that is easy, too. Say the class you were calling from was AccountStripeCommunicator, then #model_class after the following would be Account:
qualified_class_name = self.class.name.chomp('StripeCommunictor')
#model_class = qualified_class_name.split('::').last.singularize.constantize
All kinds of possibilities.
Related
Let's imagine I have a class
class Test < ActiveRecord::Base
include AuthenticatorHelper
def test
authenticate_or_fail!
puts "If I fail, this should be unreachable"
end
end
and
module AuthenticationHelper
def authenticate_or_fail!
#user = User.find(params[:token])
unless #user
render :json => {code: 401, :err => 'Unauthorized'} and return
end
end
end
What I want to do is either authenticate or reply with a json msg. However, it will obviously ignore my return statement due to nesting and it will always print my message
If I fail, this should be unreachable
Regarding the question
You could extract the call into a before_filter/before_action (based on the rails version).
class Test < ActiveRecord::Base
include AuthenticatorHelper
before_action :authenticate_or_fail!
def test
puts "If I fail, this should be unreachable"
end
end
Please see the documentation for further details.
Because your helper method renders in case of a failure, rails will prevent the test method to be called. You will not need the and return part then, which would only have returned from the method anyway and as such was a NoOp.
Apart from the question but also noteworthy:
I don't want to point out errors for the sake of it. I just want to prevent the OP from running into a series of bugs later on.
User.find(params[:token])
Will raise an exception if no record is found. Because of that, the unless #user part will not be evaluated in case of an invalid token. You could use
User.find_by(id: params[:token])
instead.
Your class which looks like it acts as a controller is named Test and inherits from ActiveRecord::Base. The first is unusual as TestsController would be more along the lines of rails and the seconds looks plain wrong. A controller has to inherit from ApplicationController (which itself inherits from ActionController::Base)
In order to drastically reduce code repetition, I want to write up a concern with a generic way to add a special around_action to a controller. It is basically supposed to catch any exception, render the right template and add the exception as a notice. However, it must be applicable to different actions, and show different templates depending on the action. My goal is basically to be able to do this:
protect_from_exception_with 'index', only: [ :update ]
In order to achieve this, I tried to write up my concern like this (Using Rails 4.1):
module CatchException
extend ActiveSupport::Concern
module ClassMethods
def protect_from_exception_with(failure_template, params)
around_action -> { catch_exception_with(failure_template) }, params
end
end
private
def log_error(e)
# Many things happen here
end
def catch_exception_with(failure_template)
yield
rescue => e
log_error(e)
render failure_template
end
end
However, this leads to an error:
LocalJumpError: no block given (yield)
I have trying to find examples for around_action or around_filter with a parameter, but could only find them for before_action.
I hope what I'm trying to achieve is at all possible, otherwise I'd need to write a new method in every controller for every action I need to achieve this.
There are some clues:
around_action receives a callback and a block as params, if we send a function as the 1st param, that function mustn't have any parameter!
We can send a block instead (like you did) but we must pass the current given block to that block as well, your code misses the passing block, that is why the exception raised.
In protect_from_exception_with, I can call block_given?, it returns true, but I don't know how to get the block out!
This works:
module CatchException
extend ActiveSupport::Concern
module ClassMethods
def protect_from_exception_with(failure_template, params)
around_action -> { catch_exception_with(failure_template) }, params
end
end
private
def log_error(e)
# Many things happen here
end
def catch_exception_with(failure_template)
self.send(params[:action])
rescue => e
log_error(e)
render failure_template
end
end
Thankfully, we still have the params in catch_exception_with, make it easy, call the action back to the controller!
So imagine you have 2 models, Person and Address, and only one address per person can be marked as 'Main'. So if I wanna change a person's main address, I need to use a transaction, to mark the new one as main and unmark the old one. And as far as I know using transactions in controllers is not good so I have a special method in model, thats what I've got:
AddressesController < ApplicationController
def update
#new_address = Address.find(params[:id])
#old_address = Address.find(params[:id2])
#new_address.exchange_status_with(#old_address)
end
end
Model:
class Address < ActiveRecord::Base
def exchange_status_with(address)
ActiveRecord::Base.transaction do
self.save!
address.save!
end
end
end
So thequestion is, if the transaction in the model method fails, I need to rescue it and notify the user about the error, how do I do that? Is there a way to make this model method return true or false depending on whether the transaction was successful or not, like save method does?
I probably could put that transaction in the controller and render the error message in the rescue part, but I guess its not right or I could put that method in a callback, but imagine there is some reason why I cant do that, whats the alternative?
PS dont pay attention to finding instances with params id and id2, just random thing to show that I have 2 instances
def exchange_status_with(address)
ActiveRecord::Base.transaction do
self.save!
address.save!
end
rescue ActiveRecord::RecordInvalid => exception
# do something with exception here
end
FYI, an exception looks like:
#<ActiveRecord::RecordInvalid: Validation failed: Email can't be blank>
And:
exception.message
# => "Validation failed: Email can't be blank"
Side note, you can change self.save! to save!
Alternate solution if you want to keep your active model errors:
class MyCustomErrorClass < StandardError; end
def exchange_status_with(address)
ActiveRecord::Base.transaction do
raise MyCustomErrorClass unless self.save
raise MyCustomErrorClass unless address.save
end
rescue MyCustomErrorClass
# here you have to check self.errors OR address.errors
end
Is it possible to add a callback to a single ActiveRecord instance? As a further constraint this is to go on a library so I don't have control over the class (except to monkey-patch it).
This is more or less what I want to do:
def do_something_creazy
message = Message.new
message.on_save_call :do_even_more_crazy_stuff
end
def do_even_more_crazy_stuff(message)
puts "Message #{message} has been saved! Hallelujah!"
end
You could do something like that by adding a callback to the object right after creating it and like you said, monkey-patching the default AR before_save method:
def do_something_ballsy
msg = Message.new
def msg.before_save(msg)
puts "Message #{msg} is saved."
# Calls before_save defined in the model
super
end
end
For something like this you can always define your own crazy handlers:
class Something < ActiveRecord::Base
before_save :run_before_save_callbacks
def before_save(&block)
#before_save_callbacks ||= [ ]
#before_save_callbacks << block
end
protected
def run_before_save_callbacks
return unless #before_save_callbacks
#before_save_callbacks.each do |callback|
callback.call
end
end
end
This could be made more generic, or an ActiveRecord::Base extension, whatever suits your problem scope. Using it should be easy:
something = Something.new
something.before_save do
Rails.logger.warn("I'm saving!")
end
I wanted to use this approach in my own project to be able to inject additional actions into the 'save' action of a model from my controller layer. I took Tadman's answer a stage further and created a module that can be injected into active model classes:
module InstanceCallbacks
extend ActiveSupport::Concern
CALLBACKS = [:before_validation, :after_validation, :before_save, :before_create, :after_create, :after_save, :after_commit]
included do
CALLBACKS.each do |callback|
class_eval <<-RUBY, __FILE__, __LINE__
#{callback} :run_#{callback}_instance_callbacks
def run_#{callback}_instance_callbacks
return unless #instance_#{callback}_callbacks
#instance_#{callback}_callbacks.each do |callback|
callback.call
end
end
def #{callback}(&callback)
#instance_#{callback}_callbacks ||= []
#instance_#{callback}_callbacks << callback
end
RUBY
end
end
end
This allows you to inject a full set of instance callbacks into any model just by including the module. In this case:
class Message
include InstanceCallbacks
end
And then you can do things like:
m = Message.new
m.after_save do
puts "In after_save callback"
end
m.save!
To add to bobthabuilda's answer - instead of defining the method on the objects metaclass, extend the object with a module:
def do_something_ballsy
callback = Module.new do
def before_save(msg)
puts "Message #{msg} is saved."
# Calls before_save defined in the model
super
end
end
msg = Message.new
msg.extend(callback)
end
This way, you can define multiple callbacks, and they will be executed in the opposite order you added them.
The following will allow you to use an ordinary before_save construction, i.e. calling it on the class, only in this case, you call it on the instance's metaclass so that no other instances of Message shall be affected. (Tested in Ruby 1.9, Rails 3.13)
msg = Message.new
class << msg
before_save -> { puts "Message #{self} is saved" } # Here, `self` is the msg instance
end
Message.before_save # Calling this with no args will ensure that it gets added to the callbacks chain (but only for your instance)
Test it thus:
msg.save # will run the before_save callback above
Message.new.save # will NOT run the before_save callback above
For a specific role (group of users) I added the :readonly to every find on the active record
def self.find(*args)
if User.current_user.has_role? 'i_can_only_read'
with_scope({:find => {:readonly => true}}) do
result = super *args
end
end
end
Of course it raises now ActiveRecord::ReadOnlyRecord Exceptions in Controller passed on to the user; not very nice.
Can I catch this type of error in one place? Like in production.rb or in the application.rb? Or can I configure a specific error page for this error?
Yes, simply override rescue_action_in_public like this:
class ApplicationController < ActionController::Base
...
def rescue_action_in_public(exception)
case exception
when ActiveRecord::ReadOnlyRecord
# DO SOME LOGIC HERE
end
end
end
end
This will execute your action when in "production", but leave you with an informative stack trace when you are in "development".
Rails has a number of other rescue_action_* methods that might be more suitable to your problem...take a look at http://api.rubyonrails.org/classes/ActionController/Rescue.html