ActAsInteractor

Simple and powerful interface for creating service objects in Ruby.

Installation

Add this line to your application's Gemfile:

gem 'act_as_interactor'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install act_as_interactor

Usage

Just include the interactor module in your service objects. For example if you have a service objects for creating blog posts in your app, then just include the AtcAsInteractor module in it and define the execute method inside it:

module Posts
  class Create
    include ActAsInteractor

    def execute(params)
      # steps to create a blog post
    end
  end
end

NOTE: the params argument of the execute is better to be a hash of parameters.

defining steps

For defining steps needed for your operation, just add simple ruby methods to your service objects and pass the params hash to it. Then, using destructuring the params hash, get the values needed for that specific step and ignore the rest.

module Posts
  class Create
    include ActAsInteractor

    def execute(params)
      yield create_post(params)
    end

    private

    def create_post(title:, body:, **)
      post = Post.create(title: title, body: body)

      return Failure(post.errors.messages) if post.invalid?

      Success(post)
    end
  end
end

NOTE: We use Failure and Success methods to wrap our steps results. Then we can use yield keyword to unwrap the output of the method. It also helps to halt the execution of the service objects if there is a Failure in execution of any of steps and returns the Failure object wrapping the real output.

Input validation

For validating the input, you only need to add a validator method to your service object that returns an object that responds to call method and receives a hash of parameters. If you define the validator method, then the validation process starts automatically before executing any step in the srevice object.

Adding validation using dry-schema:

module Posts
  class Create
    include ActAsInteractor

    def execute(params)
      yield create_post(params)
    end

    private

    def create_post(title:, body:, **)
      post = Post.create(title: title, body: body)

      return Failure(post.errors.messages) if post.invalid?

      Success(post)
    end

    def validator
      Dry::Schema.Params do
        required(:title).filled(:str?)
        required(:body).filled(:str?)
      end
    end
  end
end

You can also use more sophisticated validation tools like dry-validation:

module Posts
  module Contract
    class Create < Dry::Validation::Contract
      params do
        required(:title).filled(:str?)
        required(:body).filled(:str?)
        required(:author_email).filled(:str?)
      end

      rule(:author_email) do
        unless /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i.match?(value)
          key.failure('has invalid format')
        end
      end
    end
  end
end


module Posts
  class Create
    include ActAsInteractor

    def execute(params)
      yield create_post(params)
    end

    private

    def create_post(title:, body:, **)
      post = Post.create(title: title, body: body)

      return Failure(post.errors.messages) if post.invalid?

      Success(post)
    end

    def validator
      Posts::Contract::Create.new
    end
  end
end

Getting the result

As we've wrapped all the steps results in Failure or Success objects, then we have access to failure? and success? methods to see if the output of the serivce is successful or a failure. We also have failure and success methods to get the unwrapped output of the service objects:

outcome = Posts::Create.call(title: "Hello world") # => Success(#<Post ...>)
outcome.success? # => true
outcome.success # => #<Post ...>

But wait! There's also a more interesting way to get the service object outcome, just pass a block. In this case, I want to use it inside a Rails controller action for exapmle:

class PostsController
  def create
    Posts::Create.call(post_params.to_h) do |outcome|
      outcome.success do |post|
        render json: post, status: :ok
      end

      outcome.failure do |errors|
        render json: errors, status: :unprocessable_entity
      end
    end
  end
end

Better error handling (Railway Oriented)

Based on the monadic type of the result of your service objects using this library, you can easily handle your different failure paths in a pretty shape without using if-else statements or throwing different types of exceptions (read more about Railway Oriented Programming).

module Posts
  class Create
    include ActAsInteractor

    def execute(params)
      # ...
      yield check_title(params)
      # ...
    end

    private

    def create_title(title:, **)
      outcome = Posts::CheckTitle.call(title: title)

      return Failure(:inappropriate_title) if outcome.failure?

      return Success()
    end

    # ...
  end
end

Then in the caller method, you can check different failure paths:

class PostsController
  def create
    Posts::Create.call(post_params.to_h) do |outcome|
      # success path
      outcome.success do |post|
        render json: post, status: :ok
      end

      # inappropriate title failure path
      outcome.failure(:inappropriate_title) do |_errors|
        # do something like send a mail or notify admins etc.
        render json: { errors: { title: ["is inappropriate"] }}
      end

      # general failure path
      outcome.failure do |errors|
        render json: { errors: errors }, status: :unprocessable_entity
      end
    end
  end
end

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/arefaslani/act_as_interactor. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the ActAsInteractor project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.