solid-result 2.0.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.
Files changed (119) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +98 -0
  3. data/.rubocop_todo.yml +12 -0
  4. data/CHANGELOG.md +600 -0
  5. data/CODE_OF_CONDUCT.md +84 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +2691 -0
  8. data/Rakefile +28 -0
  9. data/Steepfile +31 -0
  10. data/examples/multiple_listeners/Rakefile +55 -0
  11. data/examples/multiple_listeners/app/models/account/member.rb +10 -0
  12. data/examples/multiple_listeners/app/models/account/owner_creation.rb +62 -0
  13. data/examples/multiple_listeners/app/models/account.rb +11 -0
  14. data/examples/multiple_listeners/app/models/user/creation.rb +67 -0
  15. data/examples/multiple_listeners/app/models/user/token/creation.rb +51 -0
  16. data/examples/multiple_listeners/app/models/user/token.rb +7 -0
  17. data/examples/multiple_listeners/app/models/user.rb +15 -0
  18. data/examples/multiple_listeners/config/boot.rb +16 -0
  19. data/examples/multiple_listeners/config/initializers/solid_result.rb +9 -0
  20. data/examples/multiple_listeners/config.rb +27 -0
  21. data/examples/multiple_listeners/db/setup.rb +60 -0
  22. data/examples/multiple_listeners/lib/event_logs_listener/stdout.rb +60 -0
  23. data/examples/multiple_listeners/lib/runtime_breaker.rb +11 -0
  24. data/examples/multiple_listeners/lib/solid/result/event_logs_record.rb +27 -0
  25. data/examples/multiple_listeners/lib/solid/result/rollback_on_failure.rb +15 -0
  26. data/examples/service_objects/Rakefile +36 -0
  27. data/examples/service_objects/app/models/account/member.rb +10 -0
  28. data/examples/service_objects/app/models/account.rb +11 -0
  29. data/examples/service_objects/app/models/user/token.rb +7 -0
  30. data/examples/service_objects/app/models/user.rb +15 -0
  31. data/examples/service_objects/app/services/account/owner_creation.rb +47 -0
  32. data/examples/service_objects/app/services/application_service.rb +79 -0
  33. data/examples/service_objects/app/services/user/creation.rb +56 -0
  34. data/examples/service_objects/app/services/user/token/creation.rb +37 -0
  35. data/examples/service_objects/config/boot.rb +17 -0
  36. data/examples/service_objects/config/initializers/solid_result.rb +9 -0
  37. data/examples/service_objects/config.rb +20 -0
  38. data/examples/service_objects/db/setup.rb +49 -0
  39. data/examples/single_listener/Rakefile +92 -0
  40. data/examples/single_listener/app/models/account/member.rb +10 -0
  41. data/examples/single_listener/app/models/account/owner_creation.rb +62 -0
  42. data/examples/single_listener/app/models/account.rb +11 -0
  43. data/examples/single_listener/app/models/user/creation.rb +67 -0
  44. data/examples/single_listener/app/models/user/token/creation.rb +51 -0
  45. data/examples/single_listener/app/models/user/token.rb +7 -0
  46. data/examples/single_listener/app/models/user.rb +15 -0
  47. data/examples/single_listener/config/boot.rb +16 -0
  48. data/examples/single_listener/config/initializers/solid_result.rb +9 -0
  49. data/examples/single_listener/config.rb +23 -0
  50. data/examples/single_listener/db/setup.rb +49 -0
  51. data/examples/single_listener/lib/runtime_breaker.rb +11 -0
  52. data/examples/single_listener/lib/single_event_logs_listener.rb +117 -0
  53. data/examples/single_listener/lib/solid/result/rollback_on_failure.rb +15 -0
  54. data/lib/solid/failure.rb +23 -0
  55. data/lib/solid/output/callable_and_then.rb +40 -0
  56. data/lib/solid/output/expectations/mixin.rb +31 -0
  57. data/lib/solid/output/expectations.rb +25 -0
  58. data/lib/solid/output/failure.rb +9 -0
  59. data/lib/solid/output/mixin.rb +57 -0
  60. data/lib/solid/output/success.rb +37 -0
  61. data/lib/solid/output.rb +115 -0
  62. data/lib/solid/result/_self.rb +198 -0
  63. data/lib/solid/result/callable_and_then/caller.rb +49 -0
  64. data/lib/solid/result/callable_and_then/config.rb +15 -0
  65. data/lib/solid/result/callable_and_then/error.rb +11 -0
  66. data/lib/solid/result/callable_and_then.rb +9 -0
  67. data/lib/solid/result/config/options.rb +27 -0
  68. data/lib/solid/result/config/switcher.rb +82 -0
  69. data/lib/solid/result/config/switchers/addons.rb +25 -0
  70. data/lib/solid/result/config/switchers/constant_aliases.rb +33 -0
  71. data/lib/solid/result/config/switchers/features.rb +32 -0
  72. data/lib/solid/result/config/switchers/pattern_matching.rb +20 -0
  73. data/lib/solid/result/config.rb +64 -0
  74. data/lib/solid/result/contract/disabled.rb +25 -0
  75. data/lib/solid/result/contract/error.rb +17 -0
  76. data/lib/solid/result/contract/evaluator.rb +45 -0
  77. data/lib/solid/result/contract/for_types.rb +29 -0
  78. data/lib/solid/result/contract/for_types_and_values.rb +46 -0
  79. data/lib/solid/result/contract/interface.rb +21 -0
  80. data/lib/solid/result/contract/type_checker.rb +37 -0
  81. data/lib/solid/result/contract.rb +33 -0
  82. data/lib/solid/result/data.rb +33 -0
  83. data/lib/solid/result/error.rb +59 -0
  84. data/lib/solid/result/event_logs/config.rb +28 -0
  85. data/lib/solid/result/event_logs/listener.rb +51 -0
  86. data/lib/solid/result/event_logs/listeners.rb +87 -0
  87. data/lib/solid/result/event_logs/tracking/disabled.rb +15 -0
  88. data/lib/solid/result/event_logs/tracking/enabled.rb +161 -0
  89. data/lib/solid/result/event_logs/tracking.rb +26 -0
  90. data/lib/solid/result/event_logs/tree.rb +141 -0
  91. data/lib/solid/result/event_logs.rb +27 -0
  92. data/lib/solid/result/expectations/mixin.rb +58 -0
  93. data/lib/solid/result/expectations.rb +75 -0
  94. data/lib/solid/result/failure.rb +11 -0
  95. data/lib/solid/result/handler/allowed_types.rb +45 -0
  96. data/lib/solid/result/handler.rb +57 -0
  97. data/lib/solid/result/ignored_types.rb +14 -0
  98. data/lib/solid/result/mixin.rb +72 -0
  99. data/lib/solid/result/success.rb +11 -0
  100. data/lib/solid/result/version.rb +7 -0
  101. data/lib/solid/result.rb +27 -0
  102. data/lib/solid/success.rb +23 -0
  103. data/lib/solid-result.rb +3 -0
  104. data/sig/solid/failure.rbs +13 -0
  105. data/sig/solid/output.rbs +175 -0
  106. data/sig/solid/result/callable_and_then.rbs +60 -0
  107. data/sig/solid/result/config.rbs +102 -0
  108. data/sig/solid/result/contract.rbs +120 -0
  109. data/sig/solid/result/data.rbs +16 -0
  110. data/sig/solid/result/error.rbs +34 -0
  111. data/sig/solid/result/event_logs.rbs +189 -0
  112. data/sig/solid/result/expectations.rbs +71 -0
  113. data/sig/solid/result/handler.rbs +47 -0
  114. data/sig/solid/result/ignored_types.rbs +9 -0
  115. data/sig/solid/result/mixin.rbs +45 -0
  116. data/sig/solid/result/version.rbs +5 -0
  117. data/sig/solid/result.rbs +85 -0
  118. data/sig/solid/success.rbs +13 -0
  119. metadata +167 -0
data/README.md ADDED
@@ -0,0 +1,2691 @@
1
+ <p align="center">
2
+ <h1 align="center" id="-solidresult">🔀 Solid::Result</h1>
3
+ <p align="center"><i>Unleash a pragmatic and observable use of Result Pattern and Railway-Oriented Programming in Ruby.</i></p>
4
+ <p align="center">
5
+ <img src="https://img.shields.io/badge/Ruby%20%3E%3D%202.7%2C%20%3C%3D%20Head-ruby.svg?colorA=444&colorB=333" alt="Ruby">
6
+ <a href="https://rubygems.org/gems/solid-result"><img src="https://badge.fury.io/rb/solid-result.svg" alt="solid-result gem version" height="18"></a>
7
+ <a href="https://codeclimate.com/github/solid-process/solid-result/maintainability"><img src="https://api.codeclimate.com/v1/badges/aa8360f8f012d7dedd62/maintainability" /></a>
8
+ <a href="https://codeclimate.com/github/solid-process/solid-result/test_coverage"><img src="https://api.codeclimate.com/v1/badges/aa8360f8f012d7dedd62/test_coverage" /></a>
9
+ </p>
10
+ </p>
11
+
12
+ It's a general-purpose result monad that allows you to create objects representing a success (`Solid::Result::Success`) or failure (`Solid::Result::Failure`).
13
+
14
+ **What problem does it solve?**
15
+
16
+ It allows you to consistently represent the concept of success and failure throughout your codebase.
17
+
18
+ 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.
19
+
20
+ Use it to enable the [Railway Oriented Programming](https://fsharpforfunandprofit.com/rop/) pattern (superpower) in your code.
21
+
22
+ - [Supported Ruby](#supported-ruby)
23
+ - [Installation](#installation)
24
+ - [Usage](#usage)
25
+ - [`Solid::Result` *versus* `Result`](#solidresult-versus-result)
26
+ - [Reference](#reference)
27
+ - [Basic methods](#basic-methods)
28
+ - [Checking types with `result.is?` or `method missing`](#checking-types-with-resultis-or-method-missing)
29
+ - [Checking types with `result.success?` or `result.failure?`](#checking-types-with-resultsuccess-or-resultfailure)
30
+ - [Result Hooks](#result-hooks)
31
+ - [`result.on`](#resulton)
32
+ - [`result.on_type`](#resulton_type)
33
+ - [`result.on_success`](#resulton_success)
34
+ - [`result.on_failure`](#resulton_failure)
35
+ - [`result.on_unknown`](#resulton_unknown)
36
+ - [`result.handle`](#resulthandle)
37
+ - [Result Value](#result-value)
38
+ - [`result.value_or`](#resultvalue_or)
39
+ - [Result Data](#result-data)
40
+ - [`result.data`](#resultdata)
41
+ - [Railway Oriented Programming](#railway-oriented-programming)
42
+ - [`result.and_then`](#resultand_then)
43
+ - [`Solid::Result.mixin`](#solidresultmixin)
44
+ - [Class example (Instance Methods)](#class-example-instance-methods)
45
+ - [Module example (Singleton Methods)](#module-example-singleton-methods)
46
+ - [Important Requirement](#important-requirement)
47
+ - [Dependency Injection](#dependency-injection)
48
+ - [Add-ons](#add-ons)
49
+ - [`Solid::Result::Expectations`](#solidresultexpectations)
50
+ - [Standalone *versus* Mixin mode](#standalone-versus-mixin-mode)
51
+ - [Type checking - Result Hooks](#type-checking---result-hooks)
52
+ - [`#success?` and `#failure?`](#success-and-failure)
53
+ - [`#on` and `#on_type`](#on-and-on_type)
54
+ - [`#on_success` and `#on_failure`](#on_success-and-on_failure)
55
+ - [`#handle`](#handle)
56
+ - [Type checking - Result Creation](#type-checking---result-creation)
57
+ - [Mixin mode](#mixin-mode)
58
+ - [Standalone mode](#standalone-mode)
59
+ - [Value checking - Result Creation](#value-checking---result-creation)
60
+ - [Success()](#success)
61
+ - [Failure()](#failure)
62
+ - [Pattern Matching Support](#pattern-matching-support)
63
+ - [`Solid::Result::Expectations.mixin` add-ons](#solidresultexpectationsmixin-add-ons)
64
+ - [`Solid::Output`](#solidoutput)
65
+ - [Defining successes and failures](#defining-successes-and-failures)
66
+ - [Hash methods](#hash-methods)
67
+ - [`Solid::Output.mixin`](#solidoutputmixin)
68
+ - [Class example (Instance Methods)](#class-example-instance-methods-1)
69
+ - [`and_expose`](#and_expose)
70
+ - [Module example (Singleton Methods)](#module-example-singleton-methods-1)
71
+ - [`Solid::Output::Expectations`](#solidoutputexpectations)
72
+ - [Mixin add-ons](#mixin-add-ons)
73
+ - [Pattern Matching](#pattern-matching)
74
+ - [`Solid::Result`](#solidresult)
75
+ - [`Array`/`Find` patterns](#arrayfind-patterns)
76
+ - [`Hash` patterns](#hash-patterns)
77
+ - [`Solid::Output`](#solidoutput-1)
78
+ - [`Array`/`Find` patterns](#arrayfind-patterns-1)
79
+ - [`Hash` patterns](#hash-patterns-1)
80
+ - [How to pattern match without the concept of success and failure](#how-to-pattern-match-without-the-concept-of-success-and-failure)
81
+ - [`Solid::Result.event_logs`](#solidresultevent_logs)
82
+ - [`metadata: {ids:}`](#metadata-ids)
83
+ - [Configuration](#configuration)
84
+ - [Turning on/off](#turning-onoff)
85
+ - [Setting a `trace_id` fetcher](#setting-a-trace_id-fetcher)
86
+ - [Setting a `listener`](#setting-a-listener)
87
+ - [Setting multiple `listeners`](#setting-multiple-listeners)
88
+ - [`Solid::Result.configuration`](#solidresultconfiguration)
89
+ - [`Solid::Result.config`](#solidresultconfig)
90
+ - [`Solid::Result#and_then!`](#solidresultand_then)
91
+ - [Dependency Injection](#dependency-injection-1)
92
+ - [Configuration](#configuration-1)
93
+ - [Analysis: Why is `and_then!` an Anti-pattern?](#analysis-why-is-and_then-an-anti-pattern)
94
+ - [`#and_then` versus `#and_then!`](#and_then-versus-and_then)
95
+ - [Analysis: Why is `#and_then` the antidote/standard?](#analysis-why-is-and_then-the-antidotestandard)
96
+ - [About](#about)
97
+ - [Development](#development)
98
+ - [Contributing](#contributing)
99
+ - [License](#license)
100
+ - [Code of Conduct](#code-of-conduct)
101
+
102
+ ## Supported Ruby
103
+
104
+ This library is tested against:
105
+
106
+ Version | 2.7 | 3.0 | 3.1 | 3.2 | 3.3 | Head
107
+ ---- | --- | --- | --- | --- | --- | ---
108
+ 100% Coverage | ✅ | ✅ | ✅ | ✅ | ✅ | ✅
109
+
110
+ ## Installation
111
+
112
+ Add this line to your application's Gemfile:
113
+
114
+ ```ruby
115
+ gem 'solid-result'
116
+ ```
117
+
118
+ And then execute:
119
+
120
+ $ bundle install
121
+
122
+ If bundler is not being used to manage dependencies, install the gem by executing:
123
+
124
+ $ gem install solid-result
125
+
126
+ And require it in your code:
127
+
128
+ require 'solid/result'
129
+
130
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
131
+
132
+ ## Usage
133
+
134
+ To create a result, you must define a type (symbol) and its value (any kind of object). e.g.,
135
+
136
+ ```ruby
137
+ Solid::Result::Success(:ok, :1) #
138
+ # The value can be any kind of object
139
+ Solid::Result::Failure(:err, 'the value') #
140
+ ```
141
+
142
+ The reason for defining a `type` is that it is very common for a method/operation to return different types of successes or failures. Because of this, the `type` will always be required. e,g.,
143
+
144
+ ```ruby
145
+ Solid::Result::Success(:ok) #
146
+ # The type is mandatory and the value is optional
147
+ Solid::Result::Failure(:err) #
148
+ ```
149
+
150
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
151
+
152
+ #### `Solid::Result` *versus* `Result`
153
+
154
+ This gem provides a way to create constant aliases for `Solid::Result` and other classes/modules.
155
+
156
+ To enable it, you must call the `Solid::Result.configuration` method and pass a block to it. You can turn the aliases you want on/off in this block.
157
+
158
+ ```ruby
159
+ Solid::Result.configuration do |config|
160
+ config.constant_alias.enable!('Result')
161
+ end
162
+ ```
163
+
164
+ So, instead of using `Solid::Result` everywhere, you can use `Result` as an alias/shortcut.
165
+
166
+ ```ruby
167
+ Result::Success(:ok) # <Solid::Result::Success type=:ok value=nil>
168
+
169
+ Result::Failure(:err) # <Solid::Result::Failure type=:err value=nil>
170
+ ```
171
+
172
+ If you have enabled constant aliasing, all examples in this README that use `Solid::Result` can be implemented using `Result`.
173
+
174
+ There are other aliases and configurations available. Check the [Solid::Result.configuration]() section for more information.
175
+
176
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
177
+
178
+ ## Reference
179
+
180
+ ### Basic methods
181
+
182
+ Both `Solid::Result::Success` and `Solid::Result::Failure` are composed of the same methods. Look at the basic ones:
183
+
184
+ **Solid::Result::Success**
185
+
186
+ ```ruby
187
+ ################
188
+ # With a value #
189
+ ################
190
+ result = Solid::Result::Success(:ok, my: 'value')
191
+
192
+ result.success? # true
193
+ result.failure? # false
194
+ result.type?(:ok) # true
195
+ result.type # :ok
196
+ result.value # {:my => "value"}
197
+
198
+ ###################
199
+ # Without a value #
200
+ ###################
201
+ result = Solid::Result::Success(:yes)
202
+
203
+ result.success? # true
204
+ result.failure? # false
205
+ result.type?(:yes) # true
206
+ result.type # :yes
207
+ result.value # nil
208
+ ```
209
+
210
+ **Solid::Result::Failure**
211
+
212
+ ```ruby
213
+ ################
214
+ # With a value #
215
+ ################
216
+ result = Solid::Result::Failure(:err, 'my_value')
217
+
218
+ result.success? # false
219
+ result.failure? # true
220
+ result.type?(:err) # true
221
+ result.type # :err
222
+ result.value # "my_value"
223
+
224
+ ###################
225
+ # Without a value #
226
+ ###################
227
+ result = Solid::Result::Failure(:no)
228
+
229
+ result.success? # false
230
+ result.failure? # true
231
+ result.type?(:no) # true
232
+ result.type # :no
233
+ result.value # nil
234
+ ```
235
+
236
+ In both cases, the `type` must be a symbol, and the `value` can be any kind of object.
237
+
238
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
239
+
240
+ #### Checking types with `result.is?` or `method missing`
241
+
242
+ Beyond the `type?` method, you can also use the `is?` method to check the result type. If you want to check the type directly, you can write the type using a method that ends with a question mark.
243
+
244
+ ```ruby
245
+ result = Solid::Result::Success(:ok)
246
+
247
+ result.is?(:ok) # true
248
+ result.ok? # true
249
+
250
+ result = Solid::Result::Failure(:err)
251
+
252
+ result.is?(:err) # true
253
+ result.err? # true
254
+ ```
255
+
256
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
257
+
258
+ #### Checking types with `result.success?` or `result.failure?`
259
+
260
+ `Solid::Result#success?` and `Solid::Result#failure?` are methods that allow you to check if the result is a success or a failure.
261
+
262
+ You can also check the result type by passing an argument to it. For example, `result.success?(:ok)` will check if the result is a success and if the type is `:ok`.
263
+
264
+ ```ruby
265
+ result = Solid::Result::Success(:ok)
266
+
267
+ result.success?(:ok)
268
+
269
+ # This is the same as:
270
+
271
+ result.success? && result.type == :ok
272
+ ```
273
+
274
+ The same is valid for `Solid::Result#failure?`.
275
+
276
+ ```ruby
277
+ result = Solid::Result::Failure(:err)
278
+
279
+ result.failure?(:err)
280
+
281
+ # This is the same as:
282
+
283
+ result.failure? && result.type == :err
284
+ ```
285
+
286
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
287
+
288
+ ### Result Hooks
289
+
290
+ Result hooks are methods that allow you to execute a block of code based on the type of result obtained.
291
+ To demonstrate their use, I will implement a method that can divide two numbers.
292
+
293
+ ```ruby
294
+ def divide(arg1, arg2)
295
+ arg1.is_a?(::Numeric) or return Solid::Result::Failure(:invalid_arg, 'arg1 must be numeric')
296
+ arg2.is_a?(::Numeric) or return Solid::Result::Failure(:invalid_arg, 'arg2 must be numeric')
297
+
298
+ return Solid::Result::Failure(:division_by_zero, 'arg2 must not be zero') if arg2.zero?
299
+
300
+ Solid::Result::Success(:division_completed, arg1 / arg2)
301
+ end
302
+ ```
303
+
304
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
305
+
306
+ #### `result.on`
307
+
308
+ When you use `Solid::Result#on`, the block will be executed only when the type matches the result type.
309
+
310
+ However, even if the block is executed, the method will always return the result itself.
311
+
312
+ The value of the result will be available as the first argument of the block.
313
+
314
+ ```ruby
315
+ result = divide(nil, 2)
316
+ #<Solid::Result::Failure type=:invalid_arg data='arg1 must be numeric'>
317
+
318
+ output =
319
+ result
320
+ .on(:invalid_arg) { |msg| puts msg }
321
+ .on(:division_by_zero) { |msg| puts msg }
322
+ .on(:division_completed) { |number| puts number }
323
+
324
+ # The code above will print 'arg1 must be numeric' and return the result itself.
325
+
326
+ result.object_id == output.object_id # true
327
+ ```
328
+
329
+ You can define multiple types to be handled by the same hook/block
330
+ ```ruby
331
+ result = divide(4, 0)
332
+
333
+ output =
334
+ result.on(:invalid_arg, :division_by_zero, :division_completed) { |value| puts value }
335
+
336
+ # The code above will print 'arg2 must not be zero' and return the result itself.
337
+
338
+ result.object_id == output.object_id # true
339
+ ```
340
+
341
+ *PS: The `divide()` implementation is [here](#result-hooks).*
342
+
343
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
344
+
345
+ #### `result.on_type`
346
+
347
+ `Solid::Result#on_type` is an alias of `Solid::Result#on`.
348
+
349
+ ```ruby
350
+ result = divide(nil, 2)
351
+ #<Solid::Result::Failure type=:invalid_arg data='arg1 must be numeric'>
352
+
353
+ output =
354
+ result
355
+ .on_type(:invalid_arg, :division_by_zero) { |msg| puts msg }
356
+ .on_type(:division_completed) { |number| puts number }
357
+
358
+ # The code above will print 'arg1 must be numeric' and return the result itself.
359
+
360
+ result.object_id == output.object_id # true
361
+ ```
362
+
363
+ *PS: The `divide()` implementation is [here](#result-hooks).*
364
+
365
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
366
+
367
+ #### `result.on_success`
368
+
369
+ The `Solid::Result#on_success` method is quite similar to the `Solid::Result#on` hook, but with a few key differences:
370
+
371
+ 1. It will only execute the block of code if the result is a success.
372
+ 2. If the type declaration is not included, the method will execute the block for any successful result, regardless of its type.
373
+
374
+ ```ruby
375
+ # In both examples, it executes the block and returns the result itself.
376
+
377
+ divide(4, 2).on_success { |number| puts number }
378
+
379
+ divide(4, 2).on_success(:division_completed) { |number| puts number }
380
+
381
+ # It doesn't execute the block as the type is different.
382
+
383
+ divide(4, 4).on_success(:ok) { |value| puts value }
384
+
385
+ # It doesn't execute the block, as the result is a success, but the hook expects a failure.
386
+
387
+ divide(4, 4).on_failure { |error| puts error }
388
+ ```
389
+
390
+ *PS: The `divide()` implementation is [here](#result-hooks).*
391
+
392
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
393
+
394
+ #### `result.on_failure`
395
+
396
+ It is the opposite of `Result#on_success`:
397
+
398
+ 1. It will only execute the block of code if the result is a failure.
399
+ 2. If the type declaration is not included, the method will execute the block for any failed result, regardless of its type.
400
+
401
+ ```ruby
402
+ # In both examples, it executes the block and returns the result itself.
403
+
404
+ divide(nil, 2).on_failure { |error| puts error }
405
+
406
+ divide(4, 0).on_failure(:division_by_zero) { |error| puts error }
407
+
408
+ # It doesn't execute the block as the type is different.
409
+
410
+ divide(4, 0).on_failure(:invalid_arg) { |error| puts error }
411
+
412
+ # It doesn't execute the block, as the result is a failure, but the hook expects a success.
413
+
414
+ divide(4, 0).on_success { |number| puts number }
415
+ ```
416
+
417
+ *PS: The `divide()` implementation is [here](#result-hooks).*
418
+
419
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
420
+
421
+ #### `result.on_unknown`
422
+
423
+ `Solid::Result#on_unknown` will execute the block when no other hook (`#on`, `#on_type`, `#on_failure`, `#on_success`) has been executed.
424
+
425
+ Regardless of the block being executed, the method will always return the result itself.
426
+
427
+ The value of the result will be available as the first argument of the block.
428
+
429
+ ```ruby
430
+ divide(4, 2)
431
+ .on(:invalid_arg) { |msg| puts msg }
432
+ .on(:division_by_zero) { |msg| puts msg }
433
+ .on_unknown { |value, type| puts [type, value].inspect }
434
+
435
+ # The code above will print '[:division_completed, 2]' and return the result itself.
436
+ ```
437
+
438
+ *PS: The `divide()` implementation is [here](#result-hooks).*
439
+
440
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
441
+
442
+ #### `result.handle`
443
+
444
+ This method lets you define blocks for each hook (type, failure, or success), but instead of returning itself, it will return the output of the first match/block execution.
445
+
446
+ ```ruby
447
+ divide(4, 2).handle do |result|
448
+ result.success { |number| number }
449
+ result.failure(:invalid_arg) { |err| puts err }
450
+ result.type(:division_by_zero) { raise ZeroDivisionError }
451
+ result.unknown { raise NotImplementedError }
452
+ end
453
+
454
+ #or
455
+
456
+ divide(4, 2).handle do |on|
457
+ on.success { |number| number }
458
+ on.failure { |err| puts err }
459
+ on.unknown { raise NotImplementedError }
460
+ end
461
+
462
+ #or
463
+
464
+ divide(4, 2).handle do |on|
465
+ on.type(:invalid_arg) { |err| puts err }
466
+ on.type(:division_by_zero) { raise ZeroDivisionError }
467
+ on.type(:division_completed) { |number| number }
468
+ on.unknown { raise NotImplementedError }
469
+ end
470
+
471
+ # or
472
+
473
+ divide(4, 2).handle do |on|
474
+ on[:invalid_arg] { |err| puts err }
475
+ on[:division_by_zero] { raise ZeroDivisionError }
476
+ on[:division_completed] { |number| number }
477
+ on.unknown { raise NotImplementedError }
478
+ end
479
+
480
+ # The [] syntax 👆 is an alias of #type.
481
+ ```
482
+
483
+ **Notes:**
484
+ * You can define multiple types to be handled by the same hook/block
485
+ * If the type is missing, it will execute the block for any success or failure handler.
486
+ * The `#type` and `#[]` handlers require at least one type/argument.
487
+
488
+ *PS: The `divide()` implementation is [here](#result-hooks).*
489
+
490
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
491
+
492
+ ### Result Value
493
+
494
+ To access the result value, you can simply call `Solid::Result#value`.
495
+
496
+ However, there may be instances where you need to retrieve the value of a successful result or a default value if the result is a failure. In such cases, you can make use of `Solid::Result#value_or`.
497
+
498
+ #### `result.value_or`
499
+
500
+ `BCCD::Result#value_or` returns the value when the result is a success. However, if it is a failure, the given block will be executed, and its outcome will be returned.
501
+
502
+ ```ruby
503
+ def divide(arg1, arg2)
504
+ arg1.is_a?(::Numeric) or return Solid::Result::Failure(:invalid_arg)
505
+ arg2.is_a?(::Numeric) or return Solid::Result::Failure(:invalid_arg)
506
+
507
+ return Solid::Result::Failure(:division_by_zero) if arg2.zero?
508
+
509
+ Solid::Result::Success(:division_completed, arg1 / arg2)
510
+ end
511
+
512
+ # When the result is success
513
+ divide(4, 2).value_or { 0 } # 2
514
+
515
+ # When the result is failure
516
+ divide('4', 2).value_or { 0 } # 0
517
+ divide(4, '2').value_or { 0 } # 0
518
+ divide(100, 0).value_or { 0 } # 0
519
+ ```
520
+
521
+ *PS: The `divide()` implementation is [here](#result-hooks).*
522
+
523
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
524
+
525
+ ### Result Data
526
+
527
+ #### `result.data`
528
+
529
+ The `Solid::Result#data` exposes the result attributes (kind, type, value) directly and as a hash (`to_h`/`to_hash`) and array (`to_a`/`to_ary`).
530
+
531
+ This is helpful if you need to access the result attributes generically or want to use Ruby features like splat (`*`) and double splat (`**`) operators.
532
+
533
+ See the examples below to understand how to use it.
534
+
535
+ ```ruby
536
+ result = Solid::Result::Success(:ok, 1)
537
+
538
+ success_data = result.data # #<Solid::Result::Data kind=:success type=:ok value=1>
539
+
540
+ success_data.kind # :success
541
+ success_data.type # :ok
542
+ success_data.value # 1
543
+
544
+ success_data.to_h # {:kind=>:success, :type=>:ok, :value=>1}
545
+ success_data.to_a # [:success, :ok, 1]
546
+
547
+ kind, type, value = success_data
548
+
549
+ [kind, type, value] # [:success, :ok, 1]
550
+
551
+ def print_to_ary(kind, type, value)
552
+ puts [kind, type, value].inspect
553
+ end
554
+
555
+ def print_to_hash(kind:, type:, value:)
556
+ puts [kind, type, value].inspect
557
+ end
558
+
559
+ print_to_ary(*success_data) # [:success, :ok, 1]
560
+
561
+ print_to_hash(**success_data) # [:success, :ok, 1]
562
+ ```
563
+
564
+ > **NOTE:** The example above uses a success result, but the same is valid for a failure result.
565
+
566
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
567
+
568
+ ### Railway Oriented Programming
569
+
570
+ ["Railway Oriented Programming (ROP)"](https://fsharpforfunandprofit.com/rop/) is a programming technique that involves linking blocks together to form a sequence of operations, also known as a pipeline.
571
+ If a failure occurs in any of the blocks, the pipeline is interrupted and subsequent blocks are skipped.
572
+
573
+ The ROP technique allows you to structure your code in a way that expresses your logic as a series of operations, with the added benefit of stopping the process at the first detection of failure.
574
+
575
+ If all blocks successfully execute, the final result of the pipeline will be a success.
576
+
577
+ #### `result.and_then`
578
+
579
+ ```ruby
580
+ module Divide
581
+ extend self
582
+
583
+ def call(arg1, arg2)
584
+ validate_numbers(arg1, arg2)
585
+ .and_then { |numbers| validate_nonzero(numbers) }
586
+ .and_then { |numbers| divide(numbers) }
587
+ end
588
+
589
+ private
590
+
591
+ def validate_numbers(arg1, arg2)
592
+ arg1.is_a?(::Numeric) or return Solid::Result::Failure(:invalid_arg, 'arg1 must be numeric')
593
+ arg2.is_a?(::Numeric) or return Solid::Result::Failure(:invalid_arg, 'arg2 must be numeric')
594
+
595
+ Solid::Result::Success(:ok, [arg1, arg2])
596
+ end
597
+
598
+ def validate_nonzero(numbers)
599
+ return Solid::Result::Success(:ok, numbers) if numbers.last.nonzero?
600
+
601
+ Solid::Result::Failure(:division_by_zero, 'arg2 must not be zero')
602
+ end
603
+
604
+ def divide((number1, number2))
605
+ Solid::Result::Success(:division_completed, number1 / number2)
606
+ end
607
+ end
608
+ ```
609
+
610
+ Example of outputs:
611
+
612
+ ```ruby
613
+ Divide.call('4', 2)
614
+ #<Solid::Result::Failure type=:invalid_arg data="arg1 must be numeric">
615
+
616
+ Divide.call(2, '2')
617
+ #<Solid::Result::Failure type=:invalid_arg data="arg2 must be numeric">
618
+
619
+ Divide.call(2, 0)
620
+ #<Solid::Result::Failure type=:division_by_zero data="arg2 must not be zero">
621
+
622
+ Divide.call(2, 2)
623
+ #<Solid::Result::Success type=:division_completed data=1>
624
+ ```
625
+
626
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
627
+
628
+ #### `Solid::Result.mixin`
629
+
630
+ This method generates a module that any object can include or extend. It adds two methods to the target object: `Success()` and `Failure()`.
631
+
632
+ The main difference between these methods and `Solid::Result::Success()`/`Solid::Result::Failure()` is that the former will utilize the target object (which has received the include/extend) as the result's source.
633
+
634
+ Because the result has a source, the `#and_then` method can call methods from it.
635
+
636
+ ##### Class example (Instance Methods)
637
+
638
+ ```ruby
639
+ class Divide
640
+ include Solid::Result.mixin
641
+
642
+ attr_reader :arg1, :arg2
643
+
644
+ def initialize(arg1, arg2)
645
+ @arg1 = arg1
646
+ @arg2 = arg2
647
+ end
648
+
649
+ def call
650
+ validate_numbers
651
+ .and_then(:validate_nonzero)
652
+ .and_then(:divide)
653
+ end
654
+
655
+ private
656
+
657
+ def validate_numbers
658
+ arg1.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg1 must be numeric')
659
+ arg2.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg2 must be numeric')
660
+
661
+ # As arg1 and arg2 are instance methods, they will be available in the instance scope.
662
+ # So, in this case, I'm passing them as an array to show how the next method can receive the value as its argument.
663
+ Success(:ok, [arg1, arg2])
664
+ end
665
+
666
+ def validate_nonzero(numbers)
667
+ return Success(:ok, numbers) unless numbers.last.zero?
668
+
669
+ Failure(:division_by_zero, 'arg2 must not be zero')
670
+ end
671
+
672
+ def divide((number1, number2))
673
+ Success(:division_completed, number1 / number2)
674
+ end
675
+ end
676
+
677
+ Divide.new(4, 2).call #<Solid::Result::Success type=:division_completed value=2>
678
+
679
+ Divide.new(4, 0).call #<Solid::Result::Failure type=:division_by_zero value="arg2 must not be zero">
680
+ Divide.new('4', 2).call #<Solid::Result::Failure type=:invalid_arg value="arg1 must be numeric">
681
+ Divide.new(4, '2').call #<Solid::Result::Failure type=:invalid_arg value="arg2 must be numeric">
682
+ ```
683
+
684
+ ##### Module example (Singleton Methods)
685
+
686
+ ```ruby
687
+ module Divide
688
+ extend self, Solid::Result.mixin
689
+
690
+ def call(arg1, arg2)
691
+ validate_numbers(arg1, arg2)
692
+ .and_then(:validate_nonzero)
693
+ .and_then(:divide)
694
+ end
695
+
696
+ private
697
+
698
+ def validate_numbers(arg1, arg2)
699
+ arg1.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg1 must be numeric')
700
+ arg2.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg2 must be numeric')
701
+
702
+ Success(:ok, [arg1, arg2])
703
+ end
704
+
705
+ def validate_nonzero(numbers)
706
+ return Success(:ok, numbers) unless numbers.last.zero?
707
+
708
+ Failure(:division_by_zero, 'arg2 must not be zero')
709
+ end
710
+
711
+ def divide((number1, number2))
712
+ Success(:division_completed, number1 / number2)
713
+ end
714
+ end
715
+
716
+ Divide.call(4, 2) #<Solid::Result::Success type=:division_completed value=2>
717
+
718
+ Divide.call(4, 0) #<Solid::Result::Failure type=:division_by_zero value="arg2 must not be zero">
719
+ Divide.call('4', 2) #<Solid::Result::Failure type=:invalid_arg value="arg1 must be numeric">
720
+ Divide.call(4, '2') #<Solid::Result::Failure type=:invalid_arg value="arg2 must be numeric">
721
+ ```
722
+
723
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
724
+
725
+ ##### Important Requirement
726
+
727
+ To use the `#and_then` method to call methods, they must use `Success()` and `Failure()` to produce the results.
728
+
729
+ If you try to use `Solid::Result::Success()`/`Solid::Result::Failure()`, or results from another `Solid::Result.mixin` instance with `#and_then`, it will raise an error because the sources are different.
730
+
731
+ **Note:** You can still use the block syntax, but all the results must be produced by the source's `Success()` and `Failure()` methods.
732
+
733
+ ```ruby
734
+ module ValidateNonzero
735
+ extend self, Solid::Result.mixin
736
+
737
+ def call(numbers)
738
+ return Success(:ok, numbers) unless numbers.last.zero?
739
+
740
+ Failure(:division_by_zero, 'arg2 must not be zero')
741
+ end
742
+ end
743
+
744
+ module Divide
745
+ extend self, Solid::Result.mixin
746
+
747
+ def call(arg1, arg2)
748
+ validate_numbers(arg1, arg2)
749
+ .and_then(:validate_nonzero)
750
+ .and_then(:divide)
751
+ end
752
+
753
+ private
754
+
755
+ def validate_numbers(arg1, arg2)
756
+ arg1.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg1 must be numeric')
757
+ arg2.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg2 must be numeric')
758
+
759
+ Success(:ok, [arg1, arg2])
760
+ end
761
+
762
+ def validate_nonzero(numbers)
763
+ ValidateNonzero.call(numbers) # This will raise an error
764
+ end
765
+
766
+ def divide((number1, number2))
767
+ Success(:division_completed, number1 / number2)
768
+ end
769
+ end
770
+ ```
771
+
772
+ Look at the error produced by the code above:
773
+
774
+ ```ruby
775
+ Divide.call(2, 0)
776
+
777
+ # You cannot call #and_then and return a result that does not belong to the same source! (Solid::Result::Error::InvalidResultSource)
778
+ # Expected source: Divide
779
+ # Given source: ValidateNonzero
780
+ # Given result: #<Solid::Result::Failure type=:division_by_zero value="arg2 must not be zero">
781
+ ```
782
+
783
+ In order to fix this, you must handle the result produced by `ValidateNonzero.call()` and return a result that belongs to the same source.
784
+
785
+ ```ruby
786
+ module ValidateNonzero
787
+ extend self, Solid::Result.mixin
788
+
789
+ def call(numbers)
790
+ return Success(:ok, numbers) unless numbers.last.zero?
791
+
792
+ Failure(:division_by_zero, 'arg2 must not be zero')
793
+ end
794
+ end
795
+
796
+ module Divide
797
+ extend self, Solid::Result.mixin
798
+
799
+ def call(arg1, arg2)
800
+ validate_numbers(arg1, arg2)
801
+ .and_then(:validate_nonzero)
802
+ .and_then(:divide)
803
+ end
804
+
805
+ private
806
+
807
+ def validate_numbers(arg1, arg2)
808
+ arg1.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg1 must be numeric')
809
+ arg2.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg2 must be numeric')
810
+
811
+ Success(:ok, [arg1, arg2])
812
+ end
813
+
814
+ def validate_nonzero(numbers)
815
+ # In this case we are handling the result from other source
816
+ # and returning our own
817
+ ValidateNonzero.call(numbers).handle do |on|
818
+ on.success { |numbers| Success(:ok, numbers) }
819
+
820
+ on.failure { |err| Failure(:division_by_zero, err) }
821
+ end
822
+ end
823
+
824
+ def divide((number1, number2))
825
+ Success(:division_completed, number1 / number2)
826
+ end
827
+ end
828
+ ```
829
+
830
+ Look at the output of the code above:
831
+
832
+ ```ruby
833
+ Divide.call(2, 0)
834
+
835
+ #<Solid::Result::Failure type=:division_by_zero value="arg2 must not be zero">
836
+ ```
837
+
838
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
839
+
840
+ ##### Dependency Injection
841
+
842
+ The `Solid::Result#and_then` accepts a second argument that will be used to share a value with the source's method.
843
+ To receive this argument, the source's method must have an arity of two, where the first argument will be the result value and the second will be the injected value.
844
+
845
+ ```ruby
846
+ require 'logger'
847
+
848
+ module Divide
849
+ extend self, Solid::Result.mixin
850
+
851
+ def call(arg1, arg2, logger: ::Logger.new(STDOUT))
852
+ validate_numbers(arg1, arg2)
853
+ .and_then(:validate_nonzero, logger)
854
+ .and_then(:divide, logger)
855
+ end
856
+
857
+ private
858
+
859
+ def validate_numbers(arg1, arg2)
860
+ arg1.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg1 must be numeric')
861
+ arg2.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg2 must be numeric')
862
+
863
+ Success(:ok, [arg1, arg2])
864
+ end
865
+
866
+ def validate_nonzero(numbers, logger)
867
+ if numbers.last.zero?
868
+ logger.error('arg2 must not be zero')
869
+
870
+ Failure(:division_by_zero, 'arg2 must not be zero')
871
+ else
872
+ logger.info('The numbers are valid')
873
+
874
+ Success(:ok, numbers)
875
+ end
876
+ end
877
+
878
+ def divide((number1, number2), logger)
879
+ division = number1 / number2
880
+
881
+ logger.info("The division result is #{division}")
882
+
883
+ Success(:division_completed, division)
884
+ end
885
+ end
886
+
887
+ Divide.call(4, 2)
888
+ # I, [2023-10-11T00:08:05.546237 #18139] INFO -- : The numbers are valid
889
+ # I, [2023-10-11T00:08:05.546337 #18139] INFO -- : The division result is 2
890
+ #=> #<Solid::Result::Success type=:division_completed value=2>
891
+
892
+ Divide.call(4, 2, logger: Logger.new(IO::NULL))
893
+ #=> #<Solid::Result::Success type=:division_completed value=2>
894
+ ```
895
+
896
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
897
+
898
+ ##### Add-ons
899
+
900
+ The `Solid::Result.mixin` also accepts the `config:` argument. It is a hash that will be used to define custom behaviors for the mixin.
901
+
902
+ **given**
903
+
904
+ This addon is enabled by default. It will create the `Given(value)` method. Use it to add a value to the result chain and invoke the next step (through `and_then`).
905
+
906
+ You can turn it off by passing `given: false` to the `config:` argument or using the `Solid::Result.configuration`.
907
+
908
+ **continue**
909
+
910
+ This addon will create the `Continue(value)` method and change the `Success()` behavior to terminate the step chain.
911
+
912
+ So, if you want to advance to the next step, you must use `Continue(value)` instead of `Success(type, value)`. Otherwise, the step chain will be terminated.
913
+
914
+ In this example below, the `validate_nonzero` will return a `Success(:division_completed, 0)` and terminate the chain if the first number is zero.
915
+
916
+ ```ruby
917
+ module Divide
918
+ extend self, Solid::Result.mixin(config: { addon: { continue: true } })
919
+
920
+ def call(arg1, arg2)
921
+ Given([arg1, arg2])
922
+ .and_then(:validate_numbers)
923
+ .and_then(:validate_nonzero)
924
+ .and_then(:divide)
925
+ end
926
+
927
+ private
928
+
929
+ def validate_numbers(numbers)
930
+ number1, number2 = numbers
931
+
932
+ number1.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg1 must be numeric')
933
+ number2.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg2 must be numeric')
934
+
935
+ Continue(numbers)
936
+ end
937
+
938
+ def validate_nonzero(numbers)
939
+ return Failure(:division_by_zero, 'arg2 must not be zero') if numbers.last.zero?
940
+
941
+ return Success(:division_completed, 0) if numbers.first.zero?
942
+
943
+ Continue(numbers)
944
+ end
945
+
946
+ def divide((number1, number2))
947
+ Success(:division_completed, number1 / number2)
948
+ end
949
+ end
950
+ ```
951
+
952
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
953
+
954
+ ### `Solid::Result::Expectations`
955
+
956
+ This feature lets you define contracts for your results' types and values. There are two ways to use it: the standalone (`Solid::Result::Expectations.new`) and the mixin (`Solid::Result::Expectations.mixin`) mode.
957
+
958
+ It was designed to ensure all the aspects of the result's type and value. So, an error will be raised if you try to create or handle a result with an unexpected type or value.
959
+
960
+ #### Standalone *versus* Mixin mode
961
+
962
+ The _**standalone mode**_ creates an object that knows how to produce and validate results based on the defined expectations. Look at the example below:
963
+
964
+ ```ruby
965
+ module Divide
966
+ Result = Solid::Result::Expectations.new(
967
+ success: %i[numbers division_completed],
968
+ failure: %i[invalid_arg division_by_zero]
969
+ )
970
+
971
+ def self.call(arg1, arg2)
972
+ arg1.is_a?(Numeric) or return Result::Failure(:invalid_arg, 'arg1 must be numeric')
973
+ arg2.is_a?(Numeric) or return Result::Failure(:invalid_arg, 'arg2 must be numeric')
974
+
975
+ arg2.zero? and return Result::Failure(:division_by_zero, 'arg2 must not be zero')
976
+
977
+ Result::Success(:division_completed, arg1 / arg2)
978
+ end
979
+ end
980
+ ```
981
+
982
+ In the code above, we define a constant `Divide::Result`. And because of this (it is a constant), we can use it inside and outside the module.
983
+
984
+ Look what happens if you try to create a result without one of the expected types.
985
+
986
+ ```ruby
987
+ Divide::Result::Success(:ok)
988
+ # type :ok is not allowed. Allowed types: :numbers, :division_completed
989
+ # (Solid::Result::Contract::Error::UnexpectedType)
990
+
991
+ Divide::Result::Failure(:err)
992
+ # type :err is not allowed. Allowed types: :invalid_arg, :division_by_zero
993
+ # (Solid::Result::Contract::Error::UnexpectedType)
994
+ ```
995
+
996
+ The _**mixin mode**_ is similar to `Solid::Result::Mixin`, but it also defines the expectations for the result's types and values.
997
+
998
+ ```ruby
999
+ class Divide
1000
+ include Solid::Result::Expectations.mixin(
1001
+ success: %i[numbers division_completed],
1002
+ failure: %i[invalid_arg division_by_zero]
1003
+ )
1004
+
1005
+ def call(arg1, arg2)
1006
+ validate_numbers(arg1, arg2)
1007
+ .and_then(:validate_nonzero)
1008
+ .and_then(:divide)
1009
+ end
1010
+
1011
+ private
1012
+
1013
+ def validate_numbers(arg1, arg2)
1014
+ arg1.is_a?(Numeric) or return Failure(:invalid_arg, 'arg1 must be numeric')
1015
+ arg2.is_a?(Numeric) or return Failure(:invalid_arg, 'arg2 must be numeric')
1016
+
1017
+ Success(:numbers, [arg1, arg2])
1018
+ end
1019
+
1020
+ def validate_nonzero(numbers)
1021
+ return Success(:numbers, numbers) unless numbers.last.zero?
1022
+
1023
+ Failure(:division_by_zero, 'arg2 must not be zero')
1024
+ end
1025
+
1026
+ def divide((number1, number2))
1027
+ Success(:division_completed, number1 / number2)
1028
+ end
1029
+ end
1030
+ ```
1031
+
1032
+ This mode also defines an `Result` constant to be used inside and outside the module.
1033
+
1034
+ > **PROTIP:**
1035
+ > You can use the `Result` constant to mock the result's type and value in your tests. As they will have the exact expectations, your tests will check if the result clients are handling the result correctly.
1036
+
1037
+ Now that you know the two modes, let's understand how expectations can be beneficial and powerful for defining contracts.
1038
+
1039
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
1040
+
1041
+ #### Type checking - Result Hooks
1042
+
1043
+ The `Solid::Result::Expectations` will check if the type of the result is valid. This checking will be performed in all methods that depend on the result’s type, such as `#success?`, `#failure?`, `#on`, `#on_type`, `#on_success`, `#on_failure`, and `#handle`.
1044
+
1045
+ ##### `#success?` and `#failure?`
1046
+
1047
+ When checking whether a result is a success or failure, `Solid::Result::Expectations` will also verify if the result type is valid/expected. In case of an invalid type, an error will be raised.
1048
+
1049
+ **Success example:**
1050
+
1051
+ ```ruby
1052
+ result = Divide.new.call(10, 2)
1053
+
1054
+ result.success? # true
1055
+ result.success?(:numbers) # false
1056
+ result.success?(:division_completed) # true
1057
+
1058
+ result.success?(:ok)
1059
+ # type :ok is not allowed. Allowed types: :numbers, :division_completed
1060
+ # (Solid::Result::Contract::Error::UnexpectedType)
1061
+ ```
1062
+
1063
+ **Failure example:**
1064
+
1065
+ ```ruby
1066
+ result = Divide.new.call(10, '2')
1067
+
1068
+ result.failure? # true
1069
+ result.failure?(:invalid_arg) # true
1070
+ result.failure?(:division_by_zero) # false
1071
+
1072
+ result.failure?(:err)
1073
+ # type :err is not allowed. Allowed types: :invalid_arg, :division_by_zero
1074
+ # (Solid::Result::Contract::Error::UnexpectedType)
1075
+ ```
1076
+
1077
+ *PS: The `Divide` implementation is [here](#standalone-versus-mixin-mode).*
1078
+
1079
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
1080
+
1081
+ ##### `#on` and `#on_type`
1082
+
1083
+ If you use `#on` or `#on_type` to execute a block, `Solid::Result::Expectations` will check whether the result type is valid/expected. Otherwise, an error will be raised.
1084
+
1085
+ ```ruby
1086
+ result = Divide.new.call(10, 2)
1087
+
1088
+ result
1089
+ .on(:invalid_arg, :division_by_zero) { |msg| puts msg }
1090
+ .on(:division_completed) { |number| puts "The result is #{number}" }
1091
+
1092
+ # The code above will print 'The result is 5'
1093
+
1094
+ result.on(:number) { |_| :this_type_does_not_exist }
1095
+ # type :number is not allowed. Allowed types: :numbers, :division_completed, :invalid_arg, :division_by_zero
1096
+ # (Solid::Result::Contract::Error::UnexpectedType)
1097
+ ```
1098
+
1099
+ *PS: The `Divide` implementation is [here](#standalone-versus-mixin-mode).*
1100
+
1101
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
1102
+
1103
+ ##### `#on_success` and `#on_failure`
1104
+
1105
+ If you use `#on_success` or `#on_failure` to execute a block, `Solid::Result::Expectations` will check whether the result type is valid/expected. Otherwise, an error will be raised.
1106
+
1107
+ ```ruby
1108
+ result = Divide.new.call(10, '2')
1109
+
1110
+ result
1111
+ .on_failure(:invalid_arg, :division_by_zero) { |msg| puts msg }
1112
+ .on_success(:division_completed) { |number| puts "The result is #{number}" }
1113
+
1114
+ result
1115
+ .on_success { |number| puts "The result is #{number}" }
1116
+ .on_failure { |msg| puts msg }
1117
+
1118
+ # Both codes above will print 'arg2 must be numeric'
1119
+
1120
+ result.on_success(:ok) { |_| :this_type_does_not_exist }
1121
+ # type :ok is not allowed. Allowed types: :numbers, :division_completed
1122
+ # (Solid::Result::Contract::Error::UnexpectedType)
1123
+
1124
+ result.on_failure(:err) { |_| :this_type_does_not_exist }
1125
+ # type :err is not allowed. Allowed types: :invalid_arg, :division_by_zero
1126
+ # (Solid::Result::Contract::Error::UnexpectedType)
1127
+ ```
1128
+
1129
+ *PS: The `Divide` implementation is [here](#standalone-versus-mixin-mode).*
1130
+
1131
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
1132
+
1133
+ ##### `#handle`
1134
+
1135
+ The `Solid::Result::Expectations` will also be applied on all the handlers defined by the `#handle` method/block.
1136
+
1137
+ ```ruby
1138
+ result = Divide.call(10, 2)
1139
+
1140
+ result.handle do |on|
1141
+ on.type(:ok) { |_| :this_type_does_not_exist }
1142
+ end
1143
+ # type :ok is not allowed. Allowed types: :numbers, :division_completed, :invalid_arg, :division_by_zero (Solid::Result::Contract::Error::UnexpectedType)
1144
+
1145
+ result.handle do |on|
1146
+ on.success(:ok) { |_| :this_type_does_not_exist }
1147
+ end
1148
+ # type :ok is not allowed. Allowed types: :numbers, :division_completed (Solid::Result::Contract::Error::UnexpectedType)
1149
+
1150
+ result.handle do |on|
1151
+ on.failure(:err) { |_| :this_type_does_not_exist }
1152
+ end
1153
+ # type :err is not allowed. Allowed types: :invalid_arg, :division_by_zero (Solid::Result::Contract::Error::UnexpectedType)
1154
+ ```
1155
+
1156
+ *PS: The `Divide` implementation is [here](#standalone-versus-mixin-mode).*
1157
+
1158
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
1159
+
1160
+ #### Type checking - Result Creation
1161
+
1162
+ The `Solid::Result::Expectations` will be used on the result creation `Success()` and `Failure()` methods. So, when the result type is valid/expected, the result will be created. Otherwise, an error will be raised.
1163
+
1164
+ This works for both modes (standalone and mixin).
1165
+
1166
+ ##### Mixin mode
1167
+
1168
+ ```ruby
1169
+ module Divide
1170
+ extend Solid::Result::Expectations.mixin(success: :ok, failure: :err)
1171
+
1172
+ def self.call(arg1, arg2)
1173
+ arg1.is_a?(Numeric) or return Failure(:invalid_arg, 'arg1 must be numeric')
1174
+ arg2.is_a?(Numeric) or return Failure(:invalid_arg, 'arg2 must be numeric')
1175
+
1176
+ arg2.zero? and return Failure(:division_by_zero, 'arg2 must not be zero')
1177
+
1178
+ Success(:division_completed, arg1 / arg2)
1179
+ end
1180
+ end
1181
+
1182
+ Divide.call('4', 2)
1183
+ # type :invalid_arg is not allowed. Allowed types: :err
1184
+ # (Solid::Result::Contract::Error::UnexpectedType)
1185
+
1186
+ Divide.call(4, 2)
1187
+ # type :division_completed is not allowed. Allowed types: :ok
1188
+ # (Solid::Result::Contract::Error::UnexpectedType)
1189
+ ```
1190
+
1191
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
1192
+
1193
+ ##### Standalone mode
1194
+
1195
+ ```ruby
1196
+ module Divide
1197
+ Result = Solid::Result::Expectations.new(success: :ok, failure: :err)
1198
+
1199
+ def self.call(arg1, arg2)
1200
+ arg1.is_a?(Numeric) or return Result::Failure(:invalid_arg, 'arg1 must be numeric')
1201
+ arg2.is_a?(Numeric) or return Result::Failure(:invalid_arg, 'arg2 must be numeric')
1202
+
1203
+ arg2.zero? and return Result::Failure(:division_by_zero, 'arg2 must not be zero')
1204
+
1205
+ Result::Success(:division_completed, arg1 / arg2)
1206
+ end
1207
+ end
1208
+
1209
+ Divide.call('4', 2)
1210
+ # type :invalid_arg is not allowed. Allowed types: :err
1211
+ # (Solid::Result::Contract::Error::UnexpectedType)
1212
+
1213
+ Divide.call(4, 2)
1214
+ # type :division_completed is not allowed. Allowed types: :ok
1215
+ # (Solid::Result::Contract::Error::UnexpectedType)
1216
+ ```
1217
+
1218
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
1219
+
1220
+ #### Value checking - Result Creation
1221
+
1222
+ The `Result::Expectations` supports two types of validations. The first is the type checking only, and the second is the type and value checking.
1223
+
1224
+ To define expectations for your result's values, you must declare a Hash with the type as the key and the value as the value. A value validator is any object that responds to `#===` (case equality operator).
1225
+
1226
+ **Mixin mode:**
1227
+
1228
+ ```ruby
1229
+ module Divide
1230
+ extend Solid::Result::Expectations.mixin(
1231
+ success: {
1232
+ numbers: ->(value) { value.is_a?(Array) && value.size == 2 && value.all?(Numeric) },
1233
+ division_completed: Numeric
1234
+ },
1235
+ failure: {
1236
+ invalid_arg: String,
1237
+ division_by_zero: String
1238
+ }
1239
+ )
1240
+
1241
+ def self.call(arg1, arg2)
1242
+ arg1.is_a?(Numeric) or return Failure(:invalid_arg, 'arg1 must be numeric')
1243
+ arg2.is_a?(Numeric) or return Failure(:invalid_arg, 'arg2 must be numeric')
1244
+
1245
+ arg2.zero? and return Failure(:division_by_zero, 'arg2 must not be zero')
1246
+
1247
+ Success(:division_completed, arg1 / arg2)
1248
+ end
1249
+ end
1250
+ ```
1251
+
1252
+ **Standalone mode:**
1253
+
1254
+ ```ruby
1255
+ module Divide
1256
+ Result = Solid::Result::Expectations.new(
1257
+ success: {
1258
+ numbers: ->(value) { value.is_a?(Array) && value.size == 2 && value.all?(Numeric) },
1259
+ division_completed: Numeric
1260
+ },
1261
+ failure: {
1262
+ invalid_arg: String,
1263
+ division_by_zero: String
1264
+ }
1265
+ )
1266
+
1267
+ def self.call(arg1, arg2)
1268
+ arg1.is_a?(Numeric) or return Result::Failure(:invalid_arg, 'arg1 must be numeric')
1269
+ arg2.is_a?(Numeric) or return Result::Failure(:invalid_arg, 'arg2 must be numeric')
1270
+
1271
+ arg2.zero? and return Result::Failure(:division_by_zero, 'arg2 must not be zero')
1272
+
1273
+ Result::Success(:division_completed, arg1 / arg2)
1274
+ end
1275
+ end
1276
+ ```
1277
+
1278
+ The value validation will only be performed through the methods `Success()` and `Failure()`.
1279
+
1280
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
1281
+
1282
+ ##### Success()
1283
+
1284
+ ```ruby
1285
+ Divide::Result::Success(:ok)
1286
+ # type :ok is not allowed. Allowed types: :numbers, :division_completed (Solid::Result::Contract::Error::UnexpectedType)
1287
+
1288
+ Divide::Result::Success(:numbers, [1])
1289
+ # value [1] is not allowed for :numbers type (Solid::Result::Contract::Error::UnexpectedValue)
1290
+
1291
+ Divide::Result::Success(:division_completed, '2')
1292
+ # value "2" is not allowed for :division_completed type (Solid::Result::Contract::Error::UnexpectedValue)
1293
+ ```
1294
+
1295
+ ##### Failure()
1296
+
1297
+ ```ruby
1298
+ Divide::Result::Failure(:err)
1299
+ # type :err is not allowed. Allowed types: :invalid_arg, :division_by_zero (Solid::Result::Contract::Error::UnexpectedType)
1300
+
1301
+ Divide::Result::Failure(:invalid_arg, :arg1_must_be_numeric)
1302
+ # value :arg1_must_be_numeric is not allowed for :invalid_arg type (Solid::Result::Contract::Error::UnexpectedValue)
1303
+
1304
+ Divide::Result::Failure(:division_by_zero, msg: 'arg2 must not be zero')
1305
+ # value {:msg=>"arg2 must not be zero"} is not allowed for :division_by_zero type (Solid::Result::Contract::Error::UnexpectedValue)
1306
+ ```
1307
+
1308
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
1309
+
1310
+ ##### Pattern Matching Support
1311
+
1312
+ The value checking has support for handling pattern-matching errors, and the cleanest way to do it is using the one-line pattern matching operators (`=>` since Ruby 3.0) and (`in` Ruby 2.7).
1313
+
1314
+ How does this operator work? They raise an error when the pattern does not match but returns nil when it matches.
1315
+
1316
+ Because of this, you will need to enable `nil` as a valid value checking. You can do it through the `Solid::Result.configuration` or by allowing it directly on the mixin config.
1317
+
1318
+ ```ruby
1319
+ module Divide
1320
+ extend Solid::Result::Expectations.mixin(
1321
+ config: {
1322
+ pattern_matching: { nil_as_valid_value_checking: true }
1323
+ },
1324
+ success: {
1325
+ division_completed: ->(value) { value => (Integer | Float) }
1326
+ },
1327
+ failure: { invalid_arg: String, division_by_zero: String }
1328
+ )
1329
+
1330
+ def self.call(arg1, arg2)
1331
+ arg1.is_a?(Numeric) or return Failure(:invalid_arg, 'arg1 must be numeric')
1332
+ arg2.is_a?(Numeric) or return Failure(:invalid_arg, 'arg2 must be numeric')
1333
+
1334
+ arg2.zero? and return Failure(:division_by_zero, 'arg2 must not be zero')
1335
+
1336
+ Success(:division_completed, String(arg1 / arg2))
1337
+ end
1338
+ end
1339
+
1340
+ Divide.call(10, 5)
1341
+ # value "2" is not allowed for :division_completed type ("2": Float === "2" does not return true) (Solid::Result::Contract::Error::UnexpectedValue)
1342
+ ```
1343
+
1344
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
1345
+
1346
+ #### `Solid::Result::Expectations.mixin` add-ons
1347
+
1348
+ The `Solid::Result::Expectations.mixin` also accepts the `config:` argument. It is a hash that can be used to define custom behaviors for the mixin.
1349
+
1350
+ **Continue**
1351
+
1352
+ It is similar to `Solid::Result.mixin(config: { addon: { continue: true } })`. The key difference is that the expectations will ignore the `Continue(value)`.
1353
+
1354
+ Based on this, use the `Success()` to produce a terminal result and `Continue()` to produce a result that will be used in the next step.
1355
+
1356
+ ```ruby
1357
+ class Divide
1358
+ include Solid::Result::Expectations.mixin(
1359
+ config: { addon: { continue: true } },
1360
+ success: :division_completed,
1361
+ failure: %i[invalid_arg division_by_zero]
1362
+ )
1363
+
1364
+ def call(arg1, arg2)
1365
+ validate_numbers(arg1, arg2)
1366
+ .and_then(:validate_nonzero)
1367
+ .and_then(:divide)
1368
+ end
1369
+
1370
+ private
1371
+
1372
+ def validate_numbers(arg1, arg2)
1373
+ arg1.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg1 must be numeric')
1374
+ arg2.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg2 must be numeric')
1375
+
1376
+ Continue([arg1, arg2])
1377
+ end
1378
+
1379
+ def validate_nonzero(numbers)
1380
+ return Continue(numbers) unless numbers.last.zero?
1381
+
1382
+ Failure(:division_by_zero, 'arg2 must not be zero')
1383
+ end
1384
+
1385
+ def divide((number1, number2))
1386
+ Success(:division_completed, number1 / number2)
1387
+ end
1388
+ end
1389
+
1390
+ result = Divide.new.call(4, 2)
1391
+ # => #<Solid::Result::Success type=:division_completed value=2>
1392
+
1393
+ # The example below shows an error because the :ok type is not allowed.
1394
+ # But look at the allowed types have only one type (:division_completed).
1395
+ # This is because the :_continue_ type is ignored by the expectations.
1396
+ #
1397
+ result.success?(:ok)
1398
+ # type :ok is not allowed. Allowed types: :division_completed (Solid::Result::Contract::Error::UnexpectedType)
1399
+ ```
1400
+
1401
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
1402
+
1403
+ ### `Solid::Output`
1404
+
1405
+ The `Solid::Output` is a `Solid::Result`, meaning it has all the features of the `Solid::Result`. The main difference is that it only accepts keyword arguments as a value, which applies to the `and_then`: The called methods must receive keyword arguments, and the dependency injection will be performed through keyword arguments.
1406
+
1407
+ As the input/output are hashes, the results of each `and_then` call will automatically accumulate. This is useful in operations chaining, as the result of the previous operations will be automatically available for the next one. Because of this behavior, the `Solid::Output` has the `#and_expose` method to expose only the desired keys from the accumulated result.
1408
+
1409
+ #### Defining successes and failures
1410
+
1411
+ As the `Solid::Result`, you can declare success and failures directly from `Solid::Output`.
1412
+
1413
+ ```ruby
1414
+ Solid::Output::Success(:ok, a: 1, b: 2)
1415
+ #<Solid::Output::Success type=:ok value={:a=>1, :b=>2}>
1416
+
1417
+ Solid::Output::Failure(:err, message: 'something went wrong')
1418
+ #<Solid::Output::Failure type=:err value={:message=>"something went wrong"}>
1419
+ ```
1420
+
1421
+ But different from `Solid::Result` that accepts any value, the `Solid::Output` only takes keyword arguments.
1422
+
1423
+ ```ruby
1424
+ Solid::Output::Success(:ok, [1, 2])
1425
+ # wrong number of arguments (given 2, expected 1) (ArgumentError)
1426
+
1427
+ Solid::Output::Failure(:err, { message: 'something went wrong' })
1428
+ # wrong number of arguments (given 2, expected 1) (ArgumentError)
1429
+
1430
+ #
1431
+ # Use ** to convert a hash to keyword arguments
1432
+ #
1433
+ Solid::Output::Success(:ok, **{ message: 'hashes can be converted to keyword arguments' })
1434
+ #<Solid::Output::Success type=:ok value={:message=>"hashes can be converted to keyword arguments"}>
1435
+ ```
1436
+
1437
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
1438
+
1439
+ #### Hash methods
1440
+
1441
+ The `Solid::Output` only accepts hashes as its values. Because of this, its instances have some Hash's methods to query/access the values. The available methods are:
1442
+
1443
+ - `#slice` to extract only the desired keys.
1444
+ - `#[]`, `#dig`, `#fetch` to access the values.
1445
+ - `#values_at` and `#fetch_values` to get the values of the desired keys.
1446
+
1447
+ ```ruby
1448
+ result = Solid::Output::Success(:ok, a: 1, b: 2, c: {d: 4})
1449
+
1450
+ result[:a] # 1
1451
+ result.fetch(:a) # 1
1452
+ result.dig(:c, :d) # 4
1453
+
1454
+ result.slice(:a, :b) # {:a=>1, :b=>2}
1455
+
1456
+ result.values_at(:a, :b) # [1, 2]
1457
+ result.fetch_values(:a, :b) # [1, 2]
1458
+ ```
1459
+
1460
+ These methods are available for `Solid::Output::Success` and `Solid::Output::Failure` instances.
1461
+
1462
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
1463
+
1464
+ #### `Solid::Output.mixin`
1465
+
1466
+ As in the `Solid::Result`, you can use the `Solid::Output.mixin` to add the `Success()` and `Failure()` methods to your classes/modules.
1467
+
1468
+ Let's see this feature and the data accumulation in action:
1469
+
1470
+ ##### Class example (Instance Methods)
1471
+
1472
+ ```ruby
1473
+ require 'logger'
1474
+
1475
+ class Divide
1476
+ include Solid::Output.mixin
1477
+
1478
+ def call(arg1, arg2, logger: ::Logger.new(STDOUT))
1479
+ validate_numbers(arg1, arg2)
1480
+ .and_then(:validate_nonzero)
1481
+ .and_then(:divide, logger: logger)
1482
+ end
1483
+
1484
+ private
1485
+
1486
+ def validate_numbers(arg1, arg2)
1487
+ arg1.is_a?(::Numeric) or return Failure(:err, message: 'arg1 must be numeric')
1488
+ arg2.is_a?(::Numeric) or return Failure(:err, message: 'arg2 must be numeric')
1489
+
1490
+ Success(:ok, number1: arg1, number2: arg2)
1491
+ end
1492
+
1493
+ def validate_nonzero(number2:, **)
1494
+ return Success(:ok) if number2.nonzero?
1495
+
1496
+ Failure(:err, message: 'arg2 must not be zero')
1497
+ end
1498
+
1499
+ #
1500
+ # The logger was injected via #and_then and keyword arguments
1501
+ #
1502
+ def divide(number1:, number2:, logger:)
1503
+ result = number1 / number2
1504
+
1505
+ logger.info("The division result is #{result}")
1506
+
1507
+ Success(:ok, number: result)
1508
+ end
1509
+ end
1510
+
1511
+ Divide.new.call(10, 5)
1512
+ # I, [2023-10-27T01:51:46.905004 #76915] INFO -- : The division result is 2
1513
+ #<Solid::Output::Success type=:ok value={:number=>2}>
1514
+
1515
+ Divide.new.call('10', 5)
1516
+ #<Solid::Output::Failure type=:err value={:message=>"arg1 must be numeric"}>
1517
+
1518
+ Divide.new.call(10, '5')
1519
+ #<Solid::Output::Failure type=:err value={:message=>"arg2 must be numeric"}>
1520
+
1521
+ Divide.new.call(10, 0)
1522
+ #<Solid::Output::Failure type=:err value={:message=>"arg2 must not be zero"}>
1523
+ ```
1524
+
1525
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
1526
+
1527
+ ##### `and_expose`
1528
+
1529
+ This allows you to expose only the desired keys from the accumulated result. It can be used with any `Solid::Output` object.
1530
+
1531
+ Let's add it to the previous example:
1532
+
1533
+ ```ruby
1534
+ class Divide
1535
+ include Solid::Output.mixin
1536
+
1537
+ def call(arg1, arg2)
1538
+ validate_numbers(arg1, arg2)
1539
+ .and_then(:validate_nonzero)
1540
+ .and_then(:divide)
1541
+ .and_expose(:division_completed, [:number])
1542
+ end
1543
+
1544
+ private
1545
+
1546
+ def validate_numbers(arg1, arg2)
1547
+ arg1.is_a?(::Numeric) or return Failure(:err, message: 'arg1 must be numeric')
1548
+ arg2.is_a?(::Numeric) or return Failure(:err, message: 'arg2 must be numeric')
1549
+
1550
+ Success(:ok, number1: arg1, number2: arg2)
1551
+ end
1552
+
1553
+ def validate_nonzero(number2:, **)
1554
+ return Success(:ok) if number2.nonzero?
1555
+
1556
+ Failure(:err, message: 'arg2 must not be zero')
1557
+ end
1558
+
1559
+ def divide(**input)
1560
+ Success(:ok, number: input.values.reduce(:/), **input)
1561
+ end
1562
+ end
1563
+
1564
+ Divide.new.call(10, 5)
1565
+ #<Solid::Output::Success type=:division_completed value={:number=>2}>
1566
+ ```
1567
+
1568
+ As you can see, even with `divide` success exposing the division number with all the accumulated data (`**input`), the `#and_expose` could generate a new success with a new type and only with the desired keys.
1569
+
1570
+ Remove the `#and_expose` call to see the difference. This will be the outcome:
1571
+
1572
+ ```ruby
1573
+ Divide.new.call(10, 5)
1574
+ #<Solid::Output::Success type=:ok value={:number=>2, :number1=>10, :number2=>5}>
1575
+ ```
1576
+
1577
+ > PS: The `#and_expose` produces a terminal success by default. This means the next step will not be executed even if you call `#and_then` after `#and_expose`. To change this behavior, you can pass `terminal: false` to `#and_expose`.
1578
+
1579
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
1580
+
1581
+ ##### Module example (Singleton Methods)
1582
+
1583
+ `Solid::Output.mixin` can also produce singleton methods. Below is an example using a module (but it could be a class, too).
1584
+
1585
+ ```ruby
1586
+ module Divide
1587
+ extend self, Solid::Output.mixin
1588
+
1589
+ def call(arg1, arg2)
1590
+ validate_numbers(arg1, arg2)
1591
+ .and_then(:validate_nonzero)
1592
+ .and_then(:divide)
1593
+ .and_expose(:division_completed, [:number])
1594
+ end
1595
+
1596
+ private
1597
+
1598
+ def validate_numbers(arg1, arg2)
1599
+ arg1.is_a?(::Numeric) or return Failure(:err, message: 'arg1 must be numeric')
1600
+ arg2.is_a?(::Numeric) or return Failure(:err, message: 'arg2 must be numeric')
1601
+
1602
+ Success(:ok, number1: arg1, number2: arg2)
1603
+ end
1604
+
1605
+ def validate_nonzero(number2:, **)
1606
+ return Success(:ok) if number2.nonzero?
1607
+
1608
+ Failure(:err, message: 'arg2 must not be zero')
1609
+ end
1610
+
1611
+ def divide(number1:, number2:)
1612
+ Success(:ok, number: number1 / number2)
1613
+ end
1614
+ end
1615
+
1616
+ Divide.call(10, 5)
1617
+ #<Solid::Output::Success type=:division_completed value={:number=>2}>
1618
+
1619
+ Divide.call('10', 5)
1620
+ #<Solid::Output::Failure type=:err value={:message=>"arg1 must be numeric"}>
1621
+
1622
+ Divide.call(10, '5')
1623
+ #<Solid::Output::Failure type=:err value={:message=>"arg2 must be numeric"}>
1624
+
1625
+ Divide.call(10, 0)
1626
+ #<Solid::Output::Failure type=:err value={:message=>"arg2 must not be zero"}>
1627
+ ```
1628
+
1629
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
1630
+
1631
+ #### `Solid::Output::Expectations`
1632
+
1633
+ The `Solid::Output::Expectations` is a `Solid::Result::Expectations` with the `Solid::Output` features.
1634
+
1635
+ This is an example using the mixin mode, but the standalone mode is also supported.
1636
+
1637
+ ```ruby
1638
+ class Divide
1639
+ include Solid::Output::Expectations.mixin(
1640
+ config: {
1641
+ pattern_matching: { nil_as_valid_value_checking: true }
1642
+ },
1643
+ success: {
1644
+ division_completed: ->(value) { value => { number: Numeric } }
1645
+ },
1646
+ failure: {
1647
+ invalid_arg: ->(value) { value => { message: String } },
1648
+ division_by_zero: ->(value) { value => { message: String } }
1649
+ }
1650
+ )
1651
+
1652
+ def call(arg1, arg2)
1653
+ arg1.is_a?(Numeric) or return Failure(:invalid_arg, message: 'arg1 must be numeric')
1654
+ arg2.is_a?(Numeric) or return Failure(:invalid_arg, message: 'arg2 must be numeric')
1655
+
1656
+ arg2.zero? and return Failure(:division_by_zero, message: 'arg2 must not be zero')
1657
+
1658
+ Success(:division_completed, number: (arg1 / arg2))
1659
+ end
1660
+ end
1661
+
1662
+ Divide.new.call(10, 5)
1663
+ #<Solid::Output::Success type=:division_completed value={:number=>2}>
1664
+ ```
1665
+
1666
+ As in the `Solid::Result::Expectations.mixin`, the `Solid::Output::Expectations.mixin` will add a Result constant in the target class. It can generate success/failure results, which ensure the mixin expectations.
1667
+
1668
+ Let's see this using the previous example:
1669
+
1670
+ ```ruby
1671
+ Divide::Result::Success(:division_completed, number: 2)
1672
+ #<Solid::Output::Success type=:division_completed value={:number=>2}>
1673
+
1674
+ Divide::Result::Success(:division_completed, number: '2')
1675
+ # value {:number=>"2"} is not allowed for :division_completed type ({:number=>"2"}: Numeric === "2" does not return true) (Solid::Result::Contract::Error::UnexpectedValue)
1676
+ ```
1677
+
1678
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
1679
+
1680
+ #### Mixin add-ons
1681
+
1682
+ The `Solid::Output.mixin` and `Solid::Output::Expectations.mixin` also accepts the `config:` argument. And it works the same way as the `Solid::Result` mixins.
1683
+
1684
+ **given**
1685
+
1686
+ This addon is enabled by default. It will create the `Given(*value)` method. Use it to add a value to the result chain and invoke the next step (through `and_then`).
1687
+
1688
+ You can turn it off by passing `given: false` to the `config:` argument or using the `Solid::Result.configuration`.
1689
+
1690
+ The `Given()` addon for a Solid::Output can be called with one or more arguments. The arguments will be converted to a hash (`to_h`) and merged to define the first value of the result chain.
1691
+
1692
+ **continue**
1693
+
1694
+ The `Solid::Output.mixin(config: { addon: { continue: true } })` or `Solid::Output::Expectations.mixin(config: { addon: { continue: true } })` creates the `Continue(value)` method and change the `Success()` behavior to terminate the step chain.
1695
+
1696
+ So, if you want to advance to the next step, you must use `Continue(**value)` instead of `Success(type, **value)`. Otherwise, the step chain will be terminated.
1697
+
1698
+ Let's use a mix of `Solid::Output` features to see in action with this add-on:
1699
+
1700
+ ```ruby
1701
+ module Division
1702
+ require 'logger'
1703
+
1704
+ extend self, Solid::Output::Expectations.mixin(
1705
+ config: {
1706
+ addon: { continue: true },
1707
+ pattern_matching: { nil_as_valid_value_checking: true }
1708
+ },
1709
+ success: {
1710
+ division_completed: ->(value) { value => { number: Numeric } }
1711
+ },
1712
+ failure: {
1713
+ invalid_arg: ->(value) { value => { message: String } },
1714
+ division_by_zero: ->(value) { value => { message: String } }
1715
+ }
1716
+ )
1717
+
1718
+ def call(arg1, arg2, logger: ::Logger.new(STDOUT))
1719
+ Given(number1: arg1, number2: arg2)
1720
+ .and_then(:require_numbers)
1721
+ .and_then(:check_for_zeros)
1722
+ .and_then(:divide, logger: logger)
1723
+ .and_expose(:division_completed, [:number])
1724
+ end
1725
+
1726
+ private
1727
+
1728
+ def require_numbers(number1:, number2:)
1729
+ number1.is_a?(::Numeric) or return Failure(:invalid_arg, message: 'arg1 must be numeric')
1730
+ number2.is_a?(::Numeric) or return Failure(:invalid_arg, message: 'arg2 must be numeric')
1731
+
1732
+ Continue()
1733
+ end
1734
+
1735
+ def check_for_zeros(number1:, number2:)
1736
+ return Failure(:division_by_zero, message: 'arg2 must not be zero') if number2.zero?
1737
+
1738
+ return Success(:division_completed, number: 0) if number1.zero?
1739
+
1740
+ Continue()
1741
+ end
1742
+
1743
+ def divide(number1:, number2:, logger:)
1744
+ result = number1 / number2
1745
+
1746
+ logger.info("The division result is #{result}")
1747
+
1748
+ Continue(number: result)
1749
+ end
1750
+ end
1751
+
1752
+ Division.call(14, 2)
1753
+ # I, [2023-10-27T02:01:05.812388 #77823] INFO -- : The division result is 7
1754
+ #<Solid::Output::Success type=:division_completed value={:number=>7}>
1755
+
1756
+ Division.call(0, 2)
1757
+ ##<Solid::Output::Success type=:division_completed value={:number=>0}>
1758
+
1759
+ Division.call('14', 2)
1760
+ #<Solid::Output::Failure type=:invalid_arg value={:message=>"arg1 must be numeric"}>
1761
+
1762
+ Division.call(14, '2')
1763
+ #<Solid::Output::Failure type=:invalid_arg value={:message=>"arg2 must be numeric"}>
1764
+
1765
+ Division.call(14, 0)
1766
+ #<Solid::Output::Failure type=:division_by_zero value={:message=>"arg2 must not be zero"}>
1767
+ ```
1768
+
1769
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
1770
+
1771
+ ## Pattern Matching
1772
+
1773
+ The `Solid::Result` and `Solid::Output` also provides support to pattern matching.
1774
+
1775
+ ### `Solid::Result`
1776
+
1777
+ In the further examples, I will use the `Divide` lambda to exemplify its usage.
1778
+
1779
+ ```ruby
1780
+ Divide = lambda do |arg1, arg2|
1781
+ arg1.is_a?(::Numeric) or return Solid::Result::Failure(:invalid_arg, 'arg1 must be numeric')
1782
+ arg2.is_a?(::Numeric) or return Solid::Result::Failure(:invalid_arg, 'arg2 must be numeric')
1783
+
1784
+ return Solid::Result::Failure(:division_by_zero, 'arg2 must not be zero') if arg2.zero?
1785
+
1786
+ Solid::Result::Success(:division_completed, arg1 / arg2)
1787
+ end
1788
+ ```
1789
+
1790
+ #### `Array`/`Find` patterns
1791
+
1792
+ ```ruby
1793
+ case Divide.call(4, 2)
1794
+ in Solid::Failure[:invalid_arg, msg] then puts msg
1795
+ in Solid::Failure[:division_by_zero, msg] then puts msg
1796
+ in Solid::Success[:division_completed, num] then puts num
1797
+ end
1798
+
1799
+ # The code above will print: 2
1800
+
1801
+ case Divide.call(4, 0)
1802
+ in Solid::Failure[:invalid_arg, msg] then puts msg
1803
+ in Solid::Failure[:division_by_zero, msg] then puts msg
1804
+ in Solid::Success[:division_completed, num] then puts num
1805
+ end
1806
+
1807
+ # The code above will print: arg2 must not be zero
1808
+ ```
1809
+
1810
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
1811
+
1812
+ #### `Hash` patterns
1813
+
1814
+ ```ruby
1815
+ case Divide.call(10, 2)
1816
+ in Solid::Failure(type: :invalid_arg, value: msg) then puts msg
1817
+ in Solid::Failure(type: :division_by_zero, value: msg) then puts msg
1818
+ in Solid::Success(type: :division_completed, value: num) then puts num
1819
+ end
1820
+
1821
+ # The code above will print: 5
1822
+
1823
+ case Divide.call('10', 2)
1824
+ in Solid::Failure(type: :invalid_arg, value: msg) then puts msg
1825
+ in Solid::Failure(type: :division_by_zero, value: msg) then puts msg
1826
+ in Solid::Success(type: :division_completed, value: num) then puts num
1827
+ end
1828
+
1829
+ # The code above will print: arg1 must be numeric
1830
+ ```
1831
+
1832
+ You can also use `Solid::Result::Success` and `Solid::Result::Failure` as patterns.
1833
+
1834
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
1835
+
1836
+
1837
+ ### `Solid::Output`
1838
+
1839
+ In the further examples, I will use the `Divide` lambda to exemplify its usage.
1840
+
1841
+ ```ruby
1842
+ Divide = lambda do |arg1, arg2|
1843
+ arg1.is_a?(::Numeric) or return Solid::Output::Failure(:invalid_arg, err: 'arg1 must be numeric')
1844
+ arg2.is_a?(::Numeric) or return Solid::Output::Failure(:invalid_arg, err: 'arg2 must be numeric')
1845
+
1846
+ return Solid::Output::Failure(:division_by_zero, err: 'arg2 must not be zero') if arg2.zero?
1847
+
1848
+ Solid::Output::Success(:division_completed, num: arg1 / arg2)
1849
+ end
1850
+ ```
1851
+
1852
+ #### `Array`/`Find` patterns
1853
+
1854
+ ```ruby
1855
+ case Divide.call(4, 2)
1856
+ in Solid::Failure[:invalid_arg, {msg:}] then puts msg
1857
+ in Solid::Failure[:division_by_zero, {msg:}] then puts msg
1858
+ in Solid::Success[:division_completed, {num:}] then puts num
1859
+ end
1860
+
1861
+ # The code above will print: 2
1862
+
1863
+ case Divide.call(4, 0)
1864
+ in Solid::Failure[:invalid_arg, {msg:}] then puts msg
1865
+ in Solid::Failure[:division_by_zero, {msg:}] then puts msg
1866
+ in Solid::Success[:division_completed, {num:}] then puts num
1867
+ end
1868
+
1869
+ # The code above will print: arg2 must not be zero
1870
+ ```
1871
+
1872
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
1873
+
1874
+ #### `Hash` patterns
1875
+
1876
+ If you don't provide the keys :type and :value, the pattern will match the result value.
1877
+
1878
+ ```ruby
1879
+ case Divide.call(10, 2)
1880
+ in Solid::Failure({msg:}) then puts msg
1881
+ in Solid::Success({num:}) then puts num
1882
+ end
1883
+ ```
1884
+
1885
+ ```ruby
1886
+ case Divide.call(10, 2)
1887
+ in Solid::Failure(type: :invalid_arg, value: {msg:}) then puts msg
1888
+ in Solid::Failure(type: :division_by_zero, value: {msg:}) then puts msg
1889
+ in Solid::Success(type: :division_completed, value: {num:}) then puts num
1890
+ end
1891
+
1892
+ # The code above will print: 5
1893
+
1894
+ case Divide.call('10', 2)
1895
+ in Solid::Failure(type: :invalid_arg, value: {msg:}) then puts {msg:}
1896
+ in Solid::Failure(type: :division_by_zero, value: {msg:}) then puts msg
1897
+ in Solid::Success(type: :division_completed, value: {num:}) then puts num
1898
+ end
1899
+
1900
+ # The code above will print: arg1 must be numeric
1901
+ ```
1902
+
1903
+ You can also use `Solid::Output::Success` and `Solid::Output::Failure` as patterns.
1904
+
1905
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
1906
+
1907
+ ### How to pattern match without the concept of success and failure
1908
+
1909
+ You can use the classes `Solid::Result` and `Solid::Output` as patterns, and the pattern matching will work without the concept of success and failure.
1910
+
1911
+ ```ruby
1912
+ case Divide.call(10, 2)
1913
+ in Solid::Output(:invalid_arg, {msg:}) then puts msg
1914
+ in Solid::Output(:division_by_zero, {msg:}) then puts msg
1915
+ in Solid::Output(:division_completed, {num:}) then puts num
1916
+ end
1917
+
1918
+ case Divide.call(10, 2)
1919
+ in Solid::Result(:invalid_arg, msg) then puts msg
1920
+ in Solid::Result(:division_by_zero, msg) then puts msg
1921
+ in Solid::Result(:division_completed, num) then puts num
1922
+ end
1923
+ ```
1924
+
1925
+ The `Solid::Result` will also work with the `Solid::Output`, but the opposite won't.
1926
+
1927
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
1928
+
1929
+ ## `Solid::Result.event_logs`
1930
+
1931
+ Use `Solid::Result.event_logs(&block)` to track all the results produced in the same or between different operations (it works with `Solid::Result` and `Solid::Output`). When there is a nesting of `event_logs` blocks, this mechanism will be able to correlate parent and child blocks and present the duration of all operations in milliseconds.
1932
+
1933
+ When you wrap the creation of the result with `Solid::Result.event_logs`, the final one will expose all the event log records through the `Solid::Result#event_logs` method.
1934
+
1935
+ ```ruby
1936
+ class Division
1937
+ include Solid::Result.mixin(config: { addon: { continue: true } })
1938
+
1939
+ def call(arg1, arg2)
1940
+ Solid::Result.event_logs(name: 'Division', desc: 'divide two numbers') do
1941
+ Given([arg1, arg2])
1942
+ .and_then(:require_numbers)
1943
+ .and_then(:check_for_zeros)
1944
+ .and_then(:divide)
1945
+ end
1946
+ end
1947
+
1948
+ private
1949
+
1950
+ ValidNumber = ->(arg) { arg.is_a?(Numeric) && (!arg.respond_to?(:finite?) || arg.finite?) }
1951
+
1952
+ def require_numbers((arg1, arg2))
1953
+ ValidNumber[arg1] or return Failure(:invalid_arg, 'arg1 must be a valid number')
1954
+ ValidNumber[arg2] or return Failure(:invalid_arg, 'arg2 must be a valid number')
1955
+
1956
+ Continue([arg1, arg2])
1957
+ end
1958
+
1959
+ def check_for_zeros(numbers)
1960
+ num1, num2 = numbers
1961
+
1962
+ return Failure(:division_by_zero, 'num2 cannot be zero') if num2.zero?
1963
+
1964
+ num1.zero? ? Success(:division_completed, 0) : Continue(numbers)
1965
+ end
1966
+
1967
+ def divide((num1, num2))
1968
+ Success(:division_completed, num1 / num2)
1969
+ end
1970
+ end
1971
+
1972
+ module SumDivisionsByTwo
1973
+ extend self, Solid::Result.mixin
1974
+
1975
+ def call(*numbers)
1976
+ Solid::Result.event_logs(name: 'SumDivisionsByTwo') do
1977
+ divisions = numbers.map { |number| Division.new.call(number, 2) }
1978
+
1979
+ if divisions.any?(&:failure?)
1980
+ Failure(:errors, divisions.select(&:failure?).map(&:value))
1981
+ else
1982
+ Success(:sum, divisions.sum(&:value))
1983
+ end
1984
+ end
1985
+ end
1986
+ end
1987
+ ```
1988
+
1989
+ Let's see the result of the `SumDivisionsByTwo` call:
1990
+
1991
+ ```ruby
1992
+ result = SumDivisionsByTwo.call(20, 10)
1993
+ # => #<Solid::Result::Success type=:sum value=15>
1994
+
1995
+ result.event_logs
1996
+ {
1997
+ :version => 1,
1998
+ :metadata => {
1999
+ :duration => 0, # milliseconds
2000
+ :trace_id => nil, # can be set through configuration
2001
+ :ids => {
2002
+ :tree => [0, [[1, []], [2, []]]],
2003
+ :matrix => { 0 => [0, 0], 1 => [1, 1], 2 => [2, 1]},
2004
+ :level_parent => { 0 => [0, 0], 1 => [1, 0], 2 => [1, 0]}
2005
+ }
2006
+ },
2007
+ :records=> [
2008
+ {
2009
+ :root => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
2010
+ :parent => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
2011
+ :current => {:id=>1, :name=>"Division", :desc=>"divide two numbers"},
2012
+ :result => {:kind=>:success, :type=>:_given_, :value=>[20, 2], :source=><Division:0x0000000102fd7ed0>},
2013
+ :and_then => {},
2014
+ :time => 2024-01-26 02:53:11.310346 UTC
2015
+ },
2016
+ {
2017
+ :root => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
2018
+ :parent => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
2019
+ :current => {:id=>1, :name=>"Division", :desc=>"divide two numbers"},
2020
+ :result => {:kind=>:success, :type=>:_continue_, :value=>[20, 2], :source=><Division:0x0000000102fd7ed0>},
2021
+ :and_then => {:type=>:method, :arg=>nil, :method_name=>:require_numbers},
2022
+ :time => 2024-01-26 02:53:11.310392 UTC
2023
+ },
2024
+ {
2025
+ :root => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
2026
+ :parent => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
2027
+ :current => {:id=>1, :name=>"Division", :desc=>"divide two numbers"},
2028
+ :result => {:kind=>:success, :type=>:_continue_, :value=>[20, 2], :source=><Division:0x0000000102fd7ed0>},
2029
+ :and_then => {:type=>:method, :arg=>nil, :method_name=>:check_for_zeros},
2030
+ :time=>2024-01-26 02:53:11.310403 UTC
2031
+ },
2032
+ {
2033
+ :root => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
2034
+ :parent => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
2035
+ :current => {:id=>1, :name=>"Division", :desc=>"divide two numbers"},
2036
+ :result => {:kind=>:success, :type=>:division_completed, :value=>10, :source=><Division:0x0000000102fd7ed0>},
2037
+ :and_then => {:type=>:method, :arg=>nil, :method_name=>:divide},
2038
+ :time => 2024-01-26 02:53:11.310409 UTC
2039
+ },
2040
+ {
2041
+ :root => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
2042
+ :parent => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
2043
+ :current => {:id=>2, :name=>"Division", :desc=>"divide two numbers"},
2044
+ :result => {:kind=>:success, :type=>:_given_, :value=>[10, 2], :source=><Division:0x0000000102fd6378>},
2045
+ :and_then => {},
2046
+ :time => 2024-01-26 02:53:11.310424 UTC
2047
+ },
2048
+ {
2049
+ :root => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
2050
+ :parent => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
2051
+ :current => {:id=>2, :name=>"Division", :desc=>"divide two numbers"},
2052
+ :result => {:kind=>:success, :type=>:_continue_, :value=>[10, 2], :source=><Division:0x0000000102fd6378>},
2053
+ :and_then => {:type=>:method, :arg=>nil, :method_name=>:require_numbers},
2054
+ :time => 2024-01-26 02:53:11.310428 UTC
2055
+ },
2056
+ {
2057
+ :root => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
2058
+ :parent => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
2059
+ :current => {:id=>2, :name=>"Division", :desc=>"divide two numbers"},
2060
+ :result => {:kind=>:success, :type=>:_continue_, :value=>[10, 2], :source=><Division:0x0000000102fd6378>},
2061
+ :and_then => {:type=>:method, :arg=>nil, :method_name=>:check_for_zeros},
2062
+ :time => 2024-01-26 02:53:11.310431 UTC
2063
+ },
2064
+ {
2065
+ :root => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
2066
+ :parent => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
2067
+ :current => {:id=>2, :name=>"Division", :desc=>"divide two numbers"},
2068
+ :result => {:kind=>:success, :type=>:division_completed, :value=>5, :source=><Division:0x0000000102fd6378>},
2069
+ :and_then => {:type=>:method, :arg=>nil, :method_name=>:divide},
2070
+ :time => 2024-01-26 02:53:11.310434 UTC
2071
+ },
2072
+ {
2073
+ :root => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
2074
+ :parent => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
2075
+ :current => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
2076
+ :result => {:kind=>:success, :type=>:sum, :value=>15, :source=>SumDivisionsByTwo},
2077
+ :and_then => {},
2078
+ :time => 2024-01-26 02:53:11.310444 UTC
2079
+ }
2080
+ ]
2081
+ }
2082
+ ```
2083
+
2084
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
2085
+
2086
+ ### `metadata: {ids:}`
2087
+
2088
+ The `:ids` metadata property is a hash with three properties:
2089
+ - `:tree`, a graph/tree representation of the id of each `event_logs` block.
2090
+ - `:level_parent`, a hash with the level (depth) of each block and its parent id.
2091
+ - `:matrix`, a matrix representation of the event logs ids. It is a simplification of the `:tree` property.
2092
+
2093
+ Use these data structures to build your own visualization.
2094
+
2095
+ > Check out [Event Logs Listener example](examples/single_listener/lib/single_event_logs_listener.rb) to see how a listener can be used to build a STDOUT visualization, using these properties.
2096
+
2097
+ ```ruby
2098
+ # tree:
2099
+ # A graph representation (array of arrays) of the each event logs block id.
2100
+ #
2101
+ 0 # [0, [
2102
+ |- 1 # [1, [[2, []]]],
2103
+ | |- 2 # [3, []],
2104
+ |- 3 # [4, [
2105
+ |- 4 # [5, []],
2106
+ | |- 5 # [6, [[7, []]]]
2107
+ | |- 6 # ]],
2108
+ | |- 7 # [8, []]
2109
+ |- 8 # ]]
2110
+
2111
+ # level_parent:
2112
+ # The event logs ids are the keys, and the level (depth) and parent id the values.
2113
+ # {
2114
+ 0 # 0 => [0, 0],
2115
+ |- 1 # 1 => [1, 0],
2116
+ | |- 2 # 2 => [2, 1],
2117
+ |- 3 # 3 => [1, 0],
2118
+ |- 4 # 4 => [1, 0],
2119
+ | |- 5 # 5 => [2, 4],
2120
+ | |- 6 # 6 => [2, 4],
2121
+ | |- 7 # 7 => [3, 6],
2122
+ |- 8 # 8 => [1, 0]
2123
+ # }
2124
+
2125
+ # matrix:
2126
+ # The rows are the direct blocks from the root block,
2127
+ # and the columns are the nested blocks from the direct ones.
2128
+ # {
2129
+ 0 | 1 | 2 | 3 | 4 # 0 => [0, 0],
2130
+ - | - | - | - | - # 1 => [1, 1],
2131
+ 0 | | | | # 2 => [1, 2],
2132
+ 1 | 1 | 2 | | # 3 => [2, 1],
2133
+ 2 | 3 | | | # 4 => [3, 1],
2134
+ 3 | 4 | 5 | 6 | 7 # 5 => [3, 2],
2135
+ 4 | 8 | | | # 6 => [3, 3],
2136
+ # 7 => [3, 4],
2137
+ # 8 => [4, 1]
2138
+ # }
2139
+ ```
2140
+
2141
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
2142
+
2143
+ ### Configuration
2144
+
2145
+ #### Turning on/off
2146
+
2147
+ You can use `Solid::Result.config.feature.disable!(event_logs)` and `Solid::Result.config.feature.enable!(event_logs)` to turn on/off the `Solid::Result.event_logs` feature.
2148
+
2149
+ ```ruby
2150
+ Solid::Result.configuration do |config|
2151
+ config.feature.disable!(event_logs)
2152
+ end
2153
+
2154
+ result = SumDivisionsByTwo.call(20, 10)
2155
+ # => #<Solid::Result::Success type=:sum value=15>
2156
+
2157
+ result.event_logs
2158
+ {
2159
+ :version=>1,
2160
+ :records=>[],
2161
+ :metadata=>{
2162
+ :duration=>0,
2163
+ :ids=>{:tree=>[], :matrix=>{}, :level_parent=>{}}, :trace_id=>nil
2164
+ }
2165
+ }
2166
+ ```
2167
+
2168
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
2169
+
2170
+ #### Setting a `trace_id` fetcher
2171
+
2172
+ You can define a lambda (arity 0) to fetch the trace_id. This lambda will be called before the first event logs block and will be used to set the `:trace_id` in the `:metadata` property.
2173
+
2174
+ Use to correlate different or the same operation (executed multiple times).
2175
+
2176
+ ```ruby
2177
+ Solid::Result.config.event_logs.trace_id = -> { Thread.current[:solid_result_event_logs_trace_id] }
2178
+ ```
2179
+
2180
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
2181
+
2182
+ #### Setting a `listener`
2183
+
2184
+ You can define a listener to be called during the event logs tracking (check out [this example](examples/single_listener/lib/single_event_logs_listener.rb)). It must be a class that includes `Solid::Result::EventLogs::Listener`.
2185
+
2186
+ Use it to build your additional logic on top of the tracking. Examples:
2187
+ - Log the event logs.
2188
+ - Perform the tracing.
2189
+ - Instrument the event logs (measure/report).
2190
+ - Build a visualization (Diagrams, using the `records:` + `metadata: {ids:}` properties).
2191
+
2192
+ After implementing your listener, you can set it to the `Solid::Result.config.event_logs.listener=`:
2193
+
2194
+ ```ruby
2195
+ Solid::Result.config.event_logs.listener = MyEventLogsListener
2196
+ ```
2197
+
2198
+ See the example below to understand how to implement one:
2199
+
2200
+ ```ruby
2201
+ class MyEventLogsListener
2202
+ include Solid::Result::EventLogs::Listener
2203
+
2204
+ # A listener will be initialized before the first event logs block, and it is discarded after the last one.
2205
+ def initialize
2206
+ end
2207
+
2208
+ # This method will be called before each event logs block.
2209
+ # The parent block will be called first in the case of nested ones.
2210
+ #
2211
+ # @param scope: {:id=>1, :name=>"SomeOperation", :desc=>"Optional description"}
2212
+ def on_start(scope:)
2213
+ end
2214
+
2215
+ # This method will wrap all the event logs in the same block.
2216
+ # It can be used to perform an instrumentation (measure/report).
2217
+ #
2218
+ # @param scope: {:id=>1, :name=>"SomeOperation", :desc=>"Optional description"}
2219
+ def around_event_logs(scope:)
2220
+ yield
2221
+ end
2222
+
2223
+ # This method will wrap each and_then call.
2224
+ # It can be used to perform an instrumentation of the and_then calls.
2225
+ #
2226
+ # @param scope: {:id=>1, :name=>"SomeOperation", :desc=>"Optional description"}
2227
+ # @param and_then:
2228
+ # {:type=>:block, :arg=>:some_injected_value}
2229
+ # {:type=>:method, :arg=>:some_injected_value, :method_name=>:some_method_name}
2230
+ def around_and_then(scope:, and_then:)
2231
+ yield
2232
+ end
2233
+
2234
+ # This method will be called after each result recording/tracking.
2235
+ #
2236
+ # @param record:
2237
+ # {
2238
+ # :root => {:id=>0, :name=>"RootOperation", :desc=>nil},
2239
+ # :parent => {:id=>0, :name=>"RootOperation", :desc=>nil},
2240
+ # :current => {:id=>1, :name=>"SomeOperation", :desc=>nil},
2241
+ # :result => {:kind=>:success, :type=>:_continue_, :value=>{some: :thing}, :source=><MyProcess:0x0000000102fd6378>},
2242
+ # :and_then => {:type=>:method, :arg=>nil, :method_name=>:some_method},
2243
+ # :time => 2024-01-26 02:53:11.310431 UTC
2244
+ # }
2245
+ def on_record(record:)
2246
+ end
2247
+
2248
+ # This method will be called at the end of the event logs tracking.
2249
+ #
2250
+ # @param event_logs:
2251
+ # {
2252
+ # :version => 1,
2253
+ # :metadata => {
2254
+ # :duration => 0,
2255
+ # :trace_id => nil,
2256
+ # :ids => {
2257
+ # :tree => [0, [[1, []], [2, []]]],
2258
+ # :matrix => { 0 => [0, 0], 1 => [1, 1], 2 => [2, 1]},
2259
+ # :level_parent => { 0 => [0, 0], 1 => [1, 0], 2 => [1, 0]}
2260
+ # }
2261
+ # },
2262
+ # :records => [
2263
+ # # ...
2264
+ # ]
2265
+ # }
2266
+ def on_finish(event_logs:)
2267
+ end
2268
+
2269
+ # This method will be called when an exception is raised during the event logs tracking.
2270
+ #
2271
+ # @param exception: Exception
2272
+ # @param event_logs: Hash
2273
+ def before_interruption(exception:, event_logs:)
2274
+ end
2275
+ end
2276
+ ```
2277
+
2278
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
2279
+
2280
+ #### Setting multiple `listeners`
2281
+
2282
+ You can use `Solid::Result::EventLogs::Listeners[]` to creates a listener of listeners (check out [this example](examples/multiple_listeners/Rakefile)), which will be called in the order they were added.
2283
+
2284
+ **Attention:** It only allows one listener to handle `around_and_then` and another `around_event_logs` records.
2285
+
2286
+ > The example below defines different listeners to handle `around_and_then` and `around_event_logs,` but it is also possible to define a listener to handle both.
2287
+
2288
+ ```ruby
2289
+ class AroundAndThenListener
2290
+ include Solid::Result::EventLogs::Listener
2291
+
2292
+ # It must be a static/singleton method.
2293
+ def self.around_and_then?
2294
+ true
2295
+ end
2296
+
2297
+ def around_and_then(scope:, and_then:)
2298
+ #...
2299
+ end
2300
+ end
2301
+
2302
+ class AroundEventLogsListener
2303
+ include Solid::Result::EventLogs::Listener
2304
+
2305
+ # It must be a static/singleton method.
2306
+ def self.around_event_logs?
2307
+ true
2308
+ end
2309
+
2310
+ def around_event_logs(scope:)
2311
+ #...
2312
+ end
2313
+ end
2314
+
2315
+ class MyEventLogsListener
2316
+ include Solid::Result::EventLogs::Listener
2317
+ end
2318
+ ```
2319
+
2320
+ How to use it:
2321
+
2322
+ ```ruby
2323
+ # The listeners will be called in the order they were added.
2324
+ Solid::Result.config.event_logs.listener = Solid::Result::EventLogs::Listeners[
2325
+ MyEventLogsListener,
2326
+ AroundAndThenListener,
2327
+ AroundEventLogsListener
2328
+ ]
2329
+ ```
2330
+
2331
+ > Check out [this example](examples/multiple_listeners) to see a listener to print the event logs and another to store them in the database.
2332
+
2333
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
2334
+
2335
+ ## `Solid::Result.configuration`
2336
+
2337
+ The `Solid::Result.configuration` allows you to configure default behaviors for `Solid::Result` and `Solid::Output` through a configuration block. After using it, the configuration is frozen, ensuring the expected behaviors for your application.
2338
+
2339
+ > Note: You can use `Solid::Result.configuration(freeze: false) {}` to avoid the freezing. This can be useful in tests. Please be sure to use it with caution.
2340
+
2341
+ ```ruby
2342
+ Solid::Result.configuration do |config|
2343
+ config.addon.enable!(:given, :continue)
2344
+
2345
+ config.constant_alias.enable!('Result', 'Solid::Output')
2346
+
2347
+ config.pattern_matching.disable!(:nil_as_valid_value_checking)
2348
+
2349
+ # config.feature.disable!(:expectations) if ::Rails.env.production?
2350
+ end
2351
+ ```
2352
+
2353
+ Use `disable!` to disable a feature and `enable!` to enable it.
2354
+
2355
+ Let's see what each configuration in the example above does:
2356
+
2357
+ ### `config.addon.enable!(:given, :continue)` <!-- omit in toc -->
2358
+
2359
+ This configuration enables the `Continue()` method for `Solid::Result.mixin`, `Solid::Output.mixin`, `Solid::Result::Expectation.mixin`, and `Solid::Output::Expectation.mixin`. Link to documentations: [(1)](#add-ons) [(2)](#mixin-add-ons).
2360
+
2361
+ It is also enabling the `Given()` which is already enabled by default. Link to documentation: [(1)](#add-ons) [(2)](#mixin-add-ons).
2362
+
2363
+ ### `config.constant_alias.enable!('Result', 'Solid::Output')` <!-- omit in toc -->
2364
+
2365
+ This configuration make `Result` a constant alias for `Solid::Result`, and `Solid::Output` a constant alias for `Solid::Output`.
2366
+
2367
+ Link to documentations:
2368
+ - [Result alias](#solidresult-versus-result)
2369
+
2370
+ ### `config.pattern_matching.disable!(:nil_as_valid_value_checking)` <!-- omit in toc -->
2371
+
2372
+ This configuration disables the `nil_as_valid_value_checking` for `Solid::Result` and `Solid::Output`. Link to [documentation](#pattern-matching-support).
2373
+
2374
+ ### `config.feature.disable!(:expectations)` <!-- omit in toc -->
2375
+
2376
+ This configuration turns off the expectations for `Solid::Result` and `Solid::Output`. The expectations are helpful in development and test environments, but they can be disabled in production environments for performance gain.
2377
+
2378
+ PS: I'm using `::Rails.env.production?` to check the environment, but you can use any logic you want.
2379
+
2380
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
2381
+
2382
+ ### `Solid::Result.config`
2383
+
2384
+ The `Solid::Result.config` allows you to access the current configuration.
2385
+
2386
+ #### **Solid::Result.config.addon** <!-- omit in toc -->
2387
+
2388
+ ```ruby
2389
+ Solid::Result.config.addon.enabled?(:continue)
2390
+ Solid::Result.config.addon.enabled?(:given)
2391
+
2392
+ Solid::Result.config.addon.options
2393
+ # {
2394
+ # :continue=>{
2395
+ # :enabled=>false,
2396
+ # :affects=>[
2397
+ # "Solid::Result.mixin",
2398
+ # "Solid::Output.mixin",
2399
+ # "Solid::Result::Expectations.mixin",
2400
+ # "Solid::Output::Expectations.mixin"
2401
+ # ]
2402
+ # },
2403
+ # :given=>{
2404
+ # :enabled=>true,
2405
+ # :affects=>[
2406
+ # "Solid::Result.mixin",
2407
+ # "Solid::Output.mixin",
2408
+ # "Solid::Result::Expectations.mixin",
2409
+ # "Solid::Output::Expectations.mixin"
2410
+ # ]
2411
+ # }
2412
+ # }
2413
+ ```
2414
+
2415
+ #### **Solid::Result.config.constant_alias** <!-- omit in toc -->
2416
+
2417
+ ```ruby
2418
+ Solid::Result.config.constant_alias.enabled?('Result')
2419
+
2420
+ Solid::Result.config.constant_alias.options
2421
+ # {
2422
+ # "Result"=>{:enabled=>false, :affects=>["Object"]}
2423
+ # }
2424
+ ```
2425
+
2426
+ #### **Solid::Result.config.pattern_matching** <!-- omit in toc -->
2427
+
2428
+ ```ruby
2429
+ Solid::Result.config.pattern_matching.enabled?(:nil_as_valid_value_checking)
2430
+
2431
+ Solid::Result.config.pattern_matching.options
2432
+ # {
2433
+ # :nil_as_valid_value_checking=>{
2434
+ # :enabled=>false,
2435
+ # :affects=>[
2436
+ # "Solid::Result::Expectations,
2437
+ # "Solid::Output::Expectations"
2438
+ # ]
2439
+ # }
2440
+ # }
2441
+ ```
2442
+
2443
+ #### **Solid::Result.config.feature** <!-- omit in toc -->
2444
+
2445
+ ```ruby
2446
+ Solid::Result.config.feature.enabled?(:expectations)
2447
+
2448
+ Solid::Result.config.feature.options
2449
+ # {
2450
+ # :expectations=>{
2451
+ # :enabled=>true,
2452
+ # :affects=>[
2453
+ # "Solid::Result::Expectations,
2454
+ # "Solid::Output::Expectations"
2455
+ # ]
2456
+ # },
2457
+ # event_logs=>{
2458
+ # :enabled=>true,
2459
+ # :affects=>[
2460
+ # "Solid::Result",
2461
+ # "Solid::Output"
2462
+ # ]
2463
+ # },
2464
+ # :and_then!=>{
2465
+ # :enabled=>false,
2466
+ # :affects=>[
2467
+ # "Solid::Result",
2468
+ # "Solid::Output"
2469
+ # ]
2470
+ # },
2471
+ # }
2472
+ ```
2473
+
2474
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
2475
+
2476
+ ## `Solid::Result#and_then!`
2477
+
2478
+ In the Ruby ecosystem, several gems facilitate operation composition using classes and modules. Two notable examples are the `interactor` gem and the `u-case` gem.
2479
+
2480
+ **`interactor` gem example**
2481
+
2482
+ ```ruby
2483
+ class PlaceOrder
2484
+ include Interactor::Organizer
2485
+
2486
+ organize CreateOrder,
2487
+ PayOrder,
2488
+ SendOrderConfirmation,
2489
+ NotifyAdmins
2490
+ end
2491
+ ```
2492
+
2493
+ **`u-case` gem example**
2494
+
2495
+ ```ruby
2496
+ class PlaceOrder < Micro::Case
2497
+ flow CreateOrder, PayOrder, SendOrderConfirmation, NotifyAdmins
2498
+ end
2499
+
2500
+ # Alternative approach
2501
+ class PlaceOrder < Micro::Case
2502
+ def call!
2503
+ call(CreateOrder)
2504
+ .then(PayOrder)
2505
+ .then(SendOrderConfirmation)
2506
+ .then(NotifyAdmins)
2507
+ end
2508
+ end
2509
+ ```
2510
+
2511
+ To facilitate migration for users accustomed to the above approaches, `solid-result` includes the `Solid::Result#and_then!`/`Solid::Output#and_then!` methods, which will invoke the method `call` of the given operation and expect it to return a `Solid::Result`/`Solid::Output` object.
2512
+
2513
+ ```ruby
2514
+ Solid::Result.configure do |config|
2515
+ config.feature.enable!(:and_then!)
2516
+ end
2517
+
2518
+ class PlaceOrder
2519
+ include Solid::Output.mixin
2520
+
2521
+ def call(**input)
2522
+ Given(input)
2523
+ .and_then!(CreateOrder.new)
2524
+ .and_then!(PayOrder.new)
2525
+ .and_then!(SendOrderConfirmation.new)
2526
+ .and_then!(NotifyAdmins.new)
2527
+ end
2528
+ end
2529
+ ```
2530
+
2531
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
2532
+
2533
+ #### Dependency Injection
2534
+
2535
+ Like `#and_then`, `#and_then!` also supports an additional argument for dependency injection.
2536
+
2537
+ **In Solid::Result**
2538
+
2539
+ ```ruby
2540
+ class PlaceOrder
2541
+ include Solid::Result.mixin
2542
+
2543
+ def call(input, logger:)
2544
+ Given(input)
2545
+ .and_then!(CreateOrder.new, logger)
2546
+ # Further method chaining...
2547
+ end
2548
+ end
2549
+ ```
2550
+
2551
+ **In Solid::Output**
2552
+
2553
+ ```ruby
2554
+ class PlaceOrder
2555
+ include Solid::Output.mixin
2556
+
2557
+ def call(logger:, **input)
2558
+ Given(input)
2559
+ .and_then!(CreateOrder.new, logger:)
2560
+ # Further method chaining...
2561
+ end
2562
+ end
2563
+ ```
2564
+
2565
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
2566
+
2567
+ #### Configuration
2568
+
2569
+ ```ruby
2570
+ Solid::Result.configure do |config|
2571
+ config.feature.enable!(:and_then!)
2572
+
2573
+ config.and_then!.default_method_name_to_call = :perform
2574
+ end
2575
+ ```
2576
+
2577
+ **Explanation:**
2578
+
2579
+ - `enable!(:and_then!)`: Activates the `and_then!` feature.
2580
+
2581
+ - `default_method_name_to_call`: Sets a default method other than `:call` for `and_then!`.
2582
+
2583
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
2584
+
2585
+ #### Analysis: Why is `and_then!` an Anti-pattern?
2586
+
2587
+ The `and_then!` approach, despite its brevity, introduces several issues:
2588
+
2589
+ - **Lack of Clarity:** The input/output relationship between the steps is not apparent.
2590
+
2591
+ - **Steps Coupling:** Each operation becomes interdependent (high coupling), complicating implementation and compromising the reusability of these operations.
2592
+
2593
+ We recommend cautious use of `#and_then!`. Due to these issues, it is turned off by default and considered an antipattern.
2594
+
2595
+ It should be a temporary solution, primarily for assisting in migration from another to gem to `solid-result`.
2596
+
2597
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
2598
+
2599
+ #### `#and_then` versus `#and_then!`
2600
+
2601
+ The main difference between the `#and_then` and `#and_then!` is that the latter does not check the result source. However, as a drawback, the result source will change.
2602
+
2603
+ Attention: to ensure the correct behavior, do not mix `#and_then` and `#and_then!` in the same result chain.
2604
+
2605
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
2606
+
2607
+ #### Analysis: Why is `#and_then` the antidote/standard?
2608
+
2609
+ The `Solid::Result#and_then`/`Solid::Output#and_then` methods diverge from the above approach by requiring explicit invocation and mapping of the outcomes at each process step. This approach has the following advantages:
2610
+
2611
+ - **Clarity:** The input/output relationship between the steps is apparent and highly understandable.
2612
+
2613
+ - **Steps uncoupling:** Each operation becomes independent (low coupling). You can even map a failure result to a success (and vice versa).
2614
+
2615
+ See this example to understand what your code should look like:
2616
+
2617
+ ```ruby
2618
+ class PlaceOrder
2619
+ include Solid::Output.mixin(config: { addon: { continue: true } })
2620
+
2621
+ def call(**input)
2622
+ Given(input)
2623
+ .and_then(:create_order)
2624
+ .and_then(:pay_order)
2625
+ .and_then(:send_order_confirmation)
2626
+ .and_then(:notify_admins)
2627
+ .and_expose(:order_placed, %i[order])
2628
+ end
2629
+
2630
+ private
2631
+
2632
+ def create_order(customer:, products:)
2633
+ CreateOrder.new.call(customer:, products:).handle do |on|
2634
+ on.success { |output| Continue(order: output[:order]) }
2635
+ on.failure { |error| Failure(:order_creation_failed, error:) }
2636
+ end
2637
+ end
2638
+
2639
+ def pay_order(customer:, order:, payment_method:, **)
2640
+ PayOrder.new.call(customer:, payment_method:, order:).handle do |on|
2641
+ on.success { |output| Continue(payment: output[:payment]) }
2642
+ on.failure { |error| Failure(:order_payment_failed, error:) }
2643
+ end
2644
+ end
2645
+
2646
+ def send_order_confirmation(customer:, order:, payment:, **)
2647
+ SendOrderConfirmation.new.call(customer:, order:, payment:).handle do |on|
2648
+ on.success { Continue() }
2649
+ on.failure { |error| Failure(:order_confirmation_failed, error:) }
2650
+ end
2651
+ end
2652
+
2653
+ def notify_admins(customer:, order:, payment:, **)
2654
+ NotifyAdmins.new.call(customer:, order:, payment:)
2655
+
2656
+ Continue()
2657
+ end
2658
+ end
2659
+ ```
2660
+
2661
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
2662
+
2663
+ ## About
2664
+
2665
+ [Rodrigo Serradura](https://github.com/serradura) created this project. He is the Solid Process 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 can be used independently, but it also contains essential features that facilitate the adoption of Solid Process (the method) in code.
2666
+
2667
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
2668
+
2669
+ ## Development
2670
+
2671
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
2672
+
2673
+ 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).
2674
+
2675
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
2676
+
2677
+ ## Contributing
2678
+
2679
+ Bug reports and pull requests are welcome on GitHub at https://github.com/solid-process/solid-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/solid-process/solid-result/blob/master/CODE_OF_CONDUCT.md).
2680
+
2681
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
2682
+
2683
+ ## License
2684
+
2685
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
2686
+
2687
+ <p align="right"><a href="#-solidresult">⬆️ &nbsp;back to top</a></p>
2688
+
2689
+ ## Code of Conduct
2690
+
2691
+ Everyone interacting in the Solid::Result project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/solid-process/solid-result/blob/master/CODE_OF_CONDUCT.md).