u-case 3.0.0.rc1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1785653f481f4622720634abe0448ef3d41dd522473ca88e8dc75d987ddd4413
4
+ data.tar.gz: f497efd7bc60619a86a3b8bb22e15551a4b513fbe4088e92a1718bd6568ec6df
5
+ SHA512:
6
+ metadata.gz: d9d77becd6c184bd58765df5d9dbbad8cee87054e6380b259e3463117401d1107cce3549a3a9626cc97f1307766b4aaef40cc32b5a7744a288790fcae6f1ee97
7
+ data.tar.gz: b65306eb69ee9a8fe7dc39d943517aca4617181ddb2c473b6ac241edf4674634b1c7517e0f3c41ad14d243c13f7c83a782e9f6971b1212fd1c87cdb0ef14bf2d
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ Gemfile.lock
10
+ tags
@@ -0,0 +1 @@
1
+ ruby 2.6.5
@@ -0,0 +1,19 @@
1
+ #!/bin/bash
2
+
3
+ ruby_v=$(ruby -v)
4
+
5
+ ACTIVEMODEL_VERSION='3.2' bundle update
6
+ ACTIVEMODEL_VERSION='3.2' bundle exec rake test
7
+
8
+ if [[ ! $ruby_v =~ '2.2.0' ]]; then
9
+ ACTIVEMODEL_VERSION='5.2' bundle update
10
+ ACTIVEMODEL_VERSION='5.2' bundle exec rake test
11
+ fi
12
+
13
+ if [[ $ruby_v =~ '2.5.' ]] || [[ $ruby_v =~ '2.6.' ]] || [[ $ruby_v =~ '2.7.' ]]; then
14
+ ACTIVEMODEL_VERSION='6.0' bundle update
15
+ ACTIVEMODEL_VERSION='6.0' bundle exec rake test
16
+ fi
17
+
18
+ bundle update
19
+ bundle exec rake test
@@ -0,0 +1,30 @@
1
+
2
+ language: ruby
3
+
4
+ sudo: false
5
+
6
+ rvm:
7
+ - 2.2.0
8
+ - 2.3.0
9
+ - 2.4.0
10
+ - 2.5.0
11
+ - 2.6.0
12
+ - 2.7.0
13
+
14
+ cache: bundler
15
+
16
+ before_install:
17
+ - gem uninstall -v '>= 2' -i $(rvm gemdir)@global -ax bundler || true
18
+ - gem install bundler -v '< 2'
19
+
20
+ install: bundle install --jobs=3 --retry=3
21
+
22
+ before_script:
23
+ - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
24
+ - chmod +x ./cc-test-reporter
25
+ - "./cc-test-reporter before-build"
26
+
27
+ script: "./.travis.sh"
28
+
29
+ after_success:
30
+ - "./cc-test-reporter after-build -t simplecov"
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at rodrigo.serradura@gmail.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,45 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ activemodel_version = ENV.fetch('ACTIVEMODEL_VERSION', '6.1.0')
6
+
7
+ activemodel = case activemodel_version
8
+ when '3.2' then '3.2.22'
9
+ when '5.2' then '5.2.3'
10
+ when '6.0' then '6.0.2'
11
+ end
12
+
13
+ if activemodel_version < '6.1.0'
14
+ gem 'activemodel', activemodel, require: false
15
+ gem 'activesupport', activemodel, require: false
16
+ end
17
+
18
+ group :test do
19
+ gem 'minitest', activemodel_version < '4.1' ? '~> 4.2' : '~> 5.0'
20
+ gem 'simplecov', require: false
21
+ end
22
+
23
+ pry_byebug_version =
24
+ case RUBY_VERSION
25
+ when /\A2.2/ then '3.6'
26
+ when /\A2.3/ then '3.7'
27
+ else '3.9'
28
+ end
29
+
30
+ pry_version =
31
+ case RUBY_VERSION
32
+ when /\A2.2/ then '0.12.2'
33
+ when /\A2.3/ then '0.12.2'
34
+ else '0.13.1'
35
+ end
36
+
37
+ group :development, :test do
38
+ gem 'awesome_print', '~> 1.8'
39
+
40
+ gem 'pry', "~> #{pry_version}"
41
+ gem 'pry-byebug', "~> #{pry_byebug_version}"
42
+ end
43
+
44
+ # Specify your gem's dependencies in u-case.gemspec
45
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 Rodrigo Serradura
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,1369 @@
1
+ ![Ruby](https://img.shields.io/badge/ruby-2.2+-ruby.svg?colorA=99004d&colorB=cc0066)
2
+ [![Gem](https://img.shields.io/gem/v/u-case.svg?style=flat-square)](https://rubygems.org/gems/u-case)
3
+ [![Build Status](https://travis-ci.com/serradura/u-case.svg?branch=master)](https://travis-ci.com/serradura/u-case)
4
+ [![Maintainability](https://api.codeclimate.com/v1/badges/5c3c8ad1b0b943f88efd/maintainability)](https://codeclimate.com/github/serradura/u-case/maintainability)
5
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/5c3c8ad1b0b943f88efd/test_coverage)](https://codeclimate.com/github/serradura/u-case/test_coverage)
6
+
7
+ μ-case (Micro::Case) <!-- omit in toc -->
8
+ ====================
9
+
10
+ Create simple and powerful use cases as objects.
11
+
12
+ The main project goals are:
13
+ 1. Easy to use and easy to learn (input **>>** process **>>** output).
14
+ 2. Promote referential transparency (transforming instead of modifying) and data integrity.
15
+ 3. No callbacks (e.g: before, after, around).
16
+ 4. Solve complex business logic, by allowing the composition of use cases.
17
+ 5. Be fast and optimized (Check out the [benchmarks](#benchmarks) section).
18
+
19
+ > Note: Check out the repo https://github.com/serradura/from-fat-controllers-to-use-cases to see a Rails application that uses this gem to handle its business logic.
20
+
21
+ ## Documentation <!-- omit in toc -->
22
+
23
+ Version | Documentation
24
+ ---------- | -------------
25
+ Unreleased | https://github.com/serradura/u-case/blob/master/README.md
26
+ 2.6.0 | https://github.com/serradura/u-case/blob/v2.x/README.md
27
+ 1.1.0 | https://github.com/serradura/u-case/blob/v1.x/README.md
28
+
29
+ ## Table of Contents <!-- omit in toc -->
30
+ - [Required Ruby version](#required-ruby-version)
31
+ - [Dependencies](#dependencies)
32
+ - [Installation](#installation)
33
+ - [Usage](#usage)
34
+ - [`Micro::Case` - How to define a use case?](#microcase---how-to-define-a-use-case)
35
+ - [`Micro::Case::Result` - What is a use case result?](#microcaseresult---what-is-a-use-case-result)
36
+ - [What are the default result types?](#what-are-the-default-result-types)
37
+ - [How to define custom result types?](#how-to-define-custom-result-types)
38
+ - [Is it possible to define a custom result type without a block?](#is-it-possible-to-define-a-custom-result-type-without-a-block)
39
+ - [How to use the result hooks?](#how-to-use-the-result-hooks)
40
+ - [Why the failure hook (without a type) exposes result itself?](#why-the-failure-hook-without-a-type-exposes-result-itself)
41
+ - [What happens if a result hook was declared multiple times?](#what-happens-if-a-result-hook-was-declared-multiple-times)
42
+ - [How to use the `Micro::Case::Result#then` method?](#how-to-use-the-microcaseresultthen-method)
43
+ - [What does happens when a `Micro::Case::Result#then` receives a block?](#what-does-happens-when-a-microcaseresultthen-receives-a-block)
44
+ - [How to make attributes data injection using this feature?](#how-to-make-attributes-data-injection-using-this-feature)
45
+ - [`Micro::Cases::Flow` - How to compose use cases?](#microcasesflow---how-to-compose-use-cases)
46
+ - [Is it possible to compose a use case flow with other ones?](#is-it-possible-to-compose-a-use-case-flow-with-other-ones)
47
+ - [Is it possible a flow accumulates its input and merges each success result to use as the argument of the next use cases?](#is-it-possible-a-flow-accumulates-its-input-and-merges-each-success-result-to-use-as-the-argument-of-the-next-use-cases)
48
+ - [How to understand what is happening during a flow execution?](#how-to-understand-what-is-happening-during-a-flow-execution)
49
+ - [`Micro::Case::Result#transitions` schema](#microcaseresulttransitions-schema)
50
+ - [Is it possible to declare a flow which includes the use case itself?](#is-it-possible-to-declare-a-flow-which-includes-the-use-case-itself)
51
+ - [`Micro::Case::Strict` - What is a strict use case?](#microcasestrict---what-is-a-strict-use-case)
52
+ - [`Micro::Case::Safe` - Is there some feature to auto handle exceptions inside of a use case or flow?](#microcasesafe---is-there-some-feature-to-auto-handle-exceptions-inside-of-a-use-case-or-flow)
53
+ - [`Micro::Cases::Safe::Flow`](#microcasessafeflow)
54
+ - [`Micro::Case::Result#on_exception`](#microcaseresulton_exception)
55
+ - [`u-case/with_activemodel_validation` - How to validate use case attributes?](#u-casewith_activemodel_validation---how-to-validate-use-case-attributes)
56
+ - [If I enabled the auto validation, is it possible to disable it only in specific use case classes?](#if-i-enabled-the-auto-validation-is-it-possible-to-disable-it-only-in-specific-use-case-classes)
57
+ - [`Kind::Validator`](#kindvalidator)
58
+ - [Benchmarks](#benchmarks)
59
+ - [`Micro::Case` (v2.6.0)](#microcase-v260)
60
+ - [Best overall](#best-overall)
61
+ - [Success results](#success-results)
62
+ - [Failure results](#failure-results)
63
+ - [`Micro::Case::Flow` (v2.6.0)](#microcaseflow-v260)
64
+ - [Comparisons](#comparisons)
65
+ - [Examples](#examples)
66
+ - [1️⃣ Rails App (API)](#1️⃣-rails-app-api)
67
+ - [2️⃣ CLI calculator](#2️⃣-cli-calculator)
68
+ - [3️⃣ Users creation](#3️⃣-users-creation)
69
+ - [4️⃣ Rescuing exception inside of the use cases](#4️⃣-rescuing-exception-inside-of-the-use-cases)
70
+ - [Development](#development)
71
+ - [Contributing](#contributing)
72
+ - [License](#license)
73
+ - [Code of Conduct](#code-of-conduct)
74
+
75
+ ## Required Ruby version
76
+
77
+ > \>= 2.2.0
78
+
79
+ ## Dependencies
80
+
81
+ 1. [`kind`](https://github.com/serradura/kind) gem.
82
+
83
+ A simple type system (at runtime) for Ruby.
84
+
85
+ Used to validate method inputs, expose `Kind.of.Micro::Case::Result` type checker and its [`activemodel validation`](https://github.com/serradura/kind#kindvalidator-activemodelvalidations) module is auto required by [`u-case/with_activemodel_validation`](#u-casewith_activemodel_validation---how-to-validate-use-case-attributes) mode.
86
+ 2. [`u-attributes`](https://github.com/serradura/u-attributes) gem.
87
+
88
+ This gem allows defining read-only attributes, that is, your objects will have only getters to access their attributes data.
89
+ It is used to define the use case attributes.
90
+
91
+ ## Installation
92
+
93
+ Add this line to your application's Gemfile:
94
+
95
+ ```ruby
96
+ gem 'u-case'
97
+ ```
98
+
99
+ And then execute:
100
+
101
+ $ bundle
102
+
103
+ Or install it yourself as:
104
+
105
+ $ gem install u-case
106
+
107
+ ## Usage
108
+
109
+ ### `Micro::Case` - How to define a use case?
110
+
111
+ ```ruby
112
+ class Multiply < Micro::Case
113
+ # 1. Define its input as attributes
114
+ attributes :a, :b
115
+
116
+ # 2. Define the method `call!` with its business logic
117
+ def call!
118
+
119
+ # 3. Wrap the use case result/output using the `Success(result: *)` or `Failure(result: *)` methods
120
+ if a.is_a?(Numeric) && b.is_a?(Numeric)
121
+ Success result: { number: a * b }
122
+ else
123
+ Failure result: { message: '`a` and `b` attributes must be numeric' }
124
+ end
125
+ end
126
+ end
127
+
128
+ #==========================#
129
+ # Calling a use case class #
130
+ #==========================#
131
+
132
+ # Success result
133
+
134
+ result = Multiply.call(a: 2, b: 2)
135
+
136
+ result.success? # true
137
+ result.data # { number: 4 }
138
+
139
+ # Failure result
140
+
141
+ bad_result = Multiply.call(a: 2, b: '2')
142
+
143
+ bad_result.failure? # true
144
+ bad_result.data # { message: "`a` and `b` attributes must be numeric" }
145
+
146
+ #-----------------------------#
147
+ # Calling a use case instance #
148
+ #-----------------------------#
149
+
150
+ result = Multiply.new(a: 2, b: 3).call
151
+
152
+ result.value # { number: 6 }
153
+
154
+ # Note:
155
+ # ----
156
+ # The result of a Micro::Case.call
157
+ # is an instance of Micro::Case::Result
158
+ ```
159
+
160
+ [⬆️ Back to Top](#table-of-contents-)
161
+
162
+ ### `Micro::Case::Result` - What is a use case result?
163
+
164
+ A `Micro::Case::Result` stores the use cases output data. These are their main methods:
165
+ - `#success?` returns true if is a successful result.
166
+ - `#failure?` returns true if is an unsuccessful result.
167
+ - `#data` the result data itself.
168
+ - `#type` a Symbol which gives meaning for the result, this is useful to declare different types of failures or success.
169
+ - `#on_success` or `#on_failure` are hook methods that help you to define the application flow.
170
+ - `#use_case` if is a failure result, the use case responsible for it will be accessible through this method. This feature is handy to handle a flow failure (this topic will be covered ahead).
171
+ - `#then` this method will allow applying a new use case if the current result was a success. The idea of this feature is to allow the creation of dynamic flows.
172
+ - `#[]` and `#values_at` are shortcuts to access the `#data` values.
173
+
174
+ > **Note:** for backward compatibility, you could use the `#value` method as an alias of `#data` method.
175
+
176
+ [⬆️ Back to Top](#table-of-contents-)
177
+
178
+ #### What are the default result types?
179
+
180
+ Every result has a type and these are the defaults:
181
+ - `:ok` when success
182
+ - `:error`/`:exception` when failures
183
+
184
+ ```ruby
185
+ class Divide < Micro::Case
186
+ attributes :a, :b
187
+
188
+ def call!
189
+ if invalid_attributes.empty?
190
+ Success result: { number: a / b }
191
+ else
192
+ Failure result: { invalid_attributes: invalid_attributes }
193
+ end
194
+ rescue => exception
195
+ Failure result: exception
196
+ end
197
+
198
+ private def invalid_attributes
199
+ attributes.select { |_key, value| !value.is_a?(Numeric) }
200
+ end
201
+ end
202
+
203
+ # Success result
204
+
205
+ result = Divide.call(a: 2, b: 2)
206
+
207
+ result.type # :ok
208
+ result.data # { number: 1 }
209
+ result.success? # true
210
+ result.use_case # raises `Micro::Case::Error::InvalidAccessToTheUseCaseObject: only a failure result can access its own use case`
211
+
212
+ # Failure result (type == :error)
213
+
214
+ bad_result = Divide.call(a: 2, b: '2')
215
+
216
+ bad_result.type # :error
217
+ bad_result.data # { invalid_attributes: { "b"=>"2" } }
218
+ bad_result.failure? # true
219
+ bad_result.use_case # #<Divide:0x0000 @__attributes={"a"=>2, "b"=>"2"}, @a=2, @b="2", @__result=#<Micro::Case::Result:0x0000 @use_case=#<Divide:0x0000 ...>, @type=:error, @value={"b"=>"2"}, @success=false>
220
+
221
+ # Failure result (type == :exception)
222
+
223
+ err_result = Divide.call(a: 2, b: 0)
224
+
225
+ err_result.type # :exception
226
+ err_result.data # { exception: <ZeroDivisionError: divided by 0> }
227
+ err_result.failure? # true
228
+ err_result.use_case # #<Divide:0x0000 @__attributes={"a"=>2, "b"=>0}, @a=2, @b=0, @__result=#<Micro::Case::Result:0x0000 @use_case=#<Divide:0x0000 ...>, @type=:exception, @value=#<ZeroDivisionError: divided by 0>, @success=false>
229
+
230
+ # Note:
231
+ # ----
232
+ # Any Exception instance which is wrapped by
233
+ # the Failure(result: *) method will receive `:exception` instead of the `:error` type.
234
+ ```
235
+
236
+ [⬆️ Back to Top](#table-of-contents-)
237
+
238
+ #### How to define custom result types?
239
+
240
+ Answer: Use a symbol as the argument of `Success()`, `Failure()` methods and declare the `result:` keyword to set the result data.
241
+
242
+ ```ruby
243
+ class Multiply < Micro::Case
244
+ attributes :a, :b
245
+
246
+ def call!
247
+ if a.is_a?(Numeric) && b.is_a?(Numeric)
248
+ Success result: { number: a * b }
249
+ else
250
+ Failure :invalid_data, result: {
251
+ attributes: attributes.reject { |_, input| input.is_a?(Numeric) }
252
+ }
253
+ end
254
+ end
255
+ end
256
+
257
+ # Success result
258
+
259
+ result = Multiply.call(a: 3, b: 2)
260
+
261
+ result.type # :ok
262
+ result.data # { number: 6 }
263
+ result.success? # true
264
+
265
+ # Failure result
266
+
267
+ bad_result = Multiply.call(a: 3, b: '2')
268
+
269
+ bad_result.type # :invalid_data
270
+ bad_result.data # { attributes: {"b"=>"2"} }
271
+ bad_result.failure? # true
272
+ ```
273
+
274
+ [⬆️ Back to Top](#table-of-contents-)
275
+
276
+ #### Is it possible to define a custom result type without a block?
277
+
278
+ Answer: Yes, it is possible. But this will have special behavior because the result data will be a hash with the given type as the key and true as its value.
279
+
280
+ ```ruby
281
+ class Multiply < Micro::Case
282
+ attributes :a, :b
283
+
284
+ def call!
285
+ if a.is_a?(Numeric) && b.is_a?(Numeric)
286
+ Success result: { number: a * b }
287
+ else
288
+ Failure(:invalid_data)
289
+ end
290
+ end
291
+ end
292
+
293
+ result = Multiply.call(a: 2, b: '2')
294
+
295
+ result.failure? # true
296
+ result.data # { :invalid_data => true }
297
+ result.type # :invalid_data
298
+ result.use_case.attributes # {"a"=>2, "b"=>"2"}
299
+
300
+ # Note:
301
+ # ----
302
+ # This feature is handy to handle failures in a flow
303
+ # (this topic will be covered ahead).
304
+ ```
305
+
306
+ [⬆️ Back to Top](#table-of-contents-)
307
+
308
+ #### How to use the result hooks?
309
+
310
+ As mentioned earlier, the `Micro::Case::Result` has two methods to improve the flow control. They are: `#on_success`, `on_failure`.
311
+
312
+ The examples below show how to use them:
313
+
314
+ ```ruby
315
+ class Double < Micro::Case
316
+ attribute :number
317
+
318
+ def call!
319
+ return Failure :invalid, result: { msg: 'number must be a numeric value' } unless number.is_a?(Numeric)
320
+ return Failure :lte_zero, result: { msg: 'number must be greater than 0' } if number <= 0
321
+
322
+ Success result: { number: number * 2 }
323
+ end
324
+ end
325
+
326
+ #================================#
327
+ # Printing the output if success #
328
+ #================================#
329
+
330
+ Double
331
+ .call(number: 3)
332
+ .on_success { |result| p result[:number] }
333
+ .on_failure(:invalid) { |result| raise TypeError, result[:msg] }
334
+ .on_failure(:lte_zero) { |result| raise ArgumentError, result[:msg] }
335
+
336
+ # The output because it is a success:
337
+ # 6
338
+
339
+ #=============================#
340
+ # Raising an error if failure #
341
+ #=============================#
342
+
343
+ Double
344
+ .call(number: -1)
345
+ .on_success { |result| p result[:number] }
346
+ .on_failure { |_result, use_case| puts "#{use_case.class.name} was the use case responsible for the failure" }
347
+ .on_failure(:invalid) { |result| raise TypeError, result[:msg] }
348
+ .on_failure(:lte_zero) { |result| raise ArgumentError, result[:msg] }
349
+
350
+ # The outputs will be:
351
+ #
352
+ # 1. Prints the message: Double was the use case responsible for the failure
353
+ # 2. Raises the exception: ArgumentError (the number must be greater than 0)
354
+
355
+ # Note:
356
+ # ----
357
+ # The use case responsible for the failure will be accessible as the second hook argument
358
+ ```
359
+
360
+ #### Why the failure hook (without a type) exposes result itself?
361
+
362
+ Answer: To allow you to define how to handle the program flow using some
363
+ conditional statement (like an `if`, `case/when`).
364
+
365
+ ```ruby
366
+ class Double < Micro::Case
367
+ attribute :number
368
+
369
+ def call!
370
+ return Failure(:invalid) unless number.is_a?(Numeric)
371
+ return Failure :lte_zero, result: attributes(:number) if number <= 0
372
+
373
+ Success result: { number: number * 2 }
374
+ end
375
+ end
376
+
377
+ #=================================#
378
+ # Using the result type and value #
379
+ #=================================#
380
+
381
+ Double
382
+ .call(-1)
383
+ .on_failure do |result, use_case|
384
+ case result.type
385
+ when :invalid then raise TypeError, "number must be a numeric value"
386
+ when :lte_zero then raise ArgumentError, "number `#{result[:number]}` must be greater than 0"
387
+ else raise NotImplementedError
388
+ end
389
+ end
390
+
391
+ # The output will be the exception:
392
+ #
393
+ # ArgumentError (number `-1` must be greater than 0)
394
+
395
+ #=========================================================#
396
+ # Using decomposition to access the result data and type #
397
+ #=========================================================#
398
+
399
+ # The syntax to decompose an Array can be used in methods, blocks and assigments.
400
+ # If you doesn't know it, check out the Ruby doc:
401
+ # https://ruby-doc.org/core-2.2.0/doc/syntax/assignment_rdoc.html#label-Array+Decomposition
402
+ #
403
+ # The object exposed in the hook failure is a Micro::Case::Result, and it can be decomposed using this syntax. e.g:
404
+
405
+ Double
406
+ .call(-2)
407
+ .on_failure do |(data, type), use_case|
408
+ case type
409
+ when :invalid then raise TypeError, 'number must be a numeric value'
410
+ when :lte_zero then raise ArgumentError, "number `#{data[:number]}` must be greater than 0"
411
+ else raise NotImplementedError
412
+ end
413
+ end
414
+
415
+ # The output will be the exception:
416
+ #
417
+ # ArgumentError (the number `-2` must be greater than 0)
418
+ ```
419
+
420
+ [⬆️ Back to Top](#table-of-contents-)
421
+
422
+ #### What happens if a result hook was declared multiple times?
423
+
424
+ Answer: The hook always will be triggered if it matches the result type.
425
+
426
+ ```ruby
427
+ class Double < Micro::Case
428
+ attributes :number
429
+
430
+ def call!
431
+ if number.is_a?(Numeric)
432
+ Success :computed, result: { number: number * 2 }
433
+ else
434
+ Failure :invalid, result: { msg: 'number must be a numeric value' }
435
+ end
436
+ end
437
+ end
438
+
439
+ result = Double.call(number: 3)
440
+ result.data # { number: 6 }
441
+ result[:number] * 4 # 24
442
+
443
+ accum = 0
444
+
445
+ result.on_success { |result| accum += result[:number] }
446
+ .on_success { |result| accum += result[:number] }
447
+ .on_success(:computed) { |result| accum += result[:number] }
448
+ .on_success(:computed) { |result| accum += result[:number] }
449
+
450
+ accum # 24
451
+
452
+ result[:number] * 4 == accum # true
453
+ ```
454
+
455
+ #### How to use the `Micro::Case::Result#then` method?
456
+
457
+ This method allows you to create dynamic flows, so, with it,
458
+ you can add new use cases or flows to continue the result transformation. e.g:
459
+
460
+ ```ruby
461
+ class ForbidNegativeNumber < Micro::Case
462
+ attribute :number
463
+
464
+ def call!
465
+ return Success result: attributes if number >= 0
466
+
467
+ Failure result: attributes
468
+ end
469
+ end
470
+
471
+ class Add3 < Micro::Case
472
+ attribute :number
473
+
474
+ def call!
475
+ Success result: { number: number + 3 }
476
+ end
477
+ end
478
+
479
+ result1 =
480
+ ForbidNegativeNumber
481
+ .call(number: -1)
482
+ .then(Add3)
483
+
484
+ result1.data # {'number' => -1}
485
+ result1.failure? # true
486
+
487
+ # ---
488
+
489
+ result2 =
490
+ ForbidNegativeNumber
491
+ .call(number: 1)
492
+ .then(Add3)
493
+
494
+ result2.data # {'number' => 4}
495
+ result2.success? # true
496
+ ```
497
+
498
+ > **Note:** this method changes the [`Micro::Case::Result#transitions`](#how-to-understand-what-is-happening-during-a-flow-execution).
499
+
500
+ [⬆️ Back to Top](#table-of-contents-)
501
+
502
+ ##### What does happens when a `Micro::Case::Result#then` receives a block?
503
+
504
+ It will yields self (a `Micro::Case::Result instance`) to the block and return the result of the block. e.g:
505
+
506
+ ```ruby
507
+ class Add < Micro::Case
508
+ attributes :a, :b
509
+
510
+ def call!
511
+ return Success result: { sum: a + b } if Kind.of.Numeric?(a, b)
512
+
513
+ Failure(:attributes_arent_numbers)
514
+ end
515
+ end
516
+
517
+ # --
518
+
519
+ success_result =
520
+ Add
521
+ .call(a: 2, b: 2)
522
+ .then { |result| result.success? ? result[:sum] : 0 }
523
+
524
+ puts success_result # 4
525
+
526
+ # --
527
+
528
+ failure_result =
529
+ Add
530
+ .call(a: 2, b: '2')
531
+ .then { |result| result.success? ? result[:sum] : 0 }
532
+
533
+ puts failure_result # 0
534
+ ```
535
+
536
+ [⬆️ Back to Top](#table-of-contents-)
537
+
538
+ ##### How to make attributes data injection using this feature?
539
+
540
+ Pass a Hash as the second argument of the `Micro::Case::Result#then` method.
541
+
542
+ ```ruby
543
+ Todo::FindAllForUser
544
+ .call(user: current_user, params: params)
545
+ .then(Paginate)
546
+ .then(Serialize::PaginatedRelationAsJson, serializer: Todo::Serializer)
547
+ .on_success { |result| render_json(200, data: result[:todos]) }
548
+ ```
549
+
550
+ [⬆️ Back to Top](#table-of-contents-)
551
+
552
+ ### `Micro::Cases::Flow` - How to compose use cases?
553
+
554
+ In this case, this will be a **flow** (`Micro::Cases::Flow`).
555
+ The main idea of this feature is to use/reuse use cases as steps of a new use case.
556
+
557
+ ```ruby
558
+ module Steps
559
+ class ConvertTextToNumbers < Micro::Case
560
+ attribute :numbers
561
+
562
+ def call!
563
+ if numbers.all? { |value| String(value) =~ /\d+/ }
564
+ Success result: { numbers: numbers.map(&:to_i) }
565
+ else
566
+ Failure result: { message: 'numbers must contain only numeric types' }
567
+ end
568
+ end
569
+ end
570
+
571
+ class Add2 < Micro::Case::Strict
572
+ attribute :numbers
573
+
574
+ def call!
575
+ Success result: { numbers: numbers.map { |number| number + 2 } }
576
+ end
577
+ end
578
+
579
+ class Double < Micro::Case::Strict
580
+ attribute :numbers
581
+
582
+ def call!
583
+ Success result: { numbers: numbers.map { |number| number * 2 } }
584
+ end
585
+ end
586
+
587
+ class Square < Micro::Case::Strict
588
+ attribute :numbers
589
+
590
+ def call!
591
+ Success result: { numbers: numbers.map { |number| number * number } }
592
+ end
593
+ end
594
+ end
595
+
596
+ #-------------------------------------------#
597
+ # Creating a flow using Micro::Cases.flow() #
598
+ #-------------------------------------------#
599
+
600
+ Add2ToAllNumbers = Micro::Cases.flow([
601
+ Steps::ConvertTextToNumbers,
602
+ Steps::Add2
603
+ ])
604
+
605
+ result = Add2ToAllNumbers.call(numbers: %w[1 1 2 2 3 4])
606
+
607
+ result.success? # true
608
+ result.data # {:numbers => [3, 3, 4, 4, 5, 6]}
609
+
610
+ #---------------------------------------------------#
611
+ # An alternative way to create a flow using classes #
612
+ #---------------------------------------------------#
613
+
614
+ class DoubleAllNumbers < Micro::Case
615
+ flow Steps::ConvertTextToNumbers,
616
+ Steps::Double
617
+ end
618
+
619
+ DoubleAllNumbers
620
+ .call(numbers: %w[1 1 b 2 3 4])
621
+ .on_failure { |message| p message } # "numbers must contain only numeric types"
622
+
623
+ # Note:
624
+ # ----
625
+ # When happening a failure, the use case responsible
626
+ # will be accessible in the result
627
+
628
+ result = DoubleAllNumbers.call(numbers: %w[1 1 b 2 3 4])
629
+
630
+ result.failure? # true
631
+ result.use_case.is_a?(Steps::ConvertTextToNumbers) # true
632
+
633
+ result.on_failure do |_message, use_case|
634
+ puts "#{use_case.class.name} was the use case responsible for the failure" # Steps::ConvertTextToNumbers was the use case responsible for the failure
635
+ end
636
+ ```
637
+
638
+ [⬆️ Back to Top](#table-of-contents-)
639
+
640
+ #### Is it possible to compose a use case flow with other ones?
641
+
642
+ Answer: Yes, it is possible.
643
+
644
+ ```ruby
645
+ module Steps
646
+ class ConvertTextToNumbers < Micro::Case
647
+ attribute :numbers
648
+
649
+ def call!
650
+ if numbers.all? { |value| String(value) =~ /\d+/ }
651
+ Success result: { numbers: numbers.map(&:to_i) }
652
+ else
653
+ Failure result: { message: 'numbers must contain only numeric types' }
654
+ end
655
+ end
656
+ end
657
+
658
+ class Add2 < Micro::Case::Strict
659
+ attribute :numbers
660
+
661
+ def call!
662
+ Success result: { numbers: numbers.map { |number| number + 2 } }
663
+ end
664
+ end
665
+
666
+ class Double < Micro::Case::Strict
667
+ attribute :numbers
668
+
669
+ def call!
670
+ Success result: { numbers: numbers.map { |number| number * 2 } }
671
+ end
672
+ end
673
+
674
+ class Square < Micro::Case::Strict
675
+ attribute :numbers
676
+
677
+ def call!
678
+ Success result: { numbers: numbers.map { |number| number * number } }
679
+ end
680
+ end
681
+ end
682
+
683
+ DoubleAllNumbers =
684
+ Micro::Cases.flow([Steps::ConvertTextToNumbers, Steps::Double])
685
+
686
+ SquareAllNumbers =
687
+ Micro::Cases.flow([Steps::ConvertTextToNumbers, Steps::Square])
688
+
689
+ DoubleAllNumbersAndAdd2 =
690
+ Micro::Cases.flow([DoubleAllNumbers, Steps::Add2])
691
+
692
+ SquareAllNumbersAndAdd2 =
693
+ Micro::Cases.flow([SquareAllNumbers, Steps::Add2])
694
+
695
+ SquareAllNumbersAndDouble =
696
+ Micro::Cases.flow([SquareAllNumbersAndAdd2, DoubleAllNumbers])
697
+
698
+ DoubleAllNumbersAndSquareAndAdd2 =
699
+ Micro::Cases.flow([DoubleAllNumbers, SquareAllNumbersAndAdd2])
700
+
701
+ SquareAllNumbersAndDouble
702
+ .call(numbers: %w[1 1 2 2 3 4])
703
+ .on_success { |value| p value[:numbers] } # [6, 6, 12, 12, 22, 36]
704
+
705
+ DoubleAllNumbersAndSquareAndAdd2
706
+ .call(numbers: %w[1 1 2 2 3 4])
707
+ .on_success { |value| p value[:numbers] } # [6, 6, 18, 18, 38, 66]
708
+ ```
709
+
710
+ Note: You can blend any of the [available syntaxes/approaches](#how-to-create-a-flow-which-has-reusable-steps-to-define-a-complex-use-case) to create use case flows - [examples](https://github.com/serradura/u-case/blob/714c6b658fc6aa02617e6833ddee09eddc760f2a/test/micro/cases/flow/blend_test.rb#L5-L35).
711
+
712
+ [⬆️ Back to Top](#table-of-contents-)
713
+
714
+ #### Is it possible a flow accumulates its input and merges each success result to use as the argument of the next use cases?
715
+
716
+ Answer: Yes, it is possible! Look at the example below to understand how the data accumulation works inside of the flow execution.
717
+
718
+ ```ruby
719
+ module Users
720
+ class FindByEmail < Micro::Case
721
+ attribute :email
722
+
723
+ def call!
724
+ user = User.find_by(email: email)
725
+
726
+ return Success result: { user: user } if user
727
+
728
+ Failure(:user_not_found)
729
+ end
730
+ end
731
+ end
732
+
733
+ module Users
734
+ class ValidatePassword < Micro::Case::Strict
735
+ attributes :user, :password
736
+
737
+ def call!
738
+ return Failure(:user_must_be_persisted) if user.new_record?
739
+ return Failure(:wrong_password) if user.wrong_password?(password)
740
+
741
+ return Success result: attributes(:user)
742
+ end
743
+ end
744
+ end
745
+
746
+ module Users
747
+ Authenticate = Micro::Cases.flow([
748
+ FindByEmail,
749
+ ValidatePassword
750
+ ])
751
+ end
752
+
753
+ Users::Authenticate
754
+ .call(email: 'somebody@test.com', password: 'password')
755
+ .on_success { |result| sign_in(result[:user]) }
756
+ .on_failure(:wrong_password) { render status: 401 }
757
+ .on_failure(:user_not_found) { render status: 404 }
758
+ ```
759
+
760
+ First, lets see the attributes used by each use case:
761
+
762
+ ```ruby
763
+ class Users::FindByEmail < Micro::Case
764
+ attribute :email
765
+ end
766
+
767
+ class Users::ValidatePassword < Micro::Case
768
+ attributes :user, :password
769
+ end
770
+ ```
771
+
772
+ As you can see the `Users::ValidatePassword` expects a user as its input. So, how does it receives the user?
773
+ It receives the user from the `Users::FindByEmail` success result!
774
+
775
+ And this, is the power of use cases composition because the output
776
+ of one step will compose the input of the next use case in the flow!
777
+
778
+ > input **>>** process **>>** output
779
+
780
+ > **Note:** Check out these test examples [Micro::Cases::Flow](https://github.com/serradura/u-case/blob/c96a3650469da40dc9f83ff678204055b7015d01/test/micro/cases/flow/result_transitions_test.rb) and [Micro::Cases::Safe::Flow](https://github.com/serradura/u-case/blob/c96a3650469da40dc9f83ff678204055b7015d01/test/micro/cases/safe/flow/result_transitions_test.rb) to see different use cases sharing their own data.
781
+
782
+ [⬆️ Back to Top](#table-of-contents-)
783
+
784
+ #### How to understand what is happening during a flow execution?
785
+
786
+ Use `Micro::Case::Result#transitions`!
787
+
788
+ Let's use the [previous section example](#is-it-possible-a-flow-accumulates-its-input-and-merges-each-success-result-to-use-as-the-argument-of-the-next-use-cases) to ilustrate how to use this feature.
789
+
790
+ ```ruby
791
+ user_authenticated =
792
+ Users::Authenticate.call(email: 'rodrigo@test.com', password: user_password)
793
+
794
+ user_authenticated.transitions
795
+ [
796
+ {
797
+ :use_case => {
798
+ :class => Users::FindByEmail,
799
+ :attributes => { :email => "rodrigo@test.com" }
800
+ },
801
+ :success => {
802
+ :type => :ok,
803
+ :result => {
804
+ :user => #<User:0x00007fb57b1c5f88 @email="rodrigo@test.com" ...>
805
+ }
806
+ },
807
+ :accessible_attributes => [ :email, :password ]
808
+ },
809
+ {
810
+ :use_case => {
811
+ :class => Users::ValidatePassword,
812
+ :attributes => {
813
+ :user => #<User:0x00007fb57b1c5f88 @email="rodrigo@test.com" ...>
814
+ :password => "123456"
815
+ }
816
+ },
817
+ :success => {
818
+ :type => :ok,
819
+ :result => {
820
+ :user => #<User:0x00007fb57b1c5f88 @email="rodrigo@test.com" ...>
821
+ }
822
+ },
823
+ :accessible_attributes => [ :email, :password, :user ]
824
+ }
825
+ ]
826
+ ```
827
+
828
+ The example above shows the output generated by the `Micro::Case::Result#transitions`.
829
+ With it is possible to analyze the use cases execution order and what were the given `inputs` (`[:attributes]`) and `outputs` (`[:success][:result]`) in the entire execution.
830
+
831
+ And look up the `accessible_attributes` property, it shows whats attributes are accessible in that flow step. For example, in the last step, you can see that the `accessible_attributes` increased because of the [data flow accumulation](#is-it-possible-a-flow-accumulates-its-input-and-merges-each-success-result-to-use-as-the-argument-of-the-next-use-cases).
832
+
833
+ > **Note:** The [`Micro::Case::Result#then`](#how-to-use-the-microcaseresultthen-method) increments the `Micro::Case::Result#transitions`.
834
+
835
+ PS: Use the `Micro::Case::Result.disable_transition_tracking` feature toggle to disable this feature (use once, because it is global) and increase the use cases' performance.
836
+
837
+ ##### `Micro::Case::Result#transitions` schema
838
+ ```ruby
839
+ [
840
+ {
841
+ use_case: {
842
+ class: <Micro::Case>,# Use case which was executed
843
+ attributes: <Hash> # (Input) The use case's attributes
844
+ },
845
+ [success:, failure:] => { # (Output)
846
+ type: <Symbol>, # Result type. Defaults:
847
+ # Success = :ok, Failure = :error/:exception
848
+ result: <Hash> # The data returned by the use case
849
+ },
850
+ accessible_attributes: <Array>, # Properties that can be accessed by the use case's attributes,
851
+ # starting with Hash used to invoke it and which are incremented
852
+ # with each result value of the flow's use cases.
853
+ }
854
+ ]
855
+ ```
856
+
857
+ #### Is it possible to declare a flow which includes the use case itself?
858
+
859
+ Answer: Yes, it is! You can use the `self.call!` macro. e.g:
860
+
861
+ ```ruby
862
+ class ConvertTextToNumber < Micro::Case
863
+ attribute :text
864
+
865
+ def call!
866
+ Success result: { number: text.to_i }
867
+ end
868
+ end
869
+
870
+ class ConvertNumberToText < Micro::Case
871
+ attribute :number
872
+
873
+ def call!
874
+ Success result: { text: number.to_s }
875
+ end
876
+ end
877
+
878
+ class Double < Micro::Case
879
+ flow ConvertTextToNumber,
880
+ self.call!,
881
+ ConvertNumberToText
882
+
883
+ attribute :number
884
+
885
+ def call!
886
+ Success result: { number: number * 2 }
887
+ end
888
+ end
889
+
890
+ result = Double.call(text: '4')
891
+
892
+ result.success? # true
893
+ result[:number] # "8"
894
+
895
+ # NOTE: This feature can be used with the Micro::Case::Safe.
896
+ # Checkout this test: https://github.com/serradura/u-case/blob/714c6b658fc6aa02617e6833ddee09eddc760f2a/test/micro/case/safe/with_inner_flow_test.rb
897
+ ```
898
+
899
+ [⬆️ Back to Top](#table-of-contents-)
900
+
901
+ ### `Micro::Case::Strict` - What is a strict use case?
902
+
903
+ Answer: Is a use case which will require all the keywords (attributes) on its initialization.
904
+
905
+ ```ruby
906
+ class Double < Micro::Case::Strict
907
+ attribute :numbers
908
+
909
+ def call!
910
+ Success result: { numbers: numbers.map { |number| number * 2 } }
911
+ end
912
+ end
913
+
914
+ Double.call({})
915
+
916
+ # The output will be the following exception:
917
+ # ArgumentError (missing keyword: :numbers)
918
+ ```
919
+
920
+ [⬆️ Back to Top](#table-of-contents-)
921
+
922
+ ### `Micro::Case::Safe` - Is there some feature to auto handle exceptions inside of a use case or flow?
923
+
924
+ Answer: Yes, there is one!
925
+
926
+ **Use cases:**
927
+
928
+ Like `Micro::Case::Strict` the `Micro::Case::Safe` is another kind of use case. It has the ability to auto intercept any exception as a failure result. e.g:
929
+
930
+ ```ruby
931
+ require 'logger'
932
+
933
+ AppLogger = Logger.new(STDOUT)
934
+
935
+ class Divide < Micro::Case::Safe
936
+ attributes :a, :b
937
+
938
+ def call!
939
+ if a.is_a?(Integer) && b.is_a?(Integer)
940
+ Success result: { number: a / b}
941
+ else
942
+ Failure(:not_an_integer)
943
+ end
944
+ end
945
+ end
946
+
947
+ result = Divide.call(a: 2, b: 0)
948
+ result.type == :exception # true
949
+ result.data # { exception: #<ZeroDivisionError...> }
950
+ result[:exception].is_a?(ZeroDivisionError) # true
951
+
952
+ result.on_failure(:exception) do |exception|
953
+ AppLogger.error(exception.message) # E, [2019-08-21T00:05:44.195506 #9532] ERROR -- : divided by 0
954
+ end
955
+
956
+ # Note:
957
+ # ----
958
+ # If you need to handle a specific error,
959
+ # I recommend the usage of a case statement. e,g:
960
+
961
+ result.on_failure(:exception) do |exception, use_case|
962
+ case exception
963
+ when ZeroDivisionError then AppLogger.error(exception.message)
964
+ else AppLogger.debug("#{use_case.class.name} was the use case responsible for the exception")
965
+ end
966
+ end
967
+
968
+ # Another note:
969
+ # ------------
970
+ # It is possible to rescue an exception even when is a safe use case.
971
+ # Examples: https://github.com/serradura/u-case/blob/714c6b658fc6aa02617e6833ddee09eddc760f2a/test/micro/case/safe_test.rb#L90-L118
972
+ ```
973
+
974
+ [⬆️ Back to Top](#table-of-contents-)
975
+
976
+ #### `Micro::Cases::Safe::Flow`
977
+
978
+ As the safe use cases, safe flows can intercept an exception in any of its steps. These are the ways to define one:
979
+
980
+ ```ruby
981
+ module Users
982
+ Create = Micro::Cases.safe_flow([
983
+ ProcessParams,
984
+ ValidateParams,
985
+ Persist,
986
+ SendToCRM
987
+ ])
988
+ end
989
+
990
+ # or within classes
991
+
992
+ module Users
993
+ class Create < Micro::Case::Safe
994
+ flow ProcessParams,
995
+ ValidateParams,
996
+ Persist,
997
+ SendToCRM
998
+ end
999
+ end
1000
+ ```
1001
+
1002
+ [⬆️ Back to Top](#table-of-contents-)
1003
+
1004
+ #### `Micro::Case::Result#on_exception`
1005
+
1006
+ In functional programming errors/exceptions are handled as regular data, the idea is to transform the output even when it happens an unexpected behavior. For many, [exceptions are very similar to the GOTO statement](https://softwareengineering.stackexchange.com/questions/189222/are-exceptions-as-control-flow-considered-a-serious-antipattern-if-so-why), jumping the application flow to paths which could be difficult to figure out how things work in a system.
1007
+
1008
+ To address this the `Micro::Case::Result` has a special hook `#on_exception` to helping you to handle the control flow in the case of exceptions.
1009
+
1010
+ > **Note**: this feature will work better if you use it with a `Micro::Case::Safe` use case/flow.
1011
+
1012
+ How does it work?
1013
+
1014
+ ```ruby
1015
+ class Divide < Micro::Case::Safe
1016
+ attributes :a, :b
1017
+
1018
+ def call!
1019
+ Success result: { division: a / b }
1020
+ end
1021
+ end
1022
+
1023
+ Divide
1024
+ .call(a: 2, b: 0)
1025
+ .on_success { |result| puts result[:division] }
1026
+ .on_exception(TypeError) { puts 'Please, use only numeric attributes.' }
1027
+ .on_exception(ZeroDivisionError) { |_error| puts "Can't divide a number by 0." }
1028
+ .on_exception { |_error, _use_case| puts 'Oh no, something went wrong!' }
1029
+
1030
+ # Output:
1031
+ # -------
1032
+ # Can't divide a number by 0
1033
+ # Oh no, something went wrong!
1034
+
1035
+ Divide.
1036
+ .call(a: 2, b: '2').
1037
+ .on_success { |result| puts result[:division] }
1038
+ .on_exception(TypeError) { puts 'Please, use only numeric attributes.' }
1039
+ .on_exception(ZeroDivisionError) { |_error| puts "Can't divide a number by 0." }
1040
+ .on_exception { |_error, _use_case| puts 'Oh no, something went wrong!' }
1041
+
1042
+ # Output:
1043
+ # -------
1044
+ # Please, use only numeric attributes.
1045
+ # Oh no, something went wrong!
1046
+ ```
1047
+
1048
+ As you can see, this hook has the same behavior of `result.on_failure(:exception)`, but, the ideia here is to have a better communication in the code, making an explicit reference when some failure happened because of an exception.
1049
+
1050
+ [⬆️ Back to Top](#table-of-contents-)
1051
+
1052
+ ### `u-case/with_activemodel_validation` - How to validate use case attributes?
1053
+
1054
+ **Requirement:**
1055
+
1056
+ To do this your application must have the [activemodel >= 3.2, < 6.1.0](https://rubygems.org/gems/activemodel) as a dependency.
1057
+
1058
+ ```ruby
1059
+ #
1060
+ # By default, if your application has the activemodel as a dependency,
1061
+ # any kind of use case can use it to validate their attributes.
1062
+ #
1063
+ class Multiply < Micro::Case
1064
+ attributes :a, :b
1065
+
1066
+ validates :a, :b, presence: true, numericality: true
1067
+
1068
+ def call!
1069
+ return Failure :validation_error, result: { errors: self.errors } if invalid?
1070
+
1071
+ Success result: { number: a * b }
1072
+ end
1073
+ end
1074
+
1075
+ #
1076
+ # But if do you want an automatic way to fail
1077
+ # your use cases on validation errors, you can use:
1078
+
1079
+ # In some file. e.g: A Rails initializer
1080
+ require 'u-case/with_activemodel_validation' # or require 'micro/case/with_validation'
1081
+
1082
+ # In the Gemfile
1083
+ gem 'u-case', require: 'u-case/with_activemodel_validation'
1084
+
1085
+ # Using this approach, you can rewrite the previous example with less code. e.g:
1086
+
1087
+ class Multiply < Micro::Case
1088
+ attributes :a, :b
1089
+
1090
+ validates :a, :b, presence: true, numericality: true
1091
+
1092
+ def call!
1093
+ Success result: { number: a * b }
1094
+ end
1095
+ end
1096
+
1097
+ # Note:
1098
+ # ----
1099
+ # After requiring the validation mode, the
1100
+ # Micro::Case::Strict and Micro::Case::Safe classes will inherit this new behavior.
1101
+ ```
1102
+
1103
+ #### If I enabled the auto validation, is it possible to disable it only in specific use case classes?
1104
+
1105
+ Answer: Yes, it is possible. To do this, you only need to use the `disable_auto_validation` macro. e.g:
1106
+
1107
+ ```ruby
1108
+ require 'u-case/with_activemodel_validation'
1109
+
1110
+ class Multiply < Micro::Case
1111
+ disable_auto_validation
1112
+
1113
+ attribute :a
1114
+ attribute :b
1115
+ validates :a, :b, presence: true, numericality: true
1116
+
1117
+ def call!
1118
+ Success result: { number: a * b }
1119
+ end
1120
+ end
1121
+
1122
+ Multiply.call(a: 2, b: 'a')
1123
+
1124
+ # The output will be the following exception:
1125
+ # TypeError (String can't be coerced into Integer)
1126
+ ```
1127
+
1128
+ [⬆️ Back to Top](#table-of-contents-)
1129
+
1130
+ #### `Kind::Validator`
1131
+
1132
+ The [kind gem](https://github.com/serradura/kind) has a module to enable the validation of data type through [`ActiveModel validations`](https://guides.rubyonrails.org/active_model_basics.html#validations). So, when you require the `'u-case/with_activemodel_validation'`, this module will require the [`Kind::Validator`](https://github.com/serradura/kind#kindvalidator-activemodelvalidations).
1133
+
1134
+ The example below shows how to validate the attributes data types.
1135
+
1136
+ ```ruby
1137
+ class Todo::List::AddItem < Micro::Case
1138
+ attributes :user, :params
1139
+
1140
+ validates :user, kind: User
1141
+ validates :params, kind: ActionController::Parameters
1142
+
1143
+ def call!
1144
+ todo_params = params.require(:todo).permit(:title, :due_at)
1145
+
1146
+ todo = user.todos.create(todo_params)
1147
+
1148
+ Success result: { todo: todo }
1149
+ rescue ActionController::ParameterMissing => e
1150
+ Failure :parameter_missing, result: { message: e.message }
1151
+ end
1152
+ end
1153
+ ```
1154
+
1155
+ ## Benchmarks
1156
+
1157
+ ### `Micro::Case` (v2.6.0)
1158
+
1159
+ #### Best overall
1160
+
1161
+ The table below contains the average between the [Success results](#success-results) and [Failure results](#failure-results) benchmarks.
1162
+
1163
+ | Gem / Abstraction | Iterations per second | Comparison |
1164
+ | ---------------------- | --------------------: | ----------------: |
1165
+ | **Micro::Case** | 105124.3 | _**The Fastest**_ |
1166
+ | Dry::Monads | 103290.1 | 0.02x slower |
1167
+ | Interactor | 21342.3 | 4.93x slower |
1168
+ | Trailblazer::Operation | 14652.7 | 7.17x slower |
1169
+ | Dry::Transaction | 5310.3 | 19.80x slower |
1170
+
1171
+ ---
1172
+
1173
+ #### Success results
1174
+
1175
+ | Gem / Abstraction | Iterations per second | Comparison |
1176
+ | ----------------- | --------------------: | ----------------: |
1177
+ | Dry::Monads | 134801.0 | _**The Fastest**_ |
1178
+ | **Micro::Case** | 105909.2 | 1.27x slower |
1179
+ | Interactor | 29458.2 | 4.58x slower |
1180
+ | Trailblazer::Operation | 14714.9 | 9.16x slower |
1181
+ | Dry::Transaction | 5642.6 | 28.89x slower |
1182
+
1183
+ <details>
1184
+ <summary>Show the full <a href="https://github.com/evanphx/benchmark-ips">benchmark/ips</a> results.</summary>
1185
+
1186
+ ```ruby
1187
+ # Warming up --------------------------------------
1188
+ # Interactor 2.897k i/100ms
1189
+ # Trailblazer::Operation 1.494k i/100ms
1190
+ # Dry::Monads 13.854k i/100ms
1191
+ # Dry::Transaction 561.000 i/100ms
1192
+ # Micro::Case 10.523k i/100ms
1193
+ # Micro::Case::Strict 7.982k i/100ms
1194
+ # Micro::Case::Safe 10.568k i/100ms
1195
+
1196
+ # Calculating -------------------------------------
1197
+ # Interactor 29.458k (± 3.4%) i/s - 147.747k in 5.021405s
1198
+ # Trailblazer::Operation 14.715k (± 1.8%) i/s - 74.700k in 5.078128s
1199
+ # Dry::Monads 134.801k (± 8.7%) i/s - 678.846k in 5.088739s
1200
+ # Dry::Transaction 5.643k (± 2.1%) i/s - 28.611k in 5.072969s
1201
+ # Micro::Case 105.909k (± 2.4%) i/s - 536.673k in 5.070329s
1202
+ # Micro::Case::Strict 84.234k (± 1.5%) i/s - 423.046k in 5.023447s
1203
+ # Micro::Case::Safe 105.725k (± 1.9%) i/s - 538.968k in 5.099817s
1204
+
1205
+ # Comparison:
1206
+ # Dry::Monads: 134801.0 i/s
1207
+ # Micro::Case: 105909.2 i/s - 1.27x (± 0.00) slower
1208
+ # Micro::Case::Safe: 105725.0 i/s - 1.28x (± 0.00) slower
1209
+ # Micro::Case::Strict: 84234.4 i/s - 1.60x (± 0.00) slower
1210
+ # Interactor: 29458.2 i/s - 4.58x (± 0.00) slower
1211
+ # Trailblazer::Operation: 14714.9 i/s - 9.16x (± 0.00) slower
1212
+ # Dry::Transaction: 5642.6 i/s - 23.89x (± 0.00) slower
1213
+ ```
1214
+ </details>
1215
+
1216
+ https://github.com/serradura/u-case/blob/master/benchmarks/use_case/with_success_result.rb
1217
+
1218
+ #### Failure results
1219
+
1220
+ | Gem / Abstraction | Iterations per second | Comparison |
1221
+ | ----------------- | --------------------: | ----------------: |
1222
+ | **Micro::Case** | 104339.4 | _**The Fastest**_ |
1223
+ | Dry::Monads | 71779.2 | 1.45x slower |
1224
+ | Trailblazer::Operation | 14590.6 | 7.15x slower |
1225
+ | Interactor | 13226.5 | 7.89x slower |
1226
+ | Dry::Transaction | 4978.1 | 20.96x slower |
1227
+
1228
+ <details>
1229
+ <summary>Show the full <a href="https://github.com/evanphx/benchmark-ips">benchmark/ips</a> results.</summary>
1230
+
1231
+ ```ruby
1232
+ # Warming up --------------------------------------
1233
+ # Interactor 1.339k i/100ms
1234
+ # Trailblazer::Operation 1.393k i/100ms
1235
+ # Dry::Monads 7.208k i/100ms
1236
+ # Dry::Transaction 423.000 i/100ms
1237
+ # Micro::Case 9.620k i/100ms
1238
+ # Micro::Case::Strict 8.238k i/100ms
1239
+ # Micro::Case::Safe 9.906k i/100ms
1240
+
1241
+ # Calculating -------------------------------------
1242
+ # Interactor 13.227k (± 3.3%) i/s - 66.950k in 5.067145s
1243
+ # Trailblazer::Operation 14.591k (± 4.0%) i/s - 73.829k in 5.069162s
1244
+ # Dry::Monads 71.779k (± 2.5%) i/s - 360.400k in 5.024294s
1245
+ # Dry::Transaction 4.978k (± 3.3%) i/s - 24.957k in 5.019153s
1246
+ # Micro::Case 103.957k (± 1.8%) i/s - 529.100k in 5.091221s
1247
+ # Micro::Case::Strict 83.094k (± 2.0%) i/s - 420.138k in 5.058233s
1248
+ # Micro::Case::Safe 104.339k (± 1.7%) i/s - 525.018k in 5.033381s
1249
+
1250
+ # Comparison:
1251
+ # Micro::Case::Safe: 104339.4 i/s
1252
+ # Micro::Case: 103957.2 i/s - same-ish: difference falls within error
1253
+ # Micro::Case::Strict: 83094.5 i/s - 1.26x (± 0.00) slower
1254
+ # Dry::Monads: 71779.2 i/s - 1.45x (± 0.00) slower
1255
+ # Trailblazer::Operation: 14590.6 i/s - 7.15x (± 0.00) slower
1256
+ # Interactor: 13226.5 i/s - 7.89x (± 0.00) slower
1257
+ # Dry::Transaction: 4978.1 i/s - 20.96x (± 0.00) slower
1258
+ ```
1259
+ </details>
1260
+
1261
+ https://github.com/serradura/u-case/blob/master/benchmarks/use_case/with_failure_result.rb
1262
+
1263
+ ---
1264
+
1265
+ ### `Micro::Case::Flow` (v2.6.0)
1266
+
1267
+ | Gems / Abstraction | [Success results](https://github.com/serradura/u-case/blob/master/benchmarks/flow/with_success_result.rb#L40) | [Failure results](https://github.com/serradura/u-case/blob/master/benchmarks/flow/with_failure_result.rb#L40) |
1268
+ | ------------------ | ----------------: | ----------------: |
1269
+ | Micro::Case::Flow | _**The Fastest**_ | _**The Fastest**_ |
1270
+ | Micro::Case::Safe::Flow | 0x slower | 0x slower |
1271
+ | Interactor::Organizer | 1.27x slower | 5.48x slower |
1272
+
1273
+ \* The `Dry::Monads`, `Dry::Transaction`, `Trailblazer::Operation` are out of this analysis because all of them doesn't have this kind of feature.
1274
+
1275
+ <details>
1276
+ <summary><strong>Success results</strong> - Show the full benchmark/ips results.</summary>
1277
+
1278
+ ```ruby
1279
+ # Warming up --------------------------------------
1280
+ # Interactor::Organizer 4.765k i/100ms
1281
+ # Micro::Case::Flow 5.372k i/100ms
1282
+ # Micro::Case::Safe::Flow 5.855k i/100ms
1283
+ # Calculating -------------------------------------
1284
+ # Interactor::Organizer 48.598k (± 5.2%) i/s - 243.015k in 5.014307s
1285
+ # Micro::Case::Flow 61.606k (± 4.4%) i/s - 311.576k in 5.068602s
1286
+ # Micro::Case::Safe::Flow 60.688k (± 4.8%) i/s - 304.460k in 5.028877s
1287
+
1288
+ # Comparison:
1289
+ # Micro::Case::Flow: 61606.3 i/s
1290
+ # Micro::Case::Safe::Flow: 60688.3 i/s - same-ish: difference falls within error
1291
+ # Interactor::Organizer: 48598.2 i/s - 1.27x slower\
1292
+ ```
1293
+ </details>
1294
+
1295
+ <details>
1296
+ <summary><strong>Failure results</strong> - Show the full benchmark/ips results.</summary>
1297
+
1298
+ ```ruby
1299
+ # Warming up --------------------------------------
1300
+ # Interactor::Organizer 2.209k i/100ms
1301
+ # Micro::Case::Flow 11.508k i/100ms
1302
+ # Micro::Case::Safe::Flow 11.605k i/100ms
1303
+
1304
+ # Calculating -------------------------------------
1305
+ # Interactor::Organizer 22.592k (± 2.8%) i/s - 114.868k in 5.088685s
1306
+ # Micro::Case::Flow 123.629k (± 2.9%) i/s - 621.432k in 5.030844s
1307
+ # Micro::Case::Safe::Flow 123.862k (± 3.0%) i/s - 626.670k in 5.064097s
1308
+
1309
+ # Comparison:
1310
+ # Micro::Case::Safe::Flow: 123862.4 i/s
1311
+ # Micro::Case::Flow: 123629.3 i/s - same-ish: difference falls within error
1312
+ # Interactor::Organizer: 22592.2 i/s - 5.48x slower
1313
+ ```
1314
+ </details>
1315
+
1316
+ https://github.com/serradura/u-case/tree/master/benchmarks/flow
1317
+
1318
+ ### Comparisons
1319
+
1320
+ Check it out implementations of the same use case with different gems/abstractions.
1321
+
1322
+ * [interactor](https://github.com/serradura/u-case/blob/master/comparisons/interactor.rb)
1323
+ * [u-case](https://github.com/serradura/u-case/blob/master/comparisons/u-case.rb)
1324
+
1325
+ [⬆️ Back to Top](#table-of-contents-)
1326
+
1327
+ ## Examples
1328
+
1329
+ ### 1️⃣ Rails App (API)
1330
+
1331
+ > This project shows different kinds of architecture (one per commit), and in the last one, how to use the Micro::Case gem to handle the application business logic.
1332
+ >
1333
+ > Link: https://github.com/serradura/from-fat-controllers-to-use-cases
1334
+
1335
+ ### 2️⃣ CLI calculator
1336
+
1337
+ > Rake tasks to demonstrate how to handle user data, and how to use different failure types to control the program flow.
1338
+ >
1339
+ > Link: https://github.com/serradura/u-case/tree/master/examples/calculator
1340
+
1341
+ ### 3️⃣ Users creation
1342
+
1343
+ > An example of a use case flow that define steps to sanitize, validate, and persist its input data.
1344
+ >
1345
+ > Link: https://github.com/serradura/u-case/blob/master/examples/users_creation.rb
1346
+
1347
+ ### 4️⃣ Rescuing exception inside of the use cases
1348
+
1349
+ > Link: https://github.com/serradura/u-case/blob/master/examples/rescuing_exceptions.rb
1350
+
1351
+ [⬆️ Back to Top](#table-of-contents-)
1352
+
1353
+ ## Development
1354
+
1355
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `./test.sh` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
1356
+
1357
+ 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 tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
1358
+
1359
+ ## Contributing
1360
+
1361
+ Bug reports and pull requests are welcome on GitHub at https://github.com/serradura/u-case. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
1362
+
1363
+ ## License
1364
+
1365
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
1366
+
1367
+ ## Code of Conduct
1368
+
1369
+ Everyone interacting in the Micro::Case project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/serradura/u-case/blob/master/CODE_OF_CONDUCT.md).