Problem
During the discussion in #74 it occurred to me that there maybe an unnecessary fragmentation or friction with the way interactors and their contexts are currently defined.
Proposal
I propose a whole new pattern for interactor definition that would be some what similar to how rake tasks are written in a given project. The idea would be you would define an interactor namespace like :user
and within that namespace you would define a set of contexts and a set of interactors like this:
# interactors/user.rb
ActiveInteractor::InteractorNamespace.new(:user) do
# we define a default interactor context
# this context would be used by all interactors in this namespace
# unless the interactor specifically specifies otherwise
context do
attributes :first_name, :last_name, :email, :user
end
context :user_and_account do
attributes :account, :user
validates :account, on: :called
end
context :create_account do
attributes :account, :user
validates :user, presence: true, on: :calling
end
# defining an interactor would be the equivalent of defining the #perform
# method in current interactor classes.
# this interactor would use the default context as it doesn't
# specify one nor is there a context with a matching name
interactor :finder do |context|
context.user = User.find_by(
email: context.email,
first_name: context.first_name,
last_name: context.last_name
)
end
# this interactor would use the default context as it doesn't
# specify one nor is there a context with a matching name
interactor :downcase_email do |context|
context.email = context.email&.downcase
end
# this interactor has a dependency on another interactor
interactor creator: %i[downcase_email] do |context|
context.user ||= User.create(
email: context.email,
first_name: context.first_name,
last_name: context.last_name
)
end
# we don't need to specify a context here it will
# use the :create_account context by default
interactor :create_account do |context|
context.account = context.user.generate_account!
end
# we can define an organizer that specifies a context
organizer :create_with_account, contextualize_with: :user_and_account do
organize :finder, :creator, :create_account
end
end
given this pattern we can then just call a single simple api to invoke the interaction like this:
context = ActiveInteractor.perform(
:'user:create_with_account',
email: '[email protected]',
first_name: 'Aaron',
last_name: 'Allen'
)
context.success? #=> true
context #=> <# Interactor::User::UserAndAccountContext ...>
additionally we can call a different interactor within that namespace
context = ActiveInteractor.perform(
:'user:finder',
email: '[email protected]',
first_name: 'Aaron',
last_name: 'Allen'
)
context.success? #=> true
context #=> <# Interactor::User::DefaultContext ...>
Additionally we could even provide helper methods for rails ActionController::Base
so that this:
# user_controller.rb
def create
result = ActiveInteractor.perform(:'user:create_with_account', user_params)
if result.success?
render json: result.user, status: :created
else
render json: { errors: result.user.errors }, status: :unprocessable_entity
end
end
could be refactored to this:
# user_controller.rb
def create
render_interactor_result_json(:'user:create_with_account', user_params)
end
The goal here would be to provide a single api for every interactor in your project something like:
ActiveInteractor.perform(interactor_name, context_attributes = {})
Things to consider before answering
- Do you think this pattern would be overall a better approach for the interactor pattern?
- Would you find value in being able to call one method to run all of your interactors?