Excom
Flexible and highly extensible Service Objects for business logic organization.
Installation
Add this line to your application's Gemfile:
gem 'excom'
And then execute:
$ bundle
Or install it yourself as:
$ gem install excom
Preface
Excom
stands for Exexcutable Comand. Initially, Excom::Command
was the main
class, provided by the gem. But it seems that "Service" name become more popular and
common for describing classes for business logic, so it was renamed in this gem too.
Usage
General idea behind every excom
service is simple: each service can have arguments,
options (named arguments), and should define execute!
method that is called during
service execution. Executed service has status
and result
.
The very basic usage of Excom
services can be shown with following example:
# app/services/todos/update.rb
module Todos
class Update < Excom::Service
# `use` class method adds a plugin to a service with specified options
use :status, success: [:ok], failure: [:unprocessable_entity]
args :todo
opts :params
def execute!
if todo.update(params)
ok todo.as_json
else
unprocessable_entity todo.errors
end
end
end
end
# app/controllers/todos/controller
class TodosController < ApplicationController
def update
service = Todos::Update.(todo, params: todo_params)
render json: todo.result, status: service.status
end
end
However, even this basic example can be highly optimized by using Excom extensions and helper methods.
Service arguments and options
Read full version on wiki.
Excom services can be initialized with arguments and options (named arguments). To specify list
of available arguments and options, use args
and opts
class methods. All arguments and options
are optional during service initialization. However, you cannot pass more arguments to service or
options that were not declared with opts
method.
class MyService < Excom::Service
args :foo
opts :bar
def execute!
# do something
end
def foo
super || 5
end
end
s1 = MyService.new
s1.foo # => 5
s1.bar # => nil
s2 = s1.with_args(1).with_opts(bar: 2)
s2.foo # => 1
s2.bar # => 2
Service Execution
Read full version on wiki.
At the core of each service's execution lies execute!
method. By default, you can use
success
, failure
and result
methods to set execution status and result. If none
were used, result and status will be set based on execute!
method's return value.
Example:
class MyService < Excom::Service
args :foo
def execute!
if foo > 2
success { foo * 2 }
else
failure { -1 }
end
end
end
service = MyService.new(3)
service.execute.success? # => true
service.result # => 6
Core API
Please read about core API and available class and instance methods on wiki
Service Extensions (Plugins)
Read full version on wiki.
Excom is built with extensions in mind. Even core functionality is organized in plugins that are
used in base Excom::Service
class. Bellow you can see a list of plugins with some description
and examples that are shipped with excom
:
:status
- Addsstatus
execution state property to the service, as well as helper methods and behavior to set it.status
property is not bound to the "success" flag of execution state and can have any value depending on your needs. It is up to you to setup which statuses correspond to successful execution and which are not. Generated status helper methods allow to atomically and more explicitly assign both status and result at the same time:
class Posts::Update < Excom::Service
use :status,
success: [:ok],
failure: [:unprocessable_entity]
args :post, :params
def execute!
if post.update(params)
ok post.as_json
else
unprocessable_entity post.errors
end
end
end
service = Posts::Update.(post, post_params)
# in case params were valid you will have:
service.success? # => true
service.status # => :ok
service.result # => {'id' => 1, ...}
Note that unlike success
, failure
, or result
methods, status helpers accept result value
as its argument rather than yield to a block to get it.
:context
- Allows you to set an execution context for a block that will be available to any service that uses this plugin viacontext
method.
# application_controller.rb
around_action :with_context
def with_context
Excom.with_context(current_user: current_user) do
yield
end
end
class Posts::Archive < Excom::Service
use :context
args :post
def execute!
post.update(archived: true, archived_by: context[:current_user])
end
end
:sentry
- Allows you to define sentry logic that will allow or deny service's execution or other related checks. This logic can be defined inline in service classes or in dedicated Sentry classes. Much like pundit Policies, but more. Where pundit governs only authorization logic, Excom's Sentries can deny execution with any reason you find appropriate.
class Posts::Destroy < Excom::Service
use :context
use :sentry
args :post
def execute!
post.destroy
end
sentry delegate: [:context] do
deny_with :unauthorized
def execute?
# only author can destroy a post
post.author_id == context[:current_user].id
end
deny_with :unprocessable_entity do
def execute?
# disallow to destroy posts that are older than 1 hour
(post.created_at + 1.hour).past?
end
end
end
end
:assertions
- Providesassert
method that can be used for different logic checks during service execution.:failure_cause
- A small helper plugin that can be used to more explicit access to cause of service failure. You can use it if you feel that failed service shouldn't have a result, but a cause of the failure instead. Example:
class Posts::Create < Excom::Service
use :status, success: [:ok], failure: [:unprocessable_entity]
use :failure_cause, cause_method_name: :errors
args :params
def execute!
if post.save
ok post.as_json
else
unprocessable_entity post.errors
end
end
private def post
@post ||= Post.new(params)
end
end
service = Posts::Create.(title: 'invalid')
service.success? # => false
service.result # => nil
service.errors # => {title: ["is invalid"]}
:dry_types
- Allows you to use dry-types attributes instead of defaultargs
andopts
.:caching
- Simple plugin that will prevent re-execution of service if it already has been executed, and will immediately return result.:rescue
- Provides:rescue
execution option. If set totrue
, any error occurred during service execution will not be raised outside.
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake spec
to run the tests.
You can also run bin/console
for an interactive prompt that will allow you to experiment.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/akuzko/excom.
License
The gem is available as open source under the terms of the MIT License.