Skip to content

Getting Started with Interactify

Mark Burns edited this page Jan 3, 2024 · 2 revisions

1. Introduction

Interactify is a Ruby gem designed to enhance Rails applications by simplifying complex business logic.

It extends interactors with additional features like conditional execution and asynchronous processing, making your codebase easier to get an overview of.

2. Installation

To get started with Interactify, add it to your Gemfile and run the bundle install command:

gem 'interactify'

Then, execute:

bundle install

3. Basic Setup

Begin by creating a new Interactifyd Class.

class CreateUser
  include Interactify

  def call
    context.user = User.create(email: context.email)
  end
end

4. Using the DSL

Interactify's DSL builds on top of interactor and interactor-contracts and allows you to define complex workflows. Here's a basic example:

class SignupUser
  include Interactify

  organize CreateUser, SendAddressConfirmationEmail, AssignOrderToUser, SetUserDashboardPath
end

class SendAddressConfirmationEmail
  include Interactify

  def call
    EmailProvider.send_confirmation(email: context.email_address)
  end
end

This organizes CreateUser SendAddressConfirmationEmail and AssignOrderToUser, and SetUserDashboardPath into a single workflow.

5. Testing

RSpec.describe SignupUser do
  let(:result) { CreateUser.call(params) }  
  let(:params) { { email: email } }
  let(:email) { '[email protected]' }=

  it 'creates a user' do
    expect(result).to be_success
    expect(result.user).to be_a User
    expect(result.user.email).to eq email
  end
end

6. Running

To execute your interactor, simply call:

def create
  @signup = SignupUser.call(params)

  if @signup.success?
    redirect_to @signup.user_dashboard_path
  else
    render :new
  end
end

7. Adding contracts

This works for creating a user, but it falls over when we try to send the email as the email address is nil. Why is that if we passed it in in the params?

If you followed closely you would have spotted that we never set the email_address on the context. We pass in email instead.

Let's stop this kind of thing from happening in future.

Add an expectation to the organizer and the interactors.

class SignupUpser
  include Interactify

  expect :email

  organize CreateUser, SendAddressConfirmationEmail, AssignOrderToUser, SetUserDashboardPath
end

class CreateUser
  include Interactify

  expect :email

  def call
    context.user = User.create(email: context.email)
  end
end

class SendAddressConfirmationEmail
  include Interactify
  expect :email

  def call
    EmailProvider.send_confirmation(email: context.email)
  end
end

Notice we now call context.email in both interactors? However we can do better than this. As an Interactor::Context wraps an OpenStruct it means any method call whatsoever is valid on context.

Interactify provides automatic delegation of any expected keys in the class. This means we can rewrite like this

class CreateUser
  include Interactify

  expect :email

  def call
    context.user = User.create(email: email)
  end
end

class SendAddressConfirmationEmail
  include Interactify
  expect :email

  def call
    EmailProvider.send_confirmation(email: email)
  end
end

OK so now if we had have put the incorrect method in there it would blow up immediately with a

NoMethodError email_address for SendAddressConfirmationEmail

And we can go a step further using the latest ruby syntax and omit the values in the keyword arguments:

class CreateUser
  include Interactify

  expect :email

  def call
    context.user = User.create(email:)
  end
end

class SendAddressConfirmationEmail
  include Interactify
  expect :email

  def call
    EmailProvider.send_confirmation(email:)
  end
end

Much simpler and more robust.

Now let's validate our contract chains. You will only need to add this once to your test suite and it will validate every interactor in the system going forwards.

RSpec.describe 'InteractorWiring' do
  it 'validates the interactors in the whole app', :aggregate_failures do
    errors = Interactify.validate_app(ignore: [/SomeClassName/, AnotherClass, 'SomeClassNameString'])

    expect(errors).to eq ''
  end
end

now if had have added an expect :email_address to the SendAddressConfirmationEmail, the test above would fail like this:

    Missing keys: :email_address
              in: SendAddressConfirmationEmail
       called by: SignupUser
Clone this wiki locally