ActiveInteractor

Jan 26 2019 6 minute read

This weekend I released v1.0.0 of ActiveInteractor, an implementation of the command pattern for Ruby with ActiveModel::Validations based on the interactor gem with rich support for attributes, callbacks, and validations, and thread safe performance methods.

I wanted to go over some of the basic usage of the gem. An interactor is a simple, single-purpose service object. Interactors can be used to reduce the responsibility of your controllers, workers, and models and encapsulate your application’s business logic. Each interactor represents one thing that your application does.

ActiveInteractor’s main component is called an interactor, each interactor has it’s own immutable context which contains everything the interactor needs to do its work. When an interactor does its single purpose, it affects its given context. There are two kinds of interactors built into ActiveInteractor: basic interactors and organizers. A basic interactor is a class that inherits from ActiveInteractor::Base and defines a #perform method.

Basic Usage

Most of the time, your application will use its interactors from its controllers. The following controller:

class SessionsController < ApplicationController
  def create
    if user = User.authenticate(session_params[:email], session_params[:password])
      session[:user_token] = user.secret_token
      redirect_to user
    else
      flash.now[:message] = "Please try again."
      render :new
    end
  end

  private

  def session_params
    params.require(:session).permit(:email, :password)
  end
end

can be refactored to:

# app/interactors/authenticate_user_context.rb
class AuthenticateUserContext < ActiveInteractor::Context::Base
  attributes :email, :password, :user, :token
  validates :email, presence: true,
                    format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :password, presence: true
  validates :user, presence: true, on: :called
end

# app/interactors/authenticate_user.rb
class AuthenticateUser < ActiveInteractor::Base
  def perform
    context.user = User.authenticate(
      context.email,
      context.password
    )
    context.token = context.user.secret_token
  end
end

# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  def create
    result = AuthenticateUser.perform(session_params)

    if result.success?
      session[:user_token] = result.token
      redirect_to result.user
    else
      flash.now[:message] = t(result.errors.full_messages)
      render :new
    end
  end

  private

  def session_params
    params.require(:session).permit(:email, :password)
  end
end

Organizers

An organizer is an important variation on the basic interactor. Its single purpose is to run other interactors.

class CreateOrder < ActiveInteractor::Base
  def perform
    ...
  end
end

class ChargeCard < ActiveInteractor::Base
  def perform
    ...
  end
end

class SendThankYou < ActiveInteractor::Base
  def perform
    ...
  end
end

class PlaceOrder < ActiveInteractor::Organizer::Base

  organize :create_order, :charge_card, :send_thank_you
end

In a controller, you can run the PlaceOrder organizer just like you would any other interactor:

class OrdersController < ApplicationController
  def create
    result = PlaceOrder.perform(order_params: order_params)

    if result.success?
      redirect_to result.order
    else
      @order = result.order
      render :new
    end
  end

  private

  def order_params
    params.require(:order).permit!
  end
end

The organizer passes its context to the interactors that it organizes, one at a time and in order. Each interactor may change that context before it’s passed along to the next interactor.

Organizing Interactors Conditionally

We can also add conditional statements to our organizer by passing a block to the .organize method:

class PlaceOrder < ActiveInteractor::Organizer::Base
  organize do
    add :create_order, if :user_registered?
    add :charge_card, if: -> { context.order }
    add :send_thank_you, if: -> { context.order }
  end

  private

  def user_registered?
    context.user&.registered?
  end
end

Working With Rails

If you’re working with a rails project ActiveInteractor comes bundled with some useful generators to help speed up development. You should first run the install generator with:

rails generate active_interactor:install

In some instances you may want to use an ActiveRecord model as a context for an interactor. You can do this by calling the .acts_as_context method on any ActiveRecord model, and then simply call the .contextualize_with method on your interactor or organizer to point it to the appropriate class.

# app/models/user.rb
class User < ApplicationRecord
  acts_as_context
end

# app/interactors/create_user.rb
class CreateUser < ApplicationInteractor
  contextualize_with :user

  def perform
    context.email&.downcase!
    context.save
  end
end

CreateUser.perform(email: 'HELLO@AARONMALLEN.ME')
#=> <#User id=1 email='hello@aaronmallen.me'>

I hope the ruby community finds this gem usefull and I’d love any feed back, issues, or stars on the repository that you’re willing to give. Detailed usage for the gem can be found on the wiki. Technical documentation for the gem can be found on rubydoc.

Share This