I am using the interactor gem in my rails project. When one of the interactors within the organizer fails, the rollback method is called. The question is, is there any way this method can be aware of why the interactor failed?
Example:
def call
context.fail! error: 'Some error'
end
def rollback
# I want to access 'Some error' here
end
You can just access the context inside rollback method. There is one catch - rollback of the "current" interactor won't be invoked. Take a look at the following code:
require "interactor"
class Foo
include Interactor
def call
end
def rollback
p "#{context.error} from Foo"
end
end
class Bar
include Interactor
def call
context.fail!(error: "error!")
end
def rollback
p "#{context.error} from Bar"
end
end
class FooBar
include Interactor::Organizer
organize Foo, Bar
end
FooBar.call
it produces "error! from Foo" as the response. Bar throws an exception so FooBar organizer goes back to the Foo and calls its rollback method. context is shared so you have access to everything that was set before.
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)
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
I'm writing my first Ruby module and I have this:
/app/module/test_modules/test.rb
test.rb looks similar to:
module TestModules
module Test
def test
puts 'this is a test'
end
end
end
When I call the following from console, I get:
(main)> TestModule::Test.test
//NoMethodError: private method `test' called for TestModules::Test:Module
How do I make test() visible?
You are calling a class method, whereas you defined test as an instance method. You could call it the way you want if you used the module via include or extend. This article does a good job explaining.
module TestModules
module Test
def self.test
puts 'this is a test'
end
end
end
Also,
1)
module TestModules
module Test
def test
puts 'this is a test'
end
module_function :test
end
end
2)
module TestModules
module Test
extend self
def test
puts 'this is a test'
end
end
end
The way that you have defined your method, it is a method on an instance of Test - thus it would work if you did:
blah = TestModule::Test.new
blah.test
note - and do use it this way, you would need to define Test as a class not a module
If you want the function to work on the class itself, then you need to define it like so:
def self.test
....
end
And then you can do TestModules::Test.test
the test method you defined is instance method...try this
module TestModules
module Test
def self.test
puts 'this is a test'
end
end
end
now you can call the method by this TestModules::Test.test
I've got a model class that overrides update_attributes:
class Foo < ActiveRecord::Base
def update_attributes(attributes)
if super(attributes)
#do some other cool stuff
end
end
end
I'm trying to figure out how to set an expectation and/or stub on the super version of update_attributes to make sure that in the success case the other stuff is done. Also I want to make sure that the super method is actually being called at all.
Here's what I have tried so far (and it didn't work, of course):
describe "#update_attributes override" do
it "calls the base class version" do
parameters = Factory.attributes_for(:foo)
foo = Factory(:foo, :title => "old title")
ActiveRecord::Base.should_receive(:update_attributes).once
foo.update_attributes(parameters)
end
end
This doesn't work, of course:
Failure/Error: ActiveRecord::Base.should_recieve(:update_attributes).once
NoMethodError:
undefined method `should_recieve' for ActiveRecord::Base:Class
Any ideas?
update_attributes is an instance method, not a class method, so you cannot stub it directly on ActiveRecord::Base with rspec-mocks, as far as I know. And I don't think that you should: the use of super is an implementation detail that you shouldn't be coupling your test to. Instead, its better to write examples that specify the behavior you want to achieve. What behavior do you get from using super that you wouldn't get if super wasn't used?
As an example, if this was the code:
class Foo < ActiveRecord::Base
def update_attributes(attributes)
if super(attributes)
MyMailer.deliver_notification_email
end
end
end
...then I think the interesting pertinent behavior is that the email is only delivered if there are no validation errors (since that will cause super to return true rather than false). So, I might spec this behavior like so:
describe Foo do
describe "#update_attributes" do
it 'sends an email when it passes validations' do
record = Foo.new
record.stub(:valid? => true)
MyMailer.should_receive(:deliver_notification_email)
record.update_attributes(:some => 'attribute')
end
it 'does not sent an email when it fails validations' do
record = Foo.new
record.stub(:valid? => false)
MyMailer.should_receive(:deliver_notification_email)
record.update_attributes(:some => 'attribute')
end
end
end
Try replacing should_recieve with should_receive.
In my mailer controller, under certain conditions (missing data) we abort sending the email.
How do I exit the controller method without still rendering a view in that case?
return if #some_email_data.nil?
Doesn't do the trick since the view is still rendered (throwing an error every place I try to use #some_email_data unless I add a lot of nil checks)
And even if I do the nil checks, it complains there's no 'sender' (because I supposed did a 'return' before getting to the line where I set the sender and subject.
Neither does render ... return
Basically, RETURN DOESN'T RETURN inside a mailer method!
A much simpler solution than the accepted answer would be something like:
class SomeMailer < ActionMailer::Base
def some_method
if #some_email_data.nil?
self.message.perform_deliveries = false
else
mail(...)
end
end
end
If you're using Rails 3.2.9 (or later things even better) - there you can finally conditionally call mail(). Here's the related GitHub thread. Now the code can be reworked like this:
class SomeMailer < ActionMailer::Base
def some_method
unless #some_email_data.nil?
mail(...)
end
end
end
I just encountered same thing here.
My solution was following:
module BulletproofMailer
class BlackholeMailMessage < Mail::Message
def self.deliver
false
end
end
class AbortDeliveryError < StandardError
end
class Base < ActionMailer::Base
def abort_delivery
raise AbortDeliveryError
end
def process(*args)
begin
super *args
rescue AbortDeliveryError
self.message = BulletproofMailer::BlackholeMailMessage
end
end
end
end
Using these wrapper mailer would look like this:
class EventMailer < BulletproofMailer::Base
include Resque::Mailer
def event_created(event_id)
begin
#event = CalendarEvent.find(event_id)
rescue ActiveRecord::RecordNotFound
abort_delivery
end
end
end
It is also posted in my blog.
I've found this method that seems the least-invasive, as it works across all mailer methods without requiring you to remember to catch an error. In our case, we just want a setting to completely disable mailers for certain environments. Tested in Rails 6, although I'm sure it'll work just fine in Rails 5 as well, maybe lower.
class ApplicationMailer < ActionMailer::Base
class AbortDeliveryError < StandardError; end
before_action :ensure_notifications_enabled
rescue_from AbortDeliveryError, with: -> {}
def ensure_notifications_enabled
raise AbortDeliveryError.new unless <your_condition>
end
...
end
The empty lambda causes Rails 6 to just return an ActionMailer::Base::NullMail instance, which doesn't get delivered (same as if your mailer method didn't call mail, or returned prematurely).
Setting self.message.perform_deliveries = false did not work for me.
I used a similar approach as some of the other answers - using error handling to control the flow and prevent the mail from being sent.
The example below is aborting mail from being sent in non-Production ENVs to non-whitelisted emails, but the helper method logic can be whatever you need for your scenario.
class BaseMailer < ActionMailer::Base
class AbortedMailer < StandardError; end
def mail(**args)
whitelist_mail_delivery(args[:to])
super(args)
rescue AbortedMailer
Rails.logger.info "Mail aborted! We do not send emails to external email accounts outside of Production ENV"
end
private
def whitelist_mail_delivery(to_email)
return if Rails.env.production?
raise AbortedMailer.new unless internal_email?(to_email)
end
def internal_email?(to_email)
to_email.include?('#widgetbusiness.com')
end
end
I just clear the #to field and return, so deliver aborts when it doesn't have anything there. (Or just return before setting #to).
I haven't spent much time with rails 3 but you could try using
redirect_to some_other_route
alternatively, if you're really just checking for missing data you could do a js validation of the form fields and only submit if it passes.