Excom

Flexible and highly extensible Commands (Service Objects) for business logic.

build status github release

Installation

Add this line to your application's Gemfile:

gem 'excom'

And then execute:

$ bundle

Or install it yourself as:

$ gem install excom

Usage

General idea behind every excom command is simple: each command can have arguments, options (named arguments), and should define run method that is called during command execution. Executed command has status and result.

The very basic usage of Excom commands can be shown with following example:

# app/commands/todos/update.rb
module Todos
  class Update < Excom::Command
    args :todo
    opts :params

    def run
      if todo.update(params)
        result success: todo.as_json
      else
        result failure: todo.errors
      end
    end
  end
end

# app/controllers/todos/controller
class TodosController < ApplicationController
  def update
    command = Todos::Update.new(todo, params: todo_params)

    if command.execute.success?
      render json: todo.result
    else
      render json: todo.result, status: :unprocessable_entity
    end
  end
end

However, even this basic example can be highly optimized by using Excom extensions and helper methods.

Command arguments and options

Read full version on wiki.

Excom commands 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 command initialization. However, you cannot pass more arguments to command or options that were not declared with opts method.

class MyCommand < Excom::Command
  args :foo
  opts :bar

  def run
    # do something
  end

  def foo
    super || 5
  end
end

c1 = MyCommand.new
c1.foo # => 5
c1.bar # => nil

c2 = c1.with_args(1).with_opts(bar: 2)
c2.foo # => 1
c2.bar # => 2

Command Execution

Read full version on wiki.

At the core of each command's execution lies run method. You can use status and/or result methods to set execution status and result. If none were used, result and status will be set based on run method's return value.

Example:

class MyCommand < Excom::Command
  alias_success :ok
  args :foo

  def run
    if foo > 2
      result ok: foo * 2
    else
      result failure: -1
    end
  end
end

command = MyCommand.new(3)
command.execute.success? # => true
command.status # => :ok
command.result # => 6

Core API

Please read about core API and available class and instance methods on wiki

Command 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::Command class. Bellow you can see a list of plugins with some description and examples that are shipped with excom:

  • :status_helpers - Allows you to define status aliases and helper methods named after them to immediately and more explicitly assign both status and result at the same time:
class Todos::Update
  use :status_helpers, success: [:ok], failure: [:unprocessable_entity]
  args :todo, :params

  def run
    if todo.update(params)
      ok todo.as_json
    else
      unprocessable_entity todo.errors
    end
  end
end

command = Todos::Update.(todo, todo_params)
# in case params were valid you will have:
command.success? # => true
command.status # => :ok
command.result # => {'id' => 1, ...}
  • :context - Allows you to set an execution context for a block that will be available to any command that uses this plugin via context 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::Command
  use :context
  args :post

  def run
    post.update(archived: true, archived_by: context[:current_user])
  end
end
  • :sentry - Allows you to provide Sentry classes for commands that use this plugin. Each Sentry class hosts logic responsible for allowing or denying corresponding command's execution or related checks. 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::Command
  use :context
  use :sentry
  args :post

  def run
    post.destroy
  end
end

class Posts::DestroySentry < Excom::Sentry
  delegate :post, :context, to: :command
  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
  • :assertions - Provides assert method that can be used for different logic checks during command execution.

  • :caching - Simple plugin that will prevent re-execution of command if it already has been executed, and will immediately return result.

  • :rescue - Provides :rescue execution option. If set to true, any error occurred during command 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.