bcdd-result 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: