bcdd-result 0.3.0 → 0.5.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 +22 -1
- data/.rubocop_todo.yml +3 -12
- data/CHANGELOG.md +96 -0
- data/README.md +583 -36
- data/lib/bcdd/result/data.rb +33 -0
- data/lib/bcdd/result/error.rb +37 -31
- data/lib/bcdd/result/expectations/contract/disabled.rb +25 -0
- data/lib/bcdd/result/expectations/contract/evaluator.rb +45 -0
- data/lib/bcdd/result/expectations/contract/for_types.rb +29 -0
- data/lib/bcdd/result/expectations/contract/for_types_and_values.rb +37 -0
- data/lib/bcdd/result/expectations/contract/interface.rb +21 -0
- data/lib/bcdd/result/expectations/contract.rb +25 -0
- data/lib/bcdd/result/expectations/error.rb +15 -0
- data/lib/bcdd/result/expectations/type_checker.rb +33 -0
- data/lib/bcdd/result/expectations.rb +62 -0
- data/lib/bcdd/result/failure.rb +6 -2
- data/lib/bcdd/result/handler/allowed_types.rb +45 -0
- data/lib/bcdd/result/handler.rb +20 -11
- data/lib/bcdd/result/mixin.rb +13 -0
- data/lib/bcdd/result/success.rb +6 -2
- data/lib/bcdd/result/version.rb +1 -1
- data/lib/bcdd/result.rb +61 -29
- data/lib/result.rb +5 -0
- data/sig/bcdd/result.rbs +224 -43
- metadata +20 -7
- data/lib/bcdd/result/type.rb +0 -17
- data/lib/bcdd/resultable.rb +0 -11
data/README.md
CHANGED
@@ -1,10 +1,13 @@
|
|
1
1
|
<p align="center">
|
2
2
|
<h1 align="center" id="-bcddresult">🔀 BCDD::Result</h1>
|
3
3
|
<p align="center"><i>Empower Ruby apps with a pragmatic use of Railway Oriented Programming.</i></p>
|
4
|
-
<
|
4
|
+
<p align="center">
|
5
|
+
<img src="https://img.shields.io/badge/ruby->%3D%202.7.0-ruby.svg?colorA=99004d&colorB=cc0066" alt="Ruby">
|
6
|
+
<img src="https://badge.fury.io/rb/bcdd-result.svg" alt="bcdd-result gem version" height="18">
|
7
|
+
</p>
|
5
8
|
</p>
|
6
9
|
|
7
|
-
|
10
|
+
It's a general-purpose result monad that allows you to create objects representing a success (`BCDD::Result::Success`) or failure (`BCDD::Result::Failure`).
|
8
11
|
|
9
12
|
**What problem does it solve?**
|
10
13
|
|
@@ -17,6 +20,7 @@ Use it to enable the [Railway Oriented Programming](https://fsharpforfunandprofi
|
|
17
20
|
- [Ruby Version](#ruby-version)
|
18
21
|
- [Installation](#installation)
|
19
22
|
- [Usage](#usage)
|
23
|
+
- [`BCDD::Result` *versus* `Result`](#bcddresult-versus-result)
|
20
24
|
- [Reference](#reference)
|
21
25
|
- [Result Attributes](#result-attributes)
|
22
26
|
- [Receiving types in `result.success?` or `result.failure?`](#receiving-types-in-resultsuccess-or-resultfailure)
|
@@ -25,16 +29,35 @@ Use it to enable the [Railway Oriented Programming](https://fsharpforfunandprofi
|
|
25
29
|
- [`result.on_type`](#resulton_type)
|
26
30
|
- [`result.on_success`](#resulton_success)
|
27
31
|
- [`result.on_failure`](#resulton_failure)
|
32
|
+
- [`result.on_unknown`](#resulton_unknown)
|
28
33
|
- [`result.handle`](#resulthandle)
|
29
34
|
- [Result Value](#result-value)
|
30
35
|
- [`result.value_or`](#resultvalue_or)
|
31
|
-
|
36
|
+
- [Result Data](#result-data)
|
37
|
+
- [`result.data`](#resultdata)
|
32
38
|
- [Railway Oriented Programming](#railway-oriented-programming)
|
33
39
|
- [`result.and_then`](#resultand_then)
|
34
|
-
- [`BCDD::
|
35
|
-
- [Class example (
|
36
|
-
- [Module example (
|
40
|
+
- [`BCDD::Result::Mixin`](#bcddresultmixin)
|
41
|
+
- [Class example (Instance Methods)](#class-example-instance-methods)
|
42
|
+
- [Module example (Singleton Methods)](#module-example-singleton-methods)
|
37
43
|
- [Restrictions](#restrictions)
|
44
|
+
- [Pattern Matching](#pattern-matching)
|
45
|
+
- [`Array`/`Find` patterns](#arrayfind-patterns)
|
46
|
+
- [`Hash` patterns](#hash-patterns)
|
47
|
+
- [BCDD::Result::Expectations](#bcddresultexpectations)
|
48
|
+
- [`BCDD::Result::Expectations`](#bcddresultexpectations-1)
|
49
|
+
- [Standalone *versus* Mixin mode](#standalone-versus-mixin-mode)
|
50
|
+
- [Type checking - Result Hooks](#type-checking---result-hooks)
|
51
|
+
- [`#success?` and `#failure?`](#success-and-failure)
|
52
|
+
- [`#on` and `#on_type`](#on-and-on_type)
|
53
|
+
- [`#on_success` and `#on_failure`](#on_success-and-on_failure)
|
54
|
+
- [`#handle`](#handle)
|
55
|
+
- [Type checking - Result Creation](#type-checking---result-creation)
|
56
|
+
- [Mixin mode](#mixin-mode)
|
57
|
+
- [Standalone mode](#standalone-mode)
|
58
|
+
- [Value checking - Result Creation](#value-checking---result-creation)
|
59
|
+
- [Success()](#success)
|
60
|
+
- [Failure()](#failure)
|
38
61
|
- [About](#about)
|
39
62
|
- [Development](#development)
|
40
63
|
- [Contributing](#contributing)
|
@@ -61,7 +84,7 @@ If bundler is not being used to manage dependencies, install the gem by executin
|
|
61
84
|
|
62
85
|
$ gem install bcdd-result
|
63
86
|
|
64
|
-
<p align="right"
|
87
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
65
88
|
|
66
89
|
## Usage
|
67
90
|
|
@@ -81,7 +104,23 @@ BCDD::Result::Success(:ok) #
|
|
81
104
|
BCDD::Result::Failure(:err) #
|
82
105
|
```
|
83
106
|
|
84
|
-
<p align="right"
|
107
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
108
|
+
|
109
|
+
#### `BCDD::Result` *versus* `Result`
|
110
|
+
|
111
|
+
The `BCDD::Result` is the main module of this gem. It contains all the features, constants, and methods you will use to create and manipulate results.
|
112
|
+
|
113
|
+
The `Result` is an alias of `BCDD::Result`. It was created to facilitate the use of this gem in the code. So, instead of requiring `BCDD::Result` everywhere, you can require `Result` and use it as an alias.
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
require 'result'
|
117
|
+
|
118
|
+
Result::Success(:ok) # <BCDD::Result::Success type=:ok value=nil>
|
119
|
+
```
|
120
|
+
|
121
|
+
All the examples in this README that use `BCDD::Result` can also be used with `Result`.
|
122
|
+
|
123
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
85
124
|
|
86
125
|
## Reference
|
87
126
|
|
@@ -137,7 +176,7 @@ result.type # :no
|
|
137
176
|
result.value # nil
|
138
177
|
```
|
139
178
|
|
140
|
-
<p align="right"
|
179
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
141
180
|
|
142
181
|
#### Receiving types in `result.success?` or `result.failure?`
|
143
182
|
|
@@ -163,7 +202,7 @@ result.failure?(:err) # true
|
|
163
202
|
result.failure?(:error) # false
|
164
203
|
```
|
165
204
|
|
166
|
-
<p align="right"
|
205
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
167
206
|
|
168
207
|
### Result Hooks
|
169
208
|
|
@@ -182,7 +221,7 @@ def divide(arg1, arg2)
|
|
182
221
|
end
|
183
222
|
```
|
184
223
|
|
185
|
-
<p align="right"
|
224
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
186
225
|
|
187
226
|
#### `result.on`
|
188
227
|
|
@@ -220,7 +259,7 @@ result.object_id == output.object_id # true
|
|
220
259
|
|
221
260
|
*PS: The `divide()` implementation is [here](#result-hooks).*
|
222
261
|
|
223
|
-
<p align="right"
|
262
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
224
263
|
|
225
264
|
#### `result.on_type`
|
226
265
|
|
@@ -249,7 +288,7 @@ divide(4, 4).on_failure { |error| puts error }
|
|
249
288
|
|
250
289
|
*PS: The `divide()` implementation is [here](#result-hooks).*
|
251
290
|
|
252
|
-
<p align="right"
|
291
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
253
292
|
|
254
293
|
#### `result.on_failure`
|
255
294
|
|
@@ -271,7 +310,28 @@ divide(4, 0).on_failure(:invalid_arg) { |error| puts error }
|
|
271
310
|
|
272
311
|
*PS: The `divide()` implementation is [here](#result-hooks).*
|
273
312
|
|
274
|
-
<p align="right"
|
313
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
314
|
+
|
315
|
+
#### `result.on_unknown`
|
316
|
+
|
317
|
+
`BCDD::Result#on_unknown` will perform the block when no other hook (`#on`, `#on_type`, `#on_failure`, `#on_success`) has been executed.
|
318
|
+
|
319
|
+
Regardless of the block being executed, the return of the method will always be the result itself.
|
320
|
+
|
321
|
+
The result value will be exposed as the first argument of the block.
|
322
|
+
|
323
|
+
```ruby
|
324
|
+
divide(4, 2)
|
325
|
+
.on(:invalid_arg) { |msg| puts msg }
|
326
|
+
.on(:division_by_zero) { |msg| puts msg }
|
327
|
+
.on_unknown { |value, type| puts [type, value].inspect }
|
328
|
+
|
329
|
+
# The code above will print '[:division_completed, 2]' and return the result itself.
|
330
|
+
```
|
331
|
+
|
332
|
+
*PS: The `divide()` implementation is [here](#result-hooks).*
|
333
|
+
|
334
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
275
335
|
|
276
336
|
#### `result.handle`
|
277
337
|
|
@@ -282,6 +342,7 @@ divide(4, 2).handle do |result|
|
|
282
342
|
result.success { |number| number }
|
283
343
|
result.failure(:invalid_arg) { |err| puts err }
|
284
344
|
result.type(:division_by_zero) { raise ZeroDivisionError }
|
345
|
+
result.unknown { raise NotImplementedError }
|
285
346
|
end
|
286
347
|
|
287
348
|
#or
|
@@ -289,6 +350,7 @@ end
|
|
289
350
|
divide(4, 2).handle do |on|
|
290
351
|
on.success { |number| number }
|
291
352
|
on.failure { |err| puts err }
|
353
|
+
on.unknown { raise NotImplementedError }
|
292
354
|
end
|
293
355
|
|
294
356
|
#or
|
@@ -297,6 +359,7 @@ divide(4, 2).handle do |on|
|
|
297
359
|
on.type(:invalid_arg) { |err| puts err }
|
298
360
|
on.type(:division_by_zero) { raise ZeroDivisionError }
|
299
361
|
on.type(:division_completed) { |number| number }
|
362
|
+
on.unknown { raise NotImplementedError }
|
300
363
|
end
|
301
364
|
|
302
365
|
# or
|
@@ -305,6 +368,7 @@ divide(4, 2).handle do |on|
|
|
305
368
|
on[:invalid_arg] { |err| puts err }
|
306
369
|
on[:division_by_zero] { raise ZeroDivisionError }
|
307
370
|
on[:division_completed] { |number| number }
|
371
|
+
on.unknown { raise NotImplementedError }
|
308
372
|
end
|
309
373
|
|
310
374
|
# The [] syntax 👆 is an alias of #type.
|
@@ -317,13 +381,13 @@ end
|
|
317
381
|
|
318
382
|
*PS: The `divide()` implementation is [here](#result-hooks).*
|
319
383
|
|
320
|
-
<p align="right"
|
384
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
321
385
|
|
322
386
|
### Result Value
|
323
387
|
|
324
|
-
The most simple way to get the result value is by calling `BCDD::Result#value
|
388
|
+
The most simple way to get the result value is by calling `BCDD::Result#value`.
|
325
389
|
|
326
|
-
But sometimes you need to get the value of a successful result or a default value if it is a failure. In this case, you can use `BCDD::Result#value_or
|
390
|
+
But sometimes you need to get the value of a successful result or a default value if it is a failure. In this case, you can use `BCDD::Result#value_or`.
|
327
391
|
|
328
392
|
#### `result.value_or`
|
329
393
|
|
@@ -350,11 +414,50 @@ divide(100, 0).value_or { 0 } # 0
|
|
350
414
|
|
351
415
|
*PS: The `divide()` implementation is [here](#result-hooks).*
|
352
416
|
|
353
|
-
<p align="right"
|
417
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
418
|
+
|
419
|
+
### Result Data
|
420
|
+
|
421
|
+
#### `result.data`
|
422
|
+
|
423
|
+
The `BCDD::Result#data` exposes the result attributes (name, type, value) directly and as a hash (`to_h`/`to_hash`) and array (`to_a`/`to_ary`).
|
424
|
+
|
425
|
+
This is helpful if you need to access the result attributes generically or want to use Ruby features like splat (`*`) and double splat (`**`) operators.
|
426
|
+
|
427
|
+
See the examples below to understand how to use it.
|
428
|
+
|
429
|
+
```ruby
|
430
|
+
result = BCDD::Result::Success(:ok, 1)
|
431
|
+
|
432
|
+
success_data = result.data # #<BCDD::Result::Data name=:success type=:ok value=1>
|
433
|
+
|
434
|
+
success_data.name # :success
|
435
|
+
success_data.type # :ok
|
436
|
+
success_data.value # 1
|
437
|
+
|
438
|
+
success_data.to_h # {:name=>:success, :type=>:ok, :value=>1}
|
439
|
+
success_data.to_a # [:success, :ok, 1]
|
354
440
|
|
355
|
-
|
441
|
+
name, type, value = success_data
|
356
442
|
|
357
|
-
|
443
|
+
[name, type, value] # [:success, :ok, 1]
|
444
|
+
|
445
|
+
def print_to_ary(name, type, value)
|
446
|
+
puts [name, type, value].inspect
|
447
|
+
end
|
448
|
+
|
449
|
+
def print_to_hash(name:, type:, value:)
|
450
|
+
puts [name, type, value].inspect
|
451
|
+
end
|
452
|
+
|
453
|
+
print_to_ary(*success_data) # [:success, :ok, 1]
|
454
|
+
|
455
|
+
print_to_hash(**success_data) # [:success, :ok, 1]
|
456
|
+
```
|
457
|
+
|
458
|
+
> **NOTE:** The example above uses a success result, but the same is valid for a failure result.
|
459
|
+
|
460
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
358
461
|
|
359
462
|
### Railway Oriented Programming
|
360
463
|
|
@@ -416,21 +519,19 @@ Divide.call(2, 2)
|
|
416
519
|
#<BCDD::Result::Success type=:division_completed data=1>
|
417
520
|
```
|
418
521
|
|
419
|
-
<p align="right"
|
420
|
-
|
421
|
-
#### `BCDD::Resultable`
|
522
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
422
523
|
|
423
|
-
|
524
|
+
#### `BCDD::Result::Mixin`
|
424
525
|
|
425
|
-
The main difference between these methods and `BCDD::Result::Success()`/`BCDD::Result::Failure()` is that the first ones will use the target object as the result's subject.
|
526
|
+
It is a module that can be included/extended by any object. It adds two methods to the target object: `Success()` and `Failure()`. The main difference between these methods and `BCDD::Result::Success()`/`BCDD::Result::Failure()` is that the first ones will use the target object (who received the include/extend) as the result's subject.
|
426
527
|
|
427
|
-
And because of this, you can use the `#and_then` method to call methods from the
|
528
|
+
And because of this, you can use the `#and_then` method to call methods from the result's subject.
|
428
529
|
|
429
|
-
##### Class example (
|
530
|
+
##### Class example (Instance Methods)
|
430
531
|
|
431
532
|
```ruby
|
432
533
|
class Divide
|
433
|
-
include BCDD::
|
534
|
+
include BCDD::Result::Mixin
|
434
535
|
|
435
536
|
attr_reader :arg1, :arg2
|
436
537
|
|
@@ -474,11 +575,11 @@ Divide.new('4', 2).call #<BCDD::Result::Failure type=:invalid_arg value="arg1 mu
|
|
474
575
|
Divide.new(4, '2').call #<BCDD::Result::Failure type=:invalid_arg value="arg2 must be numeric">
|
475
576
|
```
|
476
577
|
|
477
|
-
##### Module example (
|
578
|
+
##### Module example (Singleton Methods)
|
478
579
|
|
479
580
|
```ruby
|
480
581
|
module Divide
|
481
|
-
extend BCDD::
|
582
|
+
extend BCDD::Result::Mixin
|
482
583
|
extend self
|
483
584
|
|
484
585
|
def call(arg1, arg2)
|
@@ -518,17 +619,463 @@ Divide.call(4, '2') #<BCDD::Result::Failure type=:invalid_arg value="arg2 must b
|
|
518
619
|
|
519
620
|
The unique condition for using the `#and_then` to call methods is that they must use the `Success()` and `Failure()` to produce their results.
|
520
621
|
|
521
|
-
If you use `BCDD::Result::Subject()`/`BCDD::Result::Failure()`, or
|
622
|
+
If you use `BCDD::Result::Subject()`/`BCDD::Result::Failure()`, or use result from another `BCDD::Result::Mixin` instance, the `#and_then` will raise an error because the subjects will be different.
|
522
623
|
|
523
624
|
> **Note**: You still can use the block syntax, but all the results must be produced by the subject's `Success()` and `Failure()` methods.
|
524
625
|
|
525
|
-
<p align="right"
|
626
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
627
|
+
|
628
|
+
### Pattern Matching
|
629
|
+
|
630
|
+
The `BCDD::Result` also provides support to pattern matching.
|
631
|
+
|
632
|
+
In the further examples, I will use the `Divide` lambda to exemplify its usage.
|
633
|
+
|
634
|
+
```ruby
|
635
|
+
Divide = lambda do |arg1, arg2|
|
636
|
+
arg1.is_a?(::Numeric) or return BCDD::Result::Failure(:invalid_arg, 'arg1 must be numeric')
|
637
|
+
arg2.is_a?(::Numeric) or return BCDD::Result::Failure(:invalid_arg, 'arg2 must be numeric')
|
638
|
+
|
639
|
+
return BCDD::Result::Failure(:division_by_zero, 'arg2 must not be zero') if arg2.zero?
|
640
|
+
|
641
|
+
BCDD::Result::Success(:division_completed, arg1 / arg2)
|
642
|
+
end
|
643
|
+
```
|
644
|
+
|
645
|
+
#### `Array`/`Find` patterns
|
646
|
+
|
647
|
+
```ruby
|
648
|
+
case Divide.call(4, 2)
|
649
|
+
in BCDD::Result::Failure[:invalid_arg, msg] then puts msg
|
650
|
+
in BCDD::Result::Failure[:division_by_zero, msg] then puts msg
|
651
|
+
in BCDD::Result::Success[:division_completed, value] then puts value
|
652
|
+
end
|
653
|
+
|
654
|
+
# The code above will print: 2
|
655
|
+
|
656
|
+
case Divide.call(4, 0)
|
657
|
+
in BCDD::Result::Failure[:invalid_arg, msg] then puts msg
|
658
|
+
in BCDD::Result::Failure[:division_by_zero, msg] then puts msg
|
659
|
+
in BCDD::Result::Success[:division_completed, value] then puts value
|
660
|
+
end
|
661
|
+
|
662
|
+
# The code above will print: arg2 must not be zero
|
663
|
+
```
|
664
|
+
|
665
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
666
|
+
|
667
|
+
#### `Hash` patterns
|
668
|
+
|
669
|
+
```ruby
|
670
|
+
case Divide.call(10, 2)
|
671
|
+
in { failure: { invalid_arg: msg } } then puts msg
|
672
|
+
in { failure: { division_by_zero: msg } } then puts msg
|
673
|
+
in { success: { division_completed: value } } then puts value
|
674
|
+
end
|
675
|
+
|
676
|
+
# The code above will print: 5
|
677
|
+
|
678
|
+
case Divide.call('10', 2)
|
679
|
+
in { failure: { invalid_arg: msg } } then puts msg
|
680
|
+
in { failure: { division_by_zero: msg } } then puts msg
|
681
|
+
in { success: { division_completed: value } } then puts value
|
682
|
+
end
|
683
|
+
|
684
|
+
# The code above will print: arg1 must be numeric
|
685
|
+
```
|
686
|
+
|
687
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
688
|
+
|
689
|
+
#### BCDD::Result::Expectations
|
690
|
+
|
691
|
+
I'd like you to please read the following section to understand how to use this feature.
|
692
|
+
|
693
|
+
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.
|
694
|
+
|
695
|
+
```ruby
|
696
|
+
module Divide
|
697
|
+
extend BCDD::Result::Expectations.mixin(
|
698
|
+
success: {
|
699
|
+
numbers: ->(value) { value in [Numeric, Numeric] },
|
700
|
+
division_completed: ->(value) { value in (Integer | Float) }
|
701
|
+
},
|
702
|
+
failure: { invalid_arg: String, division_by_zero: String }
|
703
|
+
)
|
704
|
+
|
705
|
+
def self.call(arg1, arg2)
|
706
|
+
arg1.is_a?(Numeric) or return Failure(:invalid_arg, 'arg1 must be numeric')
|
707
|
+
arg2.is_a?(Numeric) or return Failure(:invalid_arg, 'arg2 must be numeric')
|
708
|
+
|
709
|
+
arg2.zero? and return Failure(:division_by_zero, 'arg2 must not be zero')
|
710
|
+
|
711
|
+
Success(:division_completed, arg1 / arg2)
|
712
|
+
end
|
713
|
+
end
|
714
|
+
```
|
715
|
+
|
716
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
717
|
+
|
718
|
+
### `BCDD::Result::Expectations`
|
719
|
+
|
720
|
+
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.
|
721
|
+
|
722
|
+
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.
|
723
|
+
|
724
|
+
#### Standalone *versus* Mixin mode
|
725
|
+
|
726
|
+
The _**standalone mode**_ creates an object that knows how to produce and validate results based on the defined expectations. Look at the example below:
|
727
|
+
|
728
|
+
```ruby
|
729
|
+
module Divide
|
730
|
+
Expected = BCDD::Result::Expectations.new(
|
731
|
+
success: %i[numbers division_completed],
|
732
|
+
failure: %i[invalid_arg division_by_zero]
|
733
|
+
)
|
734
|
+
|
735
|
+
def self.call(arg1, arg2)
|
736
|
+
arg1.is_a?(Numeric) or return Expected::Failure(:invalid_arg, 'arg1 must be numeric')
|
737
|
+
arg2.is_a?(Numeric) or return Expected::Failure(:invalid_arg, 'arg2 must be numeric')
|
738
|
+
|
739
|
+
arg2.zero? and return Expected::Failure(:division_by_zero, 'arg2 must not be zero')
|
740
|
+
|
741
|
+
Expected::Success(:division_completed, arg1 / arg2)
|
742
|
+
end
|
743
|
+
end
|
744
|
+
```
|
745
|
+
|
746
|
+
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.
|
747
|
+
|
748
|
+
Look what happens if you try to create a result without one of the expected types.
|
749
|
+
|
750
|
+
```ruby
|
751
|
+
Divide::Expected::Success(:ok)
|
752
|
+
# type :ok is not allowed. Allowed types: :numbers, :division_completed
|
753
|
+
# (BCDD::Result::Expectations::Error::UnexpectedType)
|
754
|
+
|
755
|
+
Divide::Expected::Failure(:err)
|
756
|
+
# type :err is not allowed. Allowed types: :invalid_arg, :division_by_zero
|
757
|
+
# (BCDD::Result::Expectations::Error::UnexpectedType)
|
758
|
+
```
|
759
|
+
|
760
|
+
The _**mixin mode**_ is similar to `BCDD::Result::Mixin`, but it also defines the expectations for the result's types and values.
|
761
|
+
|
762
|
+
```ruby
|
763
|
+
class Divide
|
764
|
+
include BCDD::Result::Expectations.mixin(
|
765
|
+
success: %i[numbers division_completed],
|
766
|
+
failure: %i[invalid_arg division_by_zero]
|
767
|
+
)
|
768
|
+
|
769
|
+
def call(arg1, arg2)
|
770
|
+
validate_numbers(arg1, arg2)
|
771
|
+
.and_then(:validate_non_zero)
|
772
|
+
.and_then(:divide)
|
773
|
+
end
|
774
|
+
|
775
|
+
private
|
776
|
+
|
777
|
+
def validate_numbers(arg1, arg2)
|
778
|
+
arg1.is_a?(Numeric) or return Failure(:invalid_arg, 'arg1 must be numeric')
|
779
|
+
arg2.is_a?(Numeric) or return Failure(:invalid_arg, 'arg2 must be numeric')
|
780
|
+
|
781
|
+
Success(:numbers, [arg1, arg2])
|
782
|
+
end
|
783
|
+
|
784
|
+
def validate_non_zero(numbers)
|
785
|
+
return Success(:numbers, numbers) unless numbers.last.zero?
|
786
|
+
|
787
|
+
Failure(:division_by_zero, 'arg2 must not be zero')
|
788
|
+
end
|
789
|
+
|
790
|
+
def divide((number1, number2))
|
791
|
+
Success(:division_completed, number1 / number2)
|
792
|
+
end
|
793
|
+
end
|
794
|
+
```
|
795
|
+
|
796
|
+
This mode also defines an `Expected` constant to be used inside and outside the module.
|
797
|
+
|
798
|
+
> **PROTIP:**
|
799
|
+
> You can use the `Expected` 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.
|
800
|
+
|
801
|
+
Now that you know the two modes, let's understand how expectations can be beneficial and powerful for defining contracts.
|
802
|
+
|
803
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
804
|
+
|
805
|
+
#### Type checking - Result Hooks
|
806
|
+
|
807
|
+
The `BCDD::Result::Expectations` will check if the result's type is valid. This checking will be performed in all the methods that rely on the result's type, like `#success?`, `#failure?`, `#on`, `#on_type`, `#on_success`, `#on_failure`, `#handle`.
|
808
|
+
|
809
|
+
##### `#success?` and `#failure?`
|
810
|
+
|
811
|
+
When you check whether a result is a success or failure, `BCDD::Result::Expectations` will check whether the result type is valid/expected. Otherwise, an error will be raised.
|
812
|
+
|
813
|
+
**Success example:**
|
814
|
+
|
815
|
+
```ruby
|
816
|
+
result = Divide.new.call(10, 2)
|
817
|
+
|
818
|
+
result.success? # true
|
819
|
+
result.success?(:numbers) # false
|
820
|
+
result.success?(:division_completed) # true
|
821
|
+
|
822
|
+
result.success?(:ok)
|
823
|
+
# type :ok is not allowed. Allowed types: :numbers, :division_completed
|
824
|
+
# (BCDD::Result::Expectations::Error::UnexpectedType)
|
825
|
+
```
|
826
|
+
|
827
|
+
**Failure example:**
|
828
|
+
|
829
|
+
```ruby
|
830
|
+
result = Divide.new.call(10, '2')
|
831
|
+
|
832
|
+
result.failure? # true
|
833
|
+
result.failure?(:invalid_arg) # true
|
834
|
+
result.failure?(:division_by_zero) # false
|
835
|
+
|
836
|
+
result.failure?(:err)
|
837
|
+
# type :err is not allowed. Allowed types: :invalid_arg, :division_by_zero
|
838
|
+
# (BCDD::Result::Expectations::Error::UnexpectedType)
|
839
|
+
```
|
840
|
+
|
841
|
+
*PS: The `Divide` implementation is [here](#standalone-versus-mixin-mode).*
|
842
|
+
|
843
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
844
|
+
|
845
|
+
##### `#on` and `#on_type`
|
846
|
+
|
847
|
+
If you use `#on` or `#on_type` to perform a block, `BCDD::Result::Expectations` will check whether the result type is valid/expected. Otherwise, an error will be raised.
|
848
|
+
|
849
|
+
```ruby
|
850
|
+
result = Divide.new.call(10, 2)
|
851
|
+
|
852
|
+
result
|
853
|
+
.on(:invalid_arg, :division_by_zero) { |msg| puts msg }
|
854
|
+
.on(:division_completed) { |number| puts "The result is #{number}" }
|
855
|
+
|
856
|
+
# The code above will print 'The result is 5'
|
857
|
+
|
858
|
+
result.on(:number) { |_| :this_type_does_not_exist }
|
859
|
+
# type :number is not allowed. Allowed types: :numbers, :division_completed, :invalid_arg, :division_by_zero
|
860
|
+
# (BCDD::Result::Expectations::Error::UnexpectedType)
|
861
|
+
```
|
862
|
+
|
863
|
+
*PS: The `Divide` implementation is [here](#standalone-versus-mixin-mode).*
|
864
|
+
|
865
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
866
|
+
|
867
|
+
##### `#on_success` and `#on_failure`
|
868
|
+
|
869
|
+
If you use `#on_success` or `#on_failure` to perform a block, `BCDD::Result::Expectations` will check whether the result type is valid/expected. Otherwise, an error will be raised.
|
870
|
+
|
871
|
+
```ruby
|
872
|
+
result = Divide.new.call(10, '2')
|
873
|
+
|
874
|
+
result
|
875
|
+
.on_failure(:invalid_arg, :division_by_zero) { |msg| puts msg }
|
876
|
+
.on_success(:division_completed) { |number| puts "The result is #{number}" }
|
877
|
+
|
878
|
+
result
|
879
|
+
.on_success { |number| puts "The result is #{number}" }
|
880
|
+
.on_failure { |msg| puts msg }
|
881
|
+
|
882
|
+
# Both codes above will print 'arg2 must be numeric'
|
883
|
+
|
884
|
+
result.on_success(:ok) { |_| :this_type_does_not_exist }
|
885
|
+
# type :ok is not allowed. Allowed types: :numbers, :division_completed
|
886
|
+
# (BCDD::Result::Expectations::Error::UnexpectedType)
|
887
|
+
|
888
|
+
result.on_failure(:err) { |_| :this_type_does_not_exist }
|
889
|
+
# type :err is not allowed. Allowed types: :invalid_arg, :division_by_zero
|
890
|
+
# (BCDD::Result::Expectations::Error::UnexpectedType)
|
891
|
+
```
|
892
|
+
|
893
|
+
*PS: The `Divide` implementation is [here](#standalone-versus-mixin-mode).*
|
894
|
+
|
895
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
896
|
+
|
897
|
+
##### `#handle`
|
898
|
+
|
899
|
+
The `BCDD::Result::Expectations` will also be applied on all the handlers defined by the `#handle` method/block.
|
900
|
+
|
901
|
+
```ruby
|
902
|
+
result = Divide.call(10, 2)
|
903
|
+
|
904
|
+
result.handle do |on|
|
905
|
+
on.type(:ok) { |_| :this_type_does_not_exist }
|
906
|
+
end
|
907
|
+
# type :ok is not allowed. Allowed types: :numbers, :division_completed, :invalid_arg, :division_by_zero (BCDD::Result::Expectations::Error::UnexpectedType)
|
908
|
+
|
909
|
+
result.handle do |on|
|
910
|
+
on.success(:ok) { |_| :this_type_does_not_exist }
|
911
|
+
end
|
912
|
+
# type :ok is not allowed. Allowed types: :numbers, :division_completed (BCDD::Result::Expectations::Error::UnexpectedType)
|
913
|
+
|
914
|
+
result.handle do |on|
|
915
|
+
on.failure(:err) { |_| :this_type_does_not_exist }
|
916
|
+
end
|
917
|
+
# type :err is not allowed. Allowed types: :numbers, :division_completed (BCDD::Result::Expectations::Error::UnexpectedType)
|
918
|
+
```
|
919
|
+
|
920
|
+
*PS: The `Divide` implementation is [here](#standalone-versus-mixin-mode).*
|
921
|
+
|
922
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
923
|
+
|
924
|
+
#### Type checking - Result Creation
|
925
|
+
|
926
|
+
The `BCDD::Result::Expectations` will be used on the result creation `Success()` and `Failure()` methods. So, when the result type is valid/expected, the result will be created. Otherwise, an error will be raised.
|
927
|
+
|
928
|
+
This works for both modes (standalone and mixin).
|
929
|
+
|
930
|
+
##### Mixin mode
|
931
|
+
|
932
|
+
```ruby
|
933
|
+
module Divide
|
934
|
+
extend BCDD::Result::Expectations.mixin(success: :ok, failure: :err)
|
935
|
+
|
936
|
+
def self.call(arg1, arg2)
|
937
|
+
arg1.is_a?(Numeric) or return Failure(:invalid_arg, 'arg1 must be numeric')
|
938
|
+
arg2.is_a?(Numeric) or return Failure(:invalid_arg, 'arg2 must be numeric')
|
939
|
+
|
940
|
+
arg2.zero? and return Failure(:division_by_zero, 'arg2 must not be zero')
|
941
|
+
|
942
|
+
Success(:division_completed, arg1 / arg2)
|
943
|
+
end
|
944
|
+
end
|
945
|
+
|
946
|
+
Divide.call('4', 2)
|
947
|
+
# type :invalid_arg is not allowed. Allowed types: :err
|
948
|
+
# (BCDD::Result::Expectations::Error::UnexpectedType)
|
949
|
+
|
950
|
+
Divide.call(4, 2)
|
951
|
+
# type :division_completed is not allowed. Allowed types: :ok
|
952
|
+
# (BCDD::Result::Expectations::Error::UnexpectedType)
|
953
|
+
```
|
954
|
+
|
955
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
956
|
+
|
957
|
+
##### Standalone mode
|
958
|
+
|
959
|
+
```ruby
|
960
|
+
module Divide
|
961
|
+
Expected = BCDD::Result::Expectations.new(success: :ok, failure: :err)
|
962
|
+
|
963
|
+
def self.call(arg1, arg2)
|
964
|
+
arg1.is_a?(Numeric) or return Expected::Failure(:invalid_arg, 'arg1 must be numeric')
|
965
|
+
arg2.is_a?(Numeric) or return Expected::Failure(:invalid_arg, 'arg2 must be numeric')
|
966
|
+
|
967
|
+
arg2.zero? and return Expected::Failure(:division_by_zero, 'arg2 must not be zero')
|
968
|
+
|
969
|
+
Expected::Success(:division_completed, arg1 / arg2)
|
970
|
+
end
|
971
|
+
end
|
972
|
+
|
973
|
+
Divide.call('4', 2)
|
974
|
+
# type :invalid_arg is not allowed. Allowed types: :err
|
975
|
+
# (BCDD::Result::Expectations::Error::UnexpectedType)
|
976
|
+
|
977
|
+
Divide.call(4, 2)
|
978
|
+
# type :division_completed is not allowed. Allowed types: :ok
|
979
|
+
# (BCDD::Result::Expectations::Error::UnexpectedType)
|
980
|
+
```
|
981
|
+
|
982
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
983
|
+
|
984
|
+
#### Value checking - Result Creation
|
985
|
+
|
986
|
+
The `Result::Expectations` supports types of validations. The first is the type checking only, and the second is the type and value checking.
|
987
|
+
|
988
|
+
To define expectations for your result's values, you must declare a Hash with the type as the key and the value as the value. A value validator is any object that responds to `#===` (case equality operator).
|
989
|
+
|
990
|
+
**Mixin mode:**
|
991
|
+
|
992
|
+
```ruby
|
993
|
+
module Divide
|
994
|
+
extend BCDD::Result::Expectations.mixin(
|
995
|
+
success: {
|
996
|
+
numbers: ->(value) { value.is_a?(Array) && value.size == 2 && value.all?(Numeric) },
|
997
|
+
division_completed: Numeric
|
998
|
+
},
|
999
|
+
failure: {
|
1000
|
+
invalid_arg: String,
|
1001
|
+
division_by_zero: String
|
1002
|
+
}
|
1003
|
+
)
|
1004
|
+
|
1005
|
+
def self.call(arg1, arg2)
|
1006
|
+
arg1.is_a?(Numeric) or return Failure(:invalid_arg, 'arg1 must be numeric')
|
1007
|
+
arg2.is_a?(Numeric) or return Failure(:invalid_arg, 'arg2 must be numeric')
|
1008
|
+
|
1009
|
+
arg2.zero? and return Failure(:division_by_zero, 'arg2 must not be zero')
|
1010
|
+
|
1011
|
+
Success(:division_completed, arg1 / arg2)
|
1012
|
+
end
|
1013
|
+
end
|
1014
|
+
```
|
1015
|
+
|
1016
|
+
**Standalone mode:**
|
1017
|
+
|
1018
|
+
```ruby
|
1019
|
+
module Divide
|
1020
|
+
Expected = BCDD::Result::Expectations.new(
|
1021
|
+
success: {
|
1022
|
+
numbers: ->(value) { value.is_a?(Array) && value.size == 2 && value.all?(Numeric) },
|
1023
|
+
division_completed: Numeric
|
1024
|
+
},
|
1025
|
+
failure: {
|
1026
|
+
invalid_arg: String,
|
1027
|
+
division_by_zero: String
|
1028
|
+
}
|
1029
|
+
)
|
1030
|
+
|
1031
|
+
def self.call(arg1, arg2)
|
1032
|
+
arg1.is_a?(Numeric) or return Expected::Failure(:invalid_arg, 'arg1 must be numeric')
|
1033
|
+
arg2.is_a?(Numeric) or return Expected::Failure(:invalid_arg, 'arg2 must be numeric')
|
1034
|
+
|
1035
|
+
arg2.zero? and return Expected::Failure(:division_by_zero, 'arg2 must not be zero')
|
1036
|
+
|
1037
|
+
Expected::Success(:division_completed, arg1 / arg2)
|
1038
|
+
end
|
1039
|
+
end
|
1040
|
+
```
|
1041
|
+
|
1042
|
+
The value validation will only be performed through the methods `Success()` and `Failure()`.
|
1043
|
+
|
1044
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
1045
|
+
|
1046
|
+
##### Success()
|
1047
|
+
|
1048
|
+
```ruby
|
1049
|
+
Divide::Expected::Success(:ok)
|
1050
|
+
# type :ok is not allowed. Allowed types: :numbers, :division_completed (BCDD::Result::Expectations::Error::UnexpectedType)
|
1051
|
+
|
1052
|
+
Divide::Expected::Success(:numbers, [1])
|
1053
|
+
# value [1] is not allowed for :numbers type (BCDD::Result::Expectations::Error::UnexpectedValue)
|
1054
|
+
|
1055
|
+
Divide::Expected::Success(:division_completed, '2')
|
1056
|
+
# value "2" is not allowed for :division_completed type (BCDD::Result::Expectations::Error::UnexpectedValue)
|
1057
|
+
```
|
1058
|
+
|
1059
|
+
##### Failure()
|
1060
|
+
|
1061
|
+
```ruby
|
1062
|
+
Divide::Expected::Failure(:err)
|
1063
|
+
# type :err is not allowed. Allowed types: :invalid_arg, :division_by_zero (BCDD::Result::Expectations::Error::UnexpectedType)
|
1064
|
+
|
1065
|
+
Divide::Expected::Failure(:invalid_arg, :arg1_must_be_numeric)
|
1066
|
+
# value :arg1_must_be_numeric is not allowed for :invalid_arg type (BCDD::Result::Expectations::Error::UnexpectedValue)
|
1067
|
+
|
1068
|
+
Divide::Expected::Failure(:division_by_zero, msg: 'arg2 must not be zero')
|
1069
|
+
# value {:msg=>"arg2 must not be zero"} is not allowed for :division_by_zero type (BCDD::Result::Expectations::Error::UnexpectedValue)
|
1070
|
+
```
|
1071
|
+
|
1072
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
526
1073
|
|
527
1074
|
## About
|
528
1075
|
|
529
1076
|
[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.
|
530
1077
|
|
531
|
-
<p align="right"
|
1078
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
532
1079
|
|
533
1080
|
## Development
|
534
1081
|
|
@@ -536,19 +1083,19 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
|
536
1083
|
|
537
1084
|
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
538
1085
|
|
539
|
-
<p align="right"
|
1086
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
540
1087
|
|
541
1088
|
## Contributing
|
542
1089
|
|
543
1090
|
Bug reports and pull requests are welcome on GitHub at https://github.com/B-CDD/bcdd-result. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/B-CDD/bcdd-result/blob/master/CODE_OF_CONDUCT.md).
|
544
1091
|
|
545
|
-
<p align="right"
|
1092
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
546
1093
|
|
547
1094
|
## License
|
548
1095
|
|
549
1096
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
550
1097
|
|
551
|
-
<p align="right"
|
1098
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
552
1099
|
|
553
1100
|
## Code of Conduct
|
554
1101
|
|