On Github LimeBlast / interactors-good-bad-ugly
class SayHello < ActiveInteraction::Base string :name interface :serializer, methods: %i[dump load] object :cow # Cow class #... endYou 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" #... endAnd 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 #... endNext, 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 endThen 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 endIf 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 endOnce 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 endActiveInteraction 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 endWhen 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 endWhich 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 endIf 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.