Project

gl-command

0.0
The project is in a healthy, maintained state
Implementation of the Command Pattern
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Runtime

>= 3.2.0
>= 3.2.0
 Project Readme

GLCommand

GLCommand is a way to encapsulate business logic.

Calling a command returns a GLCommand::Context which has these properties:

  • The arguments that were passed in to the command .call method (set via allows requires)
  • The returns from the .call method
  • error (which contains the error, if an error was raised)
  • full_error_message - which renders a string from the error, or can be set explicitly (used to show a legible error to the user).
  • success - true if the command executed without an error (false if there is an error)

Installation

Add the following line to your Gemfile:

gem 'gl_command'

Download and install the gem:

bundle install

Using GLCommand

Invoke a command with .call or .call!

.call will return the GLCommand::Context, with error assigned (if there is an error)

.call! will raise the error (if there is an error), otherwise it will return the GLCommand::Context

General rules for deciding whether to use .call!

  • In controllers use .call (make sure you check that it succeeds and render errors appropriately)
  • In background jobs and rake tasks, use .call!
  • Use .call! when calling a command within another command. If the inner command fails, it will assign errors to the outer command.
class SomeCommand < GLCommand::Callable
  returns :data

  def call
    # If OtherCommand fails, SomeCommand will also fail - with the error from OtherCommand
    result = OtherCommand.call!
    context.data = result.data
  end
end

Success/Failure

GLCommand context's are successful by default (successful? aliases success?).

They are a failure (success? == false) if the context has an error.

Here are the ways of adding an error to a command:

  • Raising an exception
    • Immediately stops execution
  • Calling stop_and_fail!
    • Immediately stops execution
  • Failing a validation
    • Validation errors are checked before the call method is invoked (if valid? == false the command will return).
    • If validations are added during the call method, the command fails after call
  • Directly assigning context.error or context.full_error_message to a non-nil value
    • Checked after call method finishes

If you invoke a command with .call! all of the above will raise an exception

If a command fails, it will call its rollback method before returning (even when invoked with .call!)

Displaying errors

In addition to encapsulating business logic, GLCommand also standardizes error handling.

This means that rather than having to rescue errors in controllers, you can just render the command's full_error_message

result = GLCommand::Callable.call(params)
if result.success?
  redirect_to new_controller_action
else
  flash[:error] = result.full_error_message
  redirect_back
end

In general, use context.full_error_message to render errors.

stop_and_fail!

Use stop_and_fail! to immediately stop a command and raise an error (GLCommand::StopAndFail by default)

The argument to stop_and_fail! is assign to the context.error

  • If you pass an exception, that exception will be raised and/or sent to Sentry
  • Otherwise, the error will be a GLCommand::StopAndFail and what was passed will be assigned to full_error_message
# Passing a string:
stop_and_fail!('An error message')
context.error # => GLCommand::StopAndFail
context.full_error_message # => 'An error message'

# Passing an exception:
stop_and_fail!(ActiveRecord::RecordNotFound)
context.error # => ActiveRecord::RecordNotFound
context.full_error_message # => ActiveRecord::RecordNotFound

# Passing an exception with an error message
stop_and_fail!(ActiveRecord::RecordNotFound.new('Some error message'))
context.error # => ActiveRecord::RecordNotFound
context.full_error_message # => 'Some error message'

You can also include no_notify: true, which prevents GLExceptionNotifier from being called.

# Sentry is notified when #call fails by default:
stop_and_fail!('An error message') # GLExceptionNotifier is called

# If you don't want to alert Sentry when the command fails in a specific way:
stop_and_fail!('An error message', no_notify: true) # GLExceptionNotifier is *not* called

Validations

You can add validations to GLCommand::Callable and GLCommand::Chainable.

If the validations fail, the command returns success: false without executing.

If validations fail, GLExceptionNotifier is not called

GLExceptionNotifier

ExceptionNotifier is Give Lively's wrapper for notify our error monitoring service (currently Sentry)

When a command fails GLExceptionNotifier is called, unless:

  • The command is invoked with call! (because an error will be raised, which will alert Sentry)
  • The failure is a validation failure
  • stop_and_fail! is called with no_notify: true - for example stop_and_fail!('An error message', no_notify: true)

NOTE: commands that invoke other commands with call! inherit the no_notify property of called command.

class InteriorCommand < GLCommand::Callable
  def call
    stop_and_fail!('An error message', no_notify: true)
  end
end

class MainCommand < GLCommand::Callable
  def call
    # Use call! in commands that invoke other commands to have the errors automatically bubble up
    InteriorCommand.call!
  end
end

# This won't call GLExceptionNotifier, because no_notify: true was used on InteriorCommand
result = MainCommand.call
result.success? # => false
result.full_error_message # => 'An error message'

Chainable

Bundle commands together with GLCommand::Chainable

  • Automatically passes the requires/allows and returns between the commands
  • Returns a GLCommand::ChainableContext, that inherits from GLCommand::Context. It adds a Commands array that contains the Command class names that were called.
  • A command in the chain failing will call rollback on itself and then each command in the context Command array (in reverse order)

If you need to do logic in the GLCommand::Chainable class, define the call method and invoke chain from that.

class SomeChain < GLCommand::Chainable
  requires :item

  returns :new_item

  chain CommmandOne, CommandTwo

  def call
    # Add some logic goes here
    chain(item:) # Automatically assigns the return to the context
    # Additional logic here
  end
end

This library is influenced by interactors and inspired by the Command Pattern.