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.
A Cuprum::Result
is a data object that encapsulates the result of calling a Cuprum command.
Each result has a #value
, an #error
object (defaults to nil
), and a #status
. By default, the status will be either :success
or :failure
, and accessible via the #success?
and #failure?
predicates.
result = Cuprum::Result.new
result.value #=> nil
result.error #=> nil
result.status #=> :success
result.success? #=> true
result.failure? #=> true
Creating a result with a value stores the value.
value = 'A result value'.freeze
result = Cuprum::Result.new(value: value)
result.value #=> 'A result value'
result.error #=> nil
result.status #=> :success
result.success? #=> true
result.failure? #=> false
Creating a Result with an error stores the error and sets the status to :failure
.
error = Cuprum::Error.new(message: "I'm sorry, something went wrong.")
result = Cuprum::Result.new(error: error)
result.value #=> nil
result.error #=> Error with message "I'm sorry, something went wrong."
result.status #=> :failure
result.success? #=> false
result.failure? #=> true
Although using a Cuprum::Error
instance as the :error
is recommended, it is not required. You can use a custom error object, or just a string message.
result = Cuprum::Result.new(error: "I'm sorry, something went wrong.")
result.value #=> nil
result.error #=> "I'm sorry, something went wrong."
result.status #=> :failure
result.success? #=> false
result.failure? #=> true
Finally, the status can be overridden via the :status
keyword.
result = Cuprum::Result.new(status: :failure)
result.error #=> nil
result.status #=> :failure
result.success? #=> false
result.failure? #=> true
error = Cuprum::Error.new(message: "I'm sorry, something went wrong.")
result = Cuprum::Result.new(error: error, status: :success)
result.error #=> Error with message "I'm sorry, something went wrong."
result.status #=> :success
result.success? #=> true
result.failure? #=> false
A Cuprum::ResultList
is a special type of Result that aggregates together a number of other results.
result_list = Cuprum::ResultList.new(
Cuprum::Result.new(status: :success, value: :ok),
Cuprum::Result.new(status: :failure),
Cuprum::Result.new(error: 'Something went wrong')
)
result_list.results
#=> [#<Cuprum::Result>, #<Cuprum::Result>, #<Cuprum::Result>]
Each ResultList defines the same interface as a standard Result: the #value
, #error
, and #status
methods and the #success?
and #failure?
predicates.
result_list.value
#=> [:ok, nil, nil]
result_list.error
#=> #<Cuprum::Errors::MultipleErrors>
result_list.error.errors
#=> [nil, nil, 'Something went wrong']
result_list.status
#=> :failure
result_list.success?
#=> false
result_list.failure?
#=> true
The status of a result list depends on the statuses of its constituent results.
An empty ResultList (with no Results) will have a status of :success
.
result_list = Cuprum::ResultList.new
result_list.status
#=> :success
A non-empty ResultList will have a status of :success
if and only if all of the Results are passing.
result_list = Cuprum::ResultList.new(
Cuprum::Result.new(status: :success),
Cuprum::Result.new(status: :success),
Cuprum::Result.new(status: :success)
)
result_list.status
#=> :success
A non-empty ResultList will have a status of :failure
if any of the Results are failing.
result_list = Cuprum::ResultList.new(
Cuprum::Result.new(status: :success),
Cuprum::Result.new(status: :failure),
Cuprum::Result.new(status: :success)
)
result_list.status
#=> :failure
The status can also be specified directly. This will override the default status for the ResultList:
result_list = Cuprum::ResultList.new(
Cuprum::Result.new(status: :success),
Cuprum::Result.new(status: :success),
Cuprum::Result.new(status: :success),
status: :failure
)
result_list.status
#=> :failure
A result list can also be configured to pass if there are any passing results (or an empty input) by setting the :allow_partial
flag to true.
result_list = Cuprum::ResultList.new(
Cuprum::Result.new(status: :success),
Cuprum::Result.new(status: :failure),
Cuprum::Result.new(status: :success),
allow_partial: true
)
result_list.status
#=> :success
By default, the #value
of a ResultList is equal to the mapped values of each constituent result. These values can also be accessed directly by calling the #values
method.
result_list = Cuprum::ResultList.new(
Cuprum::Result.new(value: 'Hello world'),
Cuprum::Result.new(value: 'Greetings, programs!'),
Cuprum::Result.new(status: :success)
)
result_list.value
#=> ['Hello world', 'Greetings, programs!', nil]
result_list.values
#=> ['Hello world', 'Greetings, programs!', nil]
A ResultList can also be initialized with a custom value.
result_list = Cuprum::ResultList.new(
Cuprum::Result.new(value: 'Hello world'),
Cuprum::Result.new(value: 'Greetings, programs!'),
Cuprum::Result.new(status: :success),
value: { ok: true }
)
result_list.value
#=> { ok: true }
result_list.values
#=> ['Hello world', 'Greetings, programs!', nil]
The individual values can also be accessed via the #values
property of the result list.
If the result list is empty, or if none of the results in the result list have an error, then the ResultList’s own #error
property will be nil
.
result_list = Cuprum::ResultList.new(
Cuprum::Result.new(status: :success),
Cuprum::Result.new(status: :failure),
Cuprum::Result.new(status: :failure)
)
result_list.error
#=> nil
result_list.errors
#=> [nil, nil, nil]
If at least one of the results has an error object, the result errors are aggregated together into a Cuprum::Errors::MultipleErrors
object.
The individual errors can also be accessed via the #errors
property of the result list.
result_list = Cuprum::ResultList.new(
Cuprum::Result.new(status: :success),
Cuprum::Result.new(
status: :failure,
error: Cuprum::Error.new(message: 'Something went wrong')),
Cuprum::Result.new(status: :failure)
)
result_list.error.class
#=> Cuprum::Errors::MultipleErrors
result_list.error.message
#=> 'the command encountered one or more errors'
result_list.error.errors
#=> [nil, #<Cuprum::Error>, nil]
result_list.errors
#=> [nil, #<Cuprum::Error>, nil]
The error can also be specified directly. This will override the default error for the ResultList:
result_list = Cuprum::ResultList.new(
Cuprum::Result.new(status: :success),
Cuprum::Result.new(
status: :failure,
error: Cuprum::Error.new(message: 'Something went wrong')),
Cuprum::Result.new(status: :failure),
error: Cuprum::Error.new(message: 'Custom error message')
)
result_list.error.class
#=> Cuprum::Error
result_list.error.message
#=> 'Custom error message'
result_list.errors
#=> [nil, #<Cuprum::Error>, nil]
Some applications may wish to extend the result interface. Rather than changing Cuprum::Result
directly, create a new subclass to represent results within the custom domain.
One use case for extended results is adding additional status types. Consider a CLI application. In addition to the existing :success
and :failure
statuses, the application may require a :pending
status to indicate a task is not fully defined, as well as an :errored
status to represent a failure in the tool itself (as opposed to the delegated command).
The result statuses are determined by the #defined_statuses
method, so to create a result with additional status values, override that method.
class Cli::Result < Cuprum::Result
STATUSES = [*Cuprum::Result::STATUSES, :errored, :pending].freeze
def errored?
@status == :errored
end
def pending?
@status == :pending
end
private
def defined_statuses
self.class::STATUSES
end
end
result = Cli::Result.new(status: :pending)
result.status #=> :pending
result.success? #=> false
result.failure? #=> false
result.pending? #=> true
Notice that we are also defining predicate methods #errored?
and #pending?
for our custom result class. This matches the interface defined for the existing status types.
Another use case for extended results is adding additional result properties. Consider a web application, where you may wish to set or access certain metadata during the request cycle, but not return it as part of the response. Examples of metadata may include the current authentication session, or details used for page rendering such as a navigation context or breadcrumbs.
class Web::Result < Cuprum::Result
def initialize(error: nil, metadata: {}, status: nil, value: nil)
super(error: error, status: status, value: value)
@metadata = metadata
end
attr_reader :metadata
def properties
super().merge(metadata: metadata)
end
alias to_h properties
end
result = Web::Result.new(
value: { 'ok' => true },
metadata: { username: 'Kevin Flynn' }
)
result.metadata
#=> { username: 'Kevin Flynn' }
result.to_h
#=> {
# metadata: { username: 'Kevin Flynn' },
# value: { 'ok' => true }
# }
result == Cuprum::Result.new(value: result.value)
#=> false
result == Web::Result.new(value: result.value)
#=> false
result == Web::Result.new(value: result.value, metadata: result.metadata)
#=> true
To add a custom property, add the relevant keyword to the constructor and update the #properties
method to return the value of the custom property.
Once you have defined an extended result class, you can then use it in your commands.
class Cli::Command < Cuprum::Command
private
def build_result(error: nil, status: nil, value: nil)
Cli::Result.new(error: error, status: status, value: value)
end
def errored(error)
build_result(error: error, status: :errored)
end
def pending
build_result(status: :pending)
end
end
Here, we are overriding the built-in #build_request
method to return an instance of our Cli::Result
class. This ensures that when the user calls the #success
or #failure
helpers, or when #process
returns a bare value, the Cli::Command
will always return an instance of our custom result class.
We are also defining new helpers to generate :errored
and :pending
results.
class Web::Command < Cuprum::Command
private
def build_result(error: nil, metadata: nil, status: nil, value: nil)
Web::Result.new(
error: error,
metadata: message,
status: status,
value: value
)
end
def failure(error, metadata: nil)
build_result(error: error, metadata: metadata)
end
def success(value, metadata: nil)
build_result(value: value, metadata: metadata)
end
end
Again, we override the #build_request
method to return an instance of Web::Result
, and we add a new :metadata
keyword to the #success
and #failure
helpers.
Back to Documentation