Skip to content

Context

Aaron Allen edited this page Jan 26, 2020 · 1 revision

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.

Adding to the 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

Failing the Context

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

Dealing with Failure

#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?.

Context Attributes

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'

Validating the Context

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:

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