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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c0043be8de0b26bac0d803f067537fbc65e4a54f3a38a2644c01bd987d87c7b5
4
- data.tar.gz: f152ac78abb8847721f2b2ec3c3fd1db66016c421f5348f038a79501a86f761a
3
+ metadata.gz: 677f88c68eb0f745246a910bbf4a659103a5695f3c5bd74f6ac6f0ba9ff3729f
4
+ data.tar.gz: 2966e659671b84bed72b2fb7ab4daaa19a1572a5480af3c19ab2a41b720d1265
5
5
  SHA512:
6
- metadata.gz: 17abccc11ad3c7ccd2cedc6f9634659decee457e08ef1c8dfb13d59d2afc7390fdeb6fc1d48749362d691815c41b789197d2bf79b5f57f9e7e07ad770ddbb304
7
- data.tar.gz: 2e0b67fbc99ef2a3a8187a2d42212e8cbadca22a2c2625c3be8e7bbfb5d2b92689b9a27b7592a295863e89437e912d34cb8f6e5a2e77e9c0afdbcbf7c4591bd6
6
+ metadata.gz: ee78777c66384e185ff7c1c816ea9fa2787ab14cde530a2fb2a9ec943755b86846a0a03cbc2742c2530cf9487380e9d23ce47663de366483d77af4bfe1fa3c8c
7
+ data.tar.gz: 7396cf7283c4d22dd7b03be855cf6f3a8e9c15ebbf8ef5194c34de762b99e9d6a41345066f7990be5a0e47e50da4bf38811191eb06916803881a12fab7a854b1
data/CHANGELOG.md CHANGED
@@ -1,41 +1,61 @@
1
1
  - [\[Unreleased\]](#unreleased)
2
- - [\[0.11.0\] - 2024-01-02](#0110---2024-01-02)
2
+ - [\[0.12.0\] - 2024-01-07](#0120---2024-01-07)
3
3
  - [Added](#added)
4
4
  - [Changed](#changed)
5
- - [\[0.10.0\] - 2023-12-31](#0100---2023-12-31)
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
- - [Fixed](#fixed)
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
- - [\[0.8.0\] - 2023-12-11](#080---2023-12-11)
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
- - [Removed](#removed)
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
- - [\[0.6.0\] - 2023-10-11](#060---2023-10-11)
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.5.0\] - 2023-10-09](#050---2023-10-09)
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-10)
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 subject.
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 subject, the `#and_then` method can call methods from it.
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::Subject()`/`BCDD::Result::Failure()`, or results from another `BCDD::Result.mixin` instance with `#and_then`, it will raise an error because the subjects will be different.
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 subject's `Success()` and `Failure()` methods.
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 subject! (BCDD::Result::Error::InvalidResultSubject)
798
- # Expected subject: Divide
799
- # Given subject: ValidateNonzero
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 subject.
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 other subject result and returning our own
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 subject's method.
862
- To receive this argument, the subject's method must have an arity of two, where the first argument will be the result value and the second will be the shared value.
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, :subject=><Division:0x0000000106099028>, :method_name=>:require_numbers},
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, :subject=><Division:0x0000000106099028>, :method_name=>:check_for_zeros},
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, :subject=><Division:0x0000000106099028>, :method_name=>:divide},
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, :subject=><Division:0x0000000106097ed0>, :method_name=>:require_numbers},
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, :subject=><Division:0x0000000106097ed0>, :method_name=>:check_for_zeros},
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, :subject=><Division:0x0000000106097ed0>, :method_name=>:divide},
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">⬆️ &nbsp;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">⬆️ &nbsp;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">⬆️ &nbsp;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">⬆️ &nbsp;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">⬆️ &nbsp;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">⬆️ &nbsp;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">⬆️ &nbsp;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
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result
4
+ module CallableAndThen
5
+ require_relative 'callable_and_then/error'
6
+ require_relative 'callable_and_then/config'
7
+ require_relative 'callable_and_then/caller'
8
+ end
9
+ 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
 
@@ -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} options=#{options.keys.sort.inspect}>"
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, subject: self)
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, subject: self)
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, subject: self, terminal: terminal)
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::Context::Success < BCDD::Result::Context
4
- include ::BCDD::Result::Success::Methods
3
+ class BCDD::Result
4
+ class Context::Success < Context
5
+ include ::BCDD::Result::Success::Methods
5
6
 
6
- def and_expose(type, keys, terminal: true)
7
- unless keys.is_a?(::Array) && !keys.empty? && keys.all?(::Symbol)
8
- raise ::ArgumentError, 'keys must be an Array of Symbols'
9
- end
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
- exposed_value = acc.merge(value).slice(*keys)
14
+ exposed_value = acc.merge(value).slice(*keys)
12
15
 
13
- self.class.new(type: type, value: exposed_value, subject: subject, terminal: terminal)
16
+ self.class.new(type: type, value: exposed_value, source: source, terminal: terminal)
17
+ end
14
18
  end
15
19
  end