🔀 BCDD::Result
Empower Ruby apps with a pragmatic use of Railway Oriented Programming.
A general-purpose result monad that allows you to create objects that represent a success (`BCDD::Result::Success`) or failure (`BCDD::Result::Failure`).
**What problem does it solve?**
It allows you to consistently represent the concept of success and failure throughout your codebase.
Furthermore, this abstraction exposes several features that will be useful to make the application flow react cleanly and securely to the result represented by these objects.
Use it to enable the [Railway Oriented Programming](https://fsharpforfunandprofit.com/rop/) pattern (superpower) in your code.
- [Ruby Version](#ruby-version)
- [Installation](#installation)
- [Usage](#usage)
- [Reference](#reference)
- [Result Attributes](#result-attributes)
- [Receiving types in `result.success?` or `result.failure?`](#receiving-types-in-resultsuccess-or-resultfailure)
- [Result Hooks](#result-hooks)
- [`result.on`](#resulton)
- [`result.on_type`](#resulton_type)
- [`result.on_success`](#resulton_success)
- [`result.on_failure`](#resulton_failure)
- [`result.handle`](#resulthandle)
- [Result Value](#result-value)
- [`result.value_or`](#resultvalue_or)
- [`result.data_or`](#resultdata_or)
- [Railway Oriented Programming](#railway-oriented-programming)
- [`result.and_then`](#resultand_then)
- [`BCDD::Resultable`](#bcddresultable)
- [Class example (instance methods)](#class-example-instance-methods)
- [Module example (singleton methods)](#module-example-singleton-methods)
- [Restrictions](#restrictions)
- [About](#about)
- [Development](#development)
- [Contributing](#contributing)
- [License](#license)
- [Code of Conduct](#code-of-conduct)
## Ruby Version
`>= 2.7.0`
## Installation
Add this line to your application's Gemfile:
```ruby
gem 'bcdd-result', require: 'bcdd/result'
```
And then execute:
$ bundle install
If bundler is not being used to manage dependencies, install the gem by executing:
$ gem install bcdd-result
(⬆️ back to top)
## Usage
To create a result, you must define a type (symbol) and its value (any kind of object). e.g.,
```ruby
BCDD::Result::Success(:ok, :1) #
# The value can be any kind of object
BCDD::Result::Failure(:err, 'the value') #
```
The reason for defining a `type` is that it is very common for a method/operation to return different types of successes or failures. Because of this, the `type` will always be required. e,g.,
```ruby
BCDD::Result::Success(:ok) #
# The type is mandatory and the value is optional
BCDD::Result::Failure(:err) #
```
(⬆️ back to top)
## Reference
### Result Attributes
Both `BCDD::Result::Success` and `BCDD::Result::Failure` are composed of the same methods. Look at the basic ones:
**BCDD::Result::Success**
```ruby
################
# With a value #
################
result = BCDD::Result::Success(:ok, my: 'value')
result.success? # true
result.failure? # false
result.type # :ok
result.value # {:my => "value"}
###################
# Without a value #
###################
result = BCDD::Result::Success(:yes)
result.success? # true
result.failure? # false
result.type # :yes
result.value # nil
```
**BCDD::Result::Failure**
```ruby
################
# With a value #
################
result = BCDD::Result::Failure(:err, my: 'value')
result.success? # false
result.failure? # true
result.type # :err
result.value # {:my => "value"}
###################
# Without a value #
###################
result = BCDD::Result::Failure(:no)
result.success? # false
result.failure? # true
result.type # :no
result.value # nil
```
(⬆️ back to top)
#### Receiving types in `result.success?` or `result.failure?`
`BCDD::Result#success?` and `BCDD::Result#failure?` are methods that allow you to check if the result is a success or a failure.
You can also check the result type by passing an argument to it. For example, `result.success?(:ok)` will check if the result is a success and if the type is `:ok`.
```ruby
result = BCDD::Result::Success(:ok)
result.success? # true
result.success?(:ok) # true
result.success?(:okay) # false
```
The same is valid for `BCDD::Result#failure?`.
```ruby
result = BCDD::Result::Failure(:err)
result.failure? # true
result.failure?(:err) # true
result.failure?(:error) # false
```
(⬆️ back to top)
### Result Hooks
Result hooks are methods that allow you to perform a block of code depending on the result type.
To exemplify the them, I will implement a method that knows how to divide two numbers.
```ruby
def divide(arg1, arg2)
arg1.is_a?(::Numeric) or return BCDD::Result::Failure(:invalid_arg, 'arg1 must be numeric')
arg2.is_a?(::Numeric) or return BCDD::Result::Failure(:invalid_arg, 'arg2 must be numeric')
return BCDD::Result::Failure(:division_by_zero, 'arg2 must not be zero') if arg2.zero?
BCDD::Result::Success(:division_completed, arg1 / arg2)
end
```
(⬆️ back to top)
#### `result.on`
`BCDD::Result#on` will perform the block when the type matches the result type.
Regardless of the block being executed, the return of the method will always be the result itself.
The result value will be exposed as the first argument of the block.
```ruby
result = divide(nil, 2)
output =
result
.on(:invalid_arg) { |msg| puts msg }
.on(:division_by_zero) { |msg| puts msg }
.on(:division_completed) { |number| puts number }
# The code above will print 'arg1 must be numeric' and return the result itself.
result.object_id == output.object_id # true
```
You can define multiple types to be handled by the same hook/block
```ruby
result = divide(4, 0)
output =
result.on(:invalid_arg, :division_by_zero, :division_completed) { |value| puts value }
# The code above will print 'arg2 must not be zero' and return the result itself.
result.object_id == output.object_id # true
```
*PS: The `divide()` implementation is [here](#result-hooks).*
(⬆️ back to top)
#### `result.on_type`
`BCDD::Result#on_type` is an alias of `BCDD::Result#on`.
#### `result.on_success`
`BCDD::Result#on_success` is very similar to the `BCDD::Result#on` hook. The main differences are:
1. Only perform the block when the result is a success.
2. If the type is missing it will perform the block for any success.
```ruby
# It performs the block and return itself.
divide(4, 2).on_success { |number| puts number }
divide(4, 2).on_success(:division_completed) { |number| puts number }
# It doesn't perform the block, but return itself.
divide(4, 4).on_success(:ok) { |value| puts value }
divide(4, 4).on_failure { |error| puts error }
```
*PS: The `divide()` implementation is [here](#result-hooks).*
(⬆️ back to top)
#### `result.on_failure`
Is the opposite of `Result#on_success`.
```ruby
# It performs the block and return itself.
divide(nil, 2).on_failure { |error| puts error }
divide(4, 0).on_failure(:invalid_arg, :division_by_zero) { |error| puts error }
# It doesn't perform the block, but return itself.
divide(4, 0).on_success { |number| puts number }
divide(4, 0).on_failure(:invalid_arg) { |error| puts error }
```
*PS: The `divide()` implementation is [here](#result-hooks).*
(⬆️ back to top)
#### `result.handle`
This method will allow you to define blocks for each hook (type, failure, success), but instead of returning itself, it will return the output of the first match/block execution.
```ruby
divide(4, 2).handle do |result|
result.success { |number| number }
result.failure(:invalid_arg) { |err| puts err }
result.type(:division_by_zero) { raise ZeroDivisionError }
end
#or
divide(4, 2).handle do |on|
on.success { |number| number }
on.failure { |err| puts err }
end
#or
divide(4, 2).handle do |on|
on.type(:invalid_arg) { |err| puts err }
on.type(:division_by_zero) { raise ZeroDivisionError }
on.type(:division_completed) { |number| number }
end
# or
divide(4, 2).handle do |on|
on[:invalid_arg] { |err| puts err }
on[:division_by_zero] { raise ZeroDivisionError }
on[:division_completed] { |number| number }
end
# The [] syntax 👆 is an alias of #type.
```
**Notes:**
* You can define multiple types to be handled by the same hook/block
* If the type is missing, it will perform the block for any success or failure handler.
* The `#type` and `#[]` handlers will require at least one type/argument.
*PS: The `divide()` implementation is [here](#result-hooks).*
(⬆️ back to top)
### Result Value
The most simple way to get the result value is by calling `BCDD::Result#value` or `BCDD::Result#data`.
But sometimes you need to get the value of a successful result or a default value if it is a failure. In this case, you can use `BCDD::Result#value_or` or `BCDD::Result#data_or`.
#### `result.value_or`
`BCCD::Result#value_or` returns the value when the result is a success, but if is a failure the block will be performed, and its outcome will be the output.
```ruby
def divide(arg1, arg2)
arg1.is_a?(::Numeric) or return BCDD::Result::Failure(:invalid_arg)
arg2.is_a?(::Numeric) or return BCDD::Result::Failure(:invalid_arg)
return BCDD::Result::Failure(:division_by_zero) if arg2.zero?
BCDD::Result::Success(:division_completed, arg1 / arg2)
end
# When the result is success
divide(4, 2).value_or { 0 } # 2
# When the result is failure
divide('4', 2).value_or { 0 } # 0
divide(4, '2').value_or { 0 } # 0
divide(100, 0).value_or { 0 } # 0
```
*PS: The `divide()` implementation is [here](#result-hooks).*
(⬆️ back to top)
#### `result.data_or`
`BCDD::Result#data_or` is an alias of `BCDD::Result#value_or`.
### Railway Oriented Programming
This feature/pattern is also known as ["Railway Oriented Programming"](https://fsharpforfunandprofit.com/rop/).
The idea is to chain blocks and creates a pipeline of operations that can be interrupted by a failure.
In other words, the block will be executed only if the result is a success.
So, if some block returns a failure, the following blocks will be skipped.
Due to this characteristic, you can use this feature to express some logic as a sequence of operations. And have the guarantee that the process will stop by the first failure detection, and if everything is ok, the final result will be a success.
#### `result.and_then`
```ruby
module Divide
extend self
def call(arg1, arg2)
validate_numbers(arg1, arg2)
.and_then { |numbers| validate_non_zero(numbers) }
.and_then { |numbers| divide(numbers) }
end
private
def validate_numbers(arg1, arg2)
arg1.is_a?(::Numeric) or return BCDD::Result::Failure(:invalid_arg, 'arg1 must be numeric')
arg2.is_a?(::Numeric) or return BCDD::Result::Failure(:invalid_arg, 'arg2 must be numeric')
BCDD::Result::Success(:ok, [arg1, arg2])
end
def validate_non_zero(numbers)
return BCDD::Result::Success(:ok, numbers) unless numbers.last.zero?
BCDD::Result::Failure(:division_by_zero, 'arg2 must not be zero')
end
def divide((number1, number2))
BCDD::Result::Success(:division_completed, number1 / number2)
end
end
```
Example of outputs:
```ruby
Divide.call('4', 2)
#
Divide.call(2, '2')
#
Divide.call(2, 0)
#
Divide.call(2, 2)
#
```
(⬆️ back to top)
#### `BCDD::Resultable`
It is a module that can be included/extended by any object. It adds two methods to the target object: `Success()` and `Failure()`.
The main difference between these methods and `BCDD::Result::Success()`/`BCDD::Result::Failure()` is that the first ones will use the target object as the result's subject.
And because of this, you can use the `#and_then` method to call methods from the target object (result's subject).
##### Class example (instance methods)
```ruby
class Divide
include BCDD::Resultable
attr_reader :arg1, :arg2
def initialize(arg1, arg2)
@arg1 = arg1
@arg2 = arg2
end
def call
validate_numbers
.and_then(:validate_non_zero)
.and_then(:divide)
end
private
def validate_numbers
arg1.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg1 must be numeric')
arg2.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg2 must be numeric')
# As arg1 and arg2 are instance methods, they will be available in the instance scope.
# So, in this case, I'm passing them as an array to show how the next method can receive the value as its argument.
Success(:ok, [arg1, arg2])
end
def validate_non_zero(numbers)
return Success(:ok, numbers) unless numbers.last.zero?
Failure(:division_by_zero, 'arg2 must not be zero')
end
def divide((number1, number2))
Success(:division_completed, number1 / number2)
end
end
Divide.new(4, 2).call #
Divide.new(4, 0).call #
Divide.new('4', 2).call #
Divide.new(4, '2').call #
```
##### Module example (singleton methods)
```ruby
module Divide
extend BCDD::Resultable
extend self
def call(arg1, arg2)
validate_numbers(arg1, arg2)
.and_then(:validate_non_zero)
.and_then(:divide)
end
private
def validate_numbers(arg1, arg2)
arg1.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg1 must be numeric')
arg2.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg2 must be numeric')
Success(:ok, [arg1, arg2])
end
def validate_non_zero(numbers)
return Success(:ok, numbers) unless numbers.last.zero?
Failure(:division_by_zero, 'arg2 must not be zero')
end
def divide((number1, number2))
Success(:division_completed, number1 / number2)
end
end
Divide.call(4, 2) #
Divide.call(4, 0) #
Divide.call('4', 2) #
Divide.call(4, '2') #
```
##### Restrictions
The unique condition for using the `#and_then` to call methods is that they must use the `Success()` and `Failure()` to produce their results.
If you use `BCDD::Result::Subject()`/`BCDD::Result::Failure()`, or call another `BCDD::Resultable` object, the `#and_then` will raise an error because the subjects will be different.
> **Note**: You still can use the block syntax, but all the results must be produced by the subject's `Success()` and `Failure()` methods.
(⬆️ back to top)
## About
[Rodrigo Serradura](https://github.com/serradura) created this project. He is the B/CDD process/method creator and has already made similar gems like the [u-case](https://github.com/serradura/u-case) and [kind](https://github.com/serradura/kind/blob/main/lib/kind/result.rb). This gem is a general-purpose abstraction/monad, but it also contains key features that serve as facilitators for adopting B/CDD in the code.
(⬆️ back to top)
## Development
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
(⬆️ back to top)
## Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/B-CDD/bcdd-result. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/B-CDD/bcdd-result/blob/master/CODE_OF_CONDUCT.md).
(⬆️ back to top)
## License
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
(⬆️ back to top)
## Code of Conduct
Everyone interacting in the BCDD::Result project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/B-CDD/bcdd-result/blob/master/CODE_OF_CONDUCT.md).