-
-
Notifications
You must be signed in to change notification settings - Fork 11
Context
Each interactor will have it's own immutable context and context class. All context classes should inherit
from ActiveInteractor::Context::Base. By default an interactor will attempt to find an existing class
following the naming conventions: InteractorName::Context
or InteractorNameContext
. If no class is found a
context class will be created using the naming convention InteractorClass::Context
for example:
class MyInteractor < ActiveInteractor::Base; end
class MyInteractor::Context < ActiveInteractor::Context::Base; end
MyInteractor.context_class
#=> MyInteractor::Context
class MyInteractorContext < ActiveInteractor::Context::Base; end
class MyInteractor < ActiveInteractor::Base; end
MyInteractor.context_class
#=> MyInteractorContext
class MyInteractor < ActiveInteractor::Base; end
MyInteractor.context_class
#=> MyInteractor::Context
Additionally you can manually specify a context for an interactor with the .contextualize_with method.
class MyGenericContext < ActiveInteractor::Context::Base; end
class MyInteractor
contextualize_with :my_generic_context
end
MyInteractor.context_class
#=> MyGenericContext
An interactor's context contains everything the interactor needs to do its work. When an interactor does its single purpose, it affects its given context.
All instances of context inherit from OpenStruct. As an interactor runs it can add information to it's context.
class MyInteractor
def perform
context.user = User.create(...)
end
end
When something goes wrong in your interactor, you can flag the context as failed.
context.fail!
When given an argument of an instance of ActiveModel::Errors, the #fail! method can also update the context. The following are equivalent:
context.errors.merge!(user.errors)
context.fail!
context.fail!(user.errors)
You can ask a context if it's a failure with the #failure? or #fail? method:
class MyInteractor
def perform
context.fail!
end
end
result = MyInteractor.perform
result.failure? #=> true
or if it's a success with the #success? or #successful? method:
class MyInteractor
def perform
context.user = User.create(...)
end
end
result = MyInteractor.perform
result.success? #=> true
#fail! always throws an exception of type ActiveInteractor::Error::ContextFailure.
Normally, however, these exceptions are not seen. In the recommended usage, the consuming object invokes the interactor using the class method .perform, then checks the #success? method of the context.
This works because the .perform method swallows exceptions. When unit testing an interactor, if calling custom business logic methods directly and bypassing .perform, be aware that #fail! will generate such exceptions.
See Using Interactors for the recommended usage of .perform and #success?.
Each context instance has basic attribute assignment methods. Assigning attributes to a context is a simple way to explicitly define what properties a context should have after an interactor has done it's work.
You can see what attributes are defined on a given context with the .attributes method:
class MyInteractorContext < ActiveInteractor::Context::Base
attributes :first_name, :last_name, :email, :user
end
class MyInteractor < ActiveInteractor::Base; end
result = MyInteractor.perform(
first_name: 'Aaron',
last_name: 'Allen',
email: 'hello@aaronmallen.me',
occupation: 'Software Dude'
)
#=> <#MyInteractor::Context first_name='Aaron' last_name='Allen' email='hello@aaronmallen.me' occupation='Software Dude'>
result.attributes
#=> { first_name: 'Aaron', last_name: 'Allen', email: 'hello@aaronmallen.me' }
result.occupation
#=> 'Software Dude'
All context instances include ActiveModel::Validations; additionally ActiveInteractor delegates all the validation
methods provided by ActiveModel::Validations onto an interactor's context class from the interactor itself.
All of the methods found in ActiveModel::Validations can be invoked directly on your interactor with the prefix
context_
. However this can be confusing and it is recommended to make all validation calls on a context class
directly.
ActiveInteractor provides two validation callback steps:
-
:calling
used before #perform is invoked on an interactor -
:called
used after #perform is invoked on an interactor
A basic implementation might look like this:
class MyInteractorContext < ActiveInteractor::Context::Base
attributes :first_name, :last_name, :email, :user
# only validates presence before perform is invoked
validates :first_name, presence: true, on: :calling
# validates before and after perform is invoked
validates :email, presence: true,
format: { with: URI::MailTo::EMAIL_REGEXP }
# validates after perform is invoked
validates :user, presence: true, on: :called
validate :user_is_a_user, on: :called
private
def user_is_a_user
return if user.is_a?(User)
errors.add(:user, :invalid)
end
end
class MyInteractor < ActiveInteractor::Base
def perform
context.user = User.create_with(
first_name: context.first_name,
last_name: context.last_name
).find_or_create_by(email: context.email)
end
end
result = MyInteractor.perform(last_name: 'Allen')
#=> <#MyInteractor::Context last_name='Allen>
result.failure?
#=> true
result.valid?
#=> false
result.errors[:first_name]
#=> ['can not be blank']
result = MyInterator.perform(first_name: 'Aaron', email: 'hello@aaronmallen.me')
#=> <#MyInteractor::Context first_name='Aaron' email='hello@aaronmallen.me' user=<#User ...>>
result.success?
#=> true
result.valid?
#=> true
result.errors.empty?
#=> true