Skip to content

Commit

Permalink
Merge pull request #17 from B-CDD/bcdd/continue_addon
Browse files Browse the repository at this point in the history
Implementation of "Success should halt the step chain when using Continue addon"
  • Loading branch information
serradura authored Dec 12, 2023
2 parents 5d1c2d4 + f1c9952 commit 5f9d3d8
Show file tree
Hide file tree
Showing 23 changed files with 999 additions and 72 deletions.
1 change: 1 addition & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ Metrics/BlockLength:

Metrics/ClassLength:
Exclude:
- lib/bcdd/result.rb
- test/**/*.rb

Minitest/MultipleAssertions:
Expand Down
30 changes: 20 additions & 10 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,40 @@
- [\[Unreleased\]](#unreleased)
- [\[0.8.0\] - 2023-12-11](#080---2023-12-11)
- [Added](#added)
- [Changed](#changed)
- [Removed](#removed)
- [\[0.7.0\] - 2023-10-27](#070---2023-10-27)
- [\[0.8.0\] - 2023-12-11](#080---2023-12-11)
- [Added](#added-1)
- [Changed](#changed-1)
- [\[0.6.0\] - 2023-10-11](#060---2023-10-11)
- [Removed](#removed)
- [\[0.7.0\] - 2023-10-27](#070---2023-10-27)
- [Added](#added-2)
- [Changed](#changed-2)
- [\[0.5.0\] - 2023-10-09](#050---2023-10-09)
- [\[0.6.0\] - 2023-10-11](#060---2023-10-11)
- [Added](#added-3)
- [\[0.4.0\] - 2023-09-28](#040---2023-09-28)
- [Added](#added-4)
- [Changed](#changed-3)
- [\[0.5.0\] - 2023-10-09](#050---2023-10-09)
- [Added](#added-4)
- [\[0.4.0\] - 2023-09-28](#040---2023-09-28)
- [Added](#added-5)
- [Changed](#changed-4)
- [Removed](#removed-1)
- [\[0.3.0\] - 2023-09-26](#030---2023-09-26)
- [Added](#added-5)
- [\[0.2.0\] - 2023-09-26](#020---2023-09-26)
- [Added](#added-6)
- [\[0.2.0\] - 2023-09-26](#020---2023-09-26)
- [Added](#added-7)
- [Removed](#removed-2)
- [\[0.1.0\] - 2023-09-25](#010---2023-09-25)
- [Added](#added-7)
- [Added](#added-8)

## [Unreleased]

### Added

- Add `BCDD::Result#halted?` to check if the result is halted. Failure results are halted by default, but you can halt a successful result by enabling the `:continue` addon.

### Changed

- **(BREAKING)** Change the `:continue` addon to halt the step chain on the first `Success()` result. So, if you want to advance to the next step, you must use `Continue(value)` instead of `Success(type, value)`. Otherwise, the step chain will be halted. (Implementation of the following proposal: https://github.com/B-CDD/result/issues/14)

## [0.8.0] - 2023-12-11

### Added
Expand Down
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<p align="center">
<h1 align="center" id="-bcddresult">🔀 BCDD::Result</h1>
<p align="center"><i>Empower Ruby apps with pragmatic use of Result monad, Railway Oriented Programming, and B/CDD.</i></p>
<p align="center"><i>Empower Ruby apps with pragmatic use of Result pattern (monad), Railway Oriented Programming, and B/CDD.</i></p>
<p align="center">
<img src="https://img.shields.io/badge/ruby->%3D%202.7.0-ruby.svg?colorA=99004d&colorB=cc0066" alt="Ruby">
<a href="https://rubygems.org/gems/bcdd-result"><img src="https://badge.fury.io/rb/bcdd-result.svg" alt="bcdd-result gem version" height="18"></a>
Expand Down Expand Up @@ -917,7 +917,9 @@ The `BCDD::Result.mixin` also accepts the `config:` argument. It is a hash that

**continue**

This addon will create the `Continue(value)` method, which will know how to produce a `Success(:continued, value)`. It is useful when you want to perform a sequence of operations but want to avoid returning a specific result for each step.
This addon will create the `Continue(value)` method and change the `Success()` behavior to halt the step chain.

So, if you want to advance to the next step, you must use `Continue(value)` instead of `Success(type, value)`. Otherwise, the step chain will be halted.

```ruby
module Divide
Expand Down Expand Up @@ -1350,7 +1352,9 @@ The `BCDD::Result::Expectations.mixin` also accepts the `config:` argument. It i

**Continue**

It is similar to `BCDD::Result.mixin(config: { addon: { continue: true } })`, the key difference is that the `Continue(value)` will be ignored by the expectations. This is extremely useful when you want to use `Continue(value)` to chain operations, but you don't want to declare N success types in the expectations.
It is similar to `BCDD::Result.mixin(config: { addon: { continue: true } })`. The key difference is that the expectations will ignore the `Continue(value)`.

Based on this, use the `Success()` to produce a terminal result and `Continue()` to produce a result that will be used in the next step.

```ruby
class Divide
Expand Down Expand Up @@ -1655,7 +1659,9 @@ The `BCDD::Result::Context.mixin` and `BCDD::Result::Context::Expectations.mixin

**Continue**

The `BCDD::Result::Context.mixin(config: { addon: { continue: true } })` or `BCDD::Result::Context::Expectations.mixin(config: { addon: { continue: true } })` adds a `Continue(**input)` that will be ignored by the expectations. This is extremely useful when you want to use `Continue()` to chain operations, but you don't want to declare N success types in the expectations.
The `BCDD::Result::Context.mixin(config: { addon: { continue: true } })` or `BCDD::Result::Context::Expectations.mixin(config: { addon: { continue: true } })` creates the `Continue(value)` method and change the `Success()` behavior to halt the step chain.

So, if you want to advance to the next step, you must use `Continue(**value)` instead of `Success(type, **value)`. Otherwise, the step chain will be halted.

Let's use a mix of `BCDD::Result::Context` features to see in action with this add-on:

Expand Down Expand Up @@ -1744,7 +1750,7 @@ Let's see what each configuration in the example above does:

#### `config.addon.enable!(:continue)`

This configuration enables the `Continue()` method for `BCDD::Result` and `BCDD::Result::Context`. Link to documentations: [(1)](#add-ons) [(2)](#mixin-add-ons).
This configuration enables the `Continue()` method for `BCDD::Result`, `BCDD::Result::Context`, `BCDD::Result::Expectation`, and `BCDD::Result::Context::Expectation`. Link to documentations: [(1)](#add-ons) [(2)](#mixin-add-ons).

#### `config.constant_alias.enable!('Result')`

Expand Down
8 changes: 5 additions & 3 deletions bcdd-result.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ Gem::Specification.new do |spec|
spec.authors = ['Rodrigo Serradura']
spec.email = ['[email protected]']

spec.summary = 'Empower Ruby apps with pragmatic use of Result monad, Railway Oriented Programming, and B/CDD.'

spec.description = 'Empower Ruby apps with pragmatic use of Result monad, Railway Oriented Programming, and B/CDD.'
'Empower Ruby apps with pragmatic use of Result pattern (monad), Railway Oriented Programming, and B/CDD.'
.then do |summary|
spec.summary = summary
spec.description = summary
end

spec.homepage = 'https://github.com/b-cdd/result'
spec.license = 'MIT'
Expand Down
11 changes: 8 additions & 3 deletions lib/bcdd/result.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
class BCDD::Result
attr_accessor :unknown

attr_reader :subject, :data, :type_checker
attr_reader :subject, :data, :type_checker, :halted

protected :subject

Expand All @@ -31,16 +31,21 @@ def self.configuration
config.freeze
end

def initialize(type:, value:, subject: nil, expectations: nil)
def initialize(type:, value:, subject: nil, expectations: nil, halted: nil)
data = Data.new(name, type, value)

@type_checker = Contract.evaluate(data, expectations)
@subject = subject
@halted = halted || name == :failure
@data = data

self.unknown = true
end

def halted?
halted
end

def type
data.type
end
Expand Down Expand Up @@ -80,7 +85,7 @@ def on_unknown
end

def and_then(method_name = nil, context = nil)
return self if failure?
return self if halted?

method_name && block_given? and raise ::ArgumentError, 'method_name and block are mutually exclusive'

Expand Down
7 changes: 4 additions & 3 deletions lib/bcdd/result/config/options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ def self.with_defaults(all_flags, config)
default_flags.merge(config_flags).slice(*default_flags.keys)
end

def self.filter_map(all_flags, config:, from:)
def self.select(all_flags, config:, from:)
with_defaults(all_flags, config)
.filter_map { |name, truthy| from[name] if truthy }
.filter_map { |name, truthy| [name, from[name]] if truthy }
.to_h
end

def self.addon(map:, from:)
filter_map(map, config: :addon, from: from)
select(map, config: :addon, from: from)
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/bcdd/result/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def self.Failure(type, **value)
Failure.new(type: type, value: value)
end

def initialize(type:, value:, subject: nil, expectations: nil)
def initialize(type:, value:, subject: nil, expectations: nil, halted: nil)
value.is_a?(::Hash) or raise ::ArgumentError, 'value must be a Hash'

@acc = {}
Expand Down
4 changes: 2 additions & 2 deletions lib/bcdd/result/context/expectations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ def self.result_factory_without_expectations
private_class_method :mixin!, :mixin_module, :result_factory_without_expectations

def Success(type, **value)
Success.new(type: type, value: value, subject: subject, expectations: contract)
_ResultAs(Success, type, value)
end

def Failure(type, **value)
Failure.new(type: type, value: value, subject: subject, expectations: contract)
_ResultAs(Failure, type, value)
end
end
end
2 changes: 1 addition & 1 deletion lib/bcdd/result/context/expectations/mixin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class BCDD::Result::Context
module Expectations::Mixin
Factory = BCDD::Result::Expectations::Mixin::Factory

METHODS = BCDD::Result::Expectations::Mixin::METHODS
Methods = BCDD::Result::Expectations::Mixin::Methods

module Addons
module Continuable
Expand Down
14 changes: 11 additions & 3 deletions lib/bcdd/result/context/mixin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,26 @@ module Mixin

module Methods
def Success(type, **value)
Success.new(type: type, value: value, subject: self)
_ResultAs(Success, type, value)
end

def Failure(type, **value)
Failure.new(type: type, value: value, subject: self)
_ResultAs(Failure, type, value)
end

private def _ResultAs(kind_class, type, value, halted: nil)
kind_class.new(type: type, value: value, subject: self, halted: halted)
end
end

module Addons
module Continuable
def Success(type, **value)
_ResultAs(Success, type, value, halted: true)
end

private def Continue(**value)
Success.new(type: :continued, value: value, subject: self)
_ResultAs(Success, :continued, value)
end
end

Expand Down
28 changes: 19 additions & 9 deletions lib/bcdd/result/expectations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ def self.mixin!(success: nil, failure: nil, config: nil)

mod = mixin_module::Factory.module!
mod.const_set(:Result, new(success: success, failure: failure, config: config).freeze)
mod.module_eval(mixin_module::METHODS)
mod.send(:include, *addons) unless addons.empty?
mod.module_eval(mixin_module::Methods.to_eval(addons), __FILE__, __LINE__ + 1)
mod.send(:include, *addons.values) unless addons.empty?
mod
end

Expand All @@ -38,28 +38,38 @@ def self.new(...)

private_class_method :mixin!, :mixin_module, :result_factory_without_expectations

def initialize(subject: nil, success: nil, failure: nil, contract: nil, config: nil)
def initialize(subject: nil, contract: nil, halted: nil, **options)
@halted = halted

@subject = subject

@contract = contract if contract.is_a?(Contract::Evaluator)

@contract ||= Contract.new(success: success, failure: failure, config: config).freeze
@contract ||= Contract.new(
success: options[:success],
failure: options[:failure],
config: options[:config]
).freeze
end

def Success(type, value = nil)
Success.new(type: type, value: value, subject: subject, expectations: contract)
_ResultAs(Success, type, value)
end

def Failure(type, value = nil)
Failure.new(type: type, value: value, subject: subject, expectations: contract)
_ResultAs(Failure, type, value)
end

def with(subject:)
self.class.new(subject: subject, contract: contract)
def with(subject:, halted: nil)
self.class.new(subject: subject, halted: halted, contract: contract)
end

private

attr_reader :subject, :contract
def _ResultAs(kind_class, type, value)
kind_class.new(type: type, value: value, subject: subject, expectations: contract, halted: halted)
end

attr_reader :subject, :halted, :contract
end
end
30 changes: 19 additions & 11 deletions lib/bcdd/result/expectations/mixin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,29 @@ def self.extended(base); base.const_set(:ResultExpectationsMixin, self); end
end
end

METHODS = <<~RUBY
def Success(...)
_Result.Success(...)
end
module Methods
BASE = <<~RUBY
def Success(...)
_Result.Success(...)
end
def Failure(...)
_Result.Failure(...)
end
def Failure(...)
_Result.Failure(...)
end
RUBY

private
FACTORY = <<~RUBY
private def _Result
@_Result ||= Result.with(subject: self, halted: %<halted>s)
end
RUBY

def self.to_eval(addons)
halted = addons.key?(:continue) ? 'true' : 'nil'

def _Result
@_Result ||= Result.with(subject: self)
"#{BASE}\n#{format(FACTORY, halted: halted)}"
end
RUBY
end

module Addons
module Continuable
Expand Down
16 changes: 12 additions & 4 deletions lib/bcdd/result/mixin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,26 @@ def self.extended(base); base.const_set(:ResultMixin, self); end

module Methods
def Success(type, value = nil)
Success.new(type: type, value: value, subject: self)
_ResultAs(Success, type, value)
end

def Failure(type, value = nil)
Failure.new(type: type, value: value, subject: self)
_ResultAs(Failure, type, value)
end

private def _ResultAs(kind_class, type, value, halted: nil)
kind_class.new(type: type, value: value, subject: self, halted: halted)
end
end

module Addons
module Continuable
def Success(type, value = nil)
_ResultAs(Success, type, value, halted: true)
end

private def Continue(value)
Success(:continued, value)
_ResultAs(Success, :continued, value)
end
end

Expand All @@ -42,7 +50,7 @@ def self.mixin(config: nil)
mod = mixin_module::Factory.module!
mod.send(:include, mixin_module::Methods)
mod.const_set(:Result, result_factory)
mod.send(:include, *addons) unless addons.empty?
mod.send(:include, *addons.values) unless addons.empty?
mod
end

Expand Down
Loading

0 comments on commit 5f9d3d8

Please sign in to comment.