CivilService is a tiny framework for service objects in Rails apps. With CivilService, you can use ActiveModel validations to do pre-flight checks before the service runs, and create your own result object classes to capture the results of complex operations.
CivilService was extracted from Intercode, a web app for convention organizers and participants. It is now maintained by ActBlue.
Add this line to your application's Gemfile:
And then execute:
Or install it yourself as:
$ gem install civil_service
What CivilService does
CivilService::Service is really a pretty tiny class. It does, however, have some opinions that create a potentially-useful abstraction for app developers:
- When called, services always return a result object that responds to (at least)
#exception. This lets your code paths that call services be consistent and simple. (If you want to return more information as a result of running the service, it's easy to define a custom result class for your service.)
- What's the difference between
#errorsis an instance of
#exceptionis an instance of an exception class (it's only present if an exception was raised inside the service call). If an exception is raised, the service result will respond true to
#failure?, false to
#success?, and the exception's message will be added to
#errors, so most of the time you can ignore
#exception- but it's there in case you need to dig into the details.
- What's the difference between
- Services include
ActiveModel::Validationsso they can easily do pre-flight checks. That means you can call
my_service.errorsjust like you can for a model, and it also means that the service will fail if it's not valid.
- In addition to
#call, which always returns a result object, services have a
#call!method, which will raise a
CivilService::ServiceFailureexception if the service fails, or pass through an exception if one is raised inside the service call. This might be easier in some workflows; for example, it will cause a rollback if used inside an ActiveRecord transaction block.
- Finally, there's a third variant:
#call_and_raise, which will re-raise any exceptions that occurred during the service execution, but will return a regular result object on failure.
Here's a simple service that changes a user's password in a hypothetical Rails app, and sends a notification email about it:
class PasswordChangeService < :: validate :ensure_valid_password attr_reader :user, :new_password def initialize(user:, new_password:) @user = user @new_password = new_password end private def inner_call user.update!(password: new_password) UserMailer.password_changed(user).deliver_later success end def ensure_valid_password return if new_password.length >= 8 errors.add(:base, "Passwords must be at least 8 characters long") end end
You might call this from a controller action like this:
class UsersController < ApplicationController def change_password service = PasswordChangeService.new(user: current_user, new_password: params[:password]) result = service.call if result.success? redirect_to root_url, notice: "Your password has been changed." else flash[:alert] = result.errors..join(', ') end end end
After checking out the repo, run
bin/setup to install dependencies. Then, run
rake test 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.
Bug reports and pull requests are welcome on GitHub at https://github.com/actblue/civil_service.