Interactor Ruby Library

repository·master·Indexed 25 days ago

https://github.com/collectiveidea/interactor

A Ruby library for encapsulating business logic into single-purpose objects. It provides a structured way to manage application logic, handle failures via Interactor::Context, and compose complex workflows using Interactor::Organizer. Features include before, after, and around hooks, as well as rollback functionality to reverse actions when a sequence of interactors fails.

Tokens
4.3K
Snippets
19
Records
25
Agent score
85%

What's inside interactor

  1. Create a basic Interactor

    master

    To define a basic interactor, create a class that includes the Interactor module and implement a call instance method. The interactor can access and modify its context within the call method.

    class AuthenticateUser
      include Interactor
    
      def call
        if user = User.authenticate(context.email, context.password)
          context.user = user
          context.token = user.secret_token
        else
          context.fail!(message: "authenticate_user.failure")
        end
      end
    end
  2. Best practices for Interactor design

    master

    To maintain clean boundaries and testability, follow these patterns:

    1. Draw clear lines: Define clear interfaces on your models (e.g., User.authenticate) so that the interactor doesn't need to know about ORM details or encryption algorithms. This prevents business logic from breaking when model concerns change.
    2. Naming: When using Rails, place interactors in app/interactors and name them using verbs (e.g., AddProductToCart, PlaceOrder, RegisterUser).
    3. Testing Strategy: Aim for 100% coverage in isolation tests before writing integration/acceptance tests to ensure all edge cases are covered.
  3. Test Interactors in isolation

    master

    Because an interactor performs a single responsibility, it can be tested in isolation by calling .call with specific input and asserting the state of the returned context. Use context.success? or context.failure? to verify the outcome, and inspect context.message for failure details. It is recommended to stub model dependencies to ensure you are testing the business logic of the interactor rather than the model's implementation.

    class AuthenticateUser
      include Interactor
    
      def call
        if user = User.authenticate(context.email, context.password)
          context.user = user
          context.token = user.secret_token
        else
          context.fail!(message: "authenticate_user.failure")
        end
      end
    end
    
    # Example RSpec test
    describe AuthenticateUser do
      subject(:context) { AuthenticateUser.call(email: "john@example.com", password: "secret") }
    
      it "succeeds" do
        expect(context).to be_a_success
      end
    
      it "provides the user" do
        expect(context.user).to eq(user)
      end
    end
  4. Simplify Controller tests using Interactors

    master

    Interactors allow you to move business logic out of controllers. This simplifies controller tests because you can mock the interactor's result rather than testing the underlying business rules. In your controller tests, you can expect the interactor to receive .call and return a double representing the context with specific success?, user, or token values.

    class SessionsController < ApplicationController
      def create
        result = AuthenticateUser.call(session_params)
    
        if result.success?
          session[:user_token] = result.token
          redirect_to result.user
        else
          flash.now[:message] = t(result.message)
          render :new
        
      end
    end
    
    # Controller test pattern
    describe SessionsController do
      describe "#create" do
        before do
          expect(AuthenticateUser).to receive(:call).once.with(email: "john@doe.com", password: "secret").and_return(context)
        end
    
        context "when successful" do
          let(:context) { double(:context, success?: true, user: user, token: "token") }
          # ... assertions on session and redirect
        end
      end
    end
  5. Invoke an Interactor from a Controller

    master

    The recommended way to run an interactor is via the .call class method. This method accepts a hash (which becomes the context), executes the interactor and its hooks, swallows any Interactor::Failure exceptions, and returns the context object.

    # Inside a controller action
    result = AuthenticateUser.call(session_params)
    
    if result.success?
      session[:user_token] = result.token
      redirect_to result.user
    else
      flash.now[:message] = t(result.message)
      render :new, status: :unprocessable_entity
    end
  6. Create an Interactor Organizer

    master

    Use Interactor::Organizer to group multiple interactors into a single execution sequence. Because Interactor::Organizer is a module, you should include it in your custom class rather than inheriting from it.

    Interactors declared via organize are executed in the order they are defined.

    class MyOrganizer
      include Interactor::Organizer
    
      organize InteractorOne, InteractorTwo
    end
  7. Manage Interactor Context

    master

    The context object holds the data required for the interactor to perform its work and stores the results.

    • Adding data: Use context.key = value to add information.
    • Failing: Use context.fail! to flag the context as failed. This throws an Interactor::Failure exception which is swallowed when using the .call class method.
    • Passing error data: context.fail!(error: "message") is equivalent to context.error = "message"; context.fail!.
    • Checking status: Use context.success? and context.failure? to check the outcome.
  8. Implement Rollback in Organizers

    master

    When an organizer is running and an interactor fails, the organizer stops and triggers a rollback method on all previously successful interactors in reverse order. Note that the interactor that actually failed is not rolled back.

    class CreateOrder
      include Interactor
    
      def call
        order = Order.create(order_params)
        if order.persisted?
          context.order = order
        else
          context.fail!
        
      end
    
      def rollback
        context.order.destroy
      end
    end
  9. Use Organizers to run multiple Interactors

    master

    An Interactor::Organizer is used to run a sequence of interactors. It passes the context from one interactor to the next. If any interactor in the sequence fails, the organizer stops immediately and does not run subsequent interactors.

    class PlaceOrder
      include Interactor::Organizer
    
      organize CreateOrder, ChargeCard, SendThankYou
    end
  10. Use Interactor Hooks (before, after, around)

    master

    Interactors support hooks to prepare context, perform teardown, or wrap execution.

    • before: Runs before the call method. Can take a block or a symbol for a method name. Used for setup.
    • after: Runs after the call method, but only on success. If fail! is called, after hooks are skipped. They run in reverse order of definition.
    • around: Wraps the execution. Must accept a single argument (the interactor) and call interactor.call to continue. If fail! is called, execution of the around block stops immediately after the call.
    # Example of around hook
    around do |interactor|
      context.start_time = Time.now
      interactor.call
      context.finish_time = Time.now
    end
  11. Handle interactor failures with Interactor::Failure

    master
    When an interactor fails, an Interactor::Failure error can be raised. This error class inherits from StandardError and carries the context that was active at the time of failure. You can rescue this error and access the context attribute to debug the state of the interactor when it failed.