bcdd-result 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e27dee9c053eeb9f6025b17e09eede49ca6cbc69cd9120947bad34b26a79859d
4
- data.tar.gz: c8c080d43a3eb46139f7bf5299c64346809d1c0df4bf0181ccb02ba3005d5ee0
3
+ metadata.gz: 96c58d6a9132fbf42fc8f3c1d9ba683f0b6297e23b1f0536fc4bcb7efb5084df
4
+ data.tar.gz: fcef45f3c5ddacc20ff5e9533012c7fbe5817878cef1ff528dabd8920e1cbccd
5
5
  SHA512:
6
- metadata.gz: 253e14ef154c2b863bb65139134a903b2a6fd035fb7a995158cf5ebab395d6ba2bd9f8e999c29305f2d2cd2f6e10db9923166a13d1f06e627df276c87c7a644d
7
- data.tar.gz: 7d62559447a5ee3cb665713645f1db7729428f9cf94bd349574758ea3f9bae361f65a218640b2d5bfeb138ef108c27dec426a44ec89dbc971620671b6eab945c
6
+ metadata.gz: d4cf048907571205c6cd0b90ca4b95c2480c34de79d082132e4f1cab1e809f4c6eec538d9d38b036ad313d9653420625b68a502272e73371b99b157f43315123
7
+ data.tar.gz: 4662aaeb33a7eb770ab51529cc1f2c4e9d86844dec70dfcdc4f37674aab33d010d6cbe7932fc635f09979d47b4b631748a3d4791021e99108a011a93469a2d73
data/.rubocop.yml CHANGED
@@ -18,5 +18,9 @@ Layout/ExtraSpacing:
18
18
  Style/ClassAndModuleChildren:
19
19
  Enabled: false
20
20
 
21
+ Naming/MethodName:
22
+ Exclude:
23
+ - lib/bcdd/resultable.rb
24
+
21
25
  Minitest/MultipleAssertions:
22
26
  Enabled: false
data/.rubocop_todo.yml CHANGED
@@ -1,19 +1,21 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2023-09-23 03:05:24 UTC using RuboCop version 1.56.3.
3
+ # on 2023-09-27 00:47:03 UTC using RuboCop version 1.56.3.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
7
7
  # versions of RuboCop, may require this file to be generated again.
8
8
 
9
- # Offense count: 8
9
+ # Offense count: 13
10
10
  # Configuration parameters: AllowedConstants.
11
11
  Style/Documentation:
12
12
  Exclude:
13
13
  - 'spec/**/*'
14
14
  - 'test/**/*'
15
15
  - 'lib/bcdd/result.rb'
16
- - 'lib/bcdd/result/base.rb'
17
16
  - 'lib/bcdd/result/error.rb'
18
17
  - 'lib/bcdd/result/failure.rb'
18
+ - 'lib/bcdd/result/handler.rb'
19
19
  - 'lib/bcdd/result/success.rb'
20
+ - 'lib/bcdd/result/type.rb'
21
+ - 'lib/bcdd/resultable.rb'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,105 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.0] - 2023-09-26
4
+
5
+ ### Added
6
+
7
+ - Add `BCDD::Result#handle`. This method allows defining blocks for each hook (type, failure, success), but instead of returning the result itself, it will return the output of the first match/block execution.
8
+
9
+ ## [0.2.0] - 2023-09-26
10
+
11
+ ### Added
12
+
13
+ - Add `BCDD::Resultable`. This module can add `Success()` and `Failure()` in any object. The target object will be the subject of the result object produced by these methods.
14
+
15
+ **Classes (instance methods)**
16
+
17
+ ```ruby
18
+ class Divide
19
+ include BCDD::Resultable
20
+
21
+ attr_reader :arg1, :arg2
22
+
23
+ def initialize(arg1, arg2)
24
+ @arg1 = arg1
25
+ @arg2 = arg2
26
+ end
27
+
28
+ def call
29
+ validate_numbers
30
+ .and_then(:validate_non_zero)
31
+ .and_then(:divide)
32
+ end
33
+
34
+ private
35
+
36
+ def validate_numbers
37
+ arg1.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg1 must be numeric')
38
+ arg2.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg2 must be numeric')
39
+
40
+ Success(:ok, [arg1, arg2])
41
+ end
42
+
43
+ def validate_non_zero(numbers)
44
+ return Success(:ok, numbers) unless numbers.last.zero?
45
+
46
+ Failure(:division_by_zero, 'arg2 must not be zero')
47
+ end
48
+
49
+ def divide((number1, number2))
50
+ Success(:division_completed, number1 / number2)
51
+ end
52
+ end
53
+ ```
54
+
55
+ **Module (singleton methods)**
56
+
57
+ ```ruby
58
+ module Divide
59
+ extend BCDD::Resultable
60
+ extend self
61
+
62
+ def call(arg1, arg2)
63
+ validate_numbers(arg1, arg2)
64
+ .and_then(:validate_non_zero)
65
+ .and_then(:divide)
66
+ end
67
+
68
+ private
69
+
70
+ def validate_numbers(arg1, arg2)
71
+ arg1.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg1 must be numeric')
72
+ arg2.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg2 must be numeric')
73
+
74
+ Success(:ok, [arg1, arg2])
75
+ end
76
+
77
+ def validate_non_zero(numbers)
78
+ return Success(:ok, numbers) unless numbers.last.zero?
79
+
80
+ Failure(:division_by_zero, 'arg2 must not be zero')
81
+ end
82
+
83
+ def divide((number1, number2))
84
+ Success(:division_completed, number1 / number2)
85
+ end
86
+ end
87
+ ```
88
+
89
+ - Make the `BCDD::Result#initialize` enabled to receive a subject.
90
+
91
+ - Make the `BCDD::Result#and_then` method receive a method name (symbol) and perform it on the result subject (added by `BCDD::Resultable`). The called method must return a result; otherwise, an error (`BCDD::Result::Error::UnexpectedOutcome`) will be raised.
92
+
93
+ - Add `BCDD::Result::Error::UnexpectedOutcome` to represent an unexpected outcome.
94
+
95
+ - Add `BCDD::Result::Error::WrongResultSubject` to represent a wrong result subject. When using `BCDD::Resultable`, the result subject must be the same as the target object.
96
+
97
+ - Add `BCDD::Result::Error::WrongSubjectMethodArity` to represent a wrong subject method arity. Valid arities are 0 and 1.
98
+
99
+ ### Removed
100
+
101
+ - **(BREAKING)** Remove `BCDD::Result::Error::UnexpectedBlockOutcome`. It was replaced by `BCDD::Result::Error::UnexpectedOutcome`.
102
+
3
103
  ## [0.1.0] - 2023-09-25
4
104
 
5
105
  ### Added
data/README.md CHANGED
@@ -1,4 +1,8 @@
1
- # BCDD::Result <!-- omit in toc -->
1
+ <p align="center">
2
+ <h1 align="center" id="-bcddresult">🔀 BCDD::Result</h1>
3
+ <p align="center"><i>Empower Ruby apps with a pragmatic use of Railway Oriented Programming.</i></p>
4
+ <br>
5
+ </p>
2
6
 
3
7
  A general-purpose result monad that allows you to create objects that represent a success (`BCDD::Result::Success`) or failure (`BCDD::Result::Failure`).
4
8
 
@@ -6,8 +10,11 @@ A general-purpose result monad that allows you to create objects that represent
6
10
 
7
11
  It allows you to consistently represent the concept of success and failure throughout your codebase.
8
12
 
9
- Furthermore, this abstraction exposes several methods that will be useful to make the code flow react clearly and cleanly to the result represented by these objects.
13
+ Furthermore, this abstraction exposes several features that will be useful to make the application flow react cleanly and securely to the result represented by these objects.
10
14
 
15
+ Use it to enable the [Railway Oriented Programming](https://fsharpforfunandprofit.com/rop/) pattern (superpower) in your code.
16
+
17
+ - [Ruby Version](#ruby-version)
11
18
  - [Installation](#installation)
12
19
  - [Usage](#usage)
13
20
  - [Reference](#reference)
@@ -18,28 +25,44 @@ Furthermore, this abstraction exposes several methods that will be useful to mak
18
25
  - [`result.on_type`](#resulton_type)
19
26
  - [`result.on_success`](#resulton_success)
20
27
  - [`result.on_failure`](#resulton_failure)
21
- - [Result value](#result-value)
28
+ - [`result.handle`](#resulthandle)
29
+ - [Result Value](#result-value)
22
30
  - [`result.value_or`](#resultvalue_or)
23
31
  - [`result.data_or`](#resultdata_or)
24
32
  - [Railway Oriented Programming](#railway-oriented-programming)
25
33
  - [`result.and_then`](#resultand_then)
34
+ - [`BCDD::Resultable`](#bcddresultable)
35
+ - [Class example (instance methods)](#class-example-instance-methods)
36
+ - [Module example (singleton methods)](#module-example-singleton-methods)
37
+ - [Restrictions](#restrictions)
26
38
  - [About](#about)
27
39
  - [Development](#development)
28
40
  - [Contributing](#contributing)
29
41
  - [License](#license)
30
42
  - [Code of Conduct](#code-of-conduct)
31
43
 
44
+ ## Ruby Version
45
+
46
+ `>= 2.7.0`
32
47
 
33
48
  ## Installation
34
49
 
35
- Install the gem and add to the application's Gemfile by executing:
50
+ Add this line to your application's Gemfile:
51
+
52
+ ```ruby
53
+ gem 'bcdd-result', require: 'bcdd/result'
54
+ ```
55
+
56
+ And then execute:
36
57
 
37
- $ bundle add bcdd-result
58
+ $ bundle install
38
59
 
39
60
  If bundler is not being used to manage dependencies, install the gem by executing:
40
61
 
41
62
  $ gem install bcdd-result
42
63
 
64
+ <p align="right">(<a href="#-bcddresult">⬆️ &nbsp;back to top</a>)</p>
65
+
43
66
  ## Usage
44
67
 
45
68
  To create a result, you must define a type (symbol) and its value (any kind of object). e.g.,
@@ -58,7 +81,7 @@ BCDD::Result::Success(:ok) #
58
81
  BCDD::Result::Failure(:err) #
59
82
  ```
60
83
 
61
- <p align="right">(<a href="#bcddresult-">⬆️ &nbsp;back to top</a>)</p>
84
+ <p align="right">(<a href="#-bcddresult">⬆️ &nbsp;back to top</a>)</p>
62
85
 
63
86
  ## Reference
64
87
 
@@ -114,7 +137,7 @@ result.type # :no
114
137
  result.value # nil
115
138
  ```
116
139
 
117
- <p align="right">(<a href="#bcddresult-">⬆️ &nbsp;back to top</a>)</p>
140
+ <p align="right">(<a href="#-bcddresult">⬆️ &nbsp;back to top</a>)</p>
118
141
 
119
142
  #### Receiving types in `result.success?` or `result.failure?`
120
143
 
@@ -140,7 +163,7 @@ result.failure?(:err) # true
140
163
  result.failure?(:error) # false
141
164
  ```
142
165
 
143
- <p align="right">(<a href="#bcddresult-">⬆️ &nbsp;back to top</a>)</p>
166
+ <p align="right">(<a href="#-bcddresult">⬆️ &nbsp;back to top</a>)</p>
144
167
 
145
168
  ### Result Hooks
146
169
 
@@ -159,6 +182,8 @@ def divide(arg1, arg2)
159
182
  end
160
183
  ```
161
184
 
185
+ <p align="right">(<a href="#-bcddresult">⬆️ &nbsp;back to top</a>)</p>
186
+
162
187
  #### `result.on`
163
188
 
164
189
  `BCDD::Result#on` will perform the block when the type matches the result type.
@@ -193,7 +218,9 @@ output =
193
218
  result.object_id == output.object_id # true
194
219
  ```
195
220
 
196
- <p align="right">(<a href="#bcddresult-">⬆️ &nbsp;back to top</a>)</p>
221
+ *PS: The `divide()` implementation is [here](#result-hooks).*
222
+
223
+ <p align="right">(<a href="#-bcddresult">⬆️ &nbsp;back to top</a>)</p>
197
224
 
198
225
  #### `result.on_type`
199
226
 
@@ -220,7 +247,9 @@ divide(4, 4).on_success(:ok) { |value| puts value }
220
247
  divide(4, 4).on_failure { |error| puts error }
221
248
  ```
222
249
 
223
- <p align="right">(<a href="#bcddresult-">⬆️ &nbsp;back to top</a>)</p>
250
+ *PS: The `divide()` implementation is [here](#result-hooks).*
251
+
252
+ <p align="right">(<a href="#-bcddresult">⬆️ &nbsp;back to top</a>)</p>
224
253
 
225
254
  #### `result.on_failure`
226
255
 
@@ -240,9 +269,57 @@ divide(4, 0).on_success { |number| puts number }
240
269
  divide(4, 0).on_failure(:invalid_arg) { |error| puts error }
241
270
  ```
242
271
 
243
- <p align="right">(<a href="#bcddresult-">⬆️ &nbsp;back to top</a>)</p>
272
+ *PS: The `divide()` implementation is [here](#result-hooks).*
273
+
274
+ <p align="right">(<a href="#-bcddresult">⬆️ &nbsp;back to top</a>)</p>
275
+
276
+ #### `result.handle`
277
+
278
+ This method will allow you to define blocks for each hook (type, failure, success), but instead of returning itself, it will return the output of the first match/block execution.
279
+
280
+ ```ruby
281
+ divide(4, 2).handle do |result|
282
+ result.success { |number| number }
283
+ result.failure(:invalid_arg) { |err| puts err }
284
+ result.type(:division_by_zero) { raise ZeroDivisionError }
285
+ end
286
+
287
+ #or
288
+
289
+ divide(4, 2).handle do |on|
290
+ on.success { |number| number }
291
+ on.failure { |err| puts err }
292
+ end
293
+
294
+ #or
295
+
296
+ divide(4, 2).handle do |on|
297
+ on.type(:invalid_arg) { |err| puts err }
298
+ on.type(:division_by_zero) { raise ZeroDivisionError }
299
+ on.type(:division_completed) { |number| number }
300
+ end
301
+
302
+ # or
303
+
304
+ divide(4, 2).handle do |on|
305
+ on[:invalid_arg] { |err| puts err }
306
+ on[:division_by_zero] { raise ZeroDivisionError }
307
+ on[:division_completed] { |number| number }
308
+ end
309
+
310
+ # The [] syntax 👆 is an alias of #type.
311
+ ```
312
+
313
+ **Notes:**
314
+ * You can define multiple types to be handled by the same hook/block
315
+ * If the type is missing, it will perform the block for any success or failure handler.
316
+ * The `#type` and `#[]` handlers will require at least one type/argument.
317
+
318
+ *PS: The `divide()` implementation is [here](#result-hooks).*
319
+
320
+ <p align="right">(<a href="#-bcddresult">⬆️ &nbsp;back to top</a>)</p>
244
321
 
245
- ### Result value
322
+ ### Result Value
246
323
 
247
324
  The most simple way to get the result value is by calling `BCDD::Result#value` or `BCDD::Result#data`.
248
325
 
@@ -271,17 +348,16 @@ divide(4, '2').value_or { 0 } # 0
271
348
  divide(100, 0).value_or { 0 } # 0
272
349
  ```
273
350
 
274
- <p align="right">(<a href="#bcddresult-">⬆️ &nbsp;back to top</a>)</p>
351
+ *PS: The `divide()` implementation is [here](#result-hooks).*
352
+
353
+ <p align="right">(<a href="#-bcddresult">⬆️ &nbsp;back to top</a>)</p>
275
354
 
276
355
  #### `result.data_or`
277
356
 
278
357
  `BCDD::Result#data_or` is an alias of `BCDD::Result#value_or`.
279
358
 
280
-
281
359
  ### Railway Oriented Programming
282
360
 
283
- #### `result.and_then`
284
-
285
361
  This feature/pattern is also known as ["Railway Oriented Programming"](https://fsharpforfunandprofit.com/rop/).
286
362
 
287
363
  The idea is to chain blocks and creates a pipeline of operations that can be interrupted by a failure.
@@ -289,7 +365,9 @@ The idea is to chain blocks and creates a pipeline of operations that can be int
289
365
  In other words, the block will be executed only if the result is a success.
290
366
  So, if some block returns a failure, the following blocks will be skipped.
291
367
 
292
- Due to this characteristic, you can use this feature to express some logic as a sequence of operations. And have the guarantee that the process will stop by the first failure detection, and if everything is ok, the final result will be a success. e.g.,
368
+ Due to this characteristic, you can use this feature to express some logic as a sequence of operations. And have the guarantee that the process will stop by the first failure detection, and if everything is ok, the final result will be a success.
369
+
370
+ #### `result.and_then`
293
371
 
294
372
  ```ruby
295
373
  module Divide
@@ -313,7 +391,7 @@ module Divide
313
391
  def validate_non_zero(numbers)
314
392
  return BCDD::Result::Success(:ok, numbers) unless numbers.last.zero?
315
393
 
316
- BCDD::Result::Failure(:division_by_zero, "arg2 must not be zero")
394
+ BCDD::Result::Failure(:division_by_zero, 'arg2 must not be zero')
317
395
  end
318
396
 
319
397
  def divide((number1, number2))
@@ -338,13 +416,119 @@ Divide.call(2, 2)
338
416
  #<BCDD::Result::Success type=:division_completed data=1>
339
417
  ```
340
418
 
341
- <p align="right">(<a href="#bcddresult-">⬆️ &nbsp;back to top</a>)</p>
419
+ <p align="right">(<a href="#-bcddresult">⬆️ &nbsp;back to top</a>)</p>
420
+
421
+ #### `BCDD::Resultable`
422
+
423
+ It is a module that can be included/extended by any object. It adds two methods to the target object: `Success()` and `Failure()`.
424
+
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.
426
+
427
+ And because of this, you can use the `#and_then` method to call methods from the target object (result's subject).
428
+
429
+ ##### Class example (instance methods)
430
+
431
+ ```ruby
432
+ class Divide
433
+ include BCDD::Resultable
434
+
435
+ attr_reader :arg1, :arg2
436
+
437
+ def initialize(arg1, arg2)
438
+ @arg1 = arg1
439
+ @arg2 = arg2
440
+ end
441
+
442
+ def call
443
+ validate_numbers
444
+ .and_then(:validate_non_zero)
445
+ .and_then(:divide)
446
+ end
447
+
448
+ private
449
+
450
+ def validate_numbers
451
+ arg1.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg1 must be numeric')
452
+ arg2.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg2 must be numeric')
453
+
454
+ # As arg1 and arg2 are instance methods, they will be available in the instance scope.
455
+ # So, in this case, I'm passing them as an array to show how the next method can receive the value as its argument.
456
+ Success(:ok, [arg1, arg2])
457
+ end
458
+
459
+ def validate_non_zero(numbers)
460
+ return Success(:ok, numbers) unless numbers.last.zero?
461
+
462
+ Failure(:division_by_zero, 'arg2 must not be zero')
463
+ end
464
+
465
+ def divide((number1, number2))
466
+ Success(:division_completed, number1 / number2)
467
+ end
468
+ end
469
+
470
+ Divide.new(4, 2).call #<BCDD::Result::Success type=:division_completed value=2>
471
+
472
+ Divide.new(4, 0).call #<BCDD::Result::Failure type=:division_by_zero value="arg2 must not be zero">
473
+ Divide.new('4', 2).call #<BCDD::Result::Failure type=:invalid_arg value="arg1 must be numeric">
474
+ Divide.new(4, '2').call #<BCDD::Result::Failure type=:invalid_arg value="arg2 must be numeric">
475
+ ```
476
+
477
+ ##### Module example (singleton methods)
478
+
479
+ ```ruby
480
+ module Divide
481
+ extend BCDD::Resultable
482
+ extend self
483
+
484
+ def call(arg1, arg2)
485
+ validate_numbers(arg1, arg2)
486
+ .and_then(:validate_non_zero)
487
+ .and_then(:divide)
488
+ end
489
+
490
+ private
491
+
492
+ def validate_numbers(arg1, arg2)
493
+ arg1.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg1 must be numeric')
494
+ arg2.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg2 must be numeric')
495
+
496
+ Success(:ok, [arg1, arg2])
497
+ end
498
+
499
+ def validate_non_zero(numbers)
500
+ return Success(:ok, numbers) unless numbers.last.zero?
501
+
502
+ Failure(:division_by_zero, 'arg2 must not be zero')
503
+ end
504
+
505
+ def divide((number1, number2))
506
+ Success(:division_completed, number1 / number2)
507
+ end
508
+ end
509
+
510
+ Divide.call(4, 2) #<BCDD::Result::Success type=:division_completed value=2>
511
+
512
+ Divide.call(4, 0) #<BCDD::Result::Failure type=:division_by_zero value="arg2 must not be zero">
513
+ Divide.call('4', 2) #<BCDD::Result::Failure type=:invalid_arg value="arg1 must be numeric">
514
+ Divide.call(4, '2') #<BCDD::Result::Failure type=:invalid_arg value="arg2 must be numeric">
515
+ ```
516
+
517
+ ##### Restrictions
518
+
519
+ 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
+
521
+ If you use `BCDD::Result::Subject()`/`BCDD::Result::Failure()`, or call another `BCDD::Resultable` object, the `#and_then` will raise an error because the subjects will be different.
522
+
523
+ > **Note**: You still can use the block syntax, but all the results must be produced by the subject's `Success()` and `Failure()` methods.
524
+
525
+ <p align="right">(<a href="#-bcddresult">⬆️ &nbsp;back to top</a>)</p>
342
526
 
343
527
  ## About
344
528
 
345
529
  [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.
346
530
 
347
- <p align="right">(<a href="#bcddresult-">⬆️ &nbsp;back to top</a>)</p>
531
+ <p align="right">(<a href="#-bcddresult">⬆️ &nbsp;back to top</a>)</p>
348
532
 
349
533
  ## Development
350
534
 
@@ -352,19 +536,19 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
352
536
 
353
537
  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).
354
538
 
355
- <p align="right">(<a href="#bcddresult-">⬆️ &nbsp;back to top</a>)</p>
539
+ <p align="right">(<a href="#-bcddresult">⬆️ &nbsp;back to top</a>)</p>
356
540
 
357
541
  ## Contributing
358
542
 
359
543
  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).
360
544
 
361
- <p align="right">(<a href="#bcddresult-">⬆️ &nbsp;back to top</a>)</p>
545
+ <p align="right">(<a href="#-bcddresult">⬆️ &nbsp;back to top</a>)</p>
362
546
 
363
547
  ## License
364
548
 
365
549
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
366
550
 
367
- <p align="right">(<a href="#bcddresult-">⬆️ &nbsp;back to top</a>)</p>
551
+ <p align="right">(<a href="#-bcddresult">⬆️ &nbsp;back to top</a>)</p>
368
552
 
369
553
  ## Code of Conduct
370
554
 
@@ -2,6 +2,10 @@
2
2
 
3
3
  class BCDD::Result
4
4
  class Error < ::StandardError
5
+ def self.build(**_kargs)
6
+ new
7
+ end
8
+
5
9
  class NotImplemented < self
6
10
  end
7
11
 
@@ -11,13 +15,31 @@ class BCDD::Result
11
15
  end
12
16
  end
13
17
 
14
- class UnexpectedBlockOutcome < self
15
- def initialize(arg = nil)
18
+ class UnexpectedOutcome < self
19
+ def self.build(outcome:, origin:)
16
20
  message =
17
- "Unexpected outcome: #{arg.inspect}. The block must return this object wrapped by " \
21
+ "Unexpected outcome: #{outcome.inspect}. The #{origin} must return this object wrapped by " \
18
22
  'BCDD::Result::Success or BCDD::Result::Failure'
19
23
 
20
- super(message)
24
+ new(message)
25
+ end
26
+ end
27
+
28
+ class WrongResultSubject < self
29
+ def self.build(given_result:, expected_subject:)
30
+ message =
31
+ "You cannot call #and_then and return a result that does not belong to the subject!\n" \
32
+ "Expected subject: #{expected_subject.inspect}\n" \
33
+ "Given subject: #{given_result.send(:subject).inspect}\n" \
34
+ "Given result: #{given_result.inspect}"
35
+
36
+ new(message)
37
+ end
38
+ end
39
+
40
+ class WrongSubjectMethodArity < self
41
+ def self.build(subject:, method:)
42
+ new("#{subject.class}##{method.name} has unsupported arity (#{method.arity}). Expected 0 or 1.")
21
43
  end
22
44
  end
23
45
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result
4
+ class Handler
5
+ UNDEFINED = ::Object.new
6
+
7
+ def initialize(result)
8
+ @outcome = UNDEFINED
9
+
10
+ @_type = result._type
11
+ @result = result
12
+ end
13
+
14
+ def [](*types, &block)
15
+ raise Error::MissingTypeArgument if types.empty?
16
+
17
+ self.outcome = block if _type.in?(types, allow_empty: false)
18
+ end
19
+
20
+ def failure(*types, &block)
21
+ self.outcome = block if result.failure? && _type.in?(types, allow_empty: true)
22
+ end
23
+
24
+ def success(*types, &block)
25
+ self.outcome = block if result.success? && _type.in?(types, allow_empty: true)
26
+ end
27
+
28
+ alias type []
29
+
30
+ private
31
+
32
+ attr_reader :_type, :result
33
+
34
+ def outcome?
35
+ @outcome != UNDEFINED
36
+ end
37
+
38
+ def outcome
39
+ @outcome if outcome?
40
+ end
41
+
42
+ def outcome=(block)
43
+ @outcome = block.call(result.value, result.type) unless outcome?
44
+ end
45
+ end
46
+
47
+ private_constant :Handler
48
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result
4
+ class Type
5
+ attr_reader :to_sym
6
+
7
+ def initialize(type)
8
+ @to_sym = type.to_sym
9
+ end
10
+
11
+ def in?(types, allow_empty: false)
12
+ (allow_empty && types.empty?) || types.any?(to_sym)
13
+ end
14
+ end
15
+
16
+ private_constant :Type
17
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module BCDD
4
4
  class Result
5
- VERSION = '0.1.0'
5
+ VERSION = '0.3.0'
6
6
  end
7
7
  end
data/lib/bcdd/result.rb CHANGED
@@ -2,15 +2,26 @@
2
2
 
3
3
  require_relative 'result/version'
4
4
  require_relative 'result/error'
5
+ require_relative 'result/type'
6
+ require_relative 'result/handler'
5
7
  require_relative 'result/failure'
6
8
  require_relative 'result/success'
7
9
 
10
+ require_relative 'resultable'
11
+
8
12
  class BCDD::Result
9
- attr_reader :type, :value
13
+ attr_reader :_type, :value, :subject
14
+
15
+ protected :subject
10
16
 
11
- def initialize(type:, value:)
12
- @type = type.to_sym
17
+ def initialize(type:, value:, subject: nil)
18
+ @_type = Type.new(type)
13
19
  @value = value
20
+ @subject = subject
21
+ end
22
+
23
+ def type
24
+ _type.to_sym
14
25
  end
15
26
 
16
27
  def success?(_type = nil)
@@ -41,25 +52,33 @@ class BCDD::Result
41
52
  def on(*types)
42
53
  raise Error::MissingTypeArgument if types.empty?
43
54
 
44
- tap { yield(value, type) if expected_type?(types) }
55
+ tap { yield(value, type) if _type.in?(types, allow_empty: false) }
45
56
  end
46
57
 
47
58
  def on_success(*types)
48
- tap { yield(value, type) if success? && allowed_to_handle?(types) }
59
+ tap { yield(value, type) if success? && _type.in?(types, allow_empty: true) }
49
60
  end
50
61
 
51
62
  def on_failure(*types)
52
- tap { yield(value, type) if failure? && allowed_to_handle?(types) }
63
+ tap { yield(value, type) if failure? && _type.in?(types, allow_empty: true) }
53
64
  end
54
65
 
55
- def and_then
66
+ def and_then(method_name = nil)
56
67
  return self if failure?
57
68
 
69
+ return call_subject_method(method_name) if method_name
70
+
58
71
  result = yield(value)
59
72
 
60
- return result if result.is_a?(::BCDD::Result)
73
+ ensure_result_object(result, origin: :block)
74
+ end
75
+
76
+ def handle
77
+ handler = Handler.new(self)
78
+
79
+ yield handler
61
80
 
62
- raise Error::UnexpectedBlockOutcome, result
81
+ handler.send(:outcome)
63
82
  end
64
83
 
65
84
  alias data value
@@ -68,11 +87,24 @@ class BCDD::Result
68
87
 
69
88
  private
70
89
 
71
- def expected_type?(types)
72
- types.any?(type)
90
+ def call_subject_method(method_name)
91
+ method = subject.method(method_name)
92
+
93
+ result =
94
+ case method.arity
95
+ when 0 then subject.send(method_name)
96
+ when 1 then subject.send(method_name, value)
97
+ else raise Error::WrongSubjectMethodArity.build(subject: subject, method: method)
98
+ end
99
+
100
+ ensure_result_object(result, origin: :method)
73
101
  end
74
102
 
75
- def allowed_to_handle?(types)
76
- types.empty? || expected_type?(types)
103
+ def ensure_result_object(result, origin:)
104
+ raise Error::UnexpectedOutcome.build(outcome: result, origin: origin) unless result.is_a?(::BCDD::Result)
105
+
106
+ return result if result.subject.equal?(subject)
107
+
108
+ raise Error::WrongResultSubject.build(given_result: result, expected_subject: subject)
77
109
  end
78
110
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BCDD::Resultable
4
+ def Success(type, value = nil)
5
+ BCDD::Result::Success.new(type: type, value: value, subject: self)
6
+ end
7
+
8
+ def Failure(type, value = nil)
9
+ BCDD::Result::Failure.new(type: type, value: value, subject: self)
10
+ end
11
+ end
data/sig/bcdd/result.rbs CHANGED
@@ -5,10 +5,13 @@ module BCDD
5
5
  end
6
6
 
7
7
  class BCDD::Result
8
- attr_reader type: Symbol
8
+ attr_reader _type: BCDD::Result::Type
9
9
  attr_reader value: untyped
10
+ attr_reader subject: untyped
10
11
 
11
- def initialize: (type: Symbol, value: untyped) -> void
12
+ def initialize: (type: Symbol, value: untyped, ?subject: untyped) -> void
13
+
14
+ def type: -> Symbol
12
15
 
13
16
  def success?: (?Symbol type) -> bool
14
17
  def failure?: (?Symbol type) -> bool
@@ -26,7 +29,9 @@ class BCDD::Result
26
29
  def on_success: (*Symbol) { (untyped, Symbol) -> void } -> BCDD::Result
27
30
  def on_failure: (*Symbol) { (untyped, Symbol) -> void } -> BCDD::Result
28
31
 
29
- def and_then: { (untyped) -> untyped } -> BCDD::Result
32
+ def and_then: (?Symbol method_name) { (untyped) -> untyped } -> BCDD::Result
33
+
34
+ def handle: { (BCDD::Result::Handler) -> void } -> untyped
30
35
 
31
36
  alias data value
32
37
  alias data_or value_or
@@ -34,33 +39,90 @@ class BCDD::Result
34
39
 
35
40
  private
36
41
 
37
- def expected_type?: (Array[Symbol]) -> bool
38
- def allowed_to_handle?: (Array[Symbol]) -> bool
42
+ def call_subject_method: (Symbol) -> BCDD::Result
43
+ def ensure_result_object: (untyped, origin: Symbol) -> BCDD::Result
39
44
  end
40
45
 
41
46
  class BCDD::Result
42
47
  class Failure < BCDD::Result
43
48
  end
44
49
 
45
- def self.Success: (Symbol type, untyped value) -> BCDD::Result::Success
50
+ def self.Success: (Symbol type, ?untyped value) -> BCDD::Result::Success
46
51
  end
47
52
 
48
53
  class BCDD::Result
49
54
  class Success < BCDD::Result
50
55
  end
51
56
 
52
- def self.Failure: (Symbol type, untyped value) -> BCDD::Result::Failure
57
+ def self.Failure: (Symbol type, ?untyped value) -> BCDD::Result::Failure
58
+ end
59
+
60
+ class BCDD::Result
61
+ class Handler
62
+ UNDEFINED: Object
63
+
64
+ def initialize: (BCDD::Result) -> void
65
+
66
+ def []: (*Symbol) { (untyped, Symbol) -> void } -> untyped
67
+ def failure: (*Symbol) { (untyped, Symbol) -> void } -> untyped
68
+ def success: (*Symbol) { (untyped, Symbol) -> void } -> untyped
69
+
70
+ alias type []
71
+
72
+ private
73
+
74
+ attr_reader _type: BCDD::Result::Type
75
+ attr_reader result: BCDD::Result
76
+
77
+ def outcome?: -> bool
78
+
79
+ def outcome: -> untyped
80
+
81
+ def outcome=: (Proc) -> void
82
+ end
83
+ end
84
+
85
+ module BCDD
86
+ module Resultable
87
+ def Success: (Symbol type, ?untyped value) -> BCDD::Result::Success
88
+
89
+ def Failure: (Symbol type, ?untyped value) -> BCDD::Result::Failure
90
+ end
91
+ end
92
+
93
+ class BCDD::Result
94
+ class Type
95
+ attr_reader to_sym: Symbol
96
+
97
+ def initialize: (Symbol) -> void
98
+
99
+ def in?: (Array[Symbol], allow_empty: bool) -> bool
100
+ end
53
101
  end
54
102
 
55
103
  class BCDD::Result
56
104
  class Error < ::StandardError
105
+ def self.build: (**untyped) -> BCDD::Result::Error
106
+
57
107
  class NotImplemented < BCDD::Result::Error
58
108
  end
59
109
 
60
110
  class MissingTypeArgument < BCDD::Result::Error
61
111
  end
62
112
 
63
- class UnexpectedBlockOutcome < BCDD::Result::Error
113
+ class UnexpectedOutcome < BCDD::Result::Error
114
+ def self.build: (outcome: untyped, origin: Symbol)
115
+ -> BCDD::Result::Error::UnexpectedOutcome
116
+ end
117
+
118
+ class WrongResultSubject < BCDD::Result::Error
119
+ def self.build: (given_result: BCDD::Result, expected_subject: untyped)
120
+ -> BCDD::Result::Error::WrongResultSubject
121
+ end
122
+
123
+ class WrongSubjectMethodArity < BCDD::Result::Error
124
+ def self.build: (subject: untyped, method: ::Method)
125
+ -> BCDD::Result::Error::WrongSubjectMethodArity
64
126
  end
65
127
  end
66
128
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bcdd-result
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rodrigo Serradura
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-09-25 00:00:00.000000000 Z
11
+ date: 2023-09-27 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A general-purpose result monad that allows you to create objects that
14
14
  represent a success (BCDD::Result::Success) or failure (BCDD::Result::Failure).
@@ -29,8 +29,11 @@ files:
29
29
  - lib/bcdd/result.rb
30
30
  - lib/bcdd/result/error.rb
31
31
  - lib/bcdd/result/failure.rb
32
+ - lib/bcdd/result/handler.rb
32
33
  - lib/bcdd/result/success.rb
34
+ - lib/bcdd/result/type.rb
33
35
  - lib/bcdd/result/version.rb
36
+ - lib/bcdd/resultable.rb
34
37
  - sig/bcdd/result.rbs
35
38
  homepage: https://github.com/b-cdd/result
36
39
  licenses: