bcdd-result 0.11.0 → 0.12.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +43 -15
- data/README.md +221 -18
- data/lib/bcdd/result/callable_and_then/caller.rb +49 -0
- data/lib/bcdd/result/callable_and_then/config.rb +15 -0
- data/lib/bcdd/result/callable_and_then/error.rb +11 -0
- data/lib/bcdd/result/callable_and_then.rb +9 -0
- data/lib/bcdd/result/config/switchers/features.rb +5 -1
- data/lib/bcdd/result/config.rb +9 -3
- data/lib/bcdd/result/context/callable_and_then.rb +39 -0
- data/lib/bcdd/result/context/expectations/mixin.rb +2 -2
- data/lib/bcdd/result/context/mixin.rb +1 -1
- data/lib/bcdd/result/context/success.rb +12 -8
- data/lib/bcdd/result/context.rb +34 -16
- data/lib/bcdd/result/error.rb +20 -11
- data/lib/bcdd/result/expectations/mixin.rb +3 -3
- data/lib/bcdd/result/expectations.rb +6 -6
- data/lib/bcdd/result/mixin.rb +1 -1
- data/lib/bcdd/result/transitions/tracking/disabled.rb +14 -4
- data/lib/bcdd/result/transitions/tracking/enabled.rb +38 -18
- data/lib/bcdd/result/transitions/tree.rb +10 -6
- data/lib/bcdd/result/transitions.rb +8 -10
- data/lib/bcdd/result/version.rb +1 -1
- data/lib/bcdd/result.rb +34 -19
- data/sig/bcdd/result/callable_and_then.rbs +60 -0
- data/sig/bcdd/result/config.rbs +2 -0
- data/sig/bcdd/result/context.rbs +56 -4
- data/sig/bcdd/result/error.rbs +9 -6
- data/sig/bcdd/result/expectations.rbs +4 -4
- data/sig/bcdd/result/transitions.rbs +16 -5
- data/sig/bcdd/result.rbs +9 -5
- metadata +8 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 677f88c68eb0f745246a910bbf4a659103a5695f3c5bd74f6ac6f0ba9ff3729f
|
4
|
+
data.tar.gz: 2966e659671b84bed72b2fb7ab4daaa19a1572a5480af3c19ab2a41b720d1265
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ee78777c66384e185ff7c1c816ea9fa2787ab14cde530a2fb2a9ec943755b86846a0a03cbc2742c2530cf9487380e9d23ce47663de366483d77af4bfe1fa3c8c
|
7
|
+
data.tar.gz: 7396cf7283c4d22dd7b03be855cf6f3a8e9c15ebbf8ef5194c34de762b99e9d6a41345066f7990be5a0e47e50da4bf38811191eb06916803881a12fab7a854b1
|
data/CHANGELOG.md
CHANGED
@@ -1,41 +1,61 @@
|
|
1
1
|
- [\[Unreleased\]](#unreleased)
|
2
|
-
- [\[0.
|
2
|
+
- [\[0.12.0\] - 2024-01-07](#0120---2024-01-07)
|
3
3
|
- [Added](#added)
|
4
4
|
- [Changed](#changed)
|
5
|
-
- [\[0.
|
5
|
+
- [\[0.11.0\] - 2024-01-02](#0110---2024-01-02)
|
6
6
|
- [Added](#added-1)
|
7
|
-
- [\[0.9.1\] - 2023-12-12](#091---2023-12-12)
|
8
7
|
- [Changed](#changed-1)
|
9
|
-
|
10
|
-
- [\[0.9.0\] - 2023-12-12](#090---2023-12-12)
|
8
|
+
- [\[0.10.0\] - 2023-12-31](#0100---2023-12-31)
|
11
9
|
- [Added](#added-2)
|
10
|
+
- [\[0.9.1\] - 2023-12-12](#091---2023-12-12)
|
12
11
|
- [Changed](#changed-2)
|
13
|
-
- [
|
12
|
+
- [Fixed](#fixed)
|
13
|
+
- [\[0.9.0\] - 2023-12-12](#090---2023-12-12)
|
14
14
|
- [Added](#added-3)
|
15
15
|
- [Changed](#changed-3)
|
16
|
-
|
17
|
-
- [\[0.7.0\] - 2023-10-27](#070---2023-10-27)
|
16
|
+
- [\[0.8.0\] - 2023-12-11](#080---2023-12-11)
|
18
17
|
- [Added](#added-4)
|
19
18
|
- [Changed](#changed-4)
|
20
|
-
- [
|
19
|
+
- [Removed](#removed)
|
20
|
+
- [\[0.7.0\] - 2023-10-27](#070---2023-10-27)
|
21
21
|
- [Added](#added-5)
|
22
22
|
- [Changed](#changed-5)
|
23
|
-
- [\[0.
|
23
|
+
- [\[0.6.0\] - 2023-10-11](#060---2023-10-11)
|
24
24
|
- [Added](#added-6)
|
25
|
-
- [\[0.4.0\] - 2023-09-28](#040---2023-09-28)
|
26
|
-
- [Added](#added-7)
|
27
25
|
- [Changed](#changed-6)
|
26
|
+
- [\[0.5.0\] - 2023-10-09](#050---2023-10-09)
|
27
|
+
- [Added](#added-7)
|
28
|
+
- [\[0.4.0\] - 2023-09-28](#040---2023-09-28)
|
29
|
+
- [Added](#added-8)
|
30
|
+
- [Changed](#changed-7)
|
28
31
|
- [Removed](#removed-1)
|
29
32
|
- [\[0.3.0\] - 2023-09-26](#030---2023-09-26)
|
30
|
-
- [Added](#added-8)
|
31
|
-
- [\[0.2.0\] - 2023-09-26](#020---2023-09-26)
|
32
33
|
- [Added](#added-9)
|
34
|
+
- [\[0.2.0\] - 2023-09-26](#020---2023-09-26)
|
35
|
+
- [Added](#added-10)
|
33
36
|
- [Removed](#removed-2)
|
34
37
|
- [\[0.1.0\] - 2023-09-25](#010---2023-09-25)
|
35
|
-
- [Added](#added-
|
38
|
+
- [Added](#added-11)
|
36
39
|
|
37
40
|
## [Unreleased]
|
38
41
|
|
42
|
+
## [0.12.0] - 2024-01-07
|
43
|
+
|
44
|
+
### Added
|
45
|
+
|
46
|
+
- Add `BCDD::Result#and_then!` and `BCDD::Result::Context#and_then!` to execute a callable object (any object that responds to `#call`) to produce a result. The main difference between the `#and_then` and `#and_then!` is that the latter does not check the result source.
|
47
|
+
- **Attention:** to ensure the correct behavior, do not mix `#and_then` and `#and_then!` in the same result chain.
|
48
|
+
- This feature is turned off by default. You can enable it through the `BCDD::Result.config.feature.enable!(:and_then!)`.
|
49
|
+
- The method called by default (`:call`) can be changed through `BCDD::Result.config.and_then!.default_method_name_to_call=`.
|
50
|
+
|
51
|
+
### Changed
|
52
|
+
|
53
|
+
- **(BREAKING)** Renames the subject concept/term to `source`. When a mixin is included/extended, it defines the `Success()` and `Failure()` methods. Since the results are generated in a context (instance or singleton where the mixin was used), they will have a defined source (instance or singleton itself).
|
54
|
+
> Definition of source
|
55
|
+
>
|
56
|
+
> From dictionary:
|
57
|
+
> * a place, person, or thing from which something comes or can be obtained.
|
58
|
+
|
39
59
|
## [0.11.0] - 2024-01-02
|
40
60
|
|
41
61
|
### Added
|
@@ -45,6 +65,14 @@
|
|
45
65
|
### Changed
|
46
66
|
|
47
67
|
- **(BREAKING)** Rename halted concept to terminal. Failures are terminal by default, but you can make a success terminal by enabling the `:continue` addon.
|
68
|
+
> Definition of terminal
|
69
|
+
>
|
70
|
+
> From dictionary:
|
71
|
+
> * of, forming, or situated at the end or extremity of something.
|
72
|
+
> * the end of a railroad or other transport route, or a station at such a point.
|
73
|
+
>
|
74
|
+
> From Wikipedia:
|
75
|
+
> * A "terminus" or "terminal" is a station at the end of a railway line.
|
48
76
|
|
49
77
|
- **(BREAKING)** Rename `BCDD::Result::Context::Success#and_expose` halted keyword argument to `terminal`.
|
50
78
|
|
data/README.md
CHANGED
@@ -78,6 +78,12 @@ Use it to enable the [Railway Oriented Programming](https://fsharpforfunandprofi
|
|
78
78
|
- [`config.pattern_matching.disable!(:nil_as_valid_value_checking)`](#configpattern_matchingdisablenil_as_valid_value_checking)
|
79
79
|
- [`config.feature.disable!(:expectations)`](#configfeaturedisableexpectations)
|
80
80
|
- [`BCDD::Result.config`](#bcddresultconfig)
|
81
|
+
- [`BCDD::Result#and_then!`](#bcddresultand_then)
|
82
|
+
- [Dependency Injection](#dependency-injection-1)
|
83
|
+
- [Configuration](#configuration-1)
|
84
|
+
- [Analysis: Why is `and_then!` an Anti-pattern?](#analysis-why-is-and_then-an-anti-pattern)
|
85
|
+
- [`#and_then` versus `#and_then!`](#and_then-versus-and_then)
|
86
|
+
- [Analysis: Why is `#and_then` the antidote/standard?](#analysis-why-is-and_then-the-antidotestandard)
|
81
87
|
- [About](#about)
|
82
88
|
- [Development](#development)
|
83
89
|
- [Contributing](#contributing)
|
@@ -649,9 +655,9 @@ Divide.call(2, 2)
|
|
649
655
|
|
650
656
|
This method generates a module that any object can include or extend. It adds two methods to the target object: `Success()` and `Failure()`.
|
651
657
|
|
652
|
-
The main difference between these methods and `BCDD::Result::Success()`/`BCDD::Result::Failure()` is that the former will utilize the target object (which has received the include/extend) as the result's
|
658
|
+
The main difference between these methods and `BCDD::Result::Success()`/`BCDD::Result::Failure()` is that the former will utilize the target object (which has received the include/extend) as the result's source.
|
653
659
|
|
654
|
-
Because the result has a
|
660
|
+
Because the result has a source, the `#and_then` method can call methods from it.
|
655
661
|
|
656
662
|
##### Class example (Instance Methods)
|
657
663
|
|
@@ -746,9 +752,9 @@ Divide.call(4, '2') #<BCDD::Result::Failure type=:invalid_arg value="arg2 must b
|
|
746
752
|
|
747
753
|
To use the `#and_then` method to call methods, they must use `Success()` and `Failure()` to produce the results.
|
748
754
|
|
749
|
-
If you try to use `BCDD::Result::
|
755
|
+
If you try to use `BCDD::Result::Success()`/`BCDD::Result::Failure()`, or results from another `BCDD::Result.mixin` instance with `#and_then`, it will raise an error because the sources are different.
|
750
756
|
|
751
|
-
**Note:** You can still use the block syntax, but all the results must be produced by the
|
757
|
+
**Note:** You can still use the block syntax, but all the results must be produced by the source's `Success()` and `Failure()` methods.
|
752
758
|
|
753
759
|
```ruby
|
754
760
|
module ValidateNonzero
|
@@ -794,13 +800,13 @@ Look at the error produced by the code above:
|
|
794
800
|
```ruby
|
795
801
|
Divide.call(2, 0)
|
796
802
|
|
797
|
-
# You cannot call #and_then and return a result that does not belong to the
|
798
|
-
# Expected
|
799
|
-
# Given
|
803
|
+
# You cannot call #and_then and return a result that does not belong to the same source! (BCDD::Result::Error::InvalidResultSource)
|
804
|
+
# Expected source: Divide
|
805
|
+
# Given source: ValidateNonzero
|
800
806
|
# Given result: #<BCDD::Result::Failure type=:division_by_zero value="arg2 must not be zero">
|
801
807
|
```
|
802
808
|
|
803
|
-
In order to fix this, you must handle the result produced by `ValidateNonzero.call()` and return a result that belongs to the
|
809
|
+
In order to fix this, you must handle the result produced by `ValidateNonzero.call()` and return a result that belongs to the same source.
|
804
810
|
|
805
811
|
```ruby
|
806
812
|
module ValidateNonzero
|
@@ -832,7 +838,8 @@ module Divide
|
|
832
838
|
end
|
833
839
|
|
834
840
|
def validate_nonzero(numbers)
|
835
|
-
# In this case we are handling the
|
841
|
+
# In this case we are handling the result from other source
|
842
|
+
# and returning our own
|
836
843
|
ValidateNonzero.call(numbers).handle do |on|
|
837
844
|
on.success { |numbers| Success(:ok, numbers) }
|
838
845
|
|
@@ -858,8 +865,8 @@ Divide.call(2, 0)
|
|
858
865
|
|
859
866
|
##### Dependency Injection
|
860
867
|
|
861
|
-
The `BCDD::Result#and_then` accepts a second argument that will be used to share a value with the
|
862
|
-
To receive this argument, the
|
868
|
+
The `BCDD::Result#and_then` accepts a second argument that will be used to share a value with the source's method.
|
869
|
+
To receive this argument, the source's method must have an arity of two, where the first argument will be the result value and the second will be the injected value.
|
863
870
|
|
864
871
|
```ruby
|
865
872
|
require 'logger'
|
@@ -1863,7 +1870,7 @@ result.transitions
|
|
1863
1870
|
:parent=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
|
1864
1871
|
:current=>{:id=>1, :name=>"Division", :desc=>"divide two numbers"},
|
1865
1872
|
:result=>{:kind=>:success, :type=>:continued, :value=>[20, 2]},
|
1866
|
-
:and_then=>{:type=>:method, :arg=>nil, :
|
1873
|
+
:and_then=>{:type=>:method, :arg=>nil, :source=><Division:0x0000000106099028>, :method_name=>:require_numbers},
|
1867
1874
|
:time=>2024-01-02 03:35:11.248558 UTC
|
1868
1875
|
},
|
1869
1876
|
{
|
@@ -1871,7 +1878,7 @@ result.transitions
|
|
1871
1878
|
:parent=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
|
1872
1879
|
:current=>{:id=>1, :name=>"Division", :desc=>"divide two numbers"},
|
1873
1880
|
:result=>{:kind=>:success, :type=>:continued, :value=>[20, 2]},
|
1874
|
-
:and_then=>{:type=>:method, :arg=>nil, :
|
1881
|
+
:and_then=>{:type=>:method, :arg=>nil, :source=><Division:0x0000000106099028>, :method_name=>:check_for_zeros},
|
1875
1882
|
:time=>2024-01-02 03:35:11.248587 UTC
|
1876
1883
|
},
|
1877
1884
|
{
|
@@ -1879,7 +1886,7 @@ result.transitions
|
|
1879
1886
|
:parent=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
|
1880
1887
|
:current=>{:id=>1, :name=>"Division", :desc=>"divide two numbers"},
|
1881
1888
|
:result=>{:kind=>:success, :type=>:division_completed, :value=>10},
|
1882
|
-
:and_then=>{:type=>:method, :arg=>nil, :
|
1889
|
+
:and_then=>{:type=>:method, :arg=>nil, :source=><Division:0x0000000106099028>, :method_name=>:divide},
|
1883
1890
|
:time=>2024-01-02 03:35:11.248607 UTC
|
1884
1891
|
},
|
1885
1892
|
{
|
@@ -1895,7 +1902,7 @@ result.transitions
|
|
1895
1902
|
:parent=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
|
1896
1903
|
:current=>{:id=>2, :name=>"Division", :desc=>"divide two numbers"},
|
1897
1904
|
:result=>{:kind=>:success, :type=>:continued, :value=>[10, 2]},
|
1898
|
-
:and_then=>{:type=>:method, :arg=>nil, :
|
1905
|
+
:and_then=>{:type=>:method, :arg=>nil, :source=><Division:0x0000000106097ed0>, :method_name=>:require_numbers},
|
1899
1906
|
:time=>2024-01-02 03:35:11.248661 UTC
|
1900
1907
|
},
|
1901
1908
|
{
|
@@ -1903,7 +1910,7 @@ result.transitions
|
|
1903
1910
|
:parent=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
|
1904
1911
|
:current=>{:id=>2, :name=>"Division", :desc=>"divide two numbers"},
|
1905
1912
|
:result=>{:kind=>:success, :type=>:continued, :value=>[10, 2]},
|
1906
|
-
:and_then=>{:type=>:method, :arg=>nil, :
|
1913
|
+
:and_then=>{:type=>:method, :arg=>nil, :source=><Division:0x0000000106097ed0>, :method_name=>:check_for_zeros},
|
1907
1914
|
:time=>2024-01-02 03:35:11.248672 UTC
|
1908
1915
|
},
|
1909
1916
|
{
|
@@ -1911,7 +1918,7 @@ result.transitions
|
|
1911
1918
|
:parent=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
|
1912
1919
|
:current=>{:id=>2, :name=>"Division", :desc=>"divide two numbers"},
|
1913
1920
|
:result=>{:kind=>:success, :type=>:division_completed, :value=>5},
|
1914
|
-
:and_then=>{:type=>:method, :arg=>nil, :
|
1921
|
+
:and_then=>{:type=>:method, :arg=>nil, :source=><Division:0x0000000106097ed0>, :method_name=>:divide},
|
1915
1922
|
:time=>2024-01-02 03:35:11.248682 UTC
|
1916
1923
|
},
|
1917
1924
|
{
|
@@ -2078,10 +2085,206 @@ BCDD::Result.config.feature.options
|
|
2078
2085
|
# "BCDD::Result",
|
2079
2086
|
# "BCDD::Result::Context"
|
2080
2087
|
# ]
|
2081
|
-
# }
|
2088
|
+
# },
|
2089
|
+
# :and_then!=>{
|
2090
|
+
# :enabled=>false,
|
2091
|
+
# :affects=>[
|
2092
|
+
# "BCDD::Result",
|
2093
|
+
# "BCDD::Result::Context"
|
2094
|
+
# ]
|
2095
|
+
# },
|
2082
2096
|
# }
|
2083
2097
|
```
|
2084
2098
|
|
2099
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
2100
|
+
|
2101
|
+
## `BCDD::Result#and_then!`
|
2102
|
+
|
2103
|
+
In the Ruby ecosystem, several gems facilitate operation composition using classes and modules. Two notable examples are the `interactor` gem and the `u-case` gem.
|
2104
|
+
|
2105
|
+
**`interactor` gem example**
|
2106
|
+
|
2107
|
+
```ruby
|
2108
|
+
class PlaceOrder
|
2109
|
+
include Interactor::Organizer
|
2110
|
+
|
2111
|
+
organize CreateOrder,
|
2112
|
+
PayOrder,
|
2113
|
+
SendOrderConfirmation,
|
2114
|
+
NotifyAdmins
|
2115
|
+
end
|
2116
|
+
```
|
2117
|
+
|
2118
|
+
**`u-case` gem example**
|
2119
|
+
|
2120
|
+
```ruby
|
2121
|
+
class PlaceOrder < Micro::Case
|
2122
|
+
flow CreateOrder, PayOrder, SendOrderConfirmation, NotifyAdmins
|
2123
|
+
end
|
2124
|
+
|
2125
|
+
# Alternative approach
|
2126
|
+
class PlaceOrder < Micro::Case
|
2127
|
+
def call!
|
2128
|
+
call(CreateOrder)
|
2129
|
+
.then(PayOrder)
|
2130
|
+
.then(SendOrderConfirmation)
|
2131
|
+
.then(NotifyAdmins)
|
2132
|
+
end
|
2133
|
+
end
|
2134
|
+
```
|
2135
|
+
|
2136
|
+
To facilitate migration for users accustomed to the above approaches, `bcdd-result` includes the `BCDD::Result#and_then!`/`BCDD::Result::Context#and_then!` methods, which will invoke the method `call` of the given operation and expect it to return a `BCDD::Result`/`BCDD::Result::Context` object.
|
2137
|
+
|
2138
|
+
```ruby
|
2139
|
+
BCDD::Result.configure do |config|
|
2140
|
+
config.feature.enable!(:and_then!)
|
2141
|
+
end
|
2142
|
+
|
2143
|
+
class PlaceOrder
|
2144
|
+
include BCDD::Result::Context.mixin
|
2145
|
+
|
2146
|
+
def call(**input)
|
2147
|
+
Given(input)
|
2148
|
+
.and_then!(CreateOrder.new)
|
2149
|
+
.and_then!(PayOrder.new)
|
2150
|
+
.and_then!(SendOrderConfirmation.new)
|
2151
|
+
.and_then!(NotifyAdmins.new)
|
2152
|
+
end
|
2153
|
+
end
|
2154
|
+
```
|
2155
|
+
|
2156
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
2157
|
+
|
2158
|
+
#### Dependency Injection
|
2159
|
+
|
2160
|
+
Like `#and_then`, `#and_then!` also supports an additional argument for dependency injection.
|
2161
|
+
|
2162
|
+
**In BCDD::Result**
|
2163
|
+
|
2164
|
+
```ruby
|
2165
|
+
class PlaceOrder
|
2166
|
+
include BCDD::Result.mixin
|
2167
|
+
|
2168
|
+
def call(input, logger:)
|
2169
|
+
Given(input)
|
2170
|
+
.and_then!(CreateOrder.new, logger)
|
2171
|
+
# Further method chaining...
|
2172
|
+
end
|
2173
|
+
end
|
2174
|
+
```
|
2175
|
+
|
2176
|
+
**In BCDD::Result::Context**
|
2177
|
+
|
2178
|
+
```ruby
|
2179
|
+
class PlaceOrder
|
2180
|
+
include BCDD::Result::Context.mixin
|
2181
|
+
|
2182
|
+
def call(logger:, **input)
|
2183
|
+
Given(input)
|
2184
|
+
.and_then!(CreateOrder.new, logger:)
|
2185
|
+
# Further method chaining...
|
2186
|
+
end
|
2187
|
+
end
|
2188
|
+
```
|
2189
|
+
|
2190
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
2191
|
+
|
2192
|
+
#### Configuration
|
2193
|
+
|
2194
|
+
```ruby
|
2195
|
+
BCDD::Result.configure do |config|
|
2196
|
+
config.feature.enable!(:and_then!)
|
2197
|
+
|
2198
|
+
config.and_then!.default_method_name_to_call = :perform
|
2199
|
+
end
|
2200
|
+
```
|
2201
|
+
|
2202
|
+
**Explanation:**
|
2203
|
+
|
2204
|
+
- `enable!(:and_then!)`: Activates the `and_then!` feature.
|
2205
|
+
|
2206
|
+
- `default_method_name_to_call`: Sets a default method other than `:call` for `and_then!`.
|
2207
|
+
|
2208
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
2209
|
+
|
2210
|
+
#### Analysis: Why is `and_then!` an Anti-pattern?
|
2211
|
+
|
2212
|
+
The `and_then!` approach, despite its brevity, introduces several issues:
|
2213
|
+
|
2214
|
+
- **Lack of Clarity:** The input/output relationship between the steps is not apparent.
|
2215
|
+
|
2216
|
+
- **Steps Coupling:** Each operation becomes interdependent (high coupling), complicating implementation and compromising the reusability of these operations.
|
2217
|
+
|
2218
|
+
We recommend cautious use of `#and_then!`. Due to these issues, it is turned off by default and considered an antipattern.
|
2219
|
+
|
2220
|
+
It should be a temporary solution, primarily for assisting in migration from another to gem to `bcdd-result`.
|
2221
|
+
|
2222
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
2223
|
+
|
2224
|
+
#### `#and_then` versus `#and_then!`
|
2225
|
+
|
2226
|
+
The main difference between the `#and_then` and `#and_then!` is that the latter does not check the result source. However, as a drawback, the result source will change.
|
2227
|
+
|
2228
|
+
Attention: to ensure the correct behavior, do not mix `#and_then` and `#and_then!` in the same result chain.
|
2229
|
+
|
2230
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
2231
|
+
|
2232
|
+
#### Analysis: Why is `#and_then` the antidote/standard?
|
2233
|
+
|
2234
|
+
The `BCDD::Result#and_then`/`BCDD::Result::Context#and_then` methods diverge from the above approach by requiring explicit invocation and mapping of the outcomes at each process step. This approach has the following advantages:
|
2235
|
+
|
2236
|
+
- **Clarity:** The input/output relationship between the steps is apparent and highly understandable.
|
2237
|
+
|
2238
|
+
- **Steps uncoupling:** Each operation becomes independent (low coupling). You can even map a failure result to a success (and vice versa).
|
2239
|
+
|
2240
|
+
See this example to understand what your code should look like:
|
2241
|
+
|
2242
|
+
```ruby
|
2243
|
+
class PlaceOrder
|
2244
|
+
include BCDD::Result::Context.mixin(config: { addon: { continue: true } })
|
2245
|
+
|
2246
|
+
def call(**input)
|
2247
|
+
Given(input)
|
2248
|
+
.and_then(:create_order)
|
2249
|
+
.and_then(:pay_order)
|
2250
|
+
.and_then(:send_order_confirmation)
|
2251
|
+
.and_then(:notify_admins)
|
2252
|
+
.and_expose(:order_placed, %i[order])
|
2253
|
+
end
|
2254
|
+
|
2255
|
+
private
|
2256
|
+
|
2257
|
+
def create_order(customer:, products:)
|
2258
|
+
CreateOrder.new.call(customer:, products:).handle do |on|
|
2259
|
+
on.success { |output| Continue(order: output[:order]) }
|
2260
|
+
on.failure { |error| Failure(:order_creation_failed, error:) }
|
2261
|
+
end
|
2262
|
+
end
|
2263
|
+
|
2264
|
+
def pay_order(customer:, order:, payment_method:, **)
|
2265
|
+
PayOrder.new.call(customer:, payment_method:, order:).handle do |on|
|
2266
|
+
on.success { |output| Continue(payment: output[:payment]) }
|
2267
|
+
on.failure { |error| Failure(:order_payment_failed, error:) }
|
2268
|
+
end
|
2269
|
+
end
|
2270
|
+
|
2271
|
+
def send_order_confirmation(customer:, order:, payment:, **)
|
2272
|
+
SendOrderConfirmation.new.call(customer:, order:, payment:).handle do |on|
|
2273
|
+
on.success { Continue() }
|
2274
|
+
on.failure { |error| Failure(:order_confirmation_failed, error:) }
|
2275
|
+
end
|
2276
|
+
end
|
2277
|
+
|
2278
|
+
def notify_admins(customer:, order:, payment:, **)
|
2279
|
+
NotifyAdmins.new.call(customer:, order:, payment:)
|
2280
|
+
|
2281
|
+
Continue()
|
2282
|
+
end
|
2283
|
+
end
|
2284
|
+
```
|
2285
|
+
|
2286
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
2287
|
+
|
2085
2288
|
## About
|
2086
2289
|
|
2087
2290
|
[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.
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class BCDD::Result
|
4
|
+
class CallableAndThen::Caller
|
5
|
+
def self.call(source, value:, injected_value:, method_name:)
|
6
|
+
method = callable_method(source, method_name)
|
7
|
+
|
8
|
+
Transitions.tracking.record_and_then(method, injected_value, source) do
|
9
|
+
result =
|
10
|
+
if source.is_a?(::Proc)
|
11
|
+
call_proc!(source, value, injected_value)
|
12
|
+
else
|
13
|
+
call_method!(source, method, value, injected_value)
|
14
|
+
end
|
15
|
+
|
16
|
+
ensure_result_object(source, value, result)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.call_proc!(source, value, injected_value)
|
21
|
+
case source.arity
|
22
|
+
when 1 then source.call(value)
|
23
|
+
when 2 then source.call(value, injected_value)
|
24
|
+
else raise CallableAndThen::Error::InvalidArity.build(source: source, method: :call, arity: '1..2')
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.call_method!(source, method, value, injected_value)
|
29
|
+
case method.arity
|
30
|
+
when 1 then source.send(method.name, value)
|
31
|
+
when 2 then source.send(method.name, value, injected_value)
|
32
|
+
else raise CallableAndThen::Error::InvalidArity.build(source: source, method: method.name, arity: '1..2')
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.callable_method(source, method_name)
|
37
|
+
source.method(method_name || Config.instance.and_then!.default_method_name_to_call)
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.ensure_result_object(source, _value, result)
|
41
|
+
return result if result.is_a?(::BCDD::Result)
|
42
|
+
|
43
|
+
raise Error::UnexpectedOutcome.build(outcome: result, origin: source)
|
44
|
+
end
|
45
|
+
|
46
|
+
private_class_method :new, :allocate
|
47
|
+
private_class_method :call_proc!, :call_method!, :callable_method, :ensure_result_object
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class BCDD::Result
|
4
|
+
class CallableAndThen::Config
|
5
|
+
attr_accessor :default_method_name_to_call
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
self.default_method_name_to_call = :call
|
9
|
+
end
|
10
|
+
|
11
|
+
def options
|
12
|
+
{ default_method_name_to_call: default_method_name_to_call }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class BCDD::Result
|
4
|
+
class CallableAndThen::Error < Error
|
5
|
+
class InvalidArity < self
|
6
|
+
def self.build(source:, method:, arity:)
|
7
|
+
new("Invalid arity for #{source.class}##{method} method. Expected arity: #{arity}")
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -10,7 +10,11 @@ class BCDD::Result
|
|
10
10
|
},
|
11
11
|
transitions: {
|
12
12
|
default: true,
|
13
|
-
affects: %w[BCDD::Result BCDD::Result::Context]
|
13
|
+
affects: %w[BCDD::Result BCDD::Result::Context BCDD::Result::Expectations BCDD::Result::Context::Expectations]
|
14
|
+
},
|
15
|
+
and_then!: {
|
16
|
+
default: false,
|
17
|
+
affects: %w[BCDD::Result BCDD::Result::Context BCDD::Result::Expectations BCDD::Result::Context::Expectations]
|
14
18
|
}
|
15
19
|
}.transform_values!(&:freeze).freeze
|
16
20
|
|
data/lib/bcdd/result/config.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'singleton'
|
4
|
-
|
5
3
|
require_relative 'config/options'
|
6
4
|
require_relative 'config/switcher'
|
7
5
|
require_relative 'config/switchers/addons'
|
@@ -20,6 +18,11 @@ class BCDD::Result
|
|
20
18
|
@feature = Features.switcher
|
21
19
|
@constant_alias = ConstantAliases.switcher
|
22
20
|
@pattern_matching = PatternMatching.switcher
|
21
|
+
@and_then_ = CallableAndThen::Config.new
|
22
|
+
end
|
23
|
+
|
24
|
+
def and_then!
|
25
|
+
@and_then_
|
23
26
|
end
|
24
27
|
|
25
28
|
def freeze
|
@@ -27,6 +30,7 @@ class BCDD::Result
|
|
27
30
|
feature.freeze
|
28
31
|
constant_alias.freeze
|
29
32
|
pattern_matching.freeze
|
33
|
+
and_then!.freeze
|
30
34
|
|
31
35
|
super
|
32
36
|
end
|
@@ -45,7 +49,9 @@ class BCDD::Result
|
|
45
49
|
end
|
46
50
|
|
47
51
|
def inspect
|
48
|
-
"#<#{self.class.name}
|
52
|
+
"#<#{self.class.name} " \
|
53
|
+
"options=#{options.keys.sort.inspect} " \
|
54
|
+
"and_then!=#{and_then!.options.inspect}>"
|
49
55
|
end
|
50
56
|
end
|
51
57
|
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class BCDD::Result
|
4
|
+
module Context::CallableAndThen
|
5
|
+
class Caller < CallableAndThen::Caller
|
6
|
+
module KeyArgs
|
7
|
+
def self.parameters?(source)
|
8
|
+
parameters = source.parameters.map(&:first)
|
9
|
+
|
10
|
+
!parameters.empty? && parameters.all?(/\Akey/)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.invalid_arity(source, method)
|
14
|
+
CallableAndThen::Error::InvalidArity.build(source: source, method: method, arity: 'only keyword args')
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.call_proc!(source, value, _injected_value)
|
19
|
+
return source.call(**value) if KeyArgs.parameters?(source)
|
20
|
+
|
21
|
+
raise KeyArgs.invalid_arity(source, :call)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.call_method!(source, method, value, _injected_value)
|
25
|
+
return source.send(method.name, **value) if KeyArgs.parameters?(method)
|
26
|
+
|
27
|
+
raise KeyArgs.invalid_arity(source, method.name)
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.ensure_result_object(source, value, result)
|
31
|
+
return result.tap { result.send(:acc).then { _1.merge!(value.merge(_1)) } } if result.is_a?(Context)
|
32
|
+
|
33
|
+
raise Error::UnexpectedOutcome.build(outcome: result, origin: source, expected: Context::EXPECTED_OUTCOME)
|
34
|
+
end
|
35
|
+
|
36
|
+
private_class_method :call_proc!, :call_method!
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -9,7 +9,7 @@ class BCDD::Result::Context
|
|
9
9
|
module Addons
|
10
10
|
module Continue
|
11
11
|
private def Continue(**value)
|
12
|
-
Success.new(type: :continued, value: value,
|
12
|
+
Success.new(type: :continued, value: value, source: self)
|
13
13
|
end
|
14
14
|
end
|
15
15
|
|
@@ -17,7 +17,7 @@ class BCDD::Result::Context
|
|
17
17
|
private def Given(*values)
|
18
18
|
value = values.map(&:to_h).reduce({}) { |acc, val| acc.merge(val) }
|
19
19
|
|
20
|
-
Success.new(type: :given, value: value,
|
20
|
+
Success.new(type: :given, value: value, source: self)
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
@@ -14,7 +14,7 @@ class BCDD::Result::Context
|
|
14
14
|
end
|
15
15
|
|
16
16
|
private def _ResultAs(kind_class, type, value, terminal: nil)
|
17
|
-
kind_class.new(type: type, value: value,
|
17
|
+
kind_class.new(type: type, value: value, source: self, terminal: terminal)
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
@@ -1,15 +1,19 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
class BCDD::Result
|
4
|
-
|
3
|
+
class BCDD::Result
|
4
|
+
class Context::Success < Context
|
5
|
+
include ::BCDD::Result::Success::Methods
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
def and_expose(type, keys, terminal: true)
|
8
|
+
unless keys.is_a?(::Array) && !keys.empty? && keys.all?(::Symbol)
|
9
|
+
raise ::ArgumentError, 'keys must be an Array of Symbols'
|
10
|
+
end
|
11
|
+
|
12
|
+
Transitions.tracking.reset_and_then!
|
10
13
|
|
11
|
-
|
14
|
+
exposed_value = acc.merge(value).slice(*keys)
|
12
15
|
|
13
|
-
|
16
|
+
self.class.new(type: type, value: exposed_value, source: source, terminal: terminal)
|
17
|
+
end
|
14
18
|
end
|
15
19
|
end
|