service_actor

Service objects for your application logic


Keywords
ruby-on-rails, service-objects
License
MIT
Install
gem install service_actor -v 2.0.0

Documentation

Actor

Tests

Ruby service objects. Lets you move your application logic into small building blocs to keep your controllers and your models thin.

Installation

Add these lines to your application's Gemfile:

# Service objects to keep the business logic
gem 'service_actor'

Usage

Actors are single-purpose actions in your application that represent your business logic. They start with a verb, inherit from Actor and implement a call method.

# app/actors/send_notification.rb
class SendNotification < Actor
  def call
    #
  end
end

Use .call to use them in your application:

SendNotification.call

Inputs

Actors can accept arguments with input:

class GreetUser < Actor
  input :user

  def call
    puts "Hello #{user.name}!"
  end
end

And receive them as arguments to call:

GreetUser.call(user: User.first)

Outputs

Use output to declare what your actor can return, then assign them to your context.

class BuildGreeting < Actor
  output :greeting

  def call
    context.greeting = "Have a wonderful day!"
  end
end

Calling an actor returns a context:

result = BuildGreeting.call
result.greeting # => "Have a wonderful day!"

Defaults

Inputs can have defaults:

class BuildGreeting < Actor
  input :adjective, default: "wonderful"
  input :length_of_time, default: -> { ["day", "week", "month"].sample }

  output :greeting

  def call
    context.greeting = "Have a #{adjective} #{length_of_time}!"
  end
end

This lets you call the actor without specifying those keys:

BuildGreeting.call.greeting # => "Have a wonderful week!"

Types

Inputs can define a type, or an array of possible types it must match:

class UpdateUser < Actor
  input :user, type: 'User'
  input :age, type: %w[Integer Float]

  #
end

Required

To check that an input must not be nil, flag it as required.

class UpdateUser < Actor
  input :user, required: true

  #
end

Conditions

You can also add conditions that the inputs must verify, with the name of your choice under must:

class UpdateAdminUser < Actor
  input :user,
        must: {
          be_an_admin: ->(user) { user.admin? }
        }
end

Result

All actors are successful by default. To stop the execution and mark an actor as having failed, use fail!:

class UpdateUser
  input :user
  input :attributes

  def call
    user.attributes = attributes

    fail!(error: "Invalid user") unless user.valid?

    #
  end
end

This will raise an error in your app.

To test for the success instead of raising, you can use .result instead of .call. For example in a Rails controller:

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def create
    result = UpdateUser.result(user: user, attributes: user_attributes)
    if result.success?
      redirect_to result.user
    else
      render :new, notice: result.error
    end
  end
end

Play

An actor can call actors in sequence by using play. Each actor will hand over the same context to the next actor.

class PlaceOrder < Actor
  play CreateOrder,
       Pay,
       SendOrderConfirmation,
       NotifyAdmins
end

Rollback

When using play, if one of the actors calls fail!, the following actors will not be called.

Also, all the previous actors that succeeded will have their rollback method triggered. For example:

class CreateOrder < Actor
  def call
    context.order = Order.create!(…)
  end

  def rollback
    context.order.destroy
  end
end

Early success

When using play you can use succeed! to stop the execution of the following actors, but still consider the actor to be successful.

Lambdas

You can use inline actions using lambdas:

class Pay
  play ->(ctx) { ctx.payment_provider = "stripe" },
       CreatePayment,
       ->(ctx) { ctx.user_to_notify = ctx.payment.user },
       SendNotification
end

Before, after and around

To do actions before or after actors, use lambdas or simply override call and use super. For example:

class Pay
  #

  def call
    Time.with_timezone('Paris') do
      super
    end
  end
end

Play conditions

Some actors in a play can be called conditionaly:

class PlaceOrder < Actor
  play CreateOrder,
       Pay
  play NotifyAdmins, if: ->(ctx) { ctx.order.amount > 42 }
end

Influences

This gem is heavily influenced by Interactor ♥. However there a a few key differences which make actor unique:

  • Defaults to raising errors on failures. Actor uses call and result instead of call! and call. This way, the default is to raise an error and failures are not hidden away.
  • Does not hide errors when an actor fails inside another actor.
  • You can use lambdas inside organizers.
  • Requires you to document the arguments with input and output.
  • Type checking of inputs and outputs.
  • Inputs and outputs can be required.
  • Defaults for inputs.
  • Conditions on inputs.
  • Shorter fail syntax: fail! vs context.fail!.
  • Trigger early success in organisers with succeed!.
  • Shorter setup syntax: inherit from < Actor vs having to include Interactor or include Interactor::Organizer.
  • Multiple organizers.
  • Conditions inside organizers.
  • No before, after and around hooks. Prefer simply overriding call with super which allows wrapping the whole method.
  • Fixes issues with OpenStruct

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/sunny/actor. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant 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 Test project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.