u-case 1.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.tool-versions +1 -0
- data/.travis.sh +13 -0
- data/.travis.yml +29 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +23 -0
- data/LICENSE.txt +21 -0
- data/README.md +704 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/micro/case.rb +12 -0
- data/lib/micro/case/base.rb +78 -0
- data/lib/micro/case/error.rb +35 -0
- data/lib/micro/case/flow.rb +56 -0
- data/lib/micro/case/flow/reducer.rb +90 -0
- data/lib/micro/case/result.rb +54 -0
- data/lib/micro/case/safe.rb +14 -0
- data/lib/micro/case/strict.rb +13 -0
- data/lib/micro/case/version.rb +7 -0
- data/lib/micro/case/with_validation.rb +17 -0
- data/lib/u-case.rb +3 -0
- data/lib/u-case/with_validation.rb +3 -0
- data/test.sh +11 -0
- data/u-case.gemspec +44 -0
- metadata +110 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/.tool-versions
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby 2.6.3
|
data/.travis.sh
ADDED
@@ -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
|
data/.travis.yml
ADDED
@@ -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"
|
data/CODE_OF_CONDUCT.md
ADDED
@@ -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
|
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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).
|