u-case 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 689ea11edc3c546b2fc21624671ec9259a6b890410c3bfe17e020584c61cbb75
4
+ data.tar.gz: 9606d1ec23bd3bd66c5b74ddccf6561a55429a426b4b0076540c71caf606638b
5
+ SHA512:
6
+ metadata.gz: e35700ce285b06f66f33051db87e2b03d7e60eaf7483d4952f4dca67cea77d0ae584ef406c7235d074bb58bbcb474f2748f1f6ce7512179028277a16e42dfe14
7
+ data.tar.gz: 0f238c86984f517e243ac148a5273cc294e3e80d01dcad22799e6a7fb865291ee6f802a172481fdbb89bec29f4d0563ef821cd617d0856a27c0368ad5ddc8307
@@ -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.3
@@ -0,0 +1,13 @@
1
+ #!/bin/bash
2
+
3
+ bundle exec rake test
4
+
5
+ ruby_v=$(ruby -v)
6
+
7
+ ACTIVEMODEL_VERSION='3.2' bundle update
8
+ ACTIVEMODEL_VERSION='3.2' bundle exec rake test
9
+
10
+ if [[ ! $ruby_v =~ '2.2.0' ]]; then
11
+ ACTIVEMODEL_VERSION='5.2' bundle update
12
+ ACTIVEMODEL_VERSION='5.2' bundle exec rake test
13
+ fi
@@ -0,0 +1,29 @@
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
+
13
+ cache: bundler
14
+
15
+ before_install:
16
+ - gem uninstall -v '>= 2' -i $(rvm gemdir)@global -ax bundler || true
17
+ - gem install bundler -v '< 2'
18
+
19
+ install: bundle install --jobs=3 --retry=3
20
+
21
+ before_script:
22
+ - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
23
+ - chmod +x ./cc-test-reporter
24
+ - "./cc-test-reporter before-build"
25
+
26
+ script: "./.travis.sh"
27
+
28
+ after_success:
29
+ - "./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,23 @@
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')
6
+
7
+ activemodel = case activemodel_version
8
+ when '3.2' then '3.2.22'
9
+ when '5.2' then '5.2.3'
10
+ end
11
+
12
+ if activemodel_version < '6.1'
13
+ gem 'activemodel', activemodel, require: false
14
+ gem 'activesupport', activemodel, require: false
15
+ end
16
+
17
+ group :test do
18
+ gem 'minitest', activemodel_version < '4.1' ? '~> 4.2' : '~> 5.0'
19
+ gem 'simplecov', require: false
20
+ end
21
+
22
+ # Specify your gem's dependencies in u-case.gemspec
23
+ 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,704 @@
1
+ [![Gem](https://img.shields.io/gem/v/u-case.svg?style=flat-square)](https://rubygems.org/gems/u-case)
2
+ [![Build Status](https://travis-ci.com/serradura/u-case.svg?branch=master)](https://travis-ci.com/serradura/u-case)
3
+ [![Maintainability](https://api.codeclimate.com/v1/badges/a30b18528a317435c2ee/maintainability)](https://codeclimate.com/github/serradura/u-case/maintainability)
4
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/a30b18528a317435c2ee/test_coverage)](https://codeclimate.com/github/serradura/u-case/test_coverage)
5
+
6
+ μ-case (Micro::Case)
7
+ ==========================
8
+
9
+ Create simple and powerful use cases as objects (aka: service objects).
10
+
11
+ The main goals of this project are:
12
+ 1. Be simple to use and easy to learn (input **>>** process/transform **>>** output).
13
+ 2. Referential transparency and data integrity.
14
+ 3. No callbacks (before, after, around...).
15
+ 4. Represent complex business logic using a composition of use cases.
16
+
17
+ ## Table of Contents <!-- omit in toc -->
18
+ - [μ-case (Micro::Case)](#%ce%bc-case-microcase)
19
+ - [Required Ruby version](#required-ruby-version)
20
+ - [Installation](#installation)
21
+ - [Usage](#usage)
22
+ - [How to define a use case?](#how-to-define-a-use-case)
23
+ - [What is a `Micro::Case::Result`?](#what-is-a-microcaseresult)
24
+ - [What are the default `Micro::Case::Result` types?](#what-are-the-default-microcaseresult-types)
25
+ - [How to define custom result types?](#how-to-define-custom-result-types)
26
+ - [Is it possible to define a custom result type without a block?](#is-it-possible-to-define-a-custom-result-type-without-a-block)
27
+ - [How to use the result hooks?](#how-to-use-the-result-hooks)
28
+ - [What happens if a result hook is declared multiple times?](#what-happens-if-a-result-hook-is-declared-multiple-times)
29
+ - [How to compose uses cases to represents complex ones?](#how-to-compose-uses-cases-to-represents-complex-ones)
30
+ - [Is it possible to compose a use case flow with other ones?](#is-it-possible-to-compose-a-use-case-flow-with-other-ones)
31
+ - [What is a strict use case?](#what-is-a-strict-use-case)
32
+ - [Is there some feature to auto handle exceptions inside of a use case or flow?](#is-there-some-feature-to-auto-handle-exceptions-inside-of-a-use-case-or-flow)
33
+ - [How to validate use case attributes?](#how-to-validate-use-case-attributes)
34
+ - [Examples](#examples)
35
+ - [Comparisons](#comparisons)
36
+ - [Benchmarks](#benchmarks)
37
+ - [Development](#development)
38
+ - [Contributing](#contributing)
39
+ - [License](#license)
40
+ - [Code of Conduct](#code-of-conduct)
41
+
42
+ ## Required Ruby version
43
+
44
+ > \>= 2.2.0
45
+
46
+ ## Installation
47
+
48
+ Add this line to your application's Gemfile:
49
+
50
+ ```ruby
51
+ gem 'u-case'
52
+ ```
53
+
54
+ And then execute:
55
+
56
+ $ bundle
57
+
58
+ Or install it yourself as:
59
+
60
+ $ gem install u-case
61
+
62
+ ## Usage
63
+
64
+ ### How to define a use case?
65
+
66
+ ```ruby
67
+ class Multiply < Micro::Case::Base
68
+ # 1. Define its input as attributes
69
+ attributes :a, :b
70
+
71
+ # 2. Define the method `call!` with its business logic
72
+ def call!
73
+
74
+ # 3. Wrap the use case result/output using the `Success()` and `Failure()` methods
75
+ if a.is_a?(Numeric) && b.is_a?(Numeric)
76
+ Success(a * b)
77
+ else
78
+ Failure { '`a` and `b` attributes must be numeric' }
79
+ end
80
+ end
81
+ end
82
+
83
+ #==========================#
84
+ # Calling a use case class #
85
+ #==========================#
86
+
87
+ # Success result
88
+
89
+ result = Multiply.call(a: 2, b: 2)
90
+
91
+ result.success? # true
92
+ result.value # 4
93
+
94
+ # Failure result
95
+
96
+ bad_result = Multiply.call(a: 2, b: '2')
97
+
98
+ bad_result.failure? # true
99
+ bad_result.value # "`a` and `b` attributes must be numeric"
100
+
101
+ #-----------------------------#
102
+ # Calling a use case instance #
103
+ #-----------------------------#
104
+
105
+ result = Multiply.new(a: 2, b: 3).call
106
+
107
+ result.value # 6
108
+
109
+ # Note:
110
+ # ----
111
+ # The result of a Micro::Case::Base.call
112
+ # is an instance of Micro::Case::Result
113
+ ```
114
+
115
+ [⬆️ Back to Top](#table-of-contents-)
116
+
117
+ ### What is a `Micro::Case::Result`?
118
+
119
+ A `Micro::Case::Result` stores use cases output data. These are their main methods:
120
+ - `#success?` returns true if is a successful result.
121
+ - `#failure?` returns true if is an unsuccessful result.
122
+ - `#value` the result value itself.
123
+ - `#type` a Symbol which gives meaning for the result, this is useful to declare different types of failures or success.
124
+ - `#on_success` or `#on_failure` are hook methods which help you define the application flow.
125
+ - `#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).
126
+
127
+ [⬆️ Back to Top](#table-of-contents-)
128
+
129
+ #### What are the default `Micro::Case::Result` types?
130
+
131
+ Every result has a type and these are the defaults:
132
+ - `:ok` when success
133
+ - `:error`/`:exception` when failures
134
+
135
+ ```ruby
136
+ class Divide < Micro::Case::Base
137
+ attributes :a, :b
138
+
139
+ def call!
140
+ invalid_attributes.empty? ? Success(a / b) : Failure(invalid_attributes)
141
+ rescue => e
142
+ Failure(e)
143
+ end
144
+
145
+ private def invalid_attributes
146
+ attributes.select { |_key, value| !value.is_a?(Numeric) }
147
+ end
148
+ end
149
+
150
+ # Success result
151
+
152
+ result = Divide.call(a: 2, b: 2)
153
+
154
+ result.type # :ok
155
+ result.value # 1
156
+ result.success? # true
157
+ result.use_case # raises `Micro::Case::Error::InvalidAccessToTheUseCaseObject: only a failure result can access its own use case`
158
+
159
+ # Failure result (type == :error)
160
+
161
+ bad_result = Divide.call(a: 2, b: '2')
162
+
163
+ bad_result.type # :error
164
+ bad_result.value # {"b"=>"2"}
165
+ bad_result.failure? # true
166
+ 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>>
167
+
168
+ # Failure result (type == :exception)
169
+
170
+ err_result = Divide.call(a: 2, b: 0)
171
+
172
+ err_result.type # :exception
173
+ err_result.value # <ZeroDivisionError: divided by 0>
174
+ err_result.failure? # true
175
+ 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>>
176
+
177
+ # Note:
178
+ # ----
179
+ # Any Exception instance which is wrapped by
180
+ # the Failure() method will receive `:exception` instead of the `:error` type.
181
+ ```
182
+
183
+ [⬆️ Back to Top](#table-of-contents-)
184
+
185
+ #### How to define custom result types?
186
+
187
+ Answer: Use a symbol as the argument of `Success()`, `Failure()` methods and declare a block to set their values.
188
+
189
+ ```ruby
190
+ class Multiply < Micro::Case::Base
191
+ attributes :a, :b
192
+
193
+ def call!
194
+ return Success(a * b) if a.is_a?(Numeric) && b.is_a?(Numeric)
195
+
196
+ Failure(:invalid_data) do
197
+ attributes.reject { |_, input| input.is_a?(Numeric) }
198
+ end
199
+ end
200
+ end
201
+
202
+ # Success result
203
+
204
+ result = Multiply.call(a: 3, b: 2)
205
+
206
+ result.type # :ok
207
+ result.value # 6
208
+ result.success? # true
209
+
210
+ # Failure result
211
+
212
+ bad_result = Multiply.call(a: 3, b: '2')
213
+
214
+ bad_result.type # :invalid_data
215
+ bad_result.value # {"b"=>"2"}
216
+ bad_result.failure? # true
217
+ ```
218
+
219
+ [⬆️ Back to Top](#table-of-contents-)
220
+
221
+ #### Is it possible to define a custom result type without a block?
222
+
223
+ Answer: Yes, it is. But only for failure results!
224
+
225
+ ```ruby
226
+ class Multiply < Micro::Case::Base
227
+ attributes :a, :b
228
+
229
+ def call!
230
+ return Failure(:invalid_data) unless a.is_a?(Numeric) && b.is_a?(Numeric)
231
+
232
+ Success(a * b)
233
+ end
234
+ end
235
+
236
+ result = Multiply.call(a: 2, b: '2')
237
+
238
+ result.failure? # true
239
+ result.value # :invalid_data
240
+ result.type # :invalid_data
241
+ result.use_case.attributes # {"a"=>2, "b"=>"2"}
242
+
243
+ # Note:
244
+ # ----
245
+ # This feature is handy to handle failures in a flow
246
+ # (this topic will be covered ahead).
247
+ ```
248
+
249
+ [⬆️ Back to Top](#table-of-contents-)
250
+
251
+ #### How to use the result hooks?
252
+
253
+ As mentioned earlier, the `Micro::Case::Result` has two methods to improve the flow control. They are: `#on_success`, `on_failure`.
254
+
255
+ The examples below show how to use them:
256
+
257
+ ```ruby
258
+ class Double < Micro::Case::Base
259
+ attributes :number
260
+
261
+ def call!
262
+ return Failure(:invalid) { 'the number must be a numeric value' } unless number.is_a?(Numeric)
263
+ return Failure(:lte_zero) { 'the number must be greater than 0' } if number <= 0
264
+
265
+ Success(number * 2)
266
+ end
267
+ end
268
+
269
+ #================================#
270
+ # Printing the output if success #
271
+ #================================#
272
+
273
+ Double
274
+ .call(number: 3)
275
+ .on_success { |number| p number }
276
+ .on_failure(:invalid) { |msg| raise TypeError, msg }
277
+ .on_failure(:lte_zero) { |msg| raise ArgumentError, msg }
278
+
279
+ # The output because it is a success:
280
+ # 6
281
+
282
+ #=============================#
283
+ # Raising an error if failure #
284
+ #=============================#
285
+
286
+ Double
287
+ .call(number: -1)
288
+ .on_success { |number| p number }
289
+ .on_failure { |_msg, use_case| puts "#{use_case.class.name} was the use case responsible for the failure" }
290
+ .on_failure(:invalid) { |msg| raise TypeError, msg }
291
+ .on_failure(:lte_zero) { |msg| raise ArgumentError, msg }
292
+
293
+ # The outputs because it is a failure:
294
+ # Double was the use case responsible for the failure
295
+ # (throws the error)
296
+ # ArgumentError (the number must be greater than 0)
297
+
298
+ # Note:
299
+ # ----
300
+ # The use case responsible for the failure will be accessible as the second hook argument
301
+ ```
302
+
303
+ [⬆️ Back to Top](#table-of-contents-)
304
+
305
+ #### What happens if a result hook is declared multiple times?
306
+
307
+ Answer: The hook will be triggered if it matches the result type.
308
+
309
+ ```ruby
310
+ class Double < Micro::Case::Base
311
+ attributes :number
312
+
313
+ def call!
314
+ return Failure(:invalid) { 'the number must be a numeric value' } unless number.is_a?(Numeric)
315
+
316
+ Success(:computed) { number * 2 }
317
+ end
318
+ end
319
+
320
+ result = Double.call(number: 3)
321
+ result.value # 6
322
+ result.value * 4 # 24
323
+
324
+ accum = 0
325
+
326
+ result.on_success { |number| accum += number }
327
+ .on_success { |number| accum += number }
328
+ .on_success(:computed) { |number| accum += number }
329
+ .on_success(:computed) { |number| accum += number }
330
+
331
+ accum # 24
332
+
333
+ result.value * 4 == accum # true
334
+ ```
335
+
336
+ [⬆️ Back to Top](#table-of-contents-)
337
+
338
+ ### How to compose uses cases to represents complex ones?
339
+
340
+ In this case, this will be is a **flow**, because the idea is to use/reuse use cases as steps which will define a more complex one.
341
+
342
+ ```ruby
343
+ module Steps
344
+ class ConvertToNumbers < Micro::Case::Base
345
+ attribute :numbers
346
+
347
+ def call!
348
+ if numbers.all? { |value| String(value) =~ /\d+/ }
349
+ Success(numbers: numbers.map(&:to_i))
350
+ else
351
+ Failure('numbers must contain only numeric types')
352
+ end
353
+ end
354
+ end
355
+
356
+ class Add2 < Micro::Case::Strict
357
+ attribute :numbers
358
+
359
+ def call!
360
+ Success(numbers: numbers.map { |number| number + 2 })
361
+ end
362
+ end
363
+
364
+ class Double < Micro::Case::Strict
365
+ attribute :numbers
366
+
367
+ def call!
368
+ Success(numbers: numbers.map { |number| number * 2 })
369
+ end
370
+ end
371
+
372
+ class Square < Micro::Case::Strict
373
+ attribute :numbers
374
+
375
+ def call!
376
+ Success(numbers: numbers.map { |number| number * number })
377
+ end
378
+ end
379
+ end
380
+
381
+ #---------------------------------------------#
382
+ # Creating a flow using the collection syntax #
383
+ #---------------------------------------------#
384
+
385
+ Add2ToAllNumbers = Micro::Case::Flow[
386
+ Steps::ConvertToNumbers,
387
+ Steps::Add2
388
+ ]
389
+
390
+ result = Add2ToAllNumbers.call(numbers: %w[1 1 2 2 3 4])
391
+
392
+ p result.success? # true
393
+ p result.value # {:numbers => [3, 3, 4, 4, 5, 6]}
394
+
395
+ #---------------------------------------------------#
396
+ # An alternative way to create a flow using classes #
397
+ #---------------------------------------------------#
398
+
399
+ class DoubleAllNumbers
400
+ include Micro::Case::Flow
401
+
402
+ flow Steps::ConvertToNumbers, Steps::Double
403
+ end
404
+
405
+ DoubleAllNumbers
406
+ .call(numbers: %w[1 1 b 2 3 4])
407
+ .on_failure { |message| p message } # "numbers must contain only numeric types"
408
+
409
+ #-------------------------------------------------------------#
410
+ # Another way to create a flow using the composition operator #
411
+ #-------------------------------------------------------------#
412
+
413
+ SquareAllNumbers =
414
+ Steps::ConvertToNumbers >> Steps::Square
415
+
416
+ SquareAllNumbers
417
+ .call(numbers: %w[1 1 2 2 3 4])
418
+ .on_success { |value| p value[:numbers] } # [1, 1, 4, 4, 9, 16]
419
+
420
+ # Note:
421
+ # ----
422
+ # When happening a failure, the use case responsible
423
+ # will be accessible in the result
424
+
425
+ result = SquareAllNumbers.call(numbers: %w[1 1 b 2 3 4])
426
+
427
+ result.failure? # true
428
+ result.use_case.is_a?(Steps::ConvertToNumbers) # true
429
+
430
+ result.on_failure do |_message, use_case|
431
+ puts "#{use_case.class.name} was the use case responsible for the failure" # Steps::ConvertToNumbers was the use case responsible for the failure
432
+ end
433
+ ```
434
+
435
+ [⬆️ Back to Top](#table-of-contents-)
436
+
437
+ #### Is it possible to compose a use case flow with other ones?
438
+
439
+ Answer: Yes, it is.
440
+
441
+ ```ruby
442
+ module Steps
443
+ class ConvertToNumbers < Micro::Case::Base
444
+ attribute :numbers
445
+
446
+ def call!
447
+ if numbers.all? { |value| String(value) =~ /\d+/ }
448
+ Success(numbers: numbers.map(&:to_i))
449
+ else
450
+ Failure('numbers must contain only numeric types')
451
+ end
452
+ end
453
+ end
454
+
455
+ class Add2 < Micro::Case::Strict
456
+ attribute :numbers
457
+
458
+ def call!
459
+ Success(numbers: numbers.map { |number| number + 2 })
460
+ end
461
+ end
462
+
463
+ class Double < Micro::Case::Strict
464
+ attribute :numbers
465
+
466
+ def call!
467
+ Success(numbers: numbers.map { |number| number * 2 })
468
+ end
469
+ end
470
+
471
+ class Square < Micro::Case::Strict
472
+ attribute :numbers
473
+
474
+ def call!
475
+ Success(numbers: numbers.map { |number| number * number })
476
+ end
477
+ end
478
+ end
479
+
480
+ Add2ToAllNumbers = Steps::ConvertToNumbers >> Steps::Add2
481
+ DoubleAllNumbers = Steps::ConvertToNumbers >> Steps::Double
482
+ SquareAllNumbers = Steps::ConvertToNumbers >> Steps::Square
483
+
484
+ DoubleAllNumbersAndAdd2 = DoubleAllNumbers >> Steps::Add2
485
+ SquareAllNumbersAndAdd2 = SquareAllNumbers >> Steps::Add2
486
+
487
+ SquareAllNumbersAndDouble = SquareAllNumbersAndAdd2 >> DoubleAllNumbers
488
+ DoubleAllNumbersAndSquareAndAdd2 = DoubleAllNumbers >> SquareAllNumbersAndAdd2
489
+
490
+ SquareAllNumbersAndDouble
491
+ .call(numbers: %w[1 1 2 2 3 4])
492
+ .on_success { |value| p value[:numbers] } # [6, 6, 12, 12, 22, 36]
493
+
494
+ DoubleAllNumbersAndSquareAndAdd2
495
+ .call(numbers: %w[1 1 2 2 3 4])
496
+ .on_success { |value| p value[:numbers] } # [6, 6, 18, 18, 38, 66]
497
+ ```
498
+
499
+ 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/master/test/micro/case/flow/blend_test.rb#L7-L34).
500
+
501
+ [⬆️ Back to Top](#table-of-contents-)
502
+
503
+ ### What is a strict use case?
504
+
505
+ Answer: Is a use case which will require all the keywords (attributes) on its initialization.
506
+
507
+ ```ruby
508
+ class Double < Micro::Case::Strict
509
+ attribute :numbers
510
+
511
+ def call!
512
+ Success(numbers.map { |number| number * 2 })
513
+ end
514
+ end
515
+
516
+ Double.call({})
517
+
518
+ # The output (raised an error):
519
+ # ArgumentError (missing keyword: :numbers)
520
+ ```
521
+
522
+ [⬆️ Back to Top](#table-of-contents-)
523
+
524
+ ### Is there some feature to auto handle exceptions inside of a use case or flow?
525
+
526
+ Answer: Yes, there is!
527
+
528
+ **Use cases:**
529
+
530
+ 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:
531
+
532
+ ```ruby
533
+ require 'logger'
534
+
535
+ AppLogger = Logger.new(STDOUT)
536
+
537
+ class Divide < Micro::Case::Safe
538
+ attributes :a, :b
539
+
540
+ def call!
541
+ return Success(a / b) if a.is_a?(Integer) && b.is_a?(Integer)
542
+ Failure(:not_an_integer)
543
+ end
544
+ end
545
+
546
+ result = Divide.call(a: 2, b: 0)
547
+ result.type == :exception # true
548
+ result.value.is_a?(ZeroDivisionError) # true
549
+
550
+ result.on_failure(:exception) do |exception|
551
+ AppLogger.error(exception.message) # E, [2019-08-21T00:05:44.195506 #9532] ERROR -- : divided by 0
552
+ end
553
+
554
+ # Note:
555
+ # ----
556
+ # If you need to handle a specific error,
557
+ # I recommend the usage of a case statement. e,g:
558
+
559
+ result.on_failure(:exception) do |exception, use_case|
560
+ case exception
561
+ when ZeroDivisionError then AppLogger.error(exception.message)
562
+ else AppLogger.debug("#{use_case.class.name} was the use case responsible for the exception")
563
+ end
564
+ end
565
+
566
+ # Another note:
567
+ # ------------
568
+ # It is possible to rescue an exception even when is a safe use case.
569
+ # Examples: https://github.com/serradura/u-case/blob/5a85fc238b63811a32737493dc6c59965f92491d/test/micro/case/safe_test.rb#L95-L123
570
+ ```
571
+
572
+ **Flows:**
573
+
574
+ As the safe use cases, safe flows can intercept an exception in any of its steps. These are the ways to define one:
575
+
576
+ ```ruby
577
+ module Users
578
+ Create = ProcessParams & ValidateParams & Persist & SendToCRM
579
+ end
580
+
581
+ # Note:
582
+ # The ampersand is based on the safe navigation operator. https://ruby-doc.org/core-2.6/doc/syntax/calling_methods_rdoc.html#label-Safe+navigation+operator
583
+
584
+ # The alternatives are:
585
+
586
+ module Users
587
+ class Create
588
+ include Micro::Case::Flow::Safe
589
+
590
+ flow ProcessParams, ValidateParams, Persist, SendToCRM
591
+ end
592
+ end
593
+
594
+ # or
595
+
596
+ module Users
597
+ Create = Micro::Case::Flow::Safe[
598
+ ProcessParams,
599
+ ValidateParams,
600
+ Persist,
601
+ SendToCRM
602
+ ]
603
+ end
604
+ ```
605
+
606
+ [⬆️ Back to Top](#table-of-contents-)
607
+
608
+ ### How to validate use case attributes?
609
+
610
+ **Requirement:**
611
+
612
+ To do this your application must have the [activemodel >= 3.2](https://rubygems.org/gems/activemodel) as a dependency.
613
+
614
+ ```ruby
615
+ #
616
+ # By default, if your application has the activemodel as a dependency,
617
+ # any kind of use case can use it to validate their attributes.
618
+ #
619
+ class Multiply < Micro::Case::Base
620
+ attributes :a, :b
621
+
622
+ validates :a, :b, presence: true, numericality: true
623
+
624
+ def call!
625
+ return Failure(:validation_error) { self.errors } unless valid?
626
+
627
+ Success(number: a * b)
628
+ end
629
+ end
630
+
631
+ #
632
+ # But if do you want an automatic way to fail
633
+ # your use cases on validation errors, you can use:
634
+
635
+ # In some file. e.g: A Rails initializer
636
+ require 'u-case/with_validation' # or require 'micro/case/with_validation'
637
+
638
+ # In the Gemfile
639
+ gem 'u-case', require: 'u-case/with_validation'
640
+
641
+ # Using this approach, you can rewrite the previous example with less code. e.g:
642
+
643
+ class Multiply < Micro::Case::Base
644
+ attributes :a, :b
645
+
646
+ validates :a, :b, presence: true, numericality: true
647
+
648
+ def call!
649
+ Success(number: a * b)
650
+ end
651
+ end
652
+
653
+ # Note:
654
+ # ----
655
+ # After requiring the validation mode, the
656
+ # Micro::Case::Strict and Micro::Case::Safe classes will inherit this new behavior.
657
+ ```
658
+
659
+ [⬆️ Back to Top](#table-of-contents-)
660
+
661
+ ### Examples
662
+
663
+ 1. [Rescuing an exception inside of use cases](https://github.com/serradura/u-case/blob/master/examples/rescuing_exceptions.rb)
664
+ 2. [Users creation](https://github.com/serradura/u-case/blob/master/examples/users_creation.rb)
665
+
666
+ An example of flow in how to define steps to sanitize, validate, and persist some input data.
667
+ 3. [CLI calculator](https://github.com/serradura/u-case/tree/master/examples/calculator)
668
+
669
+ A more complex example which use rake tasks to demonstrate how to handle user data, and how to use different failures type to control the program flow.
670
+
671
+ [⬆️ Back to Top](#table-of-contents-)
672
+
673
+ ## Comparisons
674
+
675
+ Check it out implementations of the same use case with different gems/abstractions.
676
+
677
+ * [interactor](https://github.com/serradura/u-case/blob/master/comparisons/interactor.rb)
678
+ * [u-case](https://github.com/serradura/u-case/blob/master/comparisons/u-case.rb)
679
+
680
+ ## Benchmarks
681
+
682
+ **[interactor](https://github.com/collectiveidea/interactor)** VS **[u-case](https://github.com/serradura/u-case)**
683
+
684
+ https://github.com/serradura/u-case/tree/master/benchmarks/interactor
685
+
686
+ ![interactor VS u-case](https://github.com/serradura/u-case/blob/master/assets/u-case_benchmarks.png?raw=true)
687
+
688
+ ## Development
689
+
690
+ 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.
691
+
692
+ 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).
693
+
694
+ ## Contributing
695
+
696
+ 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.
697
+
698
+ ## License
699
+
700
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
701
+
702
+ ## Code of Conduct
703
+
704
+ 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).