An opinionated implementation of the Command pattern for Ruby applications. Cuprum wraps your business logic in a consistent, object-oriented interface and features status and error management, composability and control flow management.
Commands are the core feature of Cuprum. In a nutshell, each Cuprum::Command is a functional object that encapsulates a business logic operation. A Command provides a consistent interface and tracking of result value and status. This minimizes boilerplate and allows for interchangeability between different implementations or strategies for managing your data and processes.
Each Command implements a #call method that wraps your defined business logic and returns an instance of Cuprum::Result. The result has a #status (either :success or :failure), and may have a #value and/or an #error object.
The recommended way to define commands is to create a subclass of Cuprum::Command and override the #process method. By convention, #process is a private method and should not be called directly.
class BuildBookCommand < Cuprum::Command
private
def process(attributes)
Book.new(attributes)
end
end
command = BuildPostCommand.new
result = command.call(title: 'The Hobbit')
result.class #=> Cuprum::Result
result.success? #=> true
book = result.value
book.class #=> Book
book.title #=> 'The Hobbit'
There are several takeaways from this example. First, we are defining a custom command class that inherits from Cuprum::Command. We are defining the #process method, which takes a single attributes parameter and returns an instance of Book. Then, we are creating an instance of the command, and invoking the #call method with an attributes hash. These attributes are passed to our #process implementation. Invoking #call returns a result, and the #value of the result is our new Book.
Because a command is just a Ruby object, we can also pass values to the constructor.
class SaveBookCommand < Cuprum::Command
def initialize(repository)
@repository = repository
end
def process(book)
if @repository.persist(book)
success(book)
else
error = Cuprum::Error.new(message: 'unable to save book')
failure(error)
end
end
end
books = [
Book.new(title: 'The Fellowship of the Ring'),
Book.new(title: 'The Two Towers'),
Book.new(title: 'The Return of the King')
]
command = SaveBookCommand.new(books_repository)
books.each { |book| command.call(book) }
Here, we are defining a command that might fail - maybe the database is unavailable, or there’s a constraint that is violated by the inserted attributes. If the call to #persist succeeds, we’re returning a Result with a status of :success and the value set to the persisted book.
Conversely, if the call to #persist fails, we’re returning a Result with a status of :failure and a custom error message. Since the #process method returns a Result, it is returned directly by #call.
Note also that we are reusing the same command three times, rather than creating a new save command for each book. Each book is persisted to the books_repository. This is also an example of how using commands can simplify code - notice that nothing about the SaveBookCommand is specific to the Book model. Thus, we could refactor this into a generic SaveModelCommand.
A command can also be defined by passing block to Cuprum::Command.new.
increment_command = Cuprum::Command.new { |int| int + 1 }
increment_command.call(2).value #=> 3
If the command is wrapping a method on the receiver, the syntax is even simpler:
inspect_command = Cuprum::Command.new { |obj| obj.inspect }
inspect_command = Cuprum::Command.new(&:inspect) # Equivalent to above.
Commands defined using Cuprum::Command.new are quick to use, but more difficult to read and to reuse. Defining your own command class is recommended if a command definition takes up more than one line, or if the command will be used in more than one place.
Calling the #call method on a Cuprum::Command instance will always return an instance of Cuprum::Result. The result’s #value property is determined by the object returned by the #process method (if the command is defined as a class) or the block (if the command is defined by passing a block to Cuprum::Command.new).
The #value depends on whether or not the returned object is a result or is compatible with the result interface. Specifically, any object that responds to the method #to_cuprum_result is considered to be a result.
If the object returned by #process is not a result, then the #value of the returned result is set to the object.
command = Cuprum::Command.new { 'Greetings, programs!' }
result = command.call
result.class #=> Cuprum::Result
result.value #=> 'Greetings, programs!'
If the object returned by #process is a result object, then result is returned directly.
command = Cuprum::Command.new { Cuprum::Result.new(value: 'Greetings, programs!') }
result = command.call
result.class #=> Cuprum::Result
result.value #=> 'Greetings, programs!'
Calling the #call method on a Cuprum::Command instance will always return an instance of Cuprum::Result. The result’s #value property is determined by the object returned by the #process method (if the command is defined as a class) or the block (if the command is defined by passing a block to Cuprum::Command.new).
The #value depends on whether or not the returned object is a result or is compatible with the result interface. Specifically, any object that responds to the method #to_cuprum_result is considered to be a result.
If the object returned by #process is not a result, then the #value of the returned result is set to the object.
command = Cuprum::Command.new { 'Greetings, programs!' }
result = command.call
result.class #=> Cuprum::Result
result.value #=> 'Greetings, programs!'
If the object returned by #process is a result object, then result is returned directly.
command = Cuprum::Command.new { Cuprum::Result.new(value: 'Greetings, programs!') }
result = command.call
result.class #=> Cuprum::Result
result.value #=> 'Greetings, programs!'
Each Result has a #status, either :success or :failure. A Result will have a status of :failure when it was created with an error object. Otherwise, a Result will have a status of :success. Returning a failing Result from a Command indicates that something went wrong while executing the Command.
class PublishBookCommand < Cuprum::Command
private
def process(book)
if book.cover.nil?
return Cuprum::Result.new(error: 'This book does not have a cover.')
end
book.published = true
book
end
end
In addition, the result object defines #success? and #failure? predicates.
book = Book.new(title: 'The Silmarillion', cover: Cover.new)
book.published? #=> false
result = PublishBookCommand.new.call(book)
result.error #=> nil
result.success? #=> true
result.failure? #=> false
result.value #=> book
book.published? #=> true
If the result does have an error, #success? will return false and #failure? will return true.
book = Book.new(title: 'The Silmarillion', cover: nil)
book.published? #=> false
result = PublishBookCommand.new.call(book)
result.error #=> 'This book does not have a cover.'
result.success? #=> false
result.failure? #=> true
result.value #=> book
book.published? #=> false
Back to Documentation | Versions | 1.0