bcdd-result 0.5.0 → 0.7.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 +30 -10
- data/CHANGELOG.md +49 -2
- data/README.md +709 -127
- data/lib/bcdd/result/context/expectations/mixin.rb +35 -0
- data/lib/bcdd/result/context/expectations.rb +41 -0
- data/lib/bcdd/result/context/failure.rb +9 -0
- data/lib/bcdd/result/context/mixin.rb +38 -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/{expectations/contract → contract}/for_types_and_values.rb +10 -8
- 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 +43 -0
- data/lib/bcdd/result/error.rb +7 -9
- data/lib/bcdd/result/expectations/mixin.rb +42 -0
- data/lib/bcdd/result/expectations.rb +27 -48
- data/lib/bcdd/result/failure/methods.rb +21 -0
- data/lib/bcdd/result/failure.rb +2 -16
- data/lib/bcdd/result/mixin.rb +36 -4
- 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 +11 -6
- data/sig/bcdd/result.rbs +245 -71
- metadata +19 -10
- data/lib/bcdd/result/expectations/contract/interface.rb +0 -21
- data/lib/bcdd/result/expectations/contract.rb +0 -25
data/README.md
CHANGED
@@ -3,7 +3,7 @@
|
|
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
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">
|
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>
|
7
7
|
</p>
|
8
8
|
</p>
|
9
9
|
|
@@ -35,17 +35,18 @@ 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
|
-
- [`BCDD::Result
|
43
|
+
- [`BCDD::Result.mixin`](#bcddresultmixin)
|
41
44
|
- [Class example (Instance Methods)](#class-example-instance-methods)
|
42
45
|
- [Module example (Singleton Methods)](#module-example-singleton-methods)
|
43
|
-
- [
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
- [BCDD::Result::Expectations](#bcddresultexpectations)
|
48
|
-
- [`BCDD::Result::Expectations`](#bcddresultexpectations-1)
|
46
|
+
- [Important Requirement](#important-requirement)
|
47
|
+
- [Dependency Injection](#dependency-injection)
|
48
|
+
- [Add-ons](#add-ons)
|
49
|
+
- [`BCDD::Result::Expectations`](#bcddresultexpectations)
|
49
50
|
- [Standalone *versus* Mixin mode](#standalone-versus-mixin-mode)
|
50
51
|
- [Type checking - Result Hooks](#type-checking---result-hooks)
|
51
52
|
- [`#success?` and `#failure?`](#success-and-failure)
|
@@ -58,6 +59,16 @@ Use it to enable the [Railway Oriented Programming](https://fsharpforfunandprofi
|
|
58
59
|
- [Value checking - Result Creation](#value-checking---result-creation)
|
59
60
|
- [Success()](#success)
|
60
61
|
- [Failure()](#failure)
|
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)
|
61
72
|
- [About](#about)
|
62
73
|
- [Development](#development)
|
63
74
|
- [Contributing](#contributing)
|
@@ -206,9 +217,8 @@ result.failure?(:error) # false
|
|
206
217
|
|
207
218
|
### Result Hooks
|
208
219
|
|
209
|
-
Result hooks are methods that allow you to
|
210
|
-
|
211
|
-
To exemplify the them, I will implement a method that knows how to divide two numbers.
|
220
|
+
Result hooks are methods that allow you to execute a block of code based on the type of result obtained.
|
221
|
+
To demonstrate their use, I will implement a function that can divide two numbers.
|
212
222
|
|
213
223
|
```ruby
|
214
224
|
def divide(arg1, arg2)
|
@@ -225,14 +235,15 @@ end
|
|
225
235
|
|
226
236
|
#### `result.on`
|
227
237
|
|
228
|
-
`BCDD::Result#on
|
238
|
+
When you use `BCDD::Result#on`, the block will be executed only when the type matches the result type.
|
229
239
|
|
230
|
-
|
240
|
+
However, even if the block is executed, the method will always return the result itself.
|
231
241
|
|
232
|
-
The result
|
242
|
+
The value of the result will be available as the first argument of the block.
|
233
243
|
|
234
244
|
```ruby
|
235
245
|
result = divide(nil, 2)
|
246
|
+
#<BCDD::Result::Failure type=:invalid_arg data='arg1 must be numeric'>
|
236
247
|
|
237
248
|
output =
|
238
249
|
result
|
@@ -265,21 +276,40 @@ result.object_id == output.object_id # true
|
|
265
276
|
|
266
277
|
`BCDD::Result#on_type` is an alias of `BCDD::Result#on`.
|
267
278
|
|
279
|
+
```ruby
|
280
|
+
result = divide(nil, 2)
|
281
|
+
#<BCDD::Result::Failure type=:invalid_arg data='arg1 must be numeric'>
|
282
|
+
|
283
|
+
output =
|
284
|
+
result
|
285
|
+
.on_type(:invalid_arg) { |msg| puts msg }
|
286
|
+
.on_type(:division_by_zero) { |msg| puts msg }
|
287
|
+
.on_type(:division_completed) { |number| puts number }
|
288
|
+
|
289
|
+
# The code above will print 'arg1 must be numeric' and return the result itself.
|
290
|
+
|
291
|
+
result.object_id == output.object_id # true
|
292
|
+
```
|
293
|
+
|
294
|
+
*PS: The `divide()` implementation is [here](#result-hooks).*
|
295
|
+
|
296
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
297
|
+
|
268
298
|
#### `result.on_success`
|
269
299
|
|
270
|
-
`BCDD::Result#on_success` is
|
300
|
+
The `BCDD::Result#on_success` method is quite similar to the `BCDD::Result#on` hook, but with a few key differences:
|
271
301
|
|
272
|
-
1.
|
273
|
-
2. If the type is
|
302
|
+
1. It will only execute the block of code if the result is a success.
|
303
|
+
2. If the type declaration is not included, the method will execute the block for any successful result, regardless of its type.
|
274
304
|
|
275
305
|
```ruby
|
276
|
-
# It
|
306
|
+
# It executes the block and return itself.
|
277
307
|
|
278
308
|
divide(4, 2).on_success { |number| puts number }
|
279
309
|
|
280
310
|
divide(4, 2).on_success(:division_completed) { |number| puts number }
|
281
311
|
|
282
|
-
# It doesn't
|
312
|
+
# It doesn't execute the block, but return itself.
|
283
313
|
|
284
314
|
divide(4, 4).on_success(:ok) { |value| puts value }
|
285
315
|
|
@@ -292,16 +322,19 @@ divide(4, 4).on_failure { |error| puts error }
|
|
292
322
|
|
293
323
|
#### `result.on_failure`
|
294
324
|
|
295
|
-
|
325
|
+
It is the opposite of `Result#on_success`:
|
326
|
+
|
327
|
+
1. It will only execute the block of code if the result is a failure.
|
328
|
+
2. If the type declaration is not included, the method will execute the block for any failed result, regardless of its type.
|
296
329
|
|
297
330
|
```ruby
|
298
|
-
# It
|
331
|
+
# It executes the block and return itself.
|
299
332
|
|
300
333
|
divide(nil, 2).on_failure { |error| puts error }
|
301
334
|
|
302
335
|
divide(4, 0).on_failure(:invalid_arg, :division_by_zero) { |error| puts error }
|
303
336
|
|
304
|
-
# It doesn't
|
337
|
+
# It doesn't execute the block, but return itself.
|
305
338
|
|
306
339
|
divide(4, 0).on_success { |number| puts number }
|
307
340
|
|
@@ -314,11 +347,11 @@ divide(4, 0).on_failure(:invalid_arg) { |error| puts error }
|
|
314
347
|
|
315
348
|
#### `result.on_unknown`
|
316
349
|
|
317
|
-
`BCDD::Result#on_unknown` will
|
350
|
+
`BCDD::Result#on_unknown` will execute the block when no other hook (`#on`, `#on_type`, `#on_failure`, `#on_success`) has been executed.
|
318
351
|
|
319
|
-
Regardless of the block being executed, the
|
352
|
+
Regardless of the block being executed, the method will always return the result itself.
|
320
353
|
|
321
|
-
The result
|
354
|
+
The value of the result will be available as the first argument of the block.
|
322
355
|
|
323
356
|
```ruby
|
324
357
|
divide(4, 2)
|
@@ -335,7 +368,7 @@ divide(4, 2)
|
|
335
368
|
|
336
369
|
#### `result.handle`
|
337
370
|
|
338
|
-
This method
|
371
|
+
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.
|
339
372
|
|
340
373
|
```ruby
|
341
374
|
divide(4, 2).handle do |result|
|
@@ -376,8 +409,8 @@ end
|
|
376
409
|
|
377
410
|
**Notes:**
|
378
411
|
* You can define multiple types to be handled by the same hook/block
|
379
|
-
* If the type is missing, it will
|
380
|
-
* The `#type` and `#[]` handlers
|
412
|
+
* If the type is missing, it will execute the block for any success or failure handler.
|
413
|
+
* The `#type` and `#[]` handlers require at least one type/argument.
|
381
414
|
|
382
415
|
*PS: The `divide()` implementation is [here](#result-hooks).*
|
383
416
|
|
@@ -385,13 +418,13 @@ end
|
|
385
418
|
|
386
419
|
### Result Value
|
387
420
|
|
388
|
-
|
421
|
+
To access the result value, you can simply call `BCDD::Result#value`.
|
389
422
|
|
390
|
-
|
423
|
+
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`.
|
391
424
|
|
392
425
|
#### `result.value_or`
|
393
426
|
|
394
|
-
`BCCD::Result#value_or` returns the value when the result is a success,
|
427
|
+
`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.
|
395
428
|
|
396
429
|
```ruby
|
397
430
|
def divide(arg1, arg2)
|
@@ -459,16 +492,75 @@ print_to_hash(**success_data) # [:success, :ok, 1]
|
|
459
492
|
|
460
493
|
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
461
494
|
|
462
|
-
###
|
495
|
+
### Pattern Matching
|
496
|
+
|
497
|
+
The `BCDD::Result` also provides support to pattern matching.
|
498
|
+
|
499
|
+
In the further examples, I will use the `Divide` lambda to exemplify its usage.
|
500
|
+
|
501
|
+
```ruby
|
502
|
+
Divide = lambda do |arg1, arg2|
|
503
|
+
arg1.is_a?(::Numeric) or return BCDD::Result::Failure(:invalid_arg, 'arg1 must be numeric')
|
504
|
+
arg2.is_a?(::Numeric) or return BCDD::Result::Failure(:invalid_arg, 'arg2 must be numeric')
|
505
|
+
|
506
|
+
return BCDD::Result::Failure(:division_by_zero, 'arg2 must not be zero') if arg2.zero?
|
507
|
+
|
508
|
+
BCDD::Result::Success(:division_completed, arg1 / arg2)
|
509
|
+
end
|
510
|
+
```
|
511
|
+
|
512
|
+
#### `Array`/`Find` patterns
|
513
|
+
|
514
|
+
```ruby
|
515
|
+
case Divide.call(4, 2)
|
516
|
+
in BCDD::Result::Failure[:invalid_arg, msg] then puts msg
|
517
|
+
in BCDD::Result::Failure[:division_by_zero, msg] then puts msg
|
518
|
+
in BCDD::Result::Success[:division_completed, value] then puts value
|
519
|
+
end
|
520
|
+
|
521
|
+
# The code above will print: 2
|
522
|
+
|
523
|
+
case Divide.call(4, 0)
|
524
|
+
in BCDD::Result::Failure[:invalid_arg, msg] then puts msg
|
525
|
+
in BCDD::Result::Failure[:division_by_zero, msg] then puts msg
|
526
|
+
in BCDD::Result::Success[:division_completed, value] then puts value
|
527
|
+
end
|
463
528
|
|
464
|
-
|
529
|
+
# The code above will print: arg2 must not be zero
|
530
|
+
```
|
465
531
|
|
466
|
-
|
532
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
467
533
|
|
468
|
-
|
469
|
-
So, if some block returns a failure, the following blocks will be skipped.
|
534
|
+
#### `Hash` patterns
|
470
535
|
|
471
|
-
|
536
|
+
```ruby
|
537
|
+
case Divide.call(10, 2)
|
538
|
+
in { failure: { invalid_arg: msg } } then puts msg
|
539
|
+
in { failure: { division_by_zero: msg } } then puts msg
|
540
|
+
in { success: { division_completed: value } } then puts value
|
541
|
+
end
|
542
|
+
|
543
|
+
# The code above will print: 5
|
544
|
+
|
545
|
+
case Divide.call('10', 2)
|
546
|
+
in { failure: { invalid_arg: msg } } then puts msg
|
547
|
+
in { failure: { division_by_zero: msg } } then puts msg
|
548
|
+
in { success: { division_completed: value } } then puts value
|
549
|
+
end
|
550
|
+
|
551
|
+
# The code above will print: arg1 must be numeric
|
552
|
+
```
|
553
|
+
|
554
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
555
|
+
|
556
|
+
### Railway Oriented Programming
|
557
|
+
|
558
|
+
["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.
|
559
|
+
If a failure occurs in any of the blocks, the pipeline is interrupted and subsequent blocks are skipped.
|
560
|
+
|
561
|
+
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.
|
562
|
+
|
563
|
+
If all blocks successfully execute, the final result of the pipeline will be a success.
|
472
564
|
|
473
565
|
#### `result.and_then`
|
474
566
|
|
@@ -521,17 +613,17 @@ Divide.call(2, 2)
|
|
521
613
|
|
522
614
|
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
523
615
|
|
524
|
-
#### `BCDD::Result
|
616
|
+
#### `BCDD::Result.mixin`
|
525
617
|
|
526
|
-
|
618
|
+
This method generates a module that can be included or 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 former will utilize the target object (which has received the include/extend) as the result's subject.
|
527
619
|
|
528
|
-
|
620
|
+
As a result, you can utilize the `#and_then` method to invoke methods from the result's subject.
|
529
621
|
|
530
622
|
##### Class example (Instance Methods)
|
531
623
|
|
532
624
|
```ruby
|
533
625
|
class Divide
|
534
|
-
include BCDD::Result
|
626
|
+
include BCDD::Result.mixin
|
535
627
|
|
536
628
|
attr_reader :arg1, :arg2
|
537
629
|
|
@@ -579,8 +671,7 @@ Divide.new(4, '2').call #<BCDD::Result::Failure type=:invalid_arg value="arg2 mu
|
|
579
671
|
|
580
672
|
```ruby
|
581
673
|
module Divide
|
582
|
-
extend BCDD::Result
|
583
|
-
extend self
|
674
|
+
extend self, BCDD::Result.mixin
|
584
675
|
|
585
676
|
def call(arg1, arg2)
|
586
677
|
validate_numbers(arg1, arg2)
|
@@ -615,100 +706,163 @@ Divide.call('4', 2) #<BCDD::Result::Failure type=:invalid_arg value="arg1 must b
|
|
615
706
|
Divide.call(4, '2') #<BCDD::Result::Failure type=:invalid_arg value="arg2 must be numeric">
|
616
707
|
```
|
617
708
|
|
618
|
-
|
709
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
619
710
|
|
620
|
-
|
711
|
+
##### Important Requirement
|
621
712
|
|
622
|
-
|
713
|
+
To use the `#and_then` method to call methods, they must use `Success()` and `Failure()` to produce the results.
|
623
714
|
|
624
|
-
|
715
|
+
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.
|
625
716
|
|
626
|
-
|
717
|
+
**Note:** You can still use the block syntax, but all the results must be produced by the subject's `Success()` and `Failure()` methods.
|
627
718
|
|
628
|
-
|
719
|
+
```ruby
|
720
|
+
module ValidateNonZero
|
721
|
+
extend self, BCDD::Result.mixin
|
629
722
|
|
630
|
-
|
723
|
+
def call(numbers)
|
724
|
+
return Success(:ok, numbers) unless numbers.last.zero?
|
631
725
|
|
632
|
-
|
726
|
+
Failure(:division_by_zero, 'arg2 must not be zero')
|
727
|
+
end
|
728
|
+
end
|
633
729
|
|
634
|
-
|
635
|
-
|
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')
|
730
|
+
class Divide
|
731
|
+
include BCDD::Result.mixin
|
638
732
|
|
639
|
-
|
733
|
+
attr_reader :arg1, :arg2
|
640
734
|
|
641
|
-
|
642
|
-
|
643
|
-
|
735
|
+
def initialize(arg1, arg2)
|
736
|
+
@arg1 = arg1
|
737
|
+
@arg2 = arg2
|
738
|
+
end
|
644
739
|
|
645
|
-
|
740
|
+
def call
|
741
|
+
validate_numbers
|
742
|
+
.and_then(:validate_non_zero)
|
743
|
+
.and_then(:divide)
|
744
|
+
end
|
646
745
|
|
647
|
-
|
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
|
746
|
+
private
|
653
747
|
|
654
|
-
|
748
|
+
def validate_numbers
|
749
|
+
arg1.is_a?(::Numeric) or return BCDD::Result::Failure(:invalid_arg, 'arg1 must be numeric') # This will raise an error
|
750
|
+
arg2.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg2 must be numeric')
|
655
751
|
|
656
|
-
|
657
|
-
|
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
|
752
|
+
BCDD::Result::Success(:ok, [arg1, arg2]) # This will raise an error
|
753
|
+
end
|
661
754
|
|
662
|
-
|
755
|
+
def validate_non_zero(numbers)
|
756
|
+
ValidateNonZero.call(numbers) # This will raise an error
|
757
|
+
|
758
|
+
# This would work:
|
759
|
+
# In this case we are handling the other subject result and returning our own
|
760
|
+
# ValidateNonZero.call(numbers).handle do |on|
|
761
|
+
# on.success { |numbers| Success(:ok, numbers) }
|
762
|
+
# on.failure { |err| Failure(:division_by_zero, err) }
|
763
|
+
# end
|
764
|
+
end
|
765
|
+
|
766
|
+
def divide((number1, number2))
|
767
|
+
Success(:division_completed, number1 / number2)
|
768
|
+
end
|
769
|
+
end
|
663
770
|
```
|
664
771
|
|
665
772
|
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
666
773
|
|
667
|
-
|
774
|
+
##### Dependency Injection
|
775
|
+
|
776
|
+
The `BCDD::Result#and_then` accepts a second argument that will be used to share a value with the subject's method. 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.
|
668
777
|
|
669
778
|
```ruby
|
670
|
-
|
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
|
779
|
+
require 'logger'
|
675
780
|
|
676
|
-
|
781
|
+
module Divide
|
782
|
+
extend self, BCDD::Result.mixin
|
677
783
|
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
784
|
+
def call(arg1, arg2, logger: ::Logger.new(STDOUT))
|
785
|
+
validate_numbers(arg1, arg2)
|
786
|
+
.and_then(:validate_non_zero, logger)
|
787
|
+
.and_then(:divide, logger)
|
788
|
+
end
|
789
|
+
|
790
|
+
private
|
791
|
+
|
792
|
+
def validate_numbers(arg1, arg2)
|
793
|
+
arg1.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg1 must be numeric')
|
794
|
+
arg2.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg2 must be numeric')
|
795
|
+
|
796
|
+
Success(:ok, [arg1, arg2])
|
797
|
+
end
|
798
|
+
|
799
|
+
def validate_non_zero(numbers, logger)
|
800
|
+
if numbers.last.zero?
|
801
|
+
logger.error('arg2 must not be zero')
|
802
|
+
|
803
|
+
Failure(:division_by_zero, 'arg2 must not be zero')
|
804
|
+
else
|
805
|
+
logger.info('The numbers are valid')
|
806
|
+
|
807
|
+
Success(:ok, numbers)
|
808
|
+
end
|
809
|
+
end
|
810
|
+
|
811
|
+
def divide((number1, number2), logger)
|
812
|
+
division = number1 / number2
|
813
|
+
|
814
|
+
logger.info("The division result is #{division}")
|
815
|
+
|
816
|
+
Success(:division_completed, division)
|
817
|
+
end
|
682
818
|
end
|
683
819
|
|
684
|
-
|
820
|
+
Divide.call(4, 2)
|
821
|
+
# I, [2023-10-11T00:08:05.546237 #18139] INFO -- : The numbers are valid
|
822
|
+
# I, [2023-10-11T00:08:05.546337 #18139] INFO -- : The division result is 2
|
823
|
+
#=> #<BCDD::Result::Success type=:division_completed value=2>
|
824
|
+
|
825
|
+
Divide.call(4, 2, logger: Logger.new(IO::NULL))
|
826
|
+
#=> #<BCDD::Result::Success type=:division_completed value=2>
|
685
827
|
```
|
686
828
|
|
687
829
|
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
688
830
|
|
689
|
-
|
831
|
+
##### Add-ons
|
690
832
|
|
691
|
-
|
833
|
+
The `BCDD::Result.mixin` also accepts the `with:` argument. It is a hash that will be used to define the methods that will be added to the target object.
|
692
834
|
|
693
|
-
|
835
|
+
**Continue**
|
836
|
+
|
837
|
+
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.
|
694
838
|
|
695
839
|
```ruby
|
696
840
|
module Divide
|
697
|
-
extend BCDD::Result
|
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
|
-
)
|
841
|
+
extend self, BCDD::Result.mixin(with: :Continue)
|
704
842
|
|
705
|
-
def
|
706
|
-
arg1
|
707
|
-
|
843
|
+
def call(arg1, arg2)
|
844
|
+
validate_numbers(arg1, arg2)
|
845
|
+
.and_then(:validate_non_zero)
|
846
|
+
.and_then(:divide)
|
847
|
+
end
|
708
848
|
|
709
|
-
|
849
|
+
private
|
710
850
|
|
711
|
-
|
851
|
+
def validate_numbers(arg1, arg2)
|
852
|
+
arg1.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg1 must be numeric')
|
853
|
+
arg2.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg2 must be numeric')
|
854
|
+
|
855
|
+
Continue([arg1, arg2])
|
856
|
+
end
|
857
|
+
|
858
|
+
def validate_non_zero(numbers)
|
859
|
+
return Continue(numbers) unless numbers.last.zero?
|
860
|
+
|
861
|
+
Failure(:division_by_zero, 'arg2 must not be zero')
|
862
|
+
end
|
863
|
+
|
864
|
+
def divide((number1, number2))
|
865
|
+
Success(:division_completed, number1 / number2)
|
712
866
|
end
|
713
867
|
end
|
714
868
|
```
|
@@ -727,18 +881,18 @@ The _**standalone mode**_ creates an object that knows how to produce and valida
|
|
727
881
|
|
728
882
|
```ruby
|
729
883
|
module Divide
|
730
|
-
|
884
|
+
Result = BCDD::Result::Expectations.new(
|
731
885
|
success: %i[numbers division_completed],
|
732
886
|
failure: %i[invalid_arg division_by_zero]
|
733
887
|
)
|
734
888
|
|
735
889
|
def self.call(arg1, arg2)
|
736
|
-
arg1.is_a?(Numeric) or return
|
737
|
-
arg2.is_a?(Numeric) or return
|
890
|
+
arg1.is_a?(Numeric) or return Result::Failure(:invalid_arg, 'arg1 must be numeric')
|
891
|
+
arg2.is_a?(Numeric) or return Result::Failure(:invalid_arg, 'arg2 must be numeric')
|
738
892
|
|
739
|
-
arg2.zero? and return
|
893
|
+
arg2.zero? and return Result::Failure(:division_by_zero, 'arg2 must not be zero')
|
740
894
|
|
741
|
-
|
895
|
+
Result::Success(:division_completed, arg1 / arg2)
|
742
896
|
end
|
743
897
|
end
|
744
898
|
```
|
@@ -748,11 +902,11 @@ In the code above, we define a constant `Divide::Expected`. And because of this
|
|
748
902
|
Look what happens if you try to create a result without one of the expected types.
|
749
903
|
|
750
904
|
```ruby
|
751
|
-
Divide::
|
905
|
+
Divide::Result::Success(:ok)
|
752
906
|
# type :ok is not allowed. Allowed types: :numbers, :division_completed
|
753
907
|
# (BCDD::Result::Expectations::Error::UnexpectedType)
|
754
908
|
|
755
|
-
Divide::
|
909
|
+
Divide::Result::Failure(:err)
|
756
910
|
# type :err is not allowed. Allowed types: :invalid_arg, :division_by_zero
|
757
911
|
# (BCDD::Result::Expectations::Error::UnexpectedType)
|
758
912
|
```
|
@@ -804,11 +958,11 @@ Now that you know the two modes, let's understand how expectations can be benefi
|
|
804
958
|
|
805
959
|
#### Type checking - Result Hooks
|
806
960
|
|
807
|
-
The `BCDD::Result::Expectations` will check if the result
|
961
|
+
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`.
|
808
962
|
|
809
963
|
##### `#success?` and `#failure?`
|
810
964
|
|
811
|
-
When
|
965
|
+
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.
|
812
966
|
|
813
967
|
**Success example:**
|
814
968
|
|
@@ -844,7 +998,7 @@ result.failure?(:err)
|
|
844
998
|
|
845
999
|
##### `#on` and `#on_type`
|
846
1000
|
|
847
|
-
If you use `#on` or `#on_type` to
|
1001
|
+
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.
|
848
1002
|
|
849
1003
|
```ruby
|
850
1004
|
result = Divide.new.call(10, 2)
|
@@ -866,7 +1020,7 @@ result.on(:number) { |_| :this_type_does_not_exist }
|
|
866
1020
|
|
867
1021
|
##### `#on_success` and `#on_failure`
|
868
1022
|
|
869
|
-
If you use `#on_success` or `#on_failure` to
|
1023
|
+
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.
|
870
1024
|
|
871
1025
|
```ruby
|
872
1026
|
result = Divide.new.call(10, '2')
|
@@ -958,15 +1112,15 @@ Divide.call(4, 2)
|
|
958
1112
|
|
959
1113
|
```ruby
|
960
1114
|
module Divide
|
961
|
-
|
1115
|
+
Result = BCDD::Result::Expectations.new(success: :ok, failure: :err)
|
962
1116
|
|
963
1117
|
def self.call(arg1, arg2)
|
964
|
-
arg1.is_a?(Numeric) or return
|
965
|
-
arg2.is_a?(Numeric) or return
|
1118
|
+
arg1.is_a?(Numeric) or return Result::Failure(:invalid_arg, 'arg1 must be numeric')
|
1119
|
+
arg2.is_a?(Numeric) or return Result::Failure(:invalid_arg, 'arg2 must be numeric')
|
966
1120
|
|
967
|
-
arg2.zero? and return
|
1121
|
+
arg2.zero? and return Result::Failure(:division_by_zero, 'arg2 must not be zero')
|
968
1122
|
|
969
|
-
|
1123
|
+
Result::Success(:division_completed, arg1 / arg2)
|
970
1124
|
end
|
971
1125
|
end
|
972
1126
|
|
@@ -1017,7 +1171,7 @@ end
|
|
1017
1171
|
|
1018
1172
|
```ruby
|
1019
1173
|
module Divide
|
1020
|
-
|
1174
|
+
Result = BCDD::Result::Expectations.new(
|
1021
1175
|
success: {
|
1022
1176
|
numbers: ->(value) { value.is_a?(Array) && value.size == 2 && value.all?(Numeric) },
|
1023
1177
|
division_completed: Numeric
|
@@ -1029,12 +1183,12 @@ module Divide
|
|
1029
1183
|
)
|
1030
1184
|
|
1031
1185
|
def self.call(arg1, arg2)
|
1032
|
-
arg1.is_a?(Numeric) or return
|
1033
|
-
arg2.is_a?(Numeric) or return
|
1186
|
+
arg1.is_a?(Numeric) or return Result::Failure(:invalid_arg, 'arg1 must be numeric')
|
1187
|
+
arg2.is_a?(Numeric) or return Result::Failure(:invalid_arg, 'arg2 must be numeric')
|
1034
1188
|
|
1035
|
-
arg2.zero? and return
|
1189
|
+
arg2.zero? and return Result::Failure(:division_by_zero, 'arg2 must not be zero')
|
1036
1190
|
|
1037
|
-
|
1191
|
+
Result::Success(:division_completed, arg1 / arg2)
|
1038
1192
|
end
|
1039
1193
|
end
|
1040
1194
|
```
|
@@ -1046,31 +1200,459 @@ The value validation will only be performed through the methods `Success()` and
|
|
1046
1200
|
##### Success()
|
1047
1201
|
|
1048
1202
|
```ruby
|
1049
|
-
Divide::
|
1203
|
+
Divide::Result::Success(:ok)
|
1050
1204
|
# type :ok is not allowed. Allowed types: :numbers, :division_completed (BCDD::Result::Expectations::Error::UnexpectedType)
|
1051
1205
|
|
1052
|
-
Divide::
|
1206
|
+
Divide::Result::Success(:numbers, [1])
|
1053
1207
|
# value [1] is not allowed for :numbers type (BCDD::Result::Expectations::Error::UnexpectedValue)
|
1054
1208
|
|
1055
|
-
Divide::
|
1209
|
+
Divide::Result::Success(:division_completed, '2')
|
1056
1210
|
# value "2" is not allowed for :division_completed type (BCDD::Result::Expectations::Error::UnexpectedValue)
|
1057
1211
|
```
|
1058
1212
|
|
1059
1213
|
##### Failure()
|
1060
1214
|
|
1061
1215
|
```ruby
|
1062
|
-
Divide::
|
1216
|
+
Divide::Result::Failure(:err)
|
1063
1217
|
# type :err is not allowed. Allowed types: :invalid_arg, :division_by_zero (BCDD::Result::Expectations::Error::UnexpectedType)
|
1064
1218
|
|
1065
|
-
Divide::
|
1219
|
+
Divide::Result::Failure(:invalid_arg, :arg1_must_be_numeric)
|
1066
1220
|
# value :arg1_must_be_numeric is not allowed for :invalid_arg type (BCDD::Result::Expectations::Error::UnexpectedValue)
|
1067
1221
|
|
1068
|
-
Divide::
|
1222
|
+
Divide::Result::Failure(:division_by_zero, msg: 'arg2 must not be zero')
|
1069
1223
|
# value {:msg=>"arg2 must not be zero"} is not allowed for :division_by_zero type (BCDD::Result::Expectations::Error::UnexpectedValue)
|
1070
1224
|
```
|
1071
1225
|
|
1072
1226
|
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
1073
1227
|
|
1228
|
+
##### Pattern Matching Support
|
1229
|
+
|
1230
|
+
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).
|
1231
|
+
|
1232
|
+
How does this operator work? They raise an error when the pattern does not match but returns nil when it matches.
|
1233
|
+
|
1234
|
+
Because of this, you will need to enable `nil` as a valid value checking. You can do it by calling the `BCDD::Result::Contract.nil_as_valid_value_checking!` method.
|
1235
|
+
|
1236
|
+
**Attention:**
|
1237
|
+
|
1238
|
+
If you decide to enable this, you will do it at the beginning of your code or in an initializer. And remember, this will affect all kinds of result expectations (`BCDD::Result::Expectations` and `BCDD::Result::Context::Expectations`). So, it is recommended to use it only when you are using pattern matching for **ALL** the result's value validations.
|
1239
|
+
|
1240
|
+
```ruby
|
1241
|
+
#
|
1242
|
+
# Put this line in an initializer or at the beginning of your code.
|
1243
|
+
# It is required if you decide to use pattern matching to validate all of your result's values.
|
1244
|
+
#
|
1245
|
+
BCDD::Result::Contract.nil_as_valid_value_checking!
|
1246
|
+
|
1247
|
+
module Divide
|
1248
|
+
extend BCDD::Result::Expectations.mixin(
|
1249
|
+
success: {
|
1250
|
+
division_completed: ->(value) { value => (Integer | Float) }
|
1251
|
+
},
|
1252
|
+
failure: { invalid_arg: String, division_by_zero: String }
|
1253
|
+
)
|
1254
|
+
|
1255
|
+
def self.call(arg1, arg2)
|
1256
|
+
arg1.is_a?(Numeric) or return Failure(:invalid_arg, 'arg1 must be numeric')
|
1257
|
+
arg2.is_a?(Numeric) or return Failure(:invalid_arg, 'arg2 must be numeric')
|
1258
|
+
|
1259
|
+
arg2.zero? and return Failure(:division_by_zero, 'arg2 must not be zero')
|
1260
|
+
|
1261
|
+
Success(:division_completed, String(arg1 / arg2))
|
1262
|
+
end
|
1263
|
+
end
|
1264
|
+
|
1265
|
+
Divide.call(10, 5)
|
1266
|
+
# value "5" is not allowed for :division_completed type ("5": Float === "5" does not return true) (BCDD::Result::Contract::Error::UnexpectedValue)
|
1267
|
+
```
|
1268
|
+
|
1269
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
1270
|
+
|
1271
|
+
#### `BCDD::Result::Expectations.mixin` add-ons
|
1272
|
+
|
1273
|
+
The `BCDD::Result::Expectations.mixin` also accepts the `with:` argument. It is a hash that will be used to define the methods that will be added to the target object.
|
1274
|
+
|
1275
|
+
**Continue**
|
1276
|
+
|
1277
|
+
It is similar to `BCDD::Result.mixin(with: :Continue)`, 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.
|
1278
|
+
|
1279
|
+
```ruby
|
1280
|
+
class Divide
|
1281
|
+
include BCDD::Result::Expectations.mixin(
|
1282
|
+
with: :Continue,
|
1283
|
+
success: :division_completed,
|
1284
|
+
failure: %i[invalid_arg division_by_zero]
|
1285
|
+
)
|
1286
|
+
|
1287
|
+
def call(arg1, arg2)
|
1288
|
+
validate_numbers(arg1, arg2)
|
1289
|
+
.and_then(:validate_non_zero)
|
1290
|
+
.and_then(:divide)
|
1291
|
+
end
|
1292
|
+
|
1293
|
+
private
|
1294
|
+
|
1295
|
+
def validate_numbers(arg1, arg2)
|
1296
|
+
arg1.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg1 must be numeric')
|
1297
|
+
arg2.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg2 must be numeric')
|
1298
|
+
|
1299
|
+
Continue([arg1, arg2])
|
1300
|
+
end
|
1301
|
+
|
1302
|
+
def validate_non_zero(numbers)
|
1303
|
+
return Continue(numbers) unless numbers.last.zero?
|
1304
|
+
|
1305
|
+
Failure(:division_by_zero, 'arg2 must not be zero')
|
1306
|
+
end
|
1307
|
+
|
1308
|
+
def divide((number1, number2))
|
1309
|
+
Success(:division_completed, number1 / number2)
|
1310
|
+
end
|
1311
|
+
end
|
1312
|
+
|
1313
|
+
result = Divide.new.call(4,2)
|
1314
|
+
# => #<BCDD::Result::Success type=:division_completed value=2>
|
1315
|
+
|
1316
|
+
# The example below shows an error because the :ok type is not allowed.
|
1317
|
+
# But look at the allowed types have only one type (:division_completed).
|
1318
|
+
# This is because the :continued type is ignored by the expectations.
|
1319
|
+
#
|
1320
|
+
result.success?(:ok)
|
1321
|
+
# type :ok is not allowed. Allowed types: :division_completed (BCDD::Result::Expectations::Error::UnexpectedType)
|
1322
|
+
```
|
1323
|
+
|
1324
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
1325
|
+
|
1326
|
+
### `BCDD::Result::Context`
|
1327
|
+
|
1328
|
+
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.
|
1329
|
+
|
1330
|
+
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.
|
1331
|
+
|
1332
|
+
#### Defining successes and failures
|
1333
|
+
|
1334
|
+
As the `BCDD::Result`, you can declare success and failures directly from `BCDD::Result::Context`.
|
1335
|
+
|
1336
|
+
```ruby
|
1337
|
+
BCDD::Result::Context::Success(:ok, a: 1, b: 2)
|
1338
|
+
#<BCDD::Result::Context::Success type=:ok value={:a=>1, :b=>2}>
|
1339
|
+
|
1340
|
+
BCDD::Result::Context::Failure(:err, message: 'something went wrong')
|
1341
|
+
#<BCDD::Result::Context::Failure type=:err value={:message=>"something went wrong"}>
|
1342
|
+
```
|
1343
|
+
|
1344
|
+
But different from `BCDD::Result` that accepts any value, the `BCDD::Result::Context` only takes keyword arguments.
|
1345
|
+
|
1346
|
+
```ruby
|
1347
|
+
BCDD::Result::Context::Success(:ok, [1, 2])
|
1348
|
+
# wrong number of arguments (given 2, expected 1) (ArgumentError)
|
1349
|
+
|
1350
|
+
BCDD::Result::Context::Failure(:err, { message: 'something went wrong' })
|
1351
|
+
# wrong number of arguments (given 2, expected 1) (ArgumentError)
|
1352
|
+
|
1353
|
+
#
|
1354
|
+
# Use ** to convert a hash to keyword arguments
|
1355
|
+
#
|
1356
|
+
BCDD::Result::Context::Success(:ok, **{ message: 'hashes can be converted to keyword arguments' })
|
1357
|
+
#<BCDD::Result::Context::Success type=:ok value={:message=>"hashes can be converted to keyword arguments"}>
|
1358
|
+
```
|
1359
|
+
|
1360
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
1361
|
+
|
1362
|
+
#### `BCDD::Result::Context.mixin`
|
1363
|
+
|
1364
|
+
As in the `BCDD::Result`, you can use the `BCDD::Result::Context.mixin` to add the `Success()` and `Failure()` methods to your classes/modules.
|
1365
|
+
|
1366
|
+
Let's see this feature and the data accumulation in action:
|
1367
|
+
|
1368
|
+
##### Class example (Instance Methods)
|
1369
|
+
|
1370
|
+
```ruby
|
1371
|
+
class Divide
|
1372
|
+
require 'logger'
|
1373
|
+
|
1374
|
+
include BCDD::Result::Context.mixin
|
1375
|
+
|
1376
|
+
def call(arg1, arg2, logger: ::Logger.new(STDOUT))
|
1377
|
+
validate_numbers(arg1, arg2)
|
1378
|
+
.and_then(:validate_non_zero)
|
1379
|
+
.and_then(:divide, logger: logger)
|
1380
|
+
end
|
1381
|
+
|
1382
|
+
private
|
1383
|
+
|
1384
|
+
def validate_numbers(arg1, arg2)
|
1385
|
+
arg1.is_a?(::Numeric) or return Failure(:err, message: 'arg1 must be numeric')
|
1386
|
+
arg2.is_a?(::Numeric) or return Failure(:err, message: 'arg2 must be numeric')
|
1387
|
+
|
1388
|
+
Success(:ok, number1: arg1, number2: arg2)
|
1389
|
+
end
|
1390
|
+
|
1391
|
+
def validate_non_zero(number2:, **)
|
1392
|
+
return Success(:ok) if number2.nonzero?
|
1393
|
+
|
1394
|
+
Failure(:err, message: 'arg2 must not be zero')
|
1395
|
+
end
|
1396
|
+
|
1397
|
+
#
|
1398
|
+
# The logger was injected via #and_then and keyword arguments
|
1399
|
+
#
|
1400
|
+
def divide(number1:, number2:, logger:)
|
1401
|
+
result = number1 / number2
|
1402
|
+
|
1403
|
+
logger.info("The division result is #{result}")
|
1404
|
+
|
1405
|
+
Success(:ok, number: result)
|
1406
|
+
end
|
1407
|
+
end
|
1408
|
+
|
1409
|
+
Divide.new.call(10, 5)
|
1410
|
+
# I, [2023-10-27T01:51:46.905004 #76915] INFO -- : The division result is 2
|
1411
|
+
#<BCDD::Result::Context::Success type=:ok value={:number=>2}>
|
1412
|
+
|
1413
|
+
Divide.new.call('10', 5)
|
1414
|
+
#<BCDD::Result::Context::Failure type=:err value={:message=>"arg1 must be numeric"}>
|
1415
|
+
|
1416
|
+
Divide.new.call(10, '5')
|
1417
|
+
#<BCDD::Result::Context::Failure type=:err value={:message=>"arg2 must be numeric"}>
|
1418
|
+
|
1419
|
+
Divide.new.call(10, 0)
|
1420
|
+
#<BCDD::Result::Context::Failure type=:err value={:message=>"arg2 must not be zero"}>
|
1421
|
+
```
|
1422
|
+
|
1423
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
1424
|
+
|
1425
|
+
##### `and_expose`
|
1426
|
+
|
1427
|
+
This allows you to expose only the desired keys from the accumulated result. It can be used with any `BCDD::Result::Context` object.
|
1428
|
+
|
1429
|
+
Let's add it to the previous example:
|
1430
|
+
|
1431
|
+
```ruby
|
1432
|
+
class Divide
|
1433
|
+
include BCDD::Result::Context.mixin
|
1434
|
+
|
1435
|
+
def call(arg1, arg2)
|
1436
|
+
validate_numbers(arg1, arg2)
|
1437
|
+
.and_then(:validate_non_zero)
|
1438
|
+
.and_then(:divide)
|
1439
|
+
end
|
1440
|
+
|
1441
|
+
private
|
1442
|
+
|
1443
|
+
def validate_numbers(arg1, arg2)
|
1444
|
+
arg1.is_a?(::Numeric) or return Failure(:err, message: 'arg1 must be numeric')
|
1445
|
+
arg2.is_a?(::Numeric) or return Failure(:err, message: 'arg2 must be numeric')
|
1446
|
+
|
1447
|
+
Success(:ok, number1: arg1, number2: arg2)
|
1448
|
+
end
|
1449
|
+
|
1450
|
+
def validate_non_zero(number2:, **)
|
1451
|
+
return Success(:ok) if number2.nonzero?
|
1452
|
+
|
1453
|
+
Failure(:err, message: 'arg2 must not be zero')
|
1454
|
+
end
|
1455
|
+
|
1456
|
+
def divide(**input)
|
1457
|
+
Success(:ok, number: input.values.reduce(:/), **input)
|
1458
|
+
end
|
1459
|
+
end
|
1460
|
+
|
1461
|
+
Divide.new.call(10, 5)
|
1462
|
+
#<BCDD::Result::Context::Success type=:division_completed value={:number=>2}>
|
1463
|
+
```
|
1464
|
+
|
1465
|
+
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.
|
1466
|
+
|
1467
|
+
Remove the `#and_expose` call to see the difference. This will be the outcome:
|
1468
|
+
|
1469
|
+
```ruby
|
1470
|
+
Divide.new.call(10, 5)
|
1471
|
+
#<BCDD::Result::Context::Success type=:ok value={:number=>2, :number1=>10, :number2=>5}>
|
1472
|
+
```
|
1473
|
+
|
1474
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
1475
|
+
|
1476
|
+
##### Module example (Singleton Methods)
|
1477
|
+
|
1478
|
+
Yes, the `BCDD::Result::Context.mixin` can produce singleton methods. Look for an example using a module (but it could be a class, too).
|
1479
|
+
|
1480
|
+
```ruby
|
1481
|
+
module Divide
|
1482
|
+
extend self, BCDD::Result::Context.mixin
|
1483
|
+
|
1484
|
+
def call(arg1, arg2)
|
1485
|
+
validate_numbers(arg1, arg2)
|
1486
|
+
.and_then(:validate_non_zero)
|
1487
|
+
.and_then(:divide)
|
1488
|
+
.and_expose(:division_completed, [:number])
|
1489
|
+
end
|
1490
|
+
|
1491
|
+
private
|
1492
|
+
|
1493
|
+
def validate_numbers(arg1, arg2)
|
1494
|
+
arg1.is_a?(::Numeric) or return Failure(:err, message: 'arg1 must be numeric')
|
1495
|
+
arg2.is_a?(::Numeric) or return Failure(:err, message: 'arg2 must be numeric')
|
1496
|
+
|
1497
|
+
Success(:ok, number1: arg1, number2: arg2)
|
1498
|
+
end
|
1499
|
+
|
1500
|
+
def validate_non_zero(number2:, **)
|
1501
|
+
return Success(:ok) if number2.nonzero?
|
1502
|
+
|
1503
|
+
Failure(:err, message: 'arg2 must not be zero')
|
1504
|
+
end
|
1505
|
+
|
1506
|
+
def divide(number1:, number2:)
|
1507
|
+
Success(:ok, number: number1 / number2)
|
1508
|
+
end
|
1509
|
+
end
|
1510
|
+
|
1511
|
+
Divide.call(10, 5)
|
1512
|
+
#<BCDD::Result::Context::Success type=:division_completed value={:number=>2}>
|
1513
|
+
|
1514
|
+
Divide.call('10', 5)
|
1515
|
+
#<BCDD::Result::Context::Failure type=:err value={:message=>"arg1 must be numeric"}>
|
1516
|
+
|
1517
|
+
Divide.call(10, '5')
|
1518
|
+
#<BCDD::Result::Context::Failure type=:err value={:message=>"arg2 must be numeric"}>
|
1519
|
+
|
1520
|
+
Divide.call(10, 0)
|
1521
|
+
#<BCDD::Result::Context::Failure type=:err value={:message=>"arg2 must not be zero"}>
|
1522
|
+
```
|
1523
|
+
|
1524
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
1525
|
+
|
1526
|
+
#### `BCDD::Result::Context::Expectations`
|
1527
|
+
|
1528
|
+
The `BCDD::Result::Context::Expectations` is a `BCDD::Result::Expectations` with the `BCDD::Result::Context` features.
|
1529
|
+
|
1530
|
+
This is an example using the mixin mode, but the standalone mode is also supported.
|
1531
|
+
|
1532
|
+
```ruby
|
1533
|
+
#
|
1534
|
+
# Put this line in an initializer or at the beginning of your code.
|
1535
|
+
# It is required if you decide to use pattern matching to validate all of your result's values.
|
1536
|
+
#
|
1537
|
+
BCDD::Result::Contract.nil_as_valid_value_checking!
|
1538
|
+
|
1539
|
+
class Divide
|
1540
|
+
include BCDD::Result::Context::Expectations.mixin(
|
1541
|
+
success: {
|
1542
|
+
division_completed: ->(value) { value => { number: Numeric } }
|
1543
|
+
},
|
1544
|
+
failure: {
|
1545
|
+
invalid_arg: ->(value) { value => { message: String } },
|
1546
|
+
division_by_zero: ->(value) { value => { message: String } }
|
1547
|
+
}
|
1548
|
+
)
|
1549
|
+
|
1550
|
+
def call(arg1, arg2)
|
1551
|
+
arg1.is_a?(Numeric) or return Failure(:invalid_arg, message: 'arg1 must be numeric')
|
1552
|
+
arg2.is_a?(Numeric) or return Failure(:invalid_arg, message: 'arg2 must be numeric')
|
1553
|
+
|
1554
|
+
arg2.zero? and return Failure(:division_by_zero, message: 'arg2 must not be zero')
|
1555
|
+
|
1556
|
+
Success(:division_completed, number: arg1 / arg2)
|
1557
|
+
end
|
1558
|
+
end
|
1559
|
+
|
1560
|
+
Divide.new.call(10, 5)
|
1561
|
+
#<BCDD::Result::Context::Success type=:division_completed value={:number=>2}>
|
1562
|
+
```
|
1563
|
+
|
1564
|
+
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.
|
1565
|
+
|
1566
|
+
Let's see this using previous example:
|
1567
|
+
|
1568
|
+
```ruby
|
1569
|
+
Divide::Result::Success(:division_completed, number: 2)
|
1570
|
+
#<BCDD::Result::Context::Success type=:division_completed value={:number=>2}>
|
1571
|
+
|
1572
|
+
Divide::Result::Success(:division_completed, number: '2')
|
1573
|
+
# value {:number=>"2"} is not allowed for :division_completed type ({:number=>"2"}: Numeric === "2" does not return true) (BCDD::Result::Contract::Error::UnexpectedValue)
|
1574
|
+
```
|
1575
|
+
|
1576
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
1577
|
+
|
1578
|
+
#### Mixin add-ons
|
1579
|
+
|
1580
|
+
The `BCDD::Result::Context.mixin` and `BCDD::Result::Context::Expectations.mixin` also accepts the `with:` argument. And it works the same way as the `BCDD::Result` mixins.
|
1581
|
+
|
1582
|
+
**Continue**
|
1583
|
+
|
1584
|
+
The `BCDD::Result::Context.mixin(with: :Continue)` or `BCDD::Result::Context::Expectations.mixin(with: :Continue)` 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.
|
1585
|
+
|
1586
|
+
Let's use a mix of `BCDD::Result::Context` features to see in action with this add-on:
|
1587
|
+
|
1588
|
+
```ruby
|
1589
|
+
#
|
1590
|
+
# Put this line in an initializer or at the beginning of your code.
|
1591
|
+
# It is required if you decide to use pattern matching to validate all of your result's values.
|
1592
|
+
#
|
1593
|
+
BCDD::Result::Contract.nil_as_valid_value_checking!
|
1594
|
+
|
1595
|
+
module Divide
|
1596
|
+
require 'logger'
|
1597
|
+
|
1598
|
+
extend self, BCDD::Result::Context::Expectations.mixin(
|
1599
|
+
with: :Continue,
|
1600
|
+
success: {
|
1601
|
+
division_completed: ->(value) { value => { number: Numeric } }
|
1602
|
+
},
|
1603
|
+
failure: {
|
1604
|
+
invalid_arg: ->(value) { value => { message: String } },
|
1605
|
+
division_by_zero: ->(value) { value => { message: String } }
|
1606
|
+
}
|
1607
|
+
)
|
1608
|
+
|
1609
|
+
def call(arg1, arg2, logger: ::Logger.new(STDOUT))
|
1610
|
+
validate_numbers(arg1, arg2)
|
1611
|
+
.and_then(:validate_non_zero)
|
1612
|
+
.and_then(:divide, logger: logger)
|
1613
|
+
.and_expose(:division_completed, [:number])
|
1614
|
+
end
|
1615
|
+
|
1616
|
+
private
|
1617
|
+
|
1618
|
+
def validate_numbers(arg1, arg2)
|
1619
|
+
arg1.is_a?(::Numeric) or return Failure(:invalid_arg, message: 'arg1 must be numeric')
|
1620
|
+
arg2.is_a?(::Numeric) or return Failure(:invalid_arg, message: 'arg2 must be numeric')
|
1621
|
+
|
1622
|
+
Continue(number1: arg1, number2: arg2)
|
1623
|
+
end
|
1624
|
+
|
1625
|
+
def validate_non_zero(number2:, **)
|
1626
|
+
return Continue() if number2.nonzero?
|
1627
|
+
|
1628
|
+
Failure(:division_by_zero, message: 'arg2 must not be zero')
|
1629
|
+
end
|
1630
|
+
|
1631
|
+
def divide(number1:, number2:, logger:)
|
1632
|
+
result = number1 / number2
|
1633
|
+
|
1634
|
+
logger.info("The division result is #{result}")
|
1635
|
+
|
1636
|
+
Continue(number: result)
|
1637
|
+
end
|
1638
|
+
end
|
1639
|
+
|
1640
|
+
Divide.call(14, 2)
|
1641
|
+
# I, [2023-10-27T02:01:05.812388 #77823] INFO -- : The division result is 7
|
1642
|
+
#<BCDD::Result::Context::Success type=:division_completed value={:number=>7}>
|
1643
|
+
|
1644
|
+
Divide.call('14', 2)
|
1645
|
+
#<BCDD::Result::Context::Failure type=:invalid_arg value={:message=>"arg1 must be numeric"}>
|
1646
|
+
|
1647
|
+
Divide.call(14, '2')
|
1648
|
+
#<BCDD::Result::Context::Failure type=:invalid_arg value={:message=>"arg2 must be numeric"}>
|
1649
|
+
|
1650
|
+
Divide.call(14, 0)
|
1651
|
+
#<BCDD::Result::Context::Failure type=:division_by_zero value={:message=>"arg2 must not be zero"}>
|
1652
|
+
```
|
1653
|
+
|
1654
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
1655
|
+
|
1074
1656
|
## About
|
1075
1657
|
|
1076
1658
|
[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.
|