# BCDD::Result 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 methods that will be useful to make the code flow react clearly and cleanly to the result represented by these objects. - [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 value](#result-value) - [`result.value_or`](#resultvalue_or) - [`result.data_or`](#resultdata_or) - [Railway Oriented Programming](#railway-oriented-programming) - [`result.and_then`](#resultand_then) - [About](#about) - [Development](#development) - [Contributing](#contributing) - [License](#license) - [Code of Conduct](#code-of-conduct) ## Installation Install the gem and add to the application's Gemfile by executing: $ bundle add bcdd-result If bundler is not being used to manage dependencies, install the gem by executing: $ gem install bcdd-result ## 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) # ```
## 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 ``` #### 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 ``` ### 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 ``` #### `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 ``` #### `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 } ``` #### `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 } ``` ### 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 ``` #### `result.data_or` `BCDD::Result#data_or` is an alias of `BCDD::Result#value_or`. ### Railway Oriented Programming #### `result.and_then` 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. e.g., ```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) #