On Github LimeBlast / interactors-good-bad-ugly
class SayHello < ActiveInteraction::Base
string :name
interface :serializer,
methods: %i[dump load]
object :cow # Cow class
#...
end
You start by defining your inputs, of which there are a large number of filters to choose from, including all the
ones you'd expect such Strings, Arrays, Booleans, etc.. as well as the ability to define an inputs that have specific
interfaces, or to be an instance of a particular class.class SayHello < ActiveInteraction::Base
string :name,
default: 'World',
desc: "Who you're saying hello to"
#...
end
And optionally, you've got the ability to set a default for each input, as well as a description which can be used
to generate documentation (although I've not looked at this aspect of it)class SayHello < ActiveInteraction::Base
string :name
validates :name,
presence: true
#...
end
Next, if you want some more control over the data which gets input, you've got the ability to set some able to use
ActiveModel validations. These are entirely optional.class SayHello < ActiveInteraction::Base
string :name
validates :name,
presence: true
def execute
"Hello, #{name}!" # or: "Hello, #{inputs[:name]}!"
end
end
Then finally you perform the logic that's required inside an execute method. All the inputs defined are available
as local variables, or as a hash named inputs.class FindAccount < ActiveInteraction::Base
integer :id
def execute
account = Account.not_deleted.find_by_id(id)
if account
account
else
errors.add(:id, 'does not exist')
end
end
end
If your logic is able to perform successfully, you simply return the result of the operation, but if something goes
wrong, you return an error object with a description of the problem.# GET /accounts/:id
def show
@account = find_account!
end
private
def find_account!
outcome = FindAccount.run(params)
if outcome.valid?
outcome.result
else
fail ActiveRecord::RecordNotFound, outcome.errors.full_messages.to_sentence
end
end
Once you've built your interactors you're able to run them in one of two ways: .run and .run!. Both have you pass in
your defined parameters as a hash, with the only different between the two being the outcome. .run returns an outcome
object which you're able to test for success, and react accordingly...SayHello.run!(name: nil) # ActiveInteraction::InvalidInteractionError: Name is required SayHello.run!(name: '') # ActiveInteraction::InvalidInteractionError: Name can't be blank SayHello.run!(name: 'Daniel') # => "Hello, Daniel!"and .run! does away with the outcome object, and either raises an exception on failure, or simply returns the value on success.
# GET /accounts/new
def new
@account = CreateAccount.new
end
# POST /accounts
def create
outcome = CreateAccount.run(params.fetch(:account, {}))
if outcome.valid?
redirect_to(outcome.result)
else
@account = outcome
render(:new)
end
end
ActiveInteraction plays nicely with rail's form_for (and by extension, simple_form and Formtastic) as the outcome
object returned from .new or .run quacks like an ActiveModel form object.<%= form_for @account, as: :account, url: accounts_path do |f| %> <%= f.text_field :first_name %> <%= f.text_field :last_name %> <%= f.submit 'Create' %> <% end %>Allowing for the form markup you're already used to.
class SayHello
include Interactor
def call
context.hello = "Hello, #{context.name}!"
end
end
result = SayHello.call({name: 'Daniel'})
result.hello
# => "Hello, Daniel!"
The most important aspect of Interactor is the context, which gets created from the values you pass in, and
manipulated accordingly by the business logic contained within. Unlike active_interactor, you don't define specific
inputs, so if you have specific input or validation requirements, you'll need to establish them elsewhere.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
When something goes wrong in your interactor, you can flag the context as failed....class SessionsController < ApplicationController
def create
result = AuthenticateUser.call(session_params)
if result.success?
session[:user_token] = result.token
redirect_to root_path
else
flash.now[:message] = t(result.message)
render :new
end
end
end
Which will automatically trigger the result object to have .success? and .failure? booleans accordingly.class PlaceOrder include Interactor::Organizer organize CreateOrder, ChargeCard, SendThankYou endAnd thanks to organisers, and the shared context which gets passed between them in the order defined, you're able to specify entire chains of events.
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
If any one of the organized interactors fails its context, the organizer stops, with any interactors that had
already run are given the chance to undo themselves, in reverse order, via a defined rollback method.