bcdd-result 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|