• Stars
    star
    3,351
  • Rank 13,365 (Top 0.3 %)
  • Language
    Ruby
  • License
    MIT License
  • Created over 11 years ago
  • Updated 8 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Interactor provides a common interface for performing complex user interactions.

Interactor

Gem Version Build Status Maintainability Test Coverage Ruby Style Guide

Getting Started

Add Interactor to your Gemfile and bundle install.

gem "interactor", "~> 3.0"

What is an Interactor?

An interactor is a simple, single-purpose object.

Interactors are used to encapsulate your application's business logic. Each interactor represents one thing that your application does.

Context

An interactor is given a context. The 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

As an interactor runs it can add information to the context.

context.user = user

Failing the Context

When something goes wrong in your interactor, you can flag the context as failed.

context.fail!

When given a hash argument, the fail! method can also update the context. The following are equivalent:

context.error = "Boom!"
context.fail!
context.fail!(error: "Boom!")

You can ask a context if it's a failure:

context.failure? # => false
context.fail!
context.failure? # => true

or if it's a success.

context.success? # => true
context.fail!
context.success? # => false

Dealing with Failure

context.fail! always throws an exception of type Interactor::Failure.

Normally, however, these exceptions are not seen. In the recommended usage, the controller invokes the interactor using the class method call, then checks the success? method of the context.

This works because the call class method swallows exceptions. When unit testing an interactor, if calling custom business logic methods directly and bypassing call, be aware that fail! will generate such exceptions.

See Interactors in the Controller, below, for the recommended usage of call and success?.

Hooks

Before Hooks

Sometimes an interactor needs to prepare its context before the interactor is even run. This can be done with before hooks on the interactor.

before do
  context.emails_sent = 0
end

A symbol argument can also be given, rather than a block.

before :zero_emails_sent

def zero_emails_sent
  context.emails_sent = 0
end

After Hooks

Interactors can also perform teardown operations after the interactor instance is run.

after do
  context.user.reload
end

NB: After hooks are only run on success. If the fail! method is called, the interactor's after hooks are not run.

Around Hooks

You can also define around hooks in the same way as before or after hooks, using either a block or a symbol method name. The difference is that an around block or method accepts a single argument. Invoking the call method on that argument will continue invocation of the interactor. For example, with a block:

around do |interactor|
  context.start_time = Time.now
  interactor.call
  context.finish_time = Time.now
end

With a method:

around :time_execution

def time_execution(interactor)
  context.start_time = Time.now
  interactor.call
  context.finish_time = Time.now
end

NB: If the fail! method is called, all of the interactor's around hooks cease execution, and no code after interactor.call will be run.

Hook Sequence

Before hooks are invoked in the order in which they were defined while after hooks are invoked in the opposite order. Around hooks are invoked outside of any defined before and after hooks. For example:

around do |interactor|
  puts "around before 1"
  interactor.call
  puts "around after 1"
end

around do |interactor|
  puts "around before 2"
  interactor.call
  puts "around after 2"
end

before do
  puts "before 1"
end

before do
  puts "before 2"
end

after do
  puts "after 1"
end

after do
  puts "after 2"
end

will output:

around before 1
around before 2
before 1
before 2
after 2
after 1
around after 2
around after 1

Interactor Concerns

An interactor can define multiple before/after hooks, allowing common hooks to be extracted into interactor concerns.

module InteractorTimer
  extend ActiveSupport::Concern

  included do
    around do |interactor|
      context.start_time = Time.now
      interactor.call
      context.finish_time = Time.now
    end
  end
end

An Example Interactor

Your application could use an interactor to authenticate a user.

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

To define an interactor, simply create a class that includes the Interactor module and give it a call instance method. The interactor can access its context from within call.

Interactors in the Controller

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:

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

  private

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

The call class method is the proper way to invoke an interactor. The hash argument is converted to the interactor instance's context. The call instance method is invoked along with any hooks that the interactor might define. Finally, the context (along with any changes made to it) is returned.

When to Use an Interactor

Given the user authentication example, your controller may look like:

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

  private

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

For such a simple use case, using an interactor can actually require more code. So why use an interactor?

Clarity

We often use interactors right off the bat for all of our destructive actions (POST, PUT and DELETE requests) and since we put our interactors in app/interactors, a glance at that directory gives any developer a quick understanding of everything the application does.

β–Ύ app/
  β–Έ controllers/
  β–Έ helpers/
  β–Ύ interactors/
      authenticate_user.rb
      cancel_account.rb
      publish_post.rb
      register_user.rb
      remove_post.rb
  β–Έ mailers/
  β–Έ models/
  β–Έ views/

TIP: Name your interactors after your business logic, not your implementation. CancelAccount will serve you better than DestroyUser as the account cancellation interaction takes on more responsibility in the future.

The Futureβ„’

SPOILER ALERT: Your use case won't stay so simple.

In our experience, a simple task like authenticating a user will eventually take on multiple responsibilities:

  • Welcoming back a user who hadn't logged in for a while
  • Prompting a user to update his or her password
  • Locking out a user in the case of too many failed attempts
  • Sending the lock-out email notification

The list goes on, and as that list grows, so does your controller. This is how fat controllers are born.

If instead you use an interactor right away, as responsibilities are added, your controller (and its tests) change very little or not at all. Choosing the right kind of interactor can also prevent simply shifting those added responsibilities to the interactor.

Kinds of Interactors

There are two kinds of interactors built into the Interactor library: basic interactors and organizers.

Interactors

A basic interactor is a class that includes Interactor and defines call.

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

Basic interactors are the building blocks. They are your application's single-purpose units of work.

Organizers

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

class PlaceOrder
  include Interactor::Organizer

  organize CreateOrder, ChargeCard, SendThankYou
end

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

class OrdersController < ApplicationController
  def create
    result = PlaceOrder.call(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.

Rollback

If any one of the organized interactors fails its context, the organizer stops. If the ChargeCard interactor fails, SendThankYou is never called.

In addition, any interactors that had already run are given the chance to undo themselves, in reverse order. Simply define the rollback method on your interactors:

class CreateOrder
  include Interactor

  def call
    order = Order.create(order_params)

    if order.persisted?
      context.order = order
    else
      context.fail!
    end
  end

  def rollback
    context.order.destroy
  end
end

NOTE: The interactor that fails is not rolled back. Because every interactor should have a single purpose, there should be no need to clean up after any failed interactor.

Testing Interactors

When written correctly, an interactor is easy to test because it only does one thing. Take the following interactor:

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

You can test just this interactor's single purpose and how it affects the context.

describe AuthenticateUser do
  subject(:context) { AuthenticateUser.call(email: "[email protected]", password: "secret") }

  describe ".call" do
    context "when given valid credentials" do
      let(:user) { double(:user, secret_token: "token") }

      before do
        allow(User).to receive(:authenticate).with("[email protected]", "secret").and_return(user)
      end

      it "succeeds" do
        expect(context).to be_a_success
      end

      it "provides the user" do
        expect(context.user).to eq(user)
      end

      it "provides the user's secret token" do
        expect(context.token).to eq("token")
      end
    end

    context "when given invalid credentials" do
      before do
        allow(User).to receive(:authenticate).with("[email protected]", "secret").and_return(nil)
      end

      it "fails" do
        expect(context).to be_a_failure
      end

      it "provides a failure message" do
        expect(context.message).to be_present
      end
    end
  end
end

We use RSpec but the same approach applies to any testing framework.

Isolation

You may notice that we stub User.authenticate in our test rather than creating users in the database. That's because our purpose in spec/interactors/authenticate_user_spec.rb is to test just the AuthenticateUser interactor. The User.authenticate method is put through its own paces in spec/models/user_spec.rb.

It's a good idea to define your own interfaces to your models. Doing so makes it easy to draw a line between which responsibilities belong to the interactor and which to the model. The User.authenticate method is a good, clear line. Imagine the interactor otherwise:

class AuthenticateUser
  include Interactor

  def call
    user = User.where(email: context.email).first

    # Yuck!
    if user && BCrypt::Password.new(user.password_digest) == context.password
      context.user = user
    else
      context.fail!(message: "authenticate_user.failure")
    end
  end
end

It would be very difficult to test this interactor in isolation and even if you did, as soon as you change your ORM or your encryption algorithm (both model concerns), your interactors (business concerns) break.

Draw clear lines.

Integration

While it's important to test your interactors in isolation, it's just as important to write good integration or acceptance tests.

One of the pitfalls of testing in isolation is that when you stub a method, you could be hiding the fact that the method is broken, has changed or doesn't even exist.

When you write full-stack tests that tie all of the pieces together, you can be sure that your application's individual pieces are working together as expected. That becomes even more important when you add a new layer to your code like interactors.

TIP: If you track your test coverage, try for 100% coverage before integrations tests. Then keep writing integration tests until you sleep well at night.

Controllers

One of the advantages of using interactors is how much they simplify controllers and their tests. Because you're testing your interactors thoroughly in isolation as well as in integration tests (right?), you can remove your business logic from your controller tests.

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

  private

  def session_params
    params.require(:session).permit(:email, :password)
  end
end
describe SessionsController do
  describe "#create" do
    before do
      expect(AuthenticateUser).to receive(:call).once.with(email: "[email protected]", password: "secret").and_return(context)
    end

    context "when successful" do
      let(:user) { double(:user, id: 1) }
      let(:context) { double(:context, success?: true, user: user, token: "token") }

      it "saves the user's secret token in the session" do
        expect {
          post :create, session: { email: "[email protected]", password: "secret" }
        }.to change {
          session[:user_token]
        }.from(nil).to("token")
      end

      it "redirects to the homepage" do
        response = post :create, session: { email: "[email protected]", password: "secret" }

        expect(response).to redirect_to(user_path(user))
      end
    end

    context "when unsuccessful" do
      let(:context) { double(:context, success?: false, message: "message") }

      it "sets a flash message" do
        expect {
          post :create, session: { email: "[email protected]", password: "secret" }
        }.to change {
          flash[:message]
        }.from(nil).to(I18n.translate("message"))
      end

      it "renders the login form" do
        response = post :create, session: { email: "[email protected]", password: "secret" }

        expect(response).to render_template(:new)
      end
    end
  end
end

This controller test will have to change very little during the life of the application because all of the magic happens in the interactor.

Rails

We love Rails, and we use Interactor with Rails. We put our interactors in app/interactors and we name them as verbs:

  • AddProductToCart
  • AuthenticateUser
  • PlaceOrder
  • RegisterUser
  • RemoveProductFromCart

See: Interactor Rails

Contributions

Interactor is open source and contributions from the community are encouraged! No contribution is too small.

See Interactor's contribution guidelines for more information.

Thank You

A very special thank you to Attila Domokos for his fantastic work on LightService. Interactor is inspired heavily by the concepts put to code by Attila.

Interactor was born from a desire for a slightly simplified interface. We understand that this is a matter of personal preference, so please take a look at LightService as well!

More Repositories

1

audited

Audited (formerly acts_as_audited) is an ORM extension that logs all changes to your Rails models.
Ruby
3,367
star
2

awesome_nested_set

An awesome replacement for acts_as_nested_set and better_nested_set.
Ruby
2,385
star
3

json_spec

Easily handle JSON in RSpec and Cucumber
Ruby
917
star
4

interactor-rails

Interactor Rails provides Rails support for the Interactor gem.
Ruby
436
star
5

delayed_job_active_record

ActiveRecord backend integration for DelayedJob 3.0+
Ruby
346
star
6

graticule

Graticule is a geocoding API for looking up address coordinates and performing distance calculations, supporting many popular APIs.
Ruby
299
star
7

tinder

Tinder is a Ruby API for interfacing with Campfire, the 37Signals chat application.
Ruby
258
star
8

inside_the_machine

HTML
255
star
9

acts_as_geocodable

Simple geocoding for Active Record models
Ruby
206
star
10

delayed_job_mongoid

Mongoid backend for delayed_job
Ruby
170
star
11

action_mailer_optional_tls

Enables TLS on SMTP connections (for services like GMail)
Ruby
127
star
12

command

Command provides a simple object-oriented interface for running shell commands from Ruby.
Ruby
94
star
13

sunspot_test

Auto-starts solr for your tests
Ruby
54
star
14

devise-mongo_mapper

MongoMapper ORM for Devise
Ruby
48
star
15

awesomeness

Collective Idea's Awesomeness. A collection of useful Rails bits and pieces.
Ruby
47
star
16

unicode_math

Fun Ruby extensions for doing math with Unicode
Ruby
47
star
17

calendar_builder

Ruby
45
star
18

acts_as_money

an Active Record plugin that makes it easier to work with the money gem
Ruby
45
star
19

caldav

A Ruby CalDAV client
Ruby
39
star
20

awesome-backup

Rails plugin that provides Rake and Capistrano tasks for making database backups
Ruby
32
star
21

merger

A gem for merging Active Record models
Ruby
30
star
22

html5

A Rails plugin for playing around with HTML5.
Ruby
29
star
23

with_action

A respond_to style helper for doing different actions based on which request parameters are passed.
Ruby
24
star
24

ARPlaneDetector

Use ARKit to find, and visualize horizontal and vertical planes.
Swift
23
star
25

CollectionGraph

Beautiful graphs made with UICollectionViews
Swift
23
star
26

rubymotion-parsedotcom-chat

A simple chat application written with RubyMotion and using Parse.com
Ruby
19
star
27

delayed_job_data_mapper

DataMapper backend for delayed_job
Ruby
16
star
28

deploy_and_deliver

Capistrano recipes for Pivotal Tracker. Can mark stories delivered on deploy.
Ruby
15
star
29

random_finders

A Rails plugin that allows quick and easy fetching of random records, or records in random order.
Ruby
14
star
30

searchparty

Simple site that aggregates Google, Delicious, and GitHub searches. See http://search.collectiveidea.com
JavaScript
13
star
31

pivotal_tracker

ActiveResource wrapper around v3 of the Pivotal Tracker API
Ruby
13
star
32

unifi

A ruby client for the (undocumented) Unifi AP Controller API
Ruby
12
star
33

measurement

A sweet Ruby measurement & converstion Library.
Ruby
12
star
34

clear_empty_attributes

When Active Record objects are saved from a form, empty fields are saved as empty strings instead of nil. This kills most validations.
Ruby
11
star
35

travis_bundle_cache

DEPRECATED: Cache the gem bundle for speedy travis builds (Travis now handles this)
Ruby
10
star
36

twirp-kmm

Twirp service generator and runtime for Kotlin Multiplatform projects.
Kotlin
10
star
37

seinfeld.sh

Show the date of your last commit when logging into a terminal according to Calendar About Nothing
Shell
10
star
38

fireside

Open Source Chat
Ruby
10
star
39

bender

Ruby
9
star
40

buildlight

Aggregating webhooks from multiple build services (Travis, Circle, Heroku) to power the stoplight in our office.
Ruby
9
star
41

tweet-wall

A Twitter visualizer
JavaScript
8
star
42

yardvote.com

Tracking political yard signs.
Ruby
7
star
43

chat

An example chat application
Ruby
7
star
44

simple-maps

Add Google Maps in seconds. Maps any and all hCards on the page and resizes the map to fit. Uses the new Google Maps JavaScript API v3. It will even do the geocoding for you!
JavaScript
7
star
45

metrics

A tool for tracking arbitrary metrics via Slack
Ruby
6
star
46

capybara-ui

Page objects for readable, resilient acceptance tests
Ruby
6
star
47

rubymotion-drawing

Quick and dirty drawing app written with RubyMotion
Ruby
6
star
48

rails_relay_authentication

Ruby
5
star
49

statsite-instrumental

Statsite Sink for Instrumental
Go
5
star
50

audit-demo

Demo application for acts_as_audited
5
star
51

migration_test_helper

Ruby
5
star
52

hourglass

Client and internal time management with Harvest
Ruby
5
star
53

acts_as_audited

acts_as_audited is now… Audited.
5
star
54

imap_authenticatable

Authenticate your Rails app using any IMAP server!
Ruby
5
star
55

rosie

CoffeeScript
4
star
56

playing-with-sunspot

A demo of sunspot and solr
Ruby
4
star
57

perfecttime

Mike West's PerfectTime.js, with a few updates.
JavaScript
4
star
58

ember-helpers

A collection of handlebars helpers for Ember.js
JavaScript
4
star
59

git-access

Secure access to hosted git repositories
Go
4
star
60

pivotalharvest

Ruby
4
star
61

pgbackups_archive_app

A very simple app to archive PGBackups to S3.
Ruby
4
star
62

recursable

Some helpers for using trees in Rails
Ruby
3
star
63

integrity-nabaztag

An Integrity notifier for Nabaztag
Ruby
3
star
64

adapter-simpledb

An adapter for Amazon's SimpleDB
Ruby
3
star
65

data-loss

Tracking when things happened.
Ruby
3
star
66

eharbor

Example Rails application for Ruby on Rails training used by Collective Idea and Idea Foundry (http://ideafoundry.info/ruby-on-rails). Note: we often use interactive rebases, so commit dates may not make sense.
Ruby
3
star
67

ios-swifter-example

Using Swifter to stub network calls
Swift
3
star
68

test-kitchen-provisioners

A collection of test-kitchen provisioners (https://github.com/opscode/test-kitchen)
Ruby
2
star
69

protoc-gen-twirp_ruby

A protoc plugin, written in Ruby, for generating Twirp-Ruby clients and/or services.
Ruby
2
star
70

twitter

A Twitter clone used for training
Ruby
2
star
71

collective-data

Gem for pulling data from the CollectiveData website
Ruby
2
star
72

get-to-work-go

Start and stop project specific, annotated Harvest timers with information from Pivotal Tracker
Go
2
star
73

dm-mongo-adapter

2
star
74

example_of_sunspot_test

A rails 3 codebase showing the usage of sunspot_test
Ruby
2
star
75

over_board

Ruby
2
star
76

namecase

Ruby
2
star
77

jquery.defaulttext

jQuery plugin to set the default text of an input using the title element
JavaScript
2
star
78

sunspot-indexing-strategies

A Rails 3 codebase that uses Sunspot to show how you can search search within words.
Ruby
2
star
79

mephisto-comment-notification

A simple comment notification plugin for the Mephisto blogging engine.
Ruby
2
star
80

mintastic

A Mint to QIF converter that also functions as a Rails + React + Redux tutorial app
Ruby
2
star
81

react-ujs-rails

React UJS package for rails, extracted from react-rails
JavaScript
1
star
82

mephisto_atompub

AtomPub plugin for mephisto
Ruby
1
star
83

donuts-android

Android client for donuts.collectiveidea.com
Kotlin
1
star
84

cucumber-web

Reusable web steps for Cucumber and Capybara
Ruby
1
star
85

cucumber_slices

Cucumber steps that we use often
Ruby
1
star
86

laketown

Calculate what you could save on internet service in Laketown Township
HTML
1
star
87

donuts

Collective Idea Donut Tracker
Ruby
1
star
88

donuts-ios

iOS Client for donuts.collectiveidea.com
Swift
1
star
89

build-test

Stupid simple way to test a continuous integration build server.
Shell
1
star
90

rubocop-config

Working toward a rubocop config we all can agree on.
1
star
91

no-phone

Not a phone.
Ruby
1
star
92

get_to_work

A rubygem to tag your Harvest time entries with Pivotal Tracker Information
Ruby
1
star
93

delayed_job_example

An example app for using Delayed Job
Ruby
1
star
94

finish_weekend

Finish Weekend
Ruby
1
star
95

twirp-rails

Make serving a Twirp RPC Services as easy and familiar as Rails controllers.
Ruby
1
star