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 +4 -4
- data/.rubocop.yml +4 -0
- data/.rubocop_todo.yml +5 -3
- data/CHANGELOG.md +100 -0
- data/README.md +207 -23
- data/lib/bcdd/result/error.rb +26 -4
- data/lib/bcdd/result/handler.rb +48 -0
- data/lib/bcdd/result/type.rb +17 -0
- data/lib/bcdd/result/version.rb +1 -1
- data/lib/bcdd/result.rb +45 -13
- data/lib/bcdd/resultable.rb +11 -0
- data/sig/bcdd/result.rbs +70 -8
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 96c58d6a9132fbf42fc8f3c1d9ba683f0b6297e23b1f0536fc4bcb7efb5084df
|
|
4
|
+
data.tar.gz: fcef45f3c5ddacc20ff5e9533012c7fbe5817878cef1ff528dabd8920e1cbccd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d4cf048907571205c6cd0b90ca4b95c2480c34de79d082132e4f1cab1e809f4c6eec538d9d38b036ad313d9653420625b68a502272e73371b99b157f43315123
|
|
7
|
+
data.tar.gz: 4662aaeb33a7eb770ab51529cc1f2c4e9d86844dec70dfcdc4f37674aab33d010d6cbe7932fc635f09979d47b4b631748a3d4791021e99108a011a93469a2d73
|
data/.rubocop.yml
CHANGED
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-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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">⬆️ 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="
|
|
84
|
+
<p align="right">(<a href="#-bcddresult">⬆️ 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="
|
|
140
|
+
<p align="right">(<a href="#-bcddresult">⬆️ 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="
|
|
166
|
+
<p align="right">(<a href="#-bcddresult">⬆️ 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">⬆️ 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
|
-
|
|
221
|
+
*PS: The `divide()` implementation is [here](#result-hooks).*
|
|
222
|
+
|
|
223
|
+
<p align="right">(<a href="#-bcddresult">⬆️ 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
|
-
|
|
250
|
+
*PS: The `divide()` implementation is [here](#result-hooks).*
|
|
251
|
+
|
|
252
|
+
<p align="right">(<a href="#-bcddresult">⬆️ 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
|
-
|
|
272
|
+
*PS: The `divide()` implementation is [here](#result-hooks).*
|
|
273
|
+
|
|
274
|
+
<p align="right">(<a href="#-bcddresult">⬆️ 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">⬆️ back to top</a>)</p>
|
|
244
321
|
|
|
245
|
-
### Result
|
|
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
|
-
|
|
351
|
+
*PS: The `divide()` implementation is [here](#result-hooks).*
|
|
352
|
+
|
|
353
|
+
<p align="right">(<a href="#-bcddresult">⬆️ 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.
|
|
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,
|
|
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="
|
|
419
|
+
<p align="right">(<a href="#-bcddresult">⬆️ 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">⬆️ 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="
|
|
531
|
+
<p align="right">(<a href="#-bcddresult">⬆️ 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="
|
|
539
|
+
<p align="right">(<a href="#-bcddresult">⬆️ 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="
|
|
545
|
+
<p align="right">(<a href="#-bcddresult">⬆️ 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="
|
|
551
|
+
<p align="right">(<a href="#-bcddresult">⬆️ back to top</a>)</p>
|
|
368
552
|
|
|
369
553
|
## Code of Conduct
|
|
370
554
|
|
data/lib/bcdd/result/error.rb
CHANGED
|
@@ -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
|
|
15
|
-
def
|
|
18
|
+
class UnexpectedOutcome < self
|
|
19
|
+
def self.build(outcome:, origin:)
|
|
16
20
|
message =
|
|
17
|
-
"Unexpected outcome: #{
|
|
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
|
-
|
|
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
|
data/lib/bcdd/result/version.rb
CHANGED
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 :
|
|
13
|
+
attr_reader :_type, :value, :subject
|
|
14
|
+
|
|
15
|
+
protected :subject
|
|
10
16
|
|
|
11
|
-
def initialize(type:, value:)
|
|
12
|
-
@
|
|
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
|
|
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? &&
|
|
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? &&
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
72
|
-
|
|
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
|
|
76
|
-
|
|
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
|
|
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
|
|
38
|
-
def
|
|
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
|
|
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.
|
|
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-
|
|
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:
|