bcdd-result 0.6.0 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +31 -11
- data/CHANGELOG.md +148 -0
- data/README.md +849 -242
- data/Rakefile +9 -3
- data/Steepfile +1 -1
- data/lib/bcdd/result/config/constant_alias.rb +33 -0
- data/lib/bcdd/result/config/options.rb +26 -0
- data/lib/bcdd/result/config/switcher.rb +82 -0
- data/lib/bcdd/result/config.rb +71 -0
- data/lib/bcdd/result/context/expectations/mixin.rb +23 -0
- data/lib/bcdd/result/context/expectations.rb +25 -0
- data/lib/bcdd/result/context/failure.rb +9 -0
- data/lib/bcdd/result/context/mixin.rb +41 -0
- data/lib/bcdd/result/context/success.rb +15 -0
- data/lib/bcdd/result/context.rb +74 -0
- data/lib/bcdd/result/{expectations/contract → contract}/disabled.rb +2 -2
- data/lib/bcdd/result/{expectations → contract}/error.rb +5 -3
- data/lib/bcdd/result/{expectations/contract → contract}/evaluator.rb +2 -2
- data/lib/bcdd/result/{expectations/contract → contract}/for_types.rb +2 -2
- data/lib/bcdd/result/contract/for_types_and_values.rb +44 -0
- data/lib/bcdd/result/contract/interface.rb +21 -0
- data/lib/bcdd/result/{expectations → contract}/type_checker.rb +1 -1
- data/lib/bcdd/result/contract.rb +33 -0
- data/lib/bcdd/result/error.rb +7 -9
- data/lib/bcdd/result/expectations/mixin.rb +19 -12
- data/lib/bcdd/result/expectations.rb +51 -36
- data/lib/bcdd/result/failure/methods.rb +21 -0
- data/lib/bcdd/result/failure.rb +2 -16
- data/lib/bcdd/result/mixin.rb +26 -8
- data/lib/bcdd/result/success/methods.rb +21 -0
- data/lib/bcdd/result/success.rb +2 -16
- data/lib/bcdd/result/version.rb +1 -1
- data/lib/bcdd/result.rb +17 -4
- data/lib/bcdd-result.rb +3 -0
- data/sig/bcdd/result.rbs +340 -88
- metadata +27 -16
- data/lib/bcdd/result/expectations/contract/for_types_and_values.rb +0 -37
- data/lib/bcdd/result/expectations/contract/interface.rb +0 -21
- data/lib/bcdd/result/expectations/contract.rb +0 -25
- data/lib/result.rb +0 -5
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
<p align="center">
|
2
2
|
<h1 align="center" id="-bcddresult">🔀 BCDD::Result</h1>
|
3
|
-
<p align="center"><i>Empower Ruby apps with
|
3
|
+
<p align="center"><i>Empower Ruby apps with pragmatic use of Result monad, Railway Oriented Programming, and B/CDD.</i></p>
|
4
4
|
<p align="center">
|
5
5
|
<img src="https://img.shields.io/badge/ruby->%3D%202.7.0-ruby.svg?colorA=99004d&colorB=cc0066" alt="Ruby">
|
6
6
|
<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>
|
@@ -23,7 +23,7 @@ Use it to enable the [Railway Oriented Programming](https://fsharpforfunandprofi
|
|
23
23
|
- [`BCDD::Result` *versus* `Result`](#bcddresult-versus-result)
|
24
24
|
- [Reference](#reference)
|
25
25
|
- [Result Attributes](#result-attributes)
|
26
|
-
- [
|
26
|
+
- [Checking types with `result.success?` or `result.failure?`](#checking-types-with-resultsuccess-or-resultfailure)
|
27
27
|
- [Result Hooks](#result-hooks)
|
28
28
|
- [`result.on`](#resulton)
|
29
29
|
- [`result.on_type`](#resulton_type)
|
@@ -35,6 +35,9 @@ Use it to enable the [Railway Oriented Programming](https://fsharpforfunandprofi
|
|
35
35
|
- [`result.value_or`](#resultvalue_or)
|
36
36
|
- [Result Data](#result-data)
|
37
37
|
- [`result.data`](#resultdata)
|
38
|
+
- [Pattern Matching](#pattern-matching)
|
39
|
+
- [`Array`/`Find` patterns](#arrayfind-patterns)
|
40
|
+
- [`Hash` patterns](#hash-patterns)
|
38
41
|
- [Railway Oriented Programming](#railway-oriented-programming)
|
39
42
|
- [`result.and_then`](#resultand_then)
|
40
43
|
- [`BCDD::Result.mixin`](#bcddresultmixin)
|
@@ -42,12 +45,8 @@ Use it to enable the [Railway Oriented Programming](https://fsharpforfunandprofi
|
|
42
45
|
- [Module example (Singleton Methods)](#module-example-singleton-methods)
|
43
46
|
- [Important Requirement](#important-requirement)
|
44
47
|
- [Dependency Injection](#dependency-injection)
|
45
|
-
- [
|
46
|
-
- [
|
47
|
-
- [`Array`/`Find` patterns](#arrayfind-patterns)
|
48
|
-
- [`Hash` patterns](#hash-patterns)
|
49
|
-
- [BCDD::Result::Expectations](#bcddresultexpectations)
|
50
|
-
- [`BCDD::Result::Expectations`](#bcddresultexpectations-1)
|
48
|
+
- [Add-ons](#add-ons)
|
49
|
+
- [`BCDD::Result::Expectations`](#bcddresultexpectations)
|
51
50
|
- [Standalone *versus* Mixin mode](#standalone-versus-mixin-mode)
|
52
51
|
- [Type checking - Result Hooks](#type-checking---result-hooks)
|
53
52
|
- [`#success?` and `#failure?`](#success-and-failure)
|
@@ -60,7 +59,22 @@ Use it to enable the [Railway Oriented Programming](https://fsharpforfunandprofi
|
|
60
59
|
- [Value checking - Result Creation](#value-checking---result-creation)
|
61
60
|
- [Success()](#success)
|
62
61
|
- [Failure()](#failure)
|
63
|
-
|
62
|
+
- [Pattern Matching Support](#pattern-matching-support)
|
63
|
+
- [`BCDD::Result::Expectations.mixin` add-ons](#bcddresultexpectationsmixin-add-ons)
|
64
|
+
- [`BCDD::Result::Context`](#bcddresultcontext)
|
65
|
+
- [Defining successes and failures](#defining-successes-and-failures)
|
66
|
+
- [`BCDD::Result::Context.mixin`](#bcddresultcontextmixin)
|
67
|
+
- [Class example (Instance Methods)](#class-example-instance-methods-1)
|
68
|
+
- [`and_expose`](#and_expose)
|
69
|
+
- [Module example (Singleton Methods)](#module-example-singleton-methods-1)
|
70
|
+
- [`BCDD::Result::Context::Expectations`](#bcddresultcontextexpectations)
|
71
|
+
- [Mixin add-ons](#mixin-add-ons)
|
72
|
+
- [`BCDD::Result.configuration`](#bcddresultconfiguration)
|
73
|
+
- [`config.addon.enable!(:continue)`](#configaddonenablecontinue)
|
74
|
+
- [`config.constant_alias.enable!('Result')`](#configconstant_aliasenableresult)
|
75
|
+
- [`config.pattern_matching.disable!(:nil_as_valid_value_checking)`](#configpattern_matchingdisablenil_as_valid_value_checking)
|
76
|
+
- [`config.feature.disable!(:expectations)`](#configfeaturedisableexpectations)
|
77
|
+
- [`BCDD::Result.config`](#bcddresultconfig)
|
64
78
|
- [About](#about)
|
65
79
|
- [Development](#development)
|
66
80
|
- [Contributing](#contributing)
|
@@ -76,7 +90,7 @@ Use it to enable the [Railway Oriented Programming](https://fsharpforfunandprofi
|
|
76
90
|
Add this line to your application's Gemfile:
|
77
91
|
|
78
92
|
```ruby
|
79
|
-
gem 'bcdd-result'
|
93
|
+
gem 'bcdd-result'
|
80
94
|
```
|
81
95
|
|
82
96
|
And then execute:
|
@@ -87,6 +101,10 @@ If bundler is not being used to manage dependencies, install the gem by executin
|
|
87
101
|
|
88
102
|
$ gem install bcdd-result
|
89
103
|
|
104
|
+
And require it in your code:
|
105
|
+
|
106
|
+
require 'bcdd/result'
|
107
|
+
|
90
108
|
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
91
109
|
|
92
110
|
## Usage
|
@@ -111,17 +129,27 @@ BCDD::Result::Failure(:err) #
|
|
111
129
|
|
112
130
|
#### `BCDD::Result` *versus* `Result`
|
113
131
|
|
114
|
-
|
132
|
+
This gem provides a way to create constant aliases for `BCDD::Result` and other classes/modules.
|
115
133
|
|
116
|
-
|
134
|
+
To enable it, you must call the `BCDD::Result.configuration` method and pass a block to it. You can turn the aliases you want on/off in this block.
|
117
135
|
|
118
136
|
```ruby
|
119
|
-
|
137
|
+
BCDD::Result.configuration do |config|
|
138
|
+
config.constant_alias.enable!('Result')
|
139
|
+
end
|
140
|
+
```
|
120
141
|
|
142
|
+
So, instead of using `BCDD::Result` everywhere, you can use `Result` as an alias/shortcut.
|
143
|
+
|
144
|
+
```ruby
|
121
145
|
Result::Success(:ok) # <BCDD::Result::Success type=:ok value=nil>
|
146
|
+
|
147
|
+
Result::Failure(:err) # <BCDD::Result::Failure type=:err value=nil>
|
122
148
|
```
|
123
149
|
|
124
|
-
|
150
|
+
If you have enabled constant aliasing, all examples in this README that use `BCDD::Result` can be implemented using `Result`.
|
151
|
+
|
152
|
+
There are other aliases and configurations available. Check the [BCDD::Result.configuration]() section for more information.
|
125
153
|
|
126
154
|
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
127
155
|
|
@@ -161,12 +189,12 @@ result.value # nil
|
|
161
189
|
################
|
162
190
|
# With a value #
|
163
191
|
################
|
164
|
-
result = BCDD::Result::Failure(:err,
|
192
|
+
result = BCDD::Result::Failure(:err, 'my_value')
|
165
193
|
|
166
194
|
result.success? # false
|
167
195
|
result.failure? # true
|
168
196
|
result.type # :err
|
169
|
-
result.value #
|
197
|
+
result.value # "my_value"
|
170
198
|
|
171
199
|
###################
|
172
200
|
# Without a value #
|
@@ -179,9 +207,11 @@ result.type # :no
|
|
179
207
|
result.value # nil
|
180
208
|
```
|
181
209
|
|
210
|
+
In both cases, the `type` must be a symbol, and the `value` can be any kind of object.
|
211
|
+
|
182
212
|
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
183
213
|
|
184
|
-
####
|
214
|
+
#### Checking types with `result.success?` or `result.failure?`
|
185
215
|
|
186
216
|
`BCDD::Result#success?` and `BCDD::Result#failure?` are methods that allow you to check if the result is a success or a failure.
|
187
217
|
|
@@ -190,9 +220,11 @@ You can also check the result type by passing an argument to it. For example, `r
|
|
190
220
|
```ruby
|
191
221
|
result = BCDD::Result::Success(:ok)
|
192
222
|
|
193
|
-
result.success?
|
194
|
-
|
195
|
-
|
223
|
+
result.success?(:ok)
|
224
|
+
|
225
|
+
# This is the same as:
|
226
|
+
|
227
|
+
result.success? && result.type == :ok
|
196
228
|
```
|
197
229
|
|
198
230
|
The same is valid for `BCDD::Result#failure?`.
|
@@ -200,18 +232,19 @@ The same is valid for `BCDD::Result#failure?`.
|
|
200
232
|
```ruby
|
201
233
|
result = BCDD::Result::Failure(:err)
|
202
234
|
|
203
|
-
result.failure?
|
204
|
-
|
205
|
-
|
235
|
+
result.failure?(:err)
|
236
|
+
|
237
|
+
# This is the same as:
|
238
|
+
|
239
|
+
result.failure? && result.type == :err
|
206
240
|
```
|
207
241
|
|
208
242
|
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
209
243
|
|
210
244
|
### Result Hooks
|
211
245
|
|
212
|
-
Result hooks are methods that allow you to
|
213
|
-
|
214
|
-
To exemplify the them, I will implement a method that knows how to divide two numbers.
|
246
|
+
Result hooks are methods that allow you to execute a block of code based on the type of result obtained.
|
247
|
+
To demonstrate their use, I will implement a method that can divide two numbers.
|
215
248
|
|
216
249
|
```ruby
|
217
250
|
def divide(arg1, arg2)
|
@@ -228,14 +261,15 @@ end
|
|
228
261
|
|
229
262
|
#### `result.on`
|
230
263
|
|
231
|
-
`BCDD::Result#on
|
264
|
+
When you use `BCDD::Result#on`, the block will be executed only when the type matches the result type.
|
232
265
|
|
233
|
-
|
266
|
+
However, even if the block is executed, the method will always return the result itself.
|
234
267
|
|
235
|
-
The result
|
268
|
+
The value of the result will be available as the first argument of the block.
|
236
269
|
|
237
270
|
```ruby
|
238
271
|
result = divide(nil, 2)
|
272
|
+
#<BCDD::Result::Failure type=:invalid_arg data='arg1 must be numeric'>
|
239
273
|
|
240
274
|
output =
|
241
275
|
result
|
@@ -268,24 +302,44 @@ result.object_id == output.object_id # true
|
|
268
302
|
|
269
303
|
`BCDD::Result#on_type` is an alias of `BCDD::Result#on`.
|
270
304
|
|
305
|
+
```ruby
|
306
|
+
result = divide(nil, 2)
|
307
|
+
#<BCDD::Result::Failure type=:invalid_arg data='arg1 must be numeric'>
|
308
|
+
|
309
|
+
output =
|
310
|
+
result
|
311
|
+
.on_type(:invalid_arg, :division_by_zero) { |msg| puts msg }
|
312
|
+
.on_type(:division_completed) { |number| puts number }
|
313
|
+
|
314
|
+
# The code above will print 'arg1 must be numeric' and return the result itself.
|
315
|
+
|
316
|
+
result.object_id == output.object_id # true
|
317
|
+
```
|
318
|
+
|
319
|
+
*PS: The `divide()` implementation is [here](#result-hooks).*
|
320
|
+
|
321
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
322
|
+
|
271
323
|
#### `result.on_success`
|
272
324
|
|
273
|
-
`BCDD::Result#on_success` is
|
325
|
+
The `BCDD::Result#on_success` method is quite similar to the `BCDD::Result#on` hook, but with a few key differences:
|
274
326
|
|
275
|
-
1.
|
276
|
-
2. If the type is
|
327
|
+
1. It will only execute the block of code if the result is a success.
|
328
|
+
2. If the type declaration is not included, the method will execute the block for any successful result, regardless of its type.
|
277
329
|
|
278
330
|
```ruby
|
279
|
-
#
|
331
|
+
# In both examples, it executes the block and returns the result itself.
|
280
332
|
|
281
333
|
divide(4, 2).on_success { |number| puts number }
|
282
334
|
|
283
335
|
divide(4, 2).on_success(:division_completed) { |number| puts number }
|
284
336
|
|
285
|
-
# It doesn't
|
337
|
+
# It doesn't execute the block as the type is different.
|
286
338
|
|
287
339
|
divide(4, 4).on_success(:ok) { |value| puts value }
|
288
340
|
|
341
|
+
# It doesn't execute the block, as the result is a success, but the hook expects a failure.
|
342
|
+
|
289
343
|
divide(4, 4).on_failure { |error| puts error }
|
290
344
|
```
|
291
345
|
|
@@ -295,20 +349,25 @@ divide(4, 4).on_failure { |error| puts error }
|
|
295
349
|
|
296
350
|
#### `result.on_failure`
|
297
351
|
|
298
|
-
|
352
|
+
It is the opposite of `Result#on_success`:
|
353
|
+
|
354
|
+
1. It will only execute the block of code if the result is a failure.
|
355
|
+
2. If the type declaration is not included, the method will execute the block for any failed result, regardless of its type.
|
299
356
|
|
300
357
|
```ruby
|
301
|
-
#
|
358
|
+
# In both examples, it executes the block and returns the result itself.
|
302
359
|
|
303
360
|
divide(nil, 2).on_failure { |error| puts error }
|
304
361
|
|
305
|
-
divide(4, 0).on_failure(:
|
362
|
+
divide(4, 0).on_failure(:division_by_zero) { |error| puts error }
|
306
363
|
|
307
|
-
# It doesn't
|
308
|
-
|
309
|
-
divide(4, 0).on_success { |number| puts number }
|
364
|
+
# It doesn't execute the block as the type is different.
|
310
365
|
|
311
366
|
divide(4, 0).on_failure(:invalid_arg) { |error| puts error }
|
367
|
+
|
368
|
+
# It doesn't execute the block, as the result is a failure, but the hook expects a success.
|
369
|
+
|
370
|
+
divide(4, 0).on_success { |number| puts number }
|
312
371
|
```
|
313
372
|
|
314
373
|
*PS: The `divide()` implementation is [here](#result-hooks).*
|
@@ -317,11 +376,11 @@ divide(4, 0).on_failure(:invalid_arg) { |error| puts error }
|
|
317
376
|
|
318
377
|
#### `result.on_unknown`
|
319
378
|
|
320
|
-
`BCDD::Result#on_unknown` will
|
379
|
+
`BCDD::Result#on_unknown` will execute the block when no other hook (`#on`, `#on_type`, `#on_failure`, `#on_success`) has been executed.
|
321
380
|
|
322
|
-
Regardless of the block being executed, the
|
381
|
+
Regardless of the block being executed, the method will always return the result itself.
|
323
382
|
|
324
|
-
The result
|
383
|
+
The value of the result will be available as the first argument of the block.
|
325
384
|
|
326
385
|
```ruby
|
327
386
|
divide(4, 2)
|
@@ -338,7 +397,7 @@ divide(4, 2)
|
|
338
397
|
|
339
398
|
#### `result.handle`
|
340
399
|
|
341
|
-
This method
|
400
|
+
This method lets you define blocks for each hook (type, failure, or success), but instead of returning itself, it will return the output of the first match/block execution.
|
342
401
|
|
343
402
|
```ruby
|
344
403
|
divide(4, 2).handle do |result|
|
@@ -379,8 +438,8 @@ end
|
|
379
438
|
|
380
439
|
**Notes:**
|
381
440
|
* You can define multiple types to be handled by the same hook/block
|
382
|
-
* If the type is missing, it will
|
383
|
-
* The `#type` and `#[]` handlers
|
441
|
+
* If the type is missing, it will execute the block for any success or failure handler.
|
442
|
+
* The `#type` and `#[]` handlers require at least one type/argument.
|
384
443
|
|
385
444
|
*PS: The `divide()` implementation is [here](#result-hooks).*
|
386
445
|
|
@@ -388,13 +447,13 @@ end
|
|
388
447
|
|
389
448
|
### Result Value
|
390
449
|
|
391
|
-
|
450
|
+
To access the result value, you can simply call `BCDD::Result#value`.
|
392
451
|
|
393
|
-
|
452
|
+
However, there may be instances where you need to retrieve the value of a successful result or a default value if the result is a failure. In such cases, you can make use of `BCDD::Result#value_or`.
|
394
453
|
|
395
454
|
#### `result.value_or`
|
396
455
|
|
397
|
-
`BCCD::Result#value_or` returns the value when the result is a success,
|
456
|
+
`BCCD::Result#value_or` returns the value when the result is a success. However, if it is a failure, the given block will be executed, and its outcome will be returned.
|
398
457
|
|
399
458
|
```ruby
|
400
459
|
def divide(arg1, arg2)
|
@@ -462,16 +521,75 @@ print_to_hash(**success_data) # [:success, :ok, 1]
|
|
462
521
|
|
463
522
|
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
464
523
|
|
465
|
-
###
|
524
|
+
### Pattern Matching
|
525
|
+
|
526
|
+
The `BCDD::Result` also provides support to pattern matching.
|
527
|
+
|
528
|
+
In the further examples, I will use the `Divide` lambda to exemplify its usage.
|
529
|
+
|
530
|
+
```ruby
|
531
|
+
Divide = lambda do |arg1, arg2|
|
532
|
+
arg1.is_a?(::Numeric) or return BCDD::Result::Failure(:invalid_arg, 'arg1 must be numeric')
|
533
|
+
arg2.is_a?(::Numeric) or return BCDD::Result::Failure(:invalid_arg, 'arg2 must be numeric')
|
534
|
+
|
535
|
+
return BCDD::Result::Failure(:division_by_zero, 'arg2 must not be zero') if arg2.zero?
|
536
|
+
|
537
|
+
BCDD::Result::Success(:division_completed, arg1 / arg2)
|
538
|
+
end
|
539
|
+
```
|
540
|
+
|
541
|
+
#### `Array`/`Find` patterns
|
466
542
|
|
467
|
-
|
543
|
+
```ruby
|
544
|
+
case Divide.call(4, 2)
|
545
|
+
in BCDD::Result::Failure[:invalid_arg, msg] then puts msg
|
546
|
+
in BCDD::Result::Failure[:division_by_zero, msg] then puts msg
|
547
|
+
in BCDD::Result::Success[:division_completed, value] then puts value
|
548
|
+
end
|
549
|
+
|
550
|
+
# The code above will print: 2
|
551
|
+
|
552
|
+
case Divide.call(4, 0)
|
553
|
+
in BCDD::Result::Failure[:invalid_arg, msg] then puts msg
|
554
|
+
in BCDD::Result::Failure[:division_by_zero, msg] then puts msg
|
555
|
+
in BCDD::Result::Success[:division_completed, value] then puts value
|
556
|
+
end
|
557
|
+
|
558
|
+
# The code above will print: arg2 must not be zero
|
559
|
+
```
|
560
|
+
|
561
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
562
|
+
|
563
|
+
#### `Hash` patterns
|
564
|
+
|
565
|
+
```ruby
|
566
|
+
case Divide.call(10, 2)
|
567
|
+
in { failure: { invalid_arg: msg } } then puts msg
|
568
|
+
in { failure: { division_by_zero: msg } } then puts msg
|
569
|
+
in { success: { division_completed: value } } then puts value
|
570
|
+
end
|
571
|
+
|
572
|
+
# The code above will print: 5
|
573
|
+
|
574
|
+
case Divide.call('10', 2)
|
575
|
+
in { failure: { invalid_arg: msg } } then puts msg
|
576
|
+
in { failure: { division_by_zero: msg } } then puts msg
|
577
|
+
in { success: { division_completed: value } } then puts value
|
578
|
+
end
|
579
|
+
|
580
|
+
# The code above will print: arg1 must be numeric
|
581
|
+
```
|
582
|
+
|
583
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
584
|
+
|
585
|
+
### Railway Oriented Programming
|
468
586
|
|
469
|
-
|
587
|
+
["Railway Oriented Programming (ROP)"](https://fsharpforfunandprofit.com/rop/) is a programming technique that involves linking blocks together to form a sequence of operations, also known as a pipeline.
|
588
|
+
If a failure occurs in any of the blocks, the pipeline is interrupted and subsequent blocks are skipped.
|
470
589
|
|
471
|
-
|
472
|
-
So, if some block returns a failure, the following blocks will be skipped.
|
590
|
+
The ROP technique allows you to structure your code in a way that expresses your logic as a series of operations, with the added benefit of stopping the process at the first detection of failure.
|
473
591
|
|
474
|
-
|
592
|
+
If all blocks successfully execute, the final result of the pipeline will be a success.
|
475
593
|
|
476
594
|
#### `result.and_then`
|
477
595
|
|
@@ -481,7 +599,7 @@ module Divide
|
|
481
599
|
|
482
600
|
def call(arg1, arg2)
|
483
601
|
validate_numbers(arg1, arg2)
|
484
|
-
.and_then { |numbers|
|
602
|
+
.and_then { |numbers| validate_nonzero(numbers) }
|
485
603
|
.and_then { |numbers| divide(numbers) }
|
486
604
|
end
|
487
605
|
|
@@ -494,8 +612,8 @@ module Divide
|
|
494
612
|
BCDD::Result::Success(:ok, [arg1, arg2])
|
495
613
|
end
|
496
614
|
|
497
|
-
def
|
498
|
-
return BCDD::Result::Success(:ok, numbers)
|
615
|
+
def validate_nonzero(numbers)
|
616
|
+
return BCDD::Result::Success(:ok, numbers) if numbers.last.nonzero?
|
499
617
|
|
500
618
|
BCDD::Result::Failure(:division_by_zero, 'arg2 must not be zero')
|
501
619
|
end
|
@@ -526,9 +644,11 @@ Divide.call(2, 2)
|
|
526
644
|
|
527
645
|
#### `BCDD::Result.mixin`
|
528
646
|
|
529
|
-
This method
|
647
|
+
This method generates a module that any object can include or extend. It adds two methods to the target object: `Success()` and `Failure()`.
|
648
|
+
|
649
|
+
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.
|
530
650
|
|
531
|
-
|
651
|
+
Because the result has a subject, the `#and_then` method can call methods from it.
|
532
652
|
|
533
653
|
##### Class example (Instance Methods)
|
534
654
|
|
@@ -545,7 +665,7 @@ class Divide
|
|
545
665
|
|
546
666
|
def call
|
547
667
|
validate_numbers
|
548
|
-
.and_then(:
|
668
|
+
.and_then(:validate_nonzero)
|
549
669
|
.and_then(:divide)
|
550
670
|
end
|
551
671
|
|
@@ -560,7 +680,7 @@ class Divide
|
|
560
680
|
Success(:ok, [arg1, arg2])
|
561
681
|
end
|
562
682
|
|
563
|
-
def
|
683
|
+
def validate_nonzero(numbers)
|
564
684
|
return Success(:ok, numbers) unless numbers.last.zero?
|
565
685
|
|
566
686
|
Failure(:division_by_zero, 'arg2 must not be zero')
|
@@ -586,7 +706,7 @@ module Divide
|
|
586
706
|
|
587
707
|
def call(arg1, arg2)
|
588
708
|
validate_numbers(arg1, arg2)
|
589
|
-
.and_then(:
|
709
|
+
.and_then(:validate_nonzero)
|
590
710
|
.and_then(:divide)
|
591
711
|
end
|
592
712
|
|
@@ -599,7 +719,7 @@ module Divide
|
|
599
719
|
Success(:ok, [arg1, arg2])
|
600
720
|
end
|
601
721
|
|
602
|
-
def
|
722
|
+
def validate_nonzero(numbers)
|
603
723
|
return Success(:ok, numbers) unless numbers.last.zero?
|
604
724
|
|
605
725
|
Failure(:division_by_zero, 'arg2 must not be zero')
|
@@ -621,17 +741,122 @@ Divide.call(4, '2') #<BCDD::Result::Failure type=:invalid_arg value="arg2 must b
|
|
621
741
|
|
622
742
|
##### Important Requirement
|
623
743
|
|
624
|
-
|
744
|
+
To use the `#and_then` method to call methods, they must use `Success()` and `Failure()` to produce the results.
|
745
|
+
|
746
|
+
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.
|
747
|
+
|
748
|
+
**Note:** You can still use the block syntax, but all the results must be produced by the subject's `Success()` and `Failure()` methods.
|
749
|
+
|
750
|
+
```ruby
|
751
|
+
module ValidateNonzero
|
752
|
+
extend self, BCDD::Result.mixin
|
753
|
+
|
754
|
+
def call(numbers)
|
755
|
+
return Success(:ok, numbers) unless numbers.last.zero?
|
756
|
+
|
757
|
+
Failure(:division_by_zero, 'arg2 must not be zero')
|
758
|
+
end
|
759
|
+
end
|
760
|
+
|
761
|
+
module Divide
|
762
|
+
extend self, BCDD::Result.mixin
|
763
|
+
|
764
|
+
def call(arg1, arg2)
|
765
|
+
validate_numbers(arg1, arg2)
|
766
|
+
.and_then(:validate_nonzero)
|
767
|
+
.and_then(:divide)
|
768
|
+
end
|
769
|
+
|
770
|
+
private
|
771
|
+
|
772
|
+
def validate_numbers(arg1, arg2)
|
773
|
+
arg1.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg1 must be numeric')
|
774
|
+
arg2.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg2 must be numeric')
|
775
|
+
|
776
|
+
Success(:ok, [arg1, arg2])
|
777
|
+
end
|
778
|
+
|
779
|
+
def validate_nonzero(numbers)
|
780
|
+
ValidateNonzero.call(numbers) # This will raise an error
|
781
|
+
end
|
782
|
+
|
783
|
+
def divide((number1, number2))
|
784
|
+
Success(:division_completed, number1 / number2)
|
785
|
+
end
|
786
|
+
end
|
787
|
+
```
|
788
|
+
|
789
|
+
Look at the error produced by the code above:
|
790
|
+
|
791
|
+
```ruby
|
792
|
+
Divide.call(2, 0)
|
793
|
+
|
794
|
+
# You cannot call #and_then and return a result that does not belong to the subject! (BCDD::Result::Error::InvalidResultSubject)
|
795
|
+
# Expected subject: Divide
|
796
|
+
# Given subject: ValidateNonzero
|
797
|
+
# Given result: #<BCDD::Result::Failure type=:division_by_zero value="arg2 must not be zero">
|
798
|
+
```
|
799
|
+
|
800
|
+
In order to fix this, you must handle the result produced by `ValidateNonzero.call()` and return a result that belongs to the subject.
|
801
|
+
|
802
|
+
```ruby
|
803
|
+
module ValidateNonzero
|
804
|
+
extend self, BCDD::Result.mixin
|
805
|
+
|
806
|
+
def call(numbers)
|
807
|
+
return Success(:ok, numbers) unless numbers.last.zero?
|
808
|
+
|
809
|
+
Failure(:division_by_zero, 'arg2 must not be zero')
|
810
|
+
end
|
811
|
+
end
|
625
812
|
|
626
|
-
|
813
|
+
module Divide
|
814
|
+
extend self, BCDD::Result.mixin
|
815
|
+
|
816
|
+
def call(arg1, arg2)
|
817
|
+
validate_numbers(arg1, arg2)
|
818
|
+
.and_then(:validate_nonzero)
|
819
|
+
.and_then(:divide)
|
820
|
+
end
|
821
|
+
|
822
|
+
private
|
823
|
+
|
824
|
+
def validate_numbers(arg1, arg2)
|
825
|
+
arg1.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg1 must be numeric')
|
826
|
+
arg2.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg2 must be numeric')
|
827
|
+
|
828
|
+
Success(:ok, [arg1, arg2])
|
829
|
+
end
|
627
830
|
|
628
|
-
|
831
|
+
def validate_nonzero(numbers)
|
832
|
+
# In this case we are handling the other subject result and returning our own
|
833
|
+
ValidateNonzero.call(numbers).handle do |on|
|
834
|
+
on.success { |numbers| Success(:ok, numbers) }
|
835
|
+
|
836
|
+
on.failure { |err| Failure(:division_by_zero, err) }
|
837
|
+
end
|
838
|
+
end
|
839
|
+
|
840
|
+
def divide((number1, number2))
|
841
|
+
Success(:division_completed, number1 / number2)
|
842
|
+
end
|
843
|
+
end
|
844
|
+
```
|
845
|
+
|
846
|
+
Look at the output of the code above:
|
847
|
+
|
848
|
+
```ruby
|
849
|
+
Divide.call(2, 0)
|
850
|
+
|
851
|
+
#<BCDD::Result::Failure type=:division_by_zero value="arg2 must not be zero">
|
852
|
+
```
|
629
853
|
|
630
854
|
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
631
855
|
|
632
856
|
##### Dependency Injection
|
633
857
|
|
634
|
-
The `BCDD::Result#and_then` accepts a second argument that will be used to share a value with the subject's method.
|
858
|
+
The `BCDD::Result#and_then` accepts a second argument that will be used to share a value with the subject's method.
|
859
|
+
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.
|
635
860
|
|
636
861
|
```ruby
|
637
862
|
require 'logger'
|
@@ -641,7 +866,7 @@ module Divide
|
|
641
866
|
|
642
867
|
def call(arg1, arg2, logger: ::Logger.new(STDOUT))
|
643
868
|
validate_numbers(arg1, arg2)
|
644
|
-
.and_then(:
|
869
|
+
.and_then(:validate_nonzero, logger)
|
645
870
|
.and_then(:divide, logger)
|
646
871
|
end
|
647
872
|
|
@@ -654,7 +879,7 @@ module Divide
|
|
654
879
|
Success(:ok, [arg1, arg2])
|
655
880
|
end
|
656
881
|
|
657
|
-
def
|
882
|
+
def validate_nonzero(numbers, logger)
|
658
883
|
if numbers.last.zero?
|
659
884
|
logger.error('arg2 must not be zero')
|
660
885
|
|
@@ -686,21 +911,21 @@ Divide.call(4, 2, logger: Logger.new(IO::NULL))
|
|
686
911
|
|
687
912
|
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
688
913
|
|
689
|
-
#####
|
914
|
+
##### Add-ons
|
690
915
|
|
691
|
-
The `BCDD::Result.mixin` also accepts the `
|
916
|
+
The `BCDD::Result.mixin` also accepts the `config:` argument. It is a hash that will be used to define custom behaviors for the mixin.
|
692
917
|
|
693
|
-
**
|
918
|
+
**continue**
|
694
919
|
|
695
920
|
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.
|
696
921
|
|
697
922
|
```ruby
|
698
923
|
module Divide
|
699
|
-
extend self, BCDD::Result.mixin(
|
924
|
+
extend self, BCDD::Result.mixin(config: { addon: { continue: true } })
|
700
925
|
|
701
926
|
def call(arg1, arg2)
|
702
927
|
validate_numbers(arg1, arg2)
|
703
|
-
.and_then(:
|
928
|
+
.and_then(:validate_nonzero)
|
704
929
|
.and_then(:divide)
|
705
930
|
end
|
706
931
|
|
@@ -713,7 +938,7 @@ module Divide
|
|
713
938
|
Continue([arg1, arg2])
|
714
939
|
end
|
715
940
|
|
716
|
-
def
|
941
|
+
def validate_nonzero(numbers)
|
717
942
|
return Continue(numbers) unless numbers.last.zero?
|
718
943
|
|
719
944
|
Failure(:division_by_zero, 'arg2 must not be zero')
|
@@ -727,139 +952,49 @@ end
|
|
727
952
|
|
728
953
|
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
729
954
|
|
730
|
-
###
|
731
|
-
|
732
|
-
The `BCDD::Result` also provides support to pattern matching.
|
733
|
-
|
734
|
-
In the further examples, I will use the `Divide` lambda to exemplify its usage.
|
955
|
+
### `BCDD::Result::Expectations`
|
735
956
|
|
736
|
-
|
737
|
-
Divide = lambda do |arg1, arg2|
|
738
|
-
arg1.is_a?(::Numeric) or return BCDD::Result::Failure(:invalid_arg, 'arg1 must be numeric')
|
739
|
-
arg2.is_a?(::Numeric) or return BCDD::Result::Failure(:invalid_arg, 'arg2 must be numeric')
|
957
|
+
This feature lets you define contracts for your results' types and values. There are two ways to use it: the standalone (`BCDD::Result::Expectations.new`) and the mixin (`BCDD::Result::Expectations.mixin`) mode.
|
740
958
|
|
741
|
-
|
959
|
+
It was designed to ensure all the aspects of the result's type and value. So, an error will be raised if you try to create or handle a result with an unexpected type or value.
|
742
960
|
|
743
|
-
|
744
|
-
end
|
745
|
-
```
|
961
|
+
#### Standalone *versus* Mixin mode
|
746
962
|
|
747
|
-
|
963
|
+
The _**standalone mode**_ creates an object that knows how to produce and validate results based on the defined expectations. Look at the example below:
|
748
964
|
|
749
965
|
```ruby
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
966
|
+
module Divide
|
967
|
+
Result = BCDD::Result::Expectations.new(
|
968
|
+
success: %i[numbers division_completed],
|
969
|
+
failure: %i[invalid_arg division_by_zero]
|
970
|
+
)
|
755
971
|
|
756
|
-
|
972
|
+
def self.call(arg1, arg2)
|
973
|
+
arg1.is_a?(Numeric) or return Result::Failure(:invalid_arg, 'arg1 must be numeric')
|
974
|
+
arg2.is_a?(Numeric) or return Result::Failure(:invalid_arg, 'arg2 must be numeric')
|
757
975
|
|
758
|
-
|
759
|
-
in BCDD::Result::Failure[:invalid_arg, msg] then puts msg
|
760
|
-
in BCDD::Result::Failure[:division_by_zero, msg] then puts msg
|
761
|
-
in BCDD::Result::Success[:division_completed, value] then puts value
|
762
|
-
end
|
976
|
+
arg2.zero? and return Result::Failure(:division_by_zero, 'arg2 must not be zero')
|
763
977
|
|
764
|
-
|
978
|
+
Result::Success(:division_completed, arg1 / arg2)
|
979
|
+
end
|
980
|
+
end
|
765
981
|
```
|
766
982
|
|
767
|
-
|
983
|
+
In the code above, we define a constant `Divide::Expected`. And because of this (it is a constant), we can use it inside and outside the module.
|
768
984
|
|
769
|
-
|
985
|
+
Look what happens if you try to create a result without one of the expected types.
|
770
986
|
|
771
987
|
```ruby
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
in { success: { division_completed: value } } then puts value
|
776
|
-
end
|
777
|
-
|
778
|
-
# The code above will print: 5
|
779
|
-
|
780
|
-
case Divide.call('10', 2)
|
781
|
-
in { failure: { invalid_arg: msg } } then puts msg
|
782
|
-
in { failure: { division_by_zero: msg } } then puts msg
|
783
|
-
in { success: { division_completed: value } } then puts value
|
784
|
-
end
|
988
|
+
Divide::Result::Success(:ok)
|
989
|
+
# type :ok is not allowed. Allowed types: :numbers, :division_completed
|
990
|
+
# (BCDD::Result::Contract::Error::UnexpectedType)
|
785
991
|
|
786
|
-
|
992
|
+
Divide::Result::Failure(:err)
|
993
|
+
# type :err is not allowed. Allowed types: :invalid_arg, :division_by_zero
|
994
|
+
# (BCDD::Result::Contract::Error::UnexpectedType)
|
787
995
|
```
|
788
996
|
|
789
|
-
|
790
|
-
|
791
|
-
#### BCDD::Result::Expectations
|
792
|
-
|
793
|
-
I'd like you to please read the following section to understand how to use this feature.
|
794
|
-
|
795
|
-
But if you are using Ruby >= 3.0, you can use the `in` operator to use the pattern matching to validate the result's value.
|
796
|
-
|
797
|
-
```ruby
|
798
|
-
module Divide
|
799
|
-
extend BCDD::Result::Expectations.mixin(
|
800
|
-
success: {
|
801
|
-
numbers: ->(value) { value in [Numeric, Numeric] },
|
802
|
-
division_completed: ->(value) { value in (Integer | Float) }
|
803
|
-
},
|
804
|
-
failure: { invalid_arg: String, division_by_zero: String }
|
805
|
-
)
|
806
|
-
|
807
|
-
def self.call(arg1, arg2)
|
808
|
-
arg1.is_a?(Numeric) or return Failure(:invalid_arg, 'arg1 must be numeric')
|
809
|
-
arg2.is_a?(Numeric) or return Failure(:invalid_arg, 'arg2 must be numeric')
|
810
|
-
|
811
|
-
arg2.zero? and return Failure(:division_by_zero, 'arg2 must not be zero')
|
812
|
-
|
813
|
-
Success(:division_completed, arg1 / arg2)
|
814
|
-
end
|
815
|
-
end
|
816
|
-
```
|
817
|
-
|
818
|
-
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
819
|
-
|
820
|
-
### `BCDD::Result::Expectations`
|
821
|
-
|
822
|
-
This feature lets you define contracts for your results' types and values. There are two ways to use it: the standalone (`BCDD::Result::Expectations.new`) and the mixin (`BCDD::Result::Expectations.mixin`) mode.
|
823
|
-
|
824
|
-
It was designed to ensure all the aspects of the result's type and value. So, an error will be raised if you try to create or handle a result with an unexpected type or value.
|
825
|
-
|
826
|
-
#### Standalone *versus* Mixin mode
|
827
|
-
|
828
|
-
The _**standalone mode**_ creates an object that knows how to produce and validate results based on the defined expectations. Look at the example below:
|
829
|
-
|
830
|
-
```ruby
|
831
|
-
module Divide
|
832
|
-
Expected = BCDD::Result::Expectations.new(
|
833
|
-
success: %i[numbers division_completed],
|
834
|
-
failure: %i[invalid_arg division_by_zero]
|
835
|
-
)
|
836
|
-
|
837
|
-
def self.call(arg1, arg2)
|
838
|
-
arg1.is_a?(Numeric) or return Expected::Failure(:invalid_arg, 'arg1 must be numeric')
|
839
|
-
arg2.is_a?(Numeric) or return Expected::Failure(:invalid_arg, 'arg2 must be numeric')
|
840
|
-
|
841
|
-
arg2.zero? and return Expected::Failure(:division_by_zero, 'arg2 must not be zero')
|
842
|
-
|
843
|
-
Expected::Success(:division_completed, arg1 / arg2)
|
844
|
-
end
|
845
|
-
end
|
846
|
-
```
|
847
|
-
|
848
|
-
In the code above, we define a constant `Divide::Expected`. And because of this (it is a constant), we can use it inside and outside the module.
|
849
|
-
|
850
|
-
Look what happens if you try to create a result without one of the expected types.
|
851
|
-
|
852
|
-
```ruby
|
853
|
-
Divide::Expected::Success(:ok)
|
854
|
-
# type :ok is not allowed. Allowed types: :numbers, :division_completed
|
855
|
-
# (BCDD::Result::Expectations::Error::UnexpectedType)
|
856
|
-
|
857
|
-
Divide::Expected::Failure(:err)
|
858
|
-
# type :err is not allowed. Allowed types: :invalid_arg, :division_by_zero
|
859
|
-
# (BCDD::Result::Expectations::Error::UnexpectedType)
|
860
|
-
```
|
861
|
-
|
862
|
-
The _**mixin mode**_ is similar to `BCDD::Result::Mixin`, but it also defines the expectations for the result's types and values.
|
997
|
+
The _**mixin mode**_ is similar to `BCDD::Result::Mixin`, but it also defines the expectations for the result's types and values.
|
863
998
|
|
864
999
|
```ruby
|
865
1000
|
class Divide
|
@@ -870,7 +1005,7 @@ class Divide
|
|
870
1005
|
|
871
1006
|
def call(arg1, arg2)
|
872
1007
|
validate_numbers(arg1, arg2)
|
873
|
-
.and_then(:
|
1008
|
+
.and_then(:validate_nonzero)
|
874
1009
|
.and_then(:divide)
|
875
1010
|
end
|
876
1011
|
|
@@ -883,7 +1018,7 @@ class Divide
|
|
883
1018
|
Success(:numbers, [arg1, arg2])
|
884
1019
|
end
|
885
1020
|
|
886
|
-
def
|
1021
|
+
def validate_nonzero(numbers)
|
887
1022
|
return Success(:numbers, numbers) unless numbers.last.zero?
|
888
1023
|
|
889
1024
|
Failure(:division_by_zero, 'arg2 must not be zero')
|
@@ -895,10 +1030,10 @@ class Divide
|
|
895
1030
|
end
|
896
1031
|
```
|
897
1032
|
|
898
|
-
This mode also defines an `
|
1033
|
+
This mode also defines an `Result` constant to be used inside and outside the module.
|
899
1034
|
|
900
1035
|
> **PROTIP:**
|
901
|
-
> You can use the `
|
1036
|
+
> You can use the `Result` constant to mock the result's type and value in your tests. As they will have the exact expectations, your tests will check if the result clients are handling the result correctly.
|
902
1037
|
|
903
1038
|
Now that you know the two modes, let's understand how expectations can be beneficial and powerful for defining contracts.
|
904
1039
|
|
@@ -906,11 +1041,11 @@ Now that you know the two modes, let's understand how expectations can be benefi
|
|
906
1041
|
|
907
1042
|
#### Type checking - Result Hooks
|
908
1043
|
|
909
|
-
The `BCDD::Result::Expectations` will check if the result
|
1044
|
+
The `BCDD::Result::Expectations` will check if the type of the result is valid. This checking will be performed in all methods that depend on the result’s type, such as `#success?`, `#failure?`, `#on`, `#on_type`, `#on_success`, `#on_failure`, and `#handle`.
|
910
1045
|
|
911
1046
|
##### `#success?` and `#failure?`
|
912
1047
|
|
913
|
-
When
|
1048
|
+
When checking whether a result is a success or failure, `BCDD::Result::Expectations` will also verify if the result type is valid/expected. In case of an invalid type, an error will be raised.
|
914
1049
|
|
915
1050
|
**Success example:**
|
916
1051
|
|
@@ -923,7 +1058,7 @@ result.success?(:division_completed) # true
|
|
923
1058
|
|
924
1059
|
result.success?(:ok)
|
925
1060
|
# type :ok is not allowed. Allowed types: :numbers, :division_completed
|
926
|
-
# (BCDD::Result::
|
1061
|
+
# (BCDD::Result::Contract::Error::UnexpectedType)
|
927
1062
|
```
|
928
1063
|
|
929
1064
|
**Failure example:**
|
@@ -937,7 +1072,7 @@ result.failure?(:division_by_zero) # false
|
|
937
1072
|
|
938
1073
|
result.failure?(:err)
|
939
1074
|
# type :err is not allowed. Allowed types: :invalid_arg, :division_by_zero
|
940
|
-
# (BCDD::Result::
|
1075
|
+
# (BCDD::Result::Contract::Error::UnexpectedType)
|
941
1076
|
```
|
942
1077
|
|
943
1078
|
*PS: The `Divide` implementation is [here](#standalone-versus-mixin-mode).*
|
@@ -946,7 +1081,7 @@ result.failure?(:err)
|
|
946
1081
|
|
947
1082
|
##### `#on` and `#on_type`
|
948
1083
|
|
949
|
-
If you use `#on` or `#on_type` to
|
1084
|
+
If you use `#on` or `#on_type` to execute a block, `BCDD::Result::Expectations` will check whether the result type is valid/expected. Otherwise, an error will be raised.
|
950
1085
|
|
951
1086
|
```ruby
|
952
1087
|
result = Divide.new.call(10, 2)
|
@@ -959,7 +1094,7 @@ result
|
|
959
1094
|
|
960
1095
|
result.on(:number) { |_| :this_type_does_not_exist }
|
961
1096
|
# type :number is not allowed. Allowed types: :numbers, :division_completed, :invalid_arg, :division_by_zero
|
962
|
-
# (BCDD::Result::
|
1097
|
+
# (BCDD::Result::Contract::Error::UnexpectedType)
|
963
1098
|
```
|
964
1099
|
|
965
1100
|
*PS: The `Divide` implementation is [here](#standalone-versus-mixin-mode).*
|
@@ -968,7 +1103,7 @@ result.on(:number) { |_| :this_type_does_not_exist }
|
|
968
1103
|
|
969
1104
|
##### `#on_success` and `#on_failure`
|
970
1105
|
|
971
|
-
If you use `#on_success` or `#on_failure` to
|
1106
|
+
If you use `#on_success` or `#on_failure` to execute a block, `BCDD::Result::Expectations` will check whether the result type is valid/expected. Otherwise, an error will be raised.
|
972
1107
|
|
973
1108
|
```ruby
|
974
1109
|
result = Divide.new.call(10, '2')
|
@@ -985,11 +1120,11 @@ result
|
|
985
1120
|
|
986
1121
|
result.on_success(:ok) { |_| :this_type_does_not_exist }
|
987
1122
|
# type :ok is not allowed. Allowed types: :numbers, :division_completed
|
988
|
-
# (BCDD::Result::
|
1123
|
+
# (BCDD::Result::Contract::Error::UnexpectedType)
|
989
1124
|
|
990
1125
|
result.on_failure(:err) { |_| :this_type_does_not_exist }
|
991
1126
|
# type :err is not allowed. Allowed types: :invalid_arg, :division_by_zero
|
992
|
-
# (BCDD::Result::
|
1127
|
+
# (BCDD::Result::Contract::Error::UnexpectedType)
|
993
1128
|
```
|
994
1129
|
|
995
1130
|
*PS: The `Divide` implementation is [here](#standalone-versus-mixin-mode).*
|
@@ -1006,17 +1141,17 @@ result = Divide.call(10, 2)
|
|
1006
1141
|
result.handle do |on|
|
1007
1142
|
on.type(:ok) { |_| :this_type_does_not_exist }
|
1008
1143
|
end
|
1009
|
-
# type :ok is not allowed. Allowed types: :numbers, :division_completed, :invalid_arg, :division_by_zero (BCDD::Result::
|
1144
|
+
# type :ok is not allowed. Allowed types: :numbers, :division_completed, :invalid_arg, :division_by_zero (BCDD::Result::Contract::Error::UnexpectedType)
|
1010
1145
|
|
1011
1146
|
result.handle do |on|
|
1012
1147
|
on.success(:ok) { |_| :this_type_does_not_exist }
|
1013
1148
|
end
|
1014
|
-
# type :ok is not allowed. Allowed types: :numbers, :division_completed (BCDD::Result::
|
1149
|
+
# type :ok is not allowed. Allowed types: :numbers, :division_completed (BCDD::Result::Contract::Error::UnexpectedType)
|
1015
1150
|
|
1016
1151
|
result.handle do |on|
|
1017
1152
|
on.failure(:err) { |_| :this_type_does_not_exist }
|
1018
1153
|
end
|
1019
|
-
# type :err is not allowed. Allowed types: :
|
1154
|
+
# type :err is not allowed. Allowed types: :invalid_arg, :division_by_zero (BCDD::Result::Contract::Error::UnexpectedType)
|
1020
1155
|
```
|
1021
1156
|
|
1022
1157
|
*PS: The `Divide` implementation is [here](#standalone-versus-mixin-mode).*
|
@@ -1047,11 +1182,11 @@ end
|
|
1047
1182
|
|
1048
1183
|
Divide.call('4', 2)
|
1049
1184
|
# type :invalid_arg is not allowed. Allowed types: :err
|
1050
|
-
# (BCDD::Result::
|
1185
|
+
# (BCDD::Result::Contract::Error::UnexpectedType)
|
1051
1186
|
|
1052
1187
|
Divide.call(4, 2)
|
1053
1188
|
# type :division_completed is not allowed. Allowed types: :ok
|
1054
|
-
# (BCDD::Result::
|
1189
|
+
# (BCDD::Result::Contract::Error::UnexpectedType)
|
1055
1190
|
```
|
1056
1191
|
|
1057
1192
|
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
@@ -1060,25 +1195,25 @@ Divide.call(4, 2)
|
|
1060
1195
|
|
1061
1196
|
```ruby
|
1062
1197
|
module Divide
|
1063
|
-
|
1198
|
+
Result = BCDD::Result::Expectations.new(success: :ok, failure: :err)
|
1064
1199
|
|
1065
1200
|
def self.call(arg1, arg2)
|
1066
|
-
arg1.is_a?(Numeric) or return
|
1067
|
-
arg2.is_a?(Numeric) or return
|
1201
|
+
arg1.is_a?(Numeric) or return Result::Failure(:invalid_arg, 'arg1 must be numeric')
|
1202
|
+
arg2.is_a?(Numeric) or return Result::Failure(:invalid_arg, 'arg2 must be numeric')
|
1068
1203
|
|
1069
|
-
arg2.zero? and return
|
1204
|
+
arg2.zero? and return Result::Failure(:division_by_zero, 'arg2 must not be zero')
|
1070
1205
|
|
1071
|
-
|
1206
|
+
Result::Success(:division_completed, arg1 / arg2)
|
1072
1207
|
end
|
1073
1208
|
end
|
1074
1209
|
|
1075
1210
|
Divide.call('4', 2)
|
1076
1211
|
# type :invalid_arg is not allowed. Allowed types: :err
|
1077
|
-
# (BCDD::Result::
|
1212
|
+
# (BCDD::Result::Contract::Error::UnexpectedType)
|
1078
1213
|
|
1079
1214
|
Divide.call(4, 2)
|
1080
1215
|
# type :division_completed is not allowed. Allowed types: :ok
|
1081
|
-
# (BCDD::Result::
|
1216
|
+
# (BCDD::Result::Contract::Error::UnexpectedType)
|
1082
1217
|
```
|
1083
1218
|
|
1084
1219
|
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
@@ -1119,7 +1254,7 @@ end
|
|
1119
1254
|
|
1120
1255
|
```ruby
|
1121
1256
|
module Divide
|
1122
|
-
|
1257
|
+
Result = BCDD::Result::Expectations.new(
|
1123
1258
|
success: {
|
1124
1259
|
numbers: ->(value) { value.is_a?(Array) && value.size == 2 && value.all?(Numeric) },
|
1125
1260
|
division_completed: Numeric
|
@@ -1131,12 +1266,12 @@ module Divide
|
|
1131
1266
|
)
|
1132
1267
|
|
1133
1268
|
def self.call(arg1, arg2)
|
1134
|
-
arg1.is_a?(Numeric) or return
|
1135
|
-
arg2.is_a?(Numeric) or return
|
1269
|
+
arg1.is_a?(Numeric) or return Result::Failure(:invalid_arg, 'arg1 must be numeric')
|
1270
|
+
arg2.is_a?(Numeric) or return Result::Failure(:invalid_arg, 'arg2 must be numeric')
|
1136
1271
|
|
1137
|
-
arg2.zero? and return
|
1272
|
+
arg2.zero? and return Result::Failure(:division_by_zero, 'arg2 must not be zero')
|
1138
1273
|
|
1139
|
-
|
1274
|
+
Result::Success(:division_completed, arg1 / arg2)
|
1140
1275
|
end
|
1141
1276
|
end
|
1142
1277
|
```
|
@@ -1148,50 +1283,86 @@ The value validation will only be performed through the methods `Success()` and
|
|
1148
1283
|
##### Success()
|
1149
1284
|
|
1150
1285
|
```ruby
|
1151
|
-
Divide::
|
1152
|
-
# type :ok is not allowed. Allowed types: :numbers, :division_completed (BCDD::Result::
|
1286
|
+
Divide::Result::Success(:ok)
|
1287
|
+
# type :ok is not allowed. Allowed types: :numbers, :division_completed (BCDD::Result::Contract::Error::UnexpectedType)
|
1153
1288
|
|
1154
|
-
Divide::
|
1155
|
-
# value [1] is not allowed for :numbers type (BCDD::Result::
|
1289
|
+
Divide::Result::Success(:numbers, [1])
|
1290
|
+
# value [1] is not allowed for :numbers type (BCDD::Result::Contract::Error::UnexpectedValue)
|
1156
1291
|
|
1157
|
-
Divide::
|
1158
|
-
# value "2" is not allowed for :division_completed type (BCDD::Result::
|
1292
|
+
Divide::Result::Success(:division_completed, '2')
|
1293
|
+
# value "2" is not allowed for :division_completed type (BCDD::Result::Contract::Error::UnexpectedValue)
|
1159
1294
|
```
|
1160
1295
|
|
1161
1296
|
##### Failure()
|
1162
1297
|
|
1163
1298
|
```ruby
|
1164
|
-
Divide::
|
1165
|
-
# type :err is not allowed. Allowed types: :invalid_arg, :division_by_zero (BCDD::Result::
|
1299
|
+
Divide::Result::Failure(:err)
|
1300
|
+
# type :err is not allowed. Allowed types: :invalid_arg, :division_by_zero (BCDD::Result::Contract::Error::UnexpectedType)
|
1301
|
+
|
1302
|
+
Divide::Result::Failure(:invalid_arg, :arg1_must_be_numeric)
|
1303
|
+
# value :arg1_must_be_numeric is not allowed for :invalid_arg type (BCDD::Result::Contract::Error::UnexpectedValue)
|
1304
|
+
|
1305
|
+
Divide::Result::Failure(:division_by_zero, msg: 'arg2 must not be zero')
|
1306
|
+
# value {:msg=>"arg2 must not be zero"} is not allowed for :division_by_zero type (BCDD::Result::Contract::Error::UnexpectedValue)
|
1307
|
+
```
|
1308
|
+
|
1309
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
1310
|
+
|
1311
|
+
##### Pattern Matching Support
|
1312
|
+
|
1313
|
+
The value checking has support for handling pattern-matching errors, and the cleanest way to do it is using the one-line pattern matching operators (`=>` since Ruby 3.0) and (`in` Ruby 2.7).
|
1314
|
+
|
1315
|
+
How does this operator work? They raise an error when the pattern does not match but returns nil when it matches.
|
1316
|
+
|
1317
|
+
Because of this, you will need to enable `nil` as a valid value checking. You can do it through the `BCDD::Result.configuration` or by allowing it directly on the mixin config.
|
1318
|
+
|
1319
|
+
```ruby
|
1320
|
+
module Divide
|
1321
|
+
extend BCDD::Result::Expectations.mixin(
|
1322
|
+
config: {
|
1323
|
+
pattern_matching: { nil_as_valid_value_checking: true }
|
1324
|
+
},
|
1325
|
+
success: {
|
1326
|
+
division_completed: ->(value) { value => (Integer | Float) }
|
1327
|
+
},
|
1328
|
+
failure: { invalid_arg: String, division_by_zero: String }
|
1329
|
+
)
|
1330
|
+
|
1331
|
+
def self.call(arg1, arg2)
|
1332
|
+
arg1.is_a?(Numeric) or return Failure(:invalid_arg, 'arg1 must be numeric')
|
1333
|
+
arg2.is_a?(Numeric) or return Failure(:invalid_arg, 'arg2 must be numeric')
|
1334
|
+
|
1335
|
+
arg2.zero? and return Failure(:division_by_zero, 'arg2 must not be zero')
|
1166
1336
|
|
1167
|
-
|
1168
|
-
|
1337
|
+
Success(:division_completed, String(arg1 / arg2))
|
1338
|
+
end
|
1339
|
+
end
|
1169
1340
|
|
1170
|
-
Divide
|
1171
|
-
# value
|
1341
|
+
Divide.call(10, 5)
|
1342
|
+
# value "2" is not allowed for :division_completed type ("2": Float === "2" does not return true) (BCDD::Result::Contract::Error::UnexpectedValue)
|
1172
1343
|
```
|
1173
1344
|
|
1174
1345
|
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
1175
1346
|
|
1176
|
-
#### `BCDD::Result::Expectations.mixin`
|
1347
|
+
#### `BCDD::Result::Expectations.mixin` add-ons
|
1177
1348
|
|
1178
|
-
The `BCDD::Result::Expectations.mixin` also accepts the `
|
1349
|
+
The `BCDD::Result::Expectations.mixin` also accepts the `config:` argument. It is a hash that can be used to define custom behaviors for the mixin.
|
1179
1350
|
|
1180
1351
|
**Continue**
|
1181
1352
|
|
1182
|
-
It is similar to `BCDD::Result.mixin(
|
1353
|
+
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.
|
1183
1354
|
|
1184
1355
|
```ruby
|
1185
1356
|
class Divide
|
1186
1357
|
include BCDD::Result::Expectations.mixin(
|
1187
|
-
|
1358
|
+
config: { addon: { continue: true } },
|
1188
1359
|
success: :division_completed,
|
1189
1360
|
failure: %i[invalid_arg division_by_zero]
|
1190
1361
|
)
|
1191
1362
|
|
1192
1363
|
def call(arg1, arg2)
|
1193
1364
|
validate_numbers(arg1, arg2)
|
1194
|
-
.and_then(:
|
1365
|
+
.and_then(:validate_nonzero)
|
1195
1366
|
.and_then(:divide)
|
1196
1367
|
end
|
1197
1368
|
|
@@ -1204,7 +1375,7 @@ class Divide
|
|
1204
1375
|
Continue([arg1, arg2])
|
1205
1376
|
end
|
1206
1377
|
|
1207
|
-
def
|
1378
|
+
def validate_nonzero(numbers)
|
1208
1379
|
return Continue(numbers) unless numbers.last.zero?
|
1209
1380
|
|
1210
1381
|
Failure(:division_by_zero, 'arg2 must not be zero')
|
@@ -1215,7 +1386,7 @@ class Divide
|
|
1215
1386
|
end
|
1216
1387
|
end
|
1217
1388
|
|
1218
|
-
result = Divide.new.call(4,2)
|
1389
|
+
result = Divide.new.call(4, 2)
|
1219
1390
|
# => #<BCDD::Result::Success type=:division_completed value=2>
|
1220
1391
|
|
1221
1392
|
# The example below shows an error because the :ok type is not allowed.
|
@@ -1223,11 +1394,447 @@ result = Divide.new.call(4,2)
|
|
1223
1394
|
# This is because the :continued type is ignored by the expectations.
|
1224
1395
|
#
|
1225
1396
|
result.success?(:ok)
|
1226
|
-
# type :ok is not allowed. Allowed types: :division_completed (BCDD::Result::
|
1397
|
+
# type :ok is not allowed. Allowed types: :division_completed (BCDD::Result::Contract::Error::UnexpectedType)
|
1398
|
+
```
|
1399
|
+
|
1400
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
1401
|
+
|
1402
|
+
### `BCDD::Result::Context`
|
1403
|
+
|
1404
|
+
The `BCDD::Result::Context` is a `BCDD::Result`, meaning it has all the features of the `BCDD::Result`. The main difference is that it only accepts keyword arguments as a value, which applies to the `and_then`: The called methods must receive keyword arguments, and the dependency injection will be performed through keyword arguments.
|
1405
|
+
|
1406
|
+
As the input/output are hashes, the results of each `and_then` call will automatically accumulate. This is useful in operations chaining, as the result of the previous operations will be automatically available for the next one. Because of this behavior, the `BCDD::Result::Context` has the `#and_expose` method to expose only the desired keys from the accumulated result.
|
1407
|
+
|
1408
|
+
#### Defining successes and failures
|
1409
|
+
|
1410
|
+
As the `BCDD::Result`, you can declare success and failures directly from `BCDD::Result::Context`.
|
1411
|
+
|
1412
|
+
```ruby
|
1413
|
+
BCDD::Result::Context::Success(:ok, a: 1, b: 2)
|
1414
|
+
#<BCDD::Result::Context::Success type=:ok value={:a=>1, :b=>2}>
|
1415
|
+
|
1416
|
+
BCDD::Result::Context::Failure(:err, message: 'something went wrong')
|
1417
|
+
#<BCDD::Result::Context::Failure type=:err value={:message=>"something went wrong"}>
|
1418
|
+
```
|
1419
|
+
|
1420
|
+
But different from `BCDD::Result` that accepts any value, the `BCDD::Result::Context` only takes keyword arguments.
|
1421
|
+
|
1422
|
+
```ruby
|
1423
|
+
BCDD::Result::Context::Success(:ok, [1, 2])
|
1424
|
+
# wrong number of arguments (given 2, expected 1) (ArgumentError)
|
1425
|
+
|
1426
|
+
BCDD::Result::Context::Failure(:err, { message: 'something went wrong' })
|
1427
|
+
# wrong number of arguments (given 2, expected 1) (ArgumentError)
|
1428
|
+
|
1429
|
+
#
|
1430
|
+
# Use ** to convert a hash to keyword arguments
|
1431
|
+
#
|
1432
|
+
BCDD::Result::Context::Success(:ok, **{ message: 'hashes can be converted to keyword arguments' })
|
1433
|
+
#<BCDD::Result::Context::Success type=:ok value={:message=>"hashes can be converted to keyword arguments"}>
|
1434
|
+
```
|
1435
|
+
|
1436
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
1437
|
+
|
1438
|
+
#### `BCDD::Result::Context.mixin`
|
1439
|
+
|
1440
|
+
As in the `BCDD::Result`, you can use the `BCDD::Result::Context.mixin` to add the `Success()` and `Failure()` methods to your classes/modules.
|
1441
|
+
|
1442
|
+
Let's see this feature and the data accumulation in action:
|
1443
|
+
|
1444
|
+
##### Class example (Instance Methods)
|
1445
|
+
|
1446
|
+
```ruby
|
1447
|
+
require 'logger'
|
1448
|
+
|
1449
|
+
class Divide
|
1450
|
+
include BCDD::Result::Context.mixin
|
1451
|
+
|
1452
|
+
def call(arg1, arg2, logger: ::Logger.new(STDOUT))
|
1453
|
+
validate_numbers(arg1, arg2)
|
1454
|
+
.and_then(:validate_nonzero)
|
1455
|
+
.and_then(:divide, logger: logger)
|
1456
|
+
end
|
1457
|
+
|
1458
|
+
private
|
1459
|
+
|
1460
|
+
def validate_numbers(arg1, arg2)
|
1461
|
+
arg1.is_a?(::Numeric) or return Failure(:err, message: 'arg1 must be numeric')
|
1462
|
+
arg2.is_a?(::Numeric) or return Failure(:err, message: 'arg2 must be numeric')
|
1463
|
+
|
1464
|
+
Success(:ok, number1: arg1, number2: arg2)
|
1465
|
+
end
|
1466
|
+
|
1467
|
+
def validate_nonzero(number2:, **)
|
1468
|
+
return Success(:ok) if number2.nonzero?
|
1469
|
+
|
1470
|
+
Failure(:err, message: 'arg2 must not be zero')
|
1471
|
+
end
|
1472
|
+
|
1473
|
+
#
|
1474
|
+
# The logger was injected via #and_then and keyword arguments
|
1475
|
+
#
|
1476
|
+
def divide(number1:, number2:, logger:)
|
1477
|
+
result = number1 / number2
|
1478
|
+
|
1479
|
+
logger.info("The division result is #{result}")
|
1480
|
+
|
1481
|
+
Success(:ok, number: result)
|
1482
|
+
end
|
1483
|
+
end
|
1484
|
+
|
1485
|
+
Divide.new.call(10, 5)
|
1486
|
+
# I, [2023-10-27T01:51:46.905004 #76915] INFO -- : The division result is 2
|
1487
|
+
#<BCDD::Result::Context::Success type=:ok value={:number=>2}>
|
1488
|
+
|
1489
|
+
Divide.new.call('10', 5)
|
1490
|
+
#<BCDD::Result::Context::Failure type=:err value={:message=>"arg1 must be numeric"}>
|
1491
|
+
|
1492
|
+
Divide.new.call(10, '5')
|
1493
|
+
#<BCDD::Result::Context::Failure type=:err value={:message=>"arg2 must be numeric"}>
|
1494
|
+
|
1495
|
+
Divide.new.call(10, 0)
|
1496
|
+
#<BCDD::Result::Context::Failure type=:err value={:message=>"arg2 must not be zero"}>
|
1497
|
+
```
|
1498
|
+
|
1499
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
1500
|
+
|
1501
|
+
##### `and_expose`
|
1502
|
+
|
1503
|
+
This allows you to expose only the desired keys from the accumulated result. It can be used with any `BCDD::Result::Context` object.
|
1504
|
+
|
1505
|
+
Let's add it to the previous example:
|
1506
|
+
|
1507
|
+
```ruby
|
1508
|
+
class Divide
|
1509
|
+
include BCDD::Result::Context.mixin
|
1510
|
+
|
1511
|
+
def call(arg1, arg2)
|
1512
|
+
validate_numbers(arg1, arg2)
|
1513
|
+
.and_then(:validate_nonzero)
|
1514
|
+
.and_then(:divide)
|
1515
|
+
.and_expose(:division_completed, [:number])
|
1516
|
+
end
|
1517
|
+
|
1518
|
+
private
|
1519
|
+
|
1520
|
+
def validate_numbers(arg1, arg2)
|
1521
|
+
arg1.is_a?(::Numeric) or return Failure(:err, message: 'arg1 must be numeric')
|
1522
|
+
arg2.is_a?(::Numeric) or return Failure(:err, message: 'arg2 must be numeric')
|
1523
|
+
|
1524
|
+
Success(:ok, number1: arg1, number2: arg2)
|
1525
|
+
end
|
1526
|
+
|
1527
|
+
def validate_nonzero(number2:, **)
|
1528
|
+
return Success(:ok) if number2.nonzero?
|
1529
|
+
|
1530
|
+
Failure(:err, message: 'arg2 must not be zero')
|
1531
|
+
end
|
1532
|
+
|
1533
|
+
def divide(**input)
|
1534
|
+
Success(:ok, number: input.values.reduce(:/), **input)
|
1535
|
+
end
|
1536
|
+
end
|
1537
|
+
|
1538
|
+
Divide.new.call(10, 5)
|
1539
|
+
#<BCDD::Result::Context::Success type=:division_completed value={:number=>2}>
|
1540
|
+
```
|
1541
|
+
|
1542
|
+
As you can see, even with `divide` success exposing the division number with all the accumulated data (`**input`), the `#and_expose` could generate a new success with a new type and only with the desired keys.
|
1543
|
+
|
1544
|
+
Remove the `#and_expose` call to see the difference. This will be the outcome:
|
1545
|
+
|
1546
|
+
```ruby
|
1547
|
+
Divide.new.call(10, 5)
|
1548
|
+
#<BCDD::Result::Context::Success type=:ok value={:number=>2, :number1=>10, :number2=>5}>
|
1549
|
+
```
|
1550
|
+
|
1551
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
1552
|
+
|
1553
|
+
##### Module example (Singleton Methods)
|
1554
|
+
|
1555
|
+
Yes, the `BCDD::Result::Context.mixin` can produce singleton methods. Look for an example using a module (but it could be a class, too).
|
1556
|
+
|
1557
|
+
```ruby
|
1558
|
+
module Divide
|
1559
|
+
extend self, BCDD::Result::Context.mixin
|
1560
|
+
|
1561
|
+
def call(arg1, arg2)
|
1562
|
+
validate_numbers(arg1, arg2)
|
1563
|
+
.and_then(:validate_nonzero)
|
1564
|
+
.and_then(:divide)
|
1565
|
+
.and_expose(:division_completed, [:number])
|
1566
|
+
end
|
1567
|
+
|
1568
|
+
private
|
1569
|
+
|
1570
|
+
def validate_numbers(arg1, arg2)
|
1571
|
+
arg1.is_a?(::Numeric) or return Failure(:err, message: 'arg1 must be numeric')
|
1572
|
+
arg2.is_a?(::Numeric) or return Failure(:err, message: 'arg2 must be numeric')
|
1573
|
+
|
1574
|
+
Success(:ok, number1: arg1, number2: arg2)
|
1575
|
+
end
|
1576
|
+
|
1577
|
+
def validate_nonzero(number2:, **)
|
1578
|
+
return Success(:ok) if number2.nonzero?
|
1579
|
+
|
1580
|
+
Failure(:err, message: 'arg2 must not be zero')
|
1581
|
+
end
|
1582
|
+
|
1583
|
+
def divide(number1:, number2:)
|
1584
|
+
Success(:ok, number: number1 / number2)
|
1585
|
+
end
|
1586
|
+
end
|
1587
|
+
|
1588
|
+
Divide.call(10, 5)
|
1589
|
+
#<BCDD::Result::Context::Success type=:division_completed value={:number=>2}>
|
1590
|
+
|
1591
|
+
Divide.call('10', 5)
|
1592
|
+
#<BCDD::Result::Context::Failure type=:err value={:message=>"arg1 must be numeric"}>
|
1593
|
+
|
1594
|
+
Divide.call(10, '5')
|
1595
|
+
#<BCDD::Result::Context::Failure type=:err value={:message=>"arg2 must be numeric"}>
|
1596
|
+
|
1597
|
+
Divide.call(10, 0)
|
1598
|
+
#<BCDD::Result::Context::Failure type=:err value={:message=>"arg2 must not be zero"}>
|
1599
|
+
```
|
1600
|
+
|
1601
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
1602
|
+
|
1603
|
+
#### `BCDD::Result::Context::Expectations`
|
1604
|
+
|
1605
|
+
The `BCDD::Result::Context::Expectations` is a `BCDD::Result::Expectations` with the `BCDD::Result::Context` features.
|
1606
|
+
|
1607
|
+
This is an example using the mixin mode, but the standalone mode is also supported.
|
1608
|
+
|
1609
|
+
```ruby
|
1610
|
+
class Divide
|
1611
|
+
include BCDD::Result::Context::Expectations.mixin(
|
1612
|
+
config: {
|
1613
|
+
pattern_matching: { nil_as_valid_value_checking: true }
|
1614
|
+
},
|
1615
|
+
success: {
|
1616
|
+
division_completed: ->(value) { value => { number: Numeric } }
|
1617
|
+
},
|
1618
|
+
failure: {
|
1619
|
+
invalid_arg: ->(value) { value => { message: String } },
|
1620
|
+
division_by_zero: ->(value) { value => { message: String } }
|
1621
|
+
}
|
1622
|
+
)
|
1623
|
+
|
1624
|
+
def call(arg1, arg2)
|
1625
|
+
arg1.is_a?(Numeric) or return Failure(:invalid_arg, message: 'arg1 must be numeric')
|
1626
|
+
arg2.is_a?(Numeric) or return Failure(:invalid_arg, message: 'arg2 must be numeric')
|
1627
|
+
|
1628
|
+
arg2.zero? and return Failure(:division_by_zero, message: 'arg2 must not be zero')
|
1629
|
+
|
1630
|
+
Success(:division_completed, number: (arg1 / arg2))
|
1631
|
+
end
|
1632
|
+
end
|
1633
|
+
|
1634
|
+
Divide.new.call(10, 5)
|
1635
|
+
#<BCDD::Result::Context::Success type=:division_completed value={:number=>2}>
|
1636
|
+
```
|
1637
|
+
|
1638
|
+
As in the `BCDD::Result::Expectations.mixin`, the `BCDD::Result::Context::Expectations.mixin` will add a Result constant in the target class. It can generate success/failure results, which ensure the mixin expectations.
|
1639
|
+
|
1640
|
+
Let's see this using previous example:
|
1641
|
+
|
1642
|
+
```ruby
|
1643
|
+
Divide::Result::Success(:division_completed, number: 2)
|
1644
|
+
#<BCDD::Result::Context::Success type=:division_completed value={:number=>2}>
|
1645
|
+
|
1646
|
+
Divide::Result::Success(:division_completed, number: '2')
|
1647
|
+
# value {:number=>"2"} is not allowed for :division_completed type ({:number=>"2"}: Numeric === "2" does not return true) (BCDD::Result::Contract::Error::UnexpectedValue)
|
1648
|
+
```
|
1649
|
+
|
1650
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
1651
|
+
|
1652
|
+
#### Mixin add-ons
|
1653
|
+
|
1654
|
+
The `BCDD::Result::Context.mixin` and `BCDD::Result::Context::Expectations.mixin` also accepts the `config:` argument. And it works the same way as the `BCDD::Result` mixins.
|
1655
|
+
|
1656
|
+
**Continue**
|
1657
|
+
|
1658
|
+
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.
|
1659
|
+
|
1660
|
+
Let's use a mix of `BCDD::Result::Context` features to see in action with this add-on:
|
1661
|
+
|
1662
|
+
```ruby
|
1663
|
+
module Divide
|
1664
|
+
require 'logger'
|
1665
|
+
|
1666
|
+
extend self, BCDD::Result::Context::Expectations.mixin(
|
1667
|
+
config: {
|
1668
|
+
addon: { continue: true },
|
1669
|
+
pattern_matching: { nil_as_valid_value_checking: true }
|
1670
|
+
},
|
1671
|
+
success: {
|
1672
|
+
division_completed: ->(value) { value => { number: Numeric } }
|
1673
|
+
},
|
1674
|
+
failure: {
|
1675
|
+
invalid_arg: ->(value) { value => { message: String } },
|
1676
|
+
division_by_zero: ->(value) { value => { message: String } }
|
1677
|
+
}
|
1678
|
+
)
|
1679
|
+
|
1680
|
+
def call(arg1, arg2, logger: ::Logger.new(STDOUT))
|
1681
|
+
validate_numbers(arg1, arg2)
|
1682
|
+
.and_then(:validate_nonzero)
|
1683
|
+
.and_then(:divide, logger: logger)
|
1684
|
+
.and_expose(:division_completed, [:number])
|
1685
|
+
end
|
1686
|
+
|
1687
|
+
private
|
1688
|
+
|
1689
|
+
def validate_numbers(arg1, arg2)
|
1690
|
+
arg1.is_a?(::Numeric) or return Failure(:invalid_arg, message: 'arg1 must be numeric')
|
1691
|
+
arg2.is_a?(::Numeric) or return Failure(:invalid_arg, message: 'arg2 must be numeric')
|
1692
|
+
|
1693
|
+
Continue(number1: arg1, number2: arg2)
|
1694
|
+
end
|
1695
|
+
|
1696
|
+
def validate_nonzero(number2:, **)
|
1697
|
+
return Continue() if number2.nonzero?
|
1698
|
+
|
1699
|
+
Failure(:division_by_zero, message: 'arg2 must not be zero')
|
1700
|
+
end
|
1701
|
+
|
1702
|
+
def divide(number1:, number2:, logger:)
|
1703
|
+
result = number1 / number2
|
1704
|
+
|
1705
|
+
logger.info("The division result is #{result}")
|
1706
|
+
|
1707
|
+
Continue(number: result)
|
1708
|
+
end
|
1709
|
+
end
|
1710
|
+
|
1711
|
+
Divide.call(14, 2)
|
1712
|
+
# I, [2023-10-27T02:01:05.812388 #77823] INFO -- : The division result is 7
|
1713
|
+
#<BCDD::Result::Context::Success type=:division_completed value={:number=>7}>
|
1714
|
+
|
1715
|
+
Divide.call('14', 2)
|
1716
|
+
#<BCDD::Result::Context::Failure type=:invalid_arg value={:message=>"arg1 must be numeric"}>
|
1717
|
+
|
1718
|
+
Divide.call(14, '2')
|
1719
|
+
#<BCDD::Result::Context::Failure type=:invalid_arg value={:message=>"arg2 must be numeric"}>
|
1720
|
+
|
1721
|
+
Divide.call(14, 0)
|
1722
|
+
#<BCDD::Result::Context::Failure type=:division_by_zero value={:message=>"arg2 must not be zero"}>
|
1227
1723
|
```
|
1228
1724
|
|
1725
|
+
### `BCDD::Result.configuration`
|
1726
|
+
|
1727
|
+
The `BCDD::Result.configuration` allows you to configure default behaviors for `BCDD::Result` and `BCDD::Result::Context` through a configuration block. After using it, the configuration is frozen, ensuring the expected behaviors for your application.
|
1728
|
+
|
1729
|
+
```ruby
|
1730
|
+
BCDD::Result.configuration do |config|
|
1731
|
+
config.addon.enable!(:continue)
|
1732
|
+
|
1733
|
+
config.constant_alias.enable!('Result')
|
1734
|
+
|
1735
|
+
config.pattern_matching.disable!(:nil_as_valid_value_checking)
|
1736
|
+
|
1737
|
+
config.feature.disable!(:expectations) if ::Rails.env.production?
|
1738
|
+
end
|
1739
|
+
```
|
1740
|
+
|
1741
|
+
Use `disable!` to disable a feature and `enable!` to enable it.
|
1742
|
+
|
1743
|
+
Let's see what each configuration in the example above does:
|
1744
|
+
|
1745
|
+
#### `config.addon.enable!(:continue)`
|
1746
|
+
|
1747
|
+
This configuration enables the `Continue()` method for `BCDD::Result` and `BCDD::Result::Context`. Link to documentations: [(1)](#add-ons) [(2)](#mixin-add-ons).
|
1748
|
+
|
1749
|
+
#### `config.constant_alias.enable!('Result')`
|
1750
|
+
|
1751
|
+
This configuration make `Result` a constant alias for `BCDD::Result`. Link to [documentation](#bcddresult-versus-result).
|
1752
|
+
|
1753
|
+
#### `config.pattern_matching.disable!(:nil_as_valid_value_checking)`
|
1754
|
+
|
1755
|
+
This configuration disables the `nil_as_valid_value_checking` for `BCDD::Result` and `BCDD::Result::Context`. Link to [documentation](#pattern-matching-support).
|
1756
|
+
|
1229
1757
|
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
1230
1758
|
|
1759
|
+
#### `config.feature.disable!(:expectations)`
|
1760
|
+
|
1761
|
+
This configuration turns off the expectations for `BCDD::Result` and `BCDD::Result::Context`. The expectations are helpful in development and test environments, but they can be disabled in production environments for performance gain.
|
1762
|
+
|
1763
|
+
PS: I'm using `::Rails.env.production?` to check the environment, but you can use any logic you want.
|
1764
|
+
|
1765
|
+
### `BCDD::Result.config`
|
1766
|
+
|
1767
|
+
The `BCDD::Result.config` allows you to access the current configuration. It is useful when you want to check the current configuration.
|
1768
|
+
|
1769
|
+
**BCDD::Result.config.addon**
|
1770
|
+
|
1771
|
+
```ruby
|
1772
|
+
BCDD::Result.config.addon.enabled?(:continue)
|
1773
|
+
|
1774
|
+
BCDD::Result.config.addon.options
|
1775
|
+
# {
|
1776
|
+
# :continue=>{
|
1777
|
+
# :enabled=>false,
|
1778
|
+
# :affects=>[
|
1779
|
+
# "BCDD::Result",
|
1780
|
+
# "BCDD::Result::Context",
|
1781
|
+
# "BCDD::Result::Expectations",
|
1782
|
+
# "BCDD::Result::Context::Expectations"
|
1783
|
+
# ]
|
1784
|
+
# }
|
1785
|
+
# }
|
1786
|
+
```
|
1787
|
+
|
1788
|
+
**BCDD::Result.config.constant_alias**
|
1789
|
+
|
1790
|
+
```ruby
|
1791
|
+
BCDD::Result.config.constant_alias.enabled?('Result')
|
1792
|
+
|
1793
|
+
BCDD::Result.config.constant_alias.options
|
1794
|
+
# {
|
1795
|
+
# "Result"=>{
|
1796
|
+
# :enabled=>false,
|
1797
|
+
# :affects=>[
|
1798
|
+
# "Object"
|
1799
|
+
# ]
|
1800
|
+
# }
|
1801
|
+
# }
|
1802
|
+
```
|
1803
|
+
|
1804
|
+
**BCDD::Result.config.pattern_matching**
|
1805
|
+
|
1806
|
+
```ruby
|
1807
|
+
BCDD::Result.config.pattern_matching.enabled?(:nil_as_valid_value_checking)
|
1808
|
+
|
1809
|
+
BCDD::Result.config.pattern_matching.options
|
1810
|
+
# {
|
1811
|
+
# :nil_as_valid_value_checking=>{
|
1812
|
+
# :enabled=>false,
|
1813
|
+
# :affects=>[
|
1814
|
+
# "BCDD::Result::Expectations,
|
1815
|
+
# "BCDD::Result::Context::Expectations"
|
1816
|
+
# ]
|
1817
|
+
# }
|
1818
|
+
# }
|
1819
|
+
```
|
1820
|
+
|
1821
|
+
**BCDD::Result.config.feature**
|
1822
|
+
|
1823
|
+
```ruby
|
1824
|
+
BCDD::Result.config.feature.enabled?(:expectations)
|
1825
|
+
|
1826
|
+
BCDD::Result.config.feature.options
|
1827
|
+
# {
|
1828
|
+
# :expectations=>{
|
1829
|
+
# :enabled=>true,
|
1830
|
+
# :affects=>[
|
1831
|
+
# "BCDD::Result::Expectations,
|
1832
|
+
# "BCDD::Result::Context::Expectations"
|
1833
|
+
# ]
|
1834
|
+
# }
|
1835
|
+
# }
|
1836
|
+
```
|
1837
|
+
|
1231
1838
|
## About
|
1232
1839
|
|
1233
1840
|
[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.
|