u-case 5.7.0 → 5.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +992 -1489
- data/README.pt-BR.md +986 -1517
- data/lib/micro/case/version.rb +1 -1
- metadata +1 -1
data/README.pt-BR.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<h1 align="center" id="-case"><img src="./assets/
|
|
3
|
-
<p align="center"><i>Represente casos de uso de forma simples e poderosa
|
|
2
|
+
<h1 align="center" id="-case"><img src="./assets/ucase_logo_v2.png" alt="μ-case" height="250"></h1>
|
|
3
|
+
<p align="center"><i>Represente casos de uso de forma simples e poderosa: escreva código modular, expressivo e sequencialmente lógico.</i></p>
|
|
4
4
|
<p align="center">
|
|
5
5
|
<a href="https://badge.fury.io/rb/u-case"><img src="https://badge.fury.io/rb/u-case.svg" alt="Gem Version" height="18"></a>
|
|
6
6
|
<a href="https://github.com/serradura/u-case/actions/workflows/ci.yml"><img alt="Build Status" src="https://github.com/serradura/u-case/actions/workflows/ci.yml/badge.svg"></a>
|
|
@@ -11,77 +11,181 @@
|
|
|
11
11
|
<img src="https://img.shields.io/badge/Ruby%20%3E%3D%202.7%2C%20%3C%3D%20Head-ruby.svg?colorA=444&colorB=333" alt="Ruby">
|
|
12
12
|
<img src="https://img.shields.io/badge/Rails%20%3E%3D%206.0%2C%20%3C%3D%20Edge-rails.svg?colorA=444&colorB=333" alt="Rails">
|
|
13
13
|
</p>
|
|
14
|
+
<p align="center">🇺🇸 <a href="https://github.com/serradura/u-case/blob/main/README.md">Read this README in English</a></p>
|
|
14
15
|
</p>
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
> [!IMPORTANT]
|
|
18
|
+
> **Sem breaking changes na API — nunca.** Daqui em diante, a API pública e os contratos de runtime do `u-case` não vão quebrar. O papel da gem é continuar sendo uma base estável e retrocompatível para os projetos que já dependem dela. Qualquer "próximo major" que repense as abstrações pertence ao [`solid-process`](https://github.com/solid-process/solid-process) (um redesign que aplica o que aprendemos desde a criação do `u-case`), e **não** a um futuro `u-case` 6.x.
|
|
19
|
+
>
|
|
20
|
+
> Bumps de versão major sinalizam apenas que uma versão do Ruby ou do Rails deixou de ser suportada.
|
|
21
|
+
>
|
|
22
|
+
> Veja a declaração completa na [issue #131](https://github.com/serradura/u-case/issues/131#issuecomment-4531231882).
|
|
23
|
+
|
|
24
|
+
## Quick start <!-- omit in toc -->
|
|
25
|
+
|
|
26
|
+
Esse é o formato inteiro: `attributes`, um método `call!`, e `Success(...)` ou `Failure(...)`. Todo o resto deste README é uma forma de tornar esse formato mais fácil de **compor**, **validar**, **observar** e **transacionar**.
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
require 'u-case'
|
|
30
|
+
|
|
31
|
+
class Slugify < Micro::Case
|
|
32
|
+
attribute :title, accept: String
|
|
33
|
+
|
|
34
|
+
def call!
|
|
35
|
+
slug = title.downcase.strip.gsub(/[^a-z0-9]+/, '-').gsub(/^-|-$/, '')
|
|
36
|
+
|
|
37
|
+
slug.empty? ? Failure(:blank_title) : Success(result: { slug: })
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
Slugify.call(title: 'Hello, World!')
|
|
42
|
+
# => #<Micro::Case::Result success? type=:ok data={ slug: "hello-world" }>
|
|
43
|
+
|
|
44
|
+
Slugify
|
|
45
|
+
.call(title: 42)
|
|
46
|
+
.on_success { puts it[:slug] }
|
|
47
|
+
.on_failure(:invalid_attributes) { warn it[:errors] }
|
|
48
|
+
# warn: { "title" => "expected to be a kind of String" }
|
|
49
|
+
|
|
50
|
+
# ---------------------------------------------
|
|
51
|
+
# Ramificando em cima do resultado? Use pattern matching:
|
|
52
|
+
# ---------------------------------------------
|
|
53
|
+
case Slugify.call(title: 'Hello, World!')
|
|
54
|
+
in { success: _, result: { slug: } }
|
|
55
|
+
redirect_to "/posts/#{slug}"
|
|
56
|
+
in { failure: :invalid_attributes, result: { errors: } }
|
|
57
|
+
render status: 422, json: { errors: }
|
|
58
|
+
in { failure: :blank_title }
|
|
59
|
+
render status: 422, json: { error: 'title required' }
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Precisa de uma entrada estruturada? Declare atributos com um bloco — os atributos filhos herdam o mix de features do host (veja [Indo além com `u-attributes`](#indo-além-com-u-attributes)):
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
class CreateOrder < Micro::Case
|
|
67
|
+
attribute :id, accept: Integer
|
|
68
|
+
|
|
69
|
+
attribute :customer do
|
|
70
|
+
attribute :name, accept: String
|
|
71
|
+
attribute :email, accept: String
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def call!
|
|
75
|
+
transaction do
|
|
76
|
+
customer = Customer.find_or_create_by!(name: customer.name, email: customer.email)
|
|
77
|
+
|
|
78
|
+
order = Order.create!(id:, customer_id: customer.id)
|
|
79
|
+
|
|
80
|
+
Success result: { customer:, order: }
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Precisa de trabalho atômico em múltiplos steps? Envolva um flow inteiro em uma transação com um único kwarg, ou escope uma `ActiveRecord::Base.transaction` num único `call!`:
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
# Um flow transacional — todos os steps dentro da mesma transação:
|
|
90
|
+
SignUp = Micro::Cases.flow(transaction: true, steps: [
|
|
91
|
+
NormalizeParams,
|
|
92
|
+
CreateUser,
|
|
93
|
+
CreateProfile
|
|
94
|
+
])
|
|
95
|
+
|
|
96
|
+
# Uma transação inline { ... } dentro do call!:
|
|
97
|
+
class CreateUserWithProfile < Micro::Case
|
|
98
|
+
def call!
|
|
99
|
+
transaction {
|
|
100
|
+
call(CreateUser).then(CreateProfile)
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Veja [Compondo casos de uso](#compondo-casos-de-uso) e [Indo além com `u-attributes`](#indo-além-com-u-attributes) para a história completa.
|
|
107
|
+
|
|
108
|
+
## Recursos <!-- omit in toc -->
|
|
22
109
|
|
|
23
|
-
|
|
110
|
+
- **Fácil** — entrada → processamento → saída. Um caso de uso é uma classe pequena com `attributes` e um método `call!` que retorna um resultado.
|
|
111
|
+
- **Imutável e sem callbacks** — nada de callbacks de ciclo de vida `before` / `after` / `around`. Os dados fluem adiante; nada é mutado in place.
|
|
112
|
+
- **Componível de três formas** — encadeie casos de uso via [`Micro::Cases.flow`](#flows), via [macro `flow` no nível da classe](#flows), ou via cadeias inline de [`Result#then`](#steps-internos--cadeias-com-resultthen).
|
|
113
|
+
- **Resultados tipados** — toda chamada retorna um [`Micro::Case::Result`](#trabalhando-com-resultados) com um discriminante `success?`/`failure?`, um símbolo `:type` e um hash `data`.
|
|
114
|
+
- **Pattern matching** — o `case`/`in` do Ruby funciona em resultados direto ([Pattern matching](#pattern-matching)).
|
|
115
|
+
- **Contratos de resultado** — declare quais tipos de resultado e quais chaves seu caso de uso pode retornar; [usos incorretos falham loudly](#contratos-de-resultado).
|
|
116
|
+
- **Execução inspecionável** — todo flow registra a entrada, a saída e os atributos acessíveis de cada step em [`result.transitions`](#inspecionando-a-execução-com-resulttransitions). Debug, log ou audite como qualquer resultado foi produzido.
|
|
117
|
+
- ⚡ **Transações sob demanda** — envolva um caso de uso, um flow em uma [transação `ActiveRecord`](#transações).
|
|
118
|
+
- **Tratamento de exceções opt-in** — [`Micro::Case::Safe`](#modo-seguro--capturando-exceções) converte exceções não tratadas em falhas do tipo `:exception`.
|
|
119
|
+
- **Rápido** — Confira os [benchmarks](#performance), sem estado global.
|
|
120
|
+
|
|
121
|
+
> Veja uma aplicação Rails real que usa essa gem: [from-fat-controllers-to-use-cases](https://github.com/serradura/from-fat-controllers-to-use-cases).
|
|
24
122
|
|
|
25
123
|
## Documentação <!-- omit in toc -->
|
|
26
124
|
|
|
27
|
-
Versão
|
|
28
|
-
|
|
29
|
-
unreleased| https://github.com/serradura/u-case/blob/main/README.md
|
|
30
|
-
5.
|
|
31
|
-
4.5.
|
|
125
|
+
| Versão | Documentação |
|
|
126
|
+
| ---------- | ------------------------------------------------------------- |
|
|
127
|
+
| unreleased | https://github.com/serradura/u-case/blob/main/README.pt-BR.md |
|
|
128
|
+
| 5.7.1 | https://github.com/serradura/u-case/blob/v5.x/README.pt-BR.md |
|
|
129
|
+
| 4.5.2 | https://github.com/serradura/u-case/blob/v4.x/README.pt-BR.md |
|
|
32
130
|
|
|
33
131
|
## Índice <!-- omit in toc -->
|
|
132
|
+
|
|
34
133
|
- [Compatibilidade](#compatibilidade)
|
|
35
134
|
- [Dependências](#dependências)
|
|
36
135
|
- [Instalação](#instalação)
|
|
37
136
|
- [Uso](#uso)
|
|
38
|
-
- [
|
|
39
|
-
|
|
40
|
-
- [
|
|
41
|
-
- [
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
- [
|
|
49
|
-
- [
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
- [
|
|
53
|
-
- [
|
|
54
|
-
- [
|
|
55
|
-
- [
|
|
56
|
-
|
|
57
|
-
- [`
|
|
58
|
-
|
|
59
|
-
- [
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
- [`
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
- [
|
|
71
|
-
- [
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
137
|
+
- [Definindo um caso de uso](#definindo-um-caso-de-uso)
|
|
138
|
+
- [O básico](#o-básico)
|
|
139
|
+
- [Modo estrito — atributos obrigatórios](#modo-estrito--atributos-obrigatórios)
|
|
140
|
+
- [Modo seguro — capturando exceções](#modo-seguro--capturando-exceções)
|
|
141
|
+
- [Flows seguros](#flows-seguros)
|
|
142
|
+
- [`Result#on_exception`](#resulton_exception)
|
|
143
|
+
- [Desabilitando o Safe](#desabilitando-o-safe)
|
|
144
|
+
- [Trabalhando com resultados](#trabalhando-com-resultados)
|
|
145
|
+
- [A API do Result](#a-api-do-result)
|
|
146
|
+
- [Tipos de resultado padrão e customizados](#tipos-de-resultado-padrão-e-customizados)
|
|
147
|
+
- [Contratos de resultado](#contratos-de-resultado)
|
|
148
|
+
- [Hooks de resultado](#hooks-de-resultado)
|
|
149
|
+
- [Pattern matching](#pattern-matching)
|
|
150
|
+
- [Decomposição](#decomposição)
|
|
151
|
+
- [Continuações dinâmicas com `Result#then`](#continuações-dinâmicas-com-resultthen)
|
|
152
|
+
- [Validando atributos](#validando-atributos)
|
|
153
|
+
- [`accept:` e `reject:` (padrão)](#accept-e-reject-padrão)
|
|
154
|
+
- [Integração com ActiveModel (opt-in)](#integração-com-activemodel-opt-in)
|
|
155
|
+
- [Desabilitando a auto-validação em um caso específico](#desabilitando-a-auto-validação-em-um-caso-específico)
|
|
156
|
+
- [`Kind::Validator`](#kindvalidator)
|
|
157
|
+
- [Compondo casos de uso](#compondo-casos-de-uso)
|
|
158
|
+
- [Flows](#flows)
|
|
159
|
+
- [Compondo flows entre si](#compondo-flows-entre-si)
|
|
160
|
+
- [Acumulação de dados através de um flow](#acumulação-de-dados-através-de-um-flow)
|
|
161
|
+
- [Inspecionando a execução com `result.transitions`](#inspecionando-a-execução-com-resulttransitions)
|
|
162
|
+
- [Compondo um flow que inclui a si mesmo](#compondo-um-flow-que-inclui-a-si-mesmo)
|
|
163
|
+
- [Steps internos — cadeias com `Result#then`](#steps-internos--cadeias-com-resultthen)
|
|
164
|
+
- [Formas aceitas de elo](#formas-aceitas-de-elo)
|
|
165
|
+
- [Um exemplo mínimo](#um-exemplo-mínimo)
|
|
166
|
+
- [Alias `|` (pipe)](#alias--pipe)
|
|
167
|
+
- [Formas Lambda / `Method`](#formas-lambda--method)
|
|
168
|
+
- [`Failure` interrompe a cadeia](#failure-interrompe-a-cadeia)
|
|
169
|
+
- [Usando um caso com steps internos dentro de um flow externo](#usando-um-caso-com-steps-internos-dentro-de-um-flow-externo)
|
|
170
|
+
- [Persistência sem transação](#persistência-sem-transação)
|
|
171
|
+
- [Transações](#transações)
|
|
172
|
+
- [`transaction { ... }` inline dentro do `call!`](#transaction----inline-dentro-do-call)
|
|
173
|
+
- [`transaction with: …` — declarando o padrão para um caso](#transaction-with---declarando-o-padrão-para-um-caso)
|
|
174
|
+
- [Transações no nível do flow](#transações-no-nível-do-flow)
|
|
175
|
+
- [Padrão global — `config.default_transaction_class { … }`](#padrão-global--configdefault_transaction_class---)
|
|
176
|
+
- [Flows com steps internos sob transações](#flows-com-steps-internos-sob-transações)
|
|
177
|
+
- [Observações de comportamento](#observações-de-comportamento)
|
|
178
|
+
- [Configuração](#configuração)
|
|
179
|
+
- [Performance](#performance)
|
|
180
|
+
- [Executando os benchmarks](#executando-os-benchmarks)
|
|
181
|
+
- [Desabilitando os checks em runtime](#desabilitando-os-checks-em-runtime)
|
|
79
182
|
- [Comparações](#comparações)
|
|
80
183
|
- [Exemplos](#exemplos)
|
|
81
|
-
- [
|
|
82
|
-
- [
|
|
83
|
-
|
|
84
|
-
- [
|
|
184
|
+
- [Um flow completo de cadastro](#um-flow-completo-de-cadastro)
|
|
185
|
+
- [Mais exemplos](#mais-exemplos)
|
|
186
|
+
- [Indo além com `u-attributes`](#indo-além-com-u-attributes)
|
|
187
|
+
- [Atributos aninhados (forma com bloco)](#atributos-aninhados-forma-com-bloco)
|
|
188
|
+
- [Aceitando outra classe de atributos](#aceitando-outra-classe-de-atributos)
|
|
85
189
|
- [Desenvolvimento](#desenvolvimento)
|
|
86
190
|
- [Contribuindo](#contribuindo)
|
|
87
191
|
- [Licença](#licença)
|
|
@@ -89,17 +193,16 @@ unreleased| https://github.com/serradura/u-case/blob/main/README.md
|
|
|
89
193
|
|
|
90
194
|
## Compatibilidade
|
|
91
195
|
|
|
92
|
-
| u-case
|
|
93
|
-
|
|
|
94
|
-
| unreleased
|
|
95
|
-
| 5.
|
|
96
|
-
| 5.
|
|
97
|
-
| 4.5.1 | v4.x | >= 2.2.0 | >= 3.2, <= 8.1 | >= 2.7, < 3.0 |
|
|
196
|
+
| u-case | branch | ruby | activemodel | u-attributes |
|
|
197
|
+
| ---------- | ------ | -------- | -------------- | ------------- |
|
|
198
|
+
| unreleased | main | >= 2.7 | >= 6.0 | >= 2.8, < 4.0 |
|
|
199
|
+
| 5.7.1 | v5.x | >= 2.7 | >= 6.0 | >= 2.8, < 4.0 |
|
|
200
|
+
| 4.5.2 | v4.x | >= 2.2.0 | >= 3.2, <= 8.1 | >= 2.7, < 3.0 |
|
|
98
201
|
|
|
99
202
|
Esta biblioteca é testada (matriz de CI) contra:
|
|
100
203
|
|
|
101
204
|
| Ruby / Rails | 6.0 | 6.1 | 7.0 | 7.1 | 7.2 | 8.0 | 8.1 | Edge |
|
|
102
|
-
|
|
205
|
+
| ------------ | --- | --- | --- | --- | --- | --- | --- | ---- |
|
|
103
206
|
| 2.7 | ✅ | ✅ | ✅ | ✅ | | | | |
|
|
104
207
|
| 3.0 | ✅ | ✅ | ✅ | ✅ | | | | |
|
|
105
208
|
| 3.1 | | | ✅ | ✅ | ✅ | | | |
|
|
@@ -109,19 +212,12 @@ Esta biblioteca é testada (matriz de CI) contra:
|
|
|
109
212
|
| 4.x | | | | | | | ✅ | ✅ |
|
|
110
213
|
| Head | | | | | | | ✅ | ✅ |
|
|
111
214
|
|
|
112
|
-
>
|
|
215
|
+
> ActiveModel é uma dependência opcional — habilite [`u-case/with_activemodel_validation`](#integração-com-activemodel-opt-in) apenas se quiser.
|
|
113
216
|
|
|
114
217
|
## Dependências
|
|
115
218
|
|
|
116
|
-
1.
|
|
117
|
-
|
|
118
|
-
Sistema de tipos simples (em runtime) para Ruby.
|
|
119
|
-
|
|
120
|
-
É usado para validar os inputs de alguns métodos do u-case, além de expor um validador de tipos através do [`activemodel validation`](https://github.com/serradura/kind#kindvalidator-activemodelvalidations) ([veja como habilitar]((#u-casewith_activemodel_validation---how-to-validate-use-case-attributes))).
|
|
121
|
-
2. [`u-attributes`](https://github.com/serradura/u-attributes) gem.
|
|
122
|
-
|
|
123
|
-
Essa gem permite definir atributos de leitura (read-only), ou seja, os seus objetos só terão getters para acessar os dados dos seus atributos.
|
|
124
|
-
Ela é usada para definir os atributos dos casos de uso.
|
|
219
|
+
1. **[`kind`](https://github.com/serradura/kind)** — um sistema de tipos em runtime para Ruby, usado para validar alguns inputs internos do `u-case`. Também expõe o [`Kind::Validator`](https://github.com/serradura/kind#kindvalidator-activemodelvalidations) que vem junto do [`u-case/with_activemodel_validation`](#integração-com-activemodel-opt-in). Os exemplos abaixo usam `Kind.of?(SomeClass, *values)` como um atalho para checagem de tipos em runtime — equivalente a `values.all? { |v| v.is_a?(SomeClass) }`.
|
|
220
|
+
2. **[`u-attributes`](https://github.com/serradura/u-attributes)** — declarações de atributos read-only (somente getters). Usada para os `attributes` do caso de uso.
|
|
125
221
|
|
|
126
222
|
## Instalação
|
|
127
223
|
|
|
@@ -131,389 +227,368 @@ Adicione essa linha ao Gemfile da sua aplicação:
|
|
|
131
227
|
gem 'u-case', '~> 5.0'
|
|
132
228
|
```
|
|
133
229
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
$ bundle
|
|
137
|
-
|
|
138
|
-
Ou instale manualmente:
|
|
139
|
-
|
|
140
|
-
$ gem install u-case
|
|
230
|
+
Então execute `bundle`, ou instale manualmente com `gem install u-case`.
|
|
141
231
|
|
|
142
232
|
## Uso
|
|
143
233
|
|
|
144
|
-
###
|
|
234
|
+
### Definindo um caso de uso
|
|
235
|
+
|
|
236
|
+
#### O básico
|
|
145
237
|
|
|
146
238
|
```ruby
|
|
147
|
-
class
|
|
148
|
-
# 1.
|
|
149
|
-
|
|
239
|
+
class ValidateEmail < Micro::Case
|
|
240
|
+
# 1. Declare a entrada como atributos
|
|
241
|
+
attribute :address
|
|
150
242
|
|
|
151
|
-
# 2.
|
|
243
|
+
# 2. Implemente call! com a regra de negócio
|
|
152
244
|
def call!
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
Success result: { number: a * b }
|
|
245
|
+
# 3. Envolva o resultado com Success(...) ou Failure(...)
|
|
246
|
+
if address.is_a?(String) && address.match?(/\A[^@\s]+@[^@\s]+\.[^@\s]+\z/)
|
|
247
|
+
Success result: { address: address.downcase }
|
|
157
248
|
else
|
|
158
|
-
Failure result: { message: '`
|
|
249
|
+
Failure result: { message: '`address` must be a valid email' }
|
|
159
250
|
end
|
|
160
251
|
end
|
|
161
252
|
end
|
|
162
253
|
|
|
163
|
-
|
|
164
|
-
#
|
|
165
|
-
|
|
254
|
+
result = ValidateEmail.call(address: 'Ada@Example.com')
|
|
255
|
+
result.success? # => true
|
|
256
|
+
result.data # => { address: "ada@example.com" }
|
|
166
257
|
|
|
167
|
-
|
|
258
|
+
bad_result = ValidateEmail.call(address: 'not-an-email')
|
|
259
|
+
bad_result.failure? # => true
|
|
260
|
+
bad_result.data # => { message: "`address` must be a valid email" }
|
|
261
|
+
```
|
|
168
262
|
|
|
169
|
-
|
|
263
|
+
O objeto retornado por `.call` é um [`Micro::Case::Result`](#trabalhando-com-resultados) — assunto da próxima seção.
|
|
170
264
|
|
|
171
|
-
|
|
172
|
-
result.data # { number: 4 }
|
|
265
|
+
#### Modo estrito — atributos obrigatórios
|
|
173
266
|
|
|
174
|
-
|
|
267
|
+
`Micro::Case::Strict` exige que todos os atributos declarados sejam passados em `.call`. Keywords faltantes lançam `ArgumentError`:
|
|
175
268
|
|
|
176
|
-
|
|
269
|
+
```ruby
|
|
270
|
+
class FormatGreeting < Micro::Case::Strict
|
|
271
|
+
attributes :name, :time_of_day
|
|
177
272
|
|
|
178
|
-
|
|
179
|
-
|
|
273
|
+
def call!
|
|
274
|
+
Success result: { message: "Good #{time_of_day}, #{name}!" }
|
|
275
|
+
end
|
|
276
|
+
end
|
|
180
277
|
|
|
181
|
-
|
|
182
|
-
#
|
|
183
|
-
# O resultado de um Micro::Case.call é uma instância de Micro::Case::Result
|
|
278
|
+
FormatGreeting.call(name: 'Ada')
|
|
279
|
+
# => ArgumentError (missing keyword: :time_of_day)
|
|
184
280
|
```
|
|
185
281
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
### `Micro::Case::Result` - O que é o resultado de um caso de uso?
|
|
282
|
+
Use quando você quer que input ausente falhe loudly em vez de deixar `time_of_day` chegar como `nil` e produzir uma mensagem silenciosamente errada.
|
|
189
283
|
|
|
190
|
-
|
|
191
|
-
- `#success?` retorna `true` se for um resultado de sucesso.
|
|
192
|
-
- `#failure?` retorna `true` se for um resultado de falha.
|
|
193
|
-
- `#use_case` retorna o caso de uso responsável pelo resultado. Essa funcionalidade é útil para lidar com falhas em flows (esse tópico será abordado mais a frente).
|
|
194
|
-
- `#type` retorna um Symbol que dá significado ao resultado, isso é útil para declarar diferentes tipos de falha e sucesso.
|
|
195
|
-
- `#data` os dados do resultado (um `Hash`).
|
|
196
|
-
- `#[]` e `#values_at` são atalhos para acessar as propriedades do `#data`.
|
|
197
|
-
- `#fetch` e `#fetch_values` são outras maneiras de acessar os valores contidos em `#data`, porém se alguma chave não existir, é levantado um `KeyError`.
|
|
198
|
-
- `#keys` retorna uma array com as chaves presentes no resultado.
|
|
199
|
-
- `#key?` retorna `true` se a chave estiver presente no `#data`.
|
|
200
|
-
- `#value?` retorna `true` se o valor estiver presente no `#data`.
|
|
201
|
-
- `#slice` retorna um novo `Hash` que inclui apenas as chaves fornecidas. Se as chaves fornecidas não existirem, um `Hash` vazio será retornado.
|
|
202
|
-
- `#on_success` or `#on_failure` são métodos de hooks que te auxiliam a definir o fluxo da aplicação.
|
|
203
|
-
- `#then` este método permite aplicar novos casos de uso ao resultado atual se ele for sucesso. A ideia dessa feature é a criação de fluxos dinâmicos.
|
|
204
|
-
- `#transitions` retorna um array com todas as transformações que um resultado [teve durante um flow](#como-entender-o-que-aconteceu-durante-a-execução-de-um-flow).
|
|
284
|
+
#### Modo seguro — capturando exceções
|
|
205
285
|
|
|
206
|
-
|
|
286
|
+
`Micro::Case::Safe` é outra classe base. Ela intercepta automaticamente qualquer exceção lançada dentro do `call!` e a converte em um `Failure` com `type: :exception`. A exceção em si fica disponível em `result[:exception]`:
|
|
207
287
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
288
|
+
```ruby
|
|
289
|
+
require 'json'
|
|
290
|
+
require 'logger'
|
|
211
291
|
|
|
212
|
-
|
|
213
|
-
- `:ok` em casos de sucesso;
|
|
214
|
-
- `:error` ou `:exception` em casos de falhas.
|
|
292
|
+
AppLogger = Logger.new(STDOUT)
|
|
215
293
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
attributes :a, :b
|
|
294
|
+
class ParseJsonPayload < Micro::Case::Safe
|
|
295
|
+
attribute :payload
|
|
219
296
|
|
|
220
297
|
def call!
|
|
221
|
-
if
|
|
222
|
-
Success result: { number: a / b }
|
|
223
|
-
else
|
|
224
|
-
Failure result: { invalid_attributes: invalid_attributes }
|
|
225
|
-
end
|
|
226
|
-
rescue => exception
|
|
227
|
-
Failure result: exception
|
|
228
|
-
end
|
|
298
|
+
return Failure(:blank_payload) if payload.to_s.empty?
|
|
229
299
|
|
|
230
|
-
|
|
231
|
-
attributes.select { |_key, value| !value.is_a?(Numeric) }
|
|
300
|
+
Success result: { data: JSON.parse(payload) }
|
|
232
301
|
end
|
|
233
302
|
end
|
|
234
303
|
|
|
235
|
-
|
|
304
|
+
result = ParseJsonPayload.call(payload: 'not-valid-json')
|
|
305
|
+
result.type # => :exception
|
|
306
|
+
result.data # => { exception: #<JSON::ParserError ...> }
|
|
307
|
+
result[:exception].is_a?(JSON::ParserError) # => true
|
|
236
308
|
|
|
237
|
-
result
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
result.success? # true
|
|
242
|
-
result.use_case # #<Divide:0x0000 @__attributes={"a"=>2, "b"=>2}, @a=2, @b=2, @__result=...>
|
|
309
|
+
result.on_failure(:exception) do
|
|
310
|
+
AppLogger.error(it[:exception].message)
|
|
311
|
+
end
|
|
312
|
+
```
|
|
243
313
|
|
|
244
|
-
|
|
314
|
+
Para decidir o que fazer em função da classe da exceção, use `case`/`when` (ou [pattern matching](#pattern-matching)) dentro do hook:
|
|
245
315
|
|
|
246
|
-
|
|
316
|
+
```ruby
|
|
317
|
+
result.on_failure(:exception) do |data, use_case|
|
|
318
|
+
case (e = data[:exception])
|
|
319
|
+
when JSON::ParserError then AppLogger.error("malformed JSON: #{e.message}")
|
|
320
|
+
else AppLogger.debug("#{use_case.class.name} raised #{e.class}")
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
```
|
|
247
324
|
|
|
248
|
-
|
|
249
|
-
bad_result.data # { invalid_attributes: { "b"=>"2" } }
|
|
250
|
-
bad_result.failure? # true
|
|
251
|
-
bad_result.use_case # #<Divide:0x0000 @__attributes={"a"=>2, "b"=>"2"}, @a=2, @b="2", @__result=...>
|
|
325
|
+
Você ainda pode capturar exceções explicitamente com `rescue` dentro de um caso de uso Safe — veja [estes exemplos de teste](https://github.com/serradura/u-case/blob/main/test/micro/case/safe_test.rb).
|
|
252
326
|
|
|
253
|
-
|
|
327
|
+
##### Flows seguros
|
|
254
328
|
|
|
255
|
-
|
|
329
|
+
Um flow seguro intercepta exceções em qualquer um de seus steps:
|
|
256
330
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
331
|
+
```ruby
|
|
332
|
+
module Users
|
|
333
|
+
Create = Micro::Cases.safe_flow([
|
|
334
|
+
ProcessParams,
|
|
335
|
+
ValidateParams,
|
|
336
|
+
Persist,
|
|
337
|
+
SendToCRM
|
|
338
|
+
])
|
|
261
339
|
|
|
262
|
-
#
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
340
|
+
# Ou como uma classe:
|
|
341
|
+
class Create < Micro::Case::Safe
|
|
342
|
+
flow ProcessParams,
|
|
343
|
+
ValidateParams,
|
|
344
|
+
Persist,
|
|
345
|
+
SendToCRM
|
|
346
|
+
end
|
|
347
|
+
end
|
|
266
348
|
```
|
|
267
349
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
#### Como definir tipos customizados de resultados?
|
|
350
|
+
##### `Result#on_exception`
|
|
271
351
|
|
|
272
|
-
|
|
352
|
+
Exceções ficam mais fáceis de acompanhar quando são tratadas como qualquer outra falha. `Result#on_exception` é um hook que dispara quando o `type` é `:exception` — funciona igual a `on_failure(:exception)`, mas torna a intenção explícita:
|
|
273
353
|
|
|
274
354
|
```ruby
|
|
275
|
-
class
|
|
276
|
-
|
|
355
|
+
class ParseJsonPayload < Micro::Case::Safe
|
|
356
|
+
attribute :payload
|
|
277
357
|
|
|
278
358
|
def call!
|
|
279
|
-
|
|
280
|
-
Success result: { number: a * b }
|
|
281
|
-
else
|
|
282
|
-
Failure :invalid_data, result: {
|
|
283
|
-
attributes: attributes.reject { |_, input| input.is_a?(Numeric) }
|
|
284
|
-
}
|
|
285
|
-
end
|
|
359
|
+
Success result: { data: JSON.parse(payload) }
|
|
286
360
|
end
|
|
287
361
|
end
|
|
288
362
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
363
|
+
ParseJsonPayload
|
|
364
|
+
.call(payload: 'not-valid-json')
|
|
365
|
+
.on_success { puts it[:data].inspect }
|
|
366
|
+
.on_exception(Encoding::CompatibilityError) { puts 'Encoding mismatch.' }
|
|
367
|
+
.on_exception(JSON::ParserError) { puts 'Malformed JSON.' }
|
|
368
|
+
.on_exception { |_e, _use_case| puts 'Something went wrong.' }
|
|
369
|
+
# Malformed JSON.
|
|
370
|
+
# Something went wrong.
|
|
371
|
+
```
|
|
292
372
|
|
|
293
|
-
|
|
294
|
-
result.data # { number: 6 }
|
|
295
|
-
result.success? # true
|
|
373
|
+
> Tanto o `on_exception(JSON::ParserError)` tipado quanto o `on_exception` genérico disparam — como todos os hooks do u-case, todo match executa na ordem em que foi declarado (veja [Hooks de resultado](#hooks-de-resultado)).
|
|
296
374
|
|
|
297
|
-
|
|
375
|
+
##### Desabilitando o Safe
|
|
298
376
|
|
|
299
|
-
|
|
377
|
+
O mecanismo Safe é opinativo: qualquer exceção não tratada vira uma falha `:exception`. Essa conveniência pode fragmentar uma codebase — algumas exceções tratadas com `rescue` dentro de `call!`, outras com `on_exception` depois. Se você prefere uma única convenção explícita (apenas `rescue` puro), desabilite o Safe inteiro:
|
|
300
378
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
379
|
+
```ruby
|
|
380
|
+
Micro::Case.config do |config|
|
|
381
|
+
config.disable_safe_features = true
|
|
382
|
+
end
|
|
304
383
|
```
|
|
305
384
|
|
|
306
|
-
|
|
385
|
+
Quando setado para `true`, os itens abaixo lançam `Micro::Case::Error::SafeFeaturesDisabled`:
|
|
386
|
+
|
|
387
|
+
- herdar de `Micro::Case::Safe`
|
|
388
|
+
- chamar `Micro::Cases.safe_flow(...)`
|
|
389
|
+
- chamar `Micro::Case::Result#on_exception`
|
|
390
|
+
|
|
391
|
+
[⬆️ Voltar ao topo](#índice-)
|
|
392
|
+
|
|
393
|
+
### Trabalhando com resultados
|
|
394
|
+
|
|
395
|
+
Um `Micro::Case::Result` carrega a saída do caso de uso. Os métodos que você mais vai usar:
|
|
396
|
+
|
|
397
|
+
#### A API do Result
|
|
398
|
+
|
|
399
|
+
- `#success?` / `#failure?` — discriminantes booleanos.
|
|
400
|
+
- `#type` — `Symbol` que descreve o resultado (`:ok`, `:error`, `:exception`, ou qualquer tipo customizado).
|
|
401
|
+
- `#data` — o hash de dados do resultado. `#value` é um alias retrocompatível.
|
|
402
|
+
- `#[]`, `#values_at`, `#fetch`, `#fetch_values`, `#keys`, `#key?`, `#value?`, `#slice` — acesso similar a `Hash` em cima de `#data`.
|
|
403
|
+
- `#use_case` — a instância do caso de uso que produziu o resultado (útil para diagnóstico de falhas dentro de um flow).
|
|
404
|
+
- `#on_success` / `#on_failure` / `#on_exception` — hooks para ramificar em função do resultado.
|
|
405
|
+
- `#then` — aplica outro caso de uso (ou lambda / method / símbolo) a um resultado de sucesso; é a base dos [steps internos](#steps-internos--cadeias-com-resultthen) e das [continuações dinâmicas](#continuações-dinâmicas-com-resultthen).
|
|
406
|
+
- `#transitions` — array com cada step que produziu esse resultado; veja [inspecionando a execução](#inspecionando-a-execução-com-resulttransitions).
|
|
407
|
+
|
|
408
|
+
Objetos `Result` também suportam [pattern matching](#pattern-matching) e [decomposição em array](#decomposição).
|
|
409
|
+
|
|
410
|
+
#### Tipos de resultado padrão e customizados
|
|
411
|
+
|
|
412
|
+
Todo resultado carrega um tipo. Os padrões:
|
|
307
413
|
|
|
308
|
-
|
|
414
|
+
- `:ok` — para `Success(...)`.
|
|
415
|
+
- `:error` — para `Failure(...)` cujo payload é um `Hash`.
|
|
416
|
+
- `:exception` — para `Failure(result: some_exception)` (uma instância de `Exception`).
|
|
309
417
|
|
|
310
|
-
|
|
418
|
+
```ruby
|
|
419
|
+
class FetchUser < Micro::Case
|
|
420
|
+
attribute :id
|
|
421
|
+
|
|
422
|
+
def call!
|
|
423
|
+
return Failure(result: { errors: { id: 'must be an Integer' } }) unless id.is_a?(Integer)
|
|
424
|
+
|
|
425
|
+
Success result: { user: User.find(id) }
|
|
426
|
+
rescue => exception
|
|
427
|
+
Failure result: exception
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
FetchUser.call(id: 1).type # => :ok
|
|
432
|
+
FetchUser.call(id: 'x').type # => :error
|
|
433
|
+
FetchUser.call(id: 999_999).type # => :exception (ActiveRecord::RecordNotFound)
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
Passe um símbolo como primeiro argumento de `Success(...)` / `Failure(...)` para dar ao resultado um tipo customizado:
|
|
311
437
|
|
|
312
438
|
```ruby
|
|
313
|
-
class
|
|
314
|
-
attributes :
|
|
439
|
+
class MergeTags < Micro::Case
|
|
440
|
+
attributes :primary, :secondary
|
|
315
441
|
|
|
316
442
|
def call!
|
|
317
|
-
if
|
|
318
|
-
Success result: {
|
|
443
|
+
if primary.is_a?(Array) && secondary.is_a?(Array)
|
|
444
|
+
Success result: { tags: (primary + secondary).uniq }
|
|
319
445
|
else
|
|
320
|
-
Failure
|
|
446
|
+
Failure :invalid_input, result: {
|
|
447
|
+
attributes: attributes.reject { |_, v| v.is_a?(Array) }
|
|
448
|
+
}
|
|
321
449
|
end
|
|
322
450
|
end
|
|
323
451
|
end
|
|
324
452
|
|
|
325
|
-
|
|
453
|
+
MergeTags.call(primary: %w[ruby], secondary: 'rails').type # => :invalid_input
|
|
454
|
+
```
|
|
326
455
|
|
|
327
|
-
result
|
|
328
|
-
result.data # { :invalid_data => true }
|
|
329
|
-
result.type # :invalid_data
|
|
330
|
-
result.use_case.attributes # {"a"=>2, "b"=>"2"}
|
|
456
|
+
Passar apenas o símbolo (sem `result:`) é válido — o data vira `{ <símbolo> => true }`. Esse formato é útil como discriminante rápido dentro de um flow:
|
|
331
457
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
# (este tópico será coberto em breve).
|
|
336
|
-
```
|
|
458
|
+
```ruby
|
|
459
|
+
def call!
|
|
460
|
+
return Failure(:invalid_input) unless primary.is_a?(Array) && secondary.is_a?(Array)
|
|
337
461
|
|
|
338
|
-
|
|
462
|
+
Success result: { tags: (primary + secondary).uniq }
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
# result.data => { invalid_input: true }
|
|
466
|
+
```
|
|
339
467
|
|
|
340
|
-
####
|
|
468
|
+
#### Contratos de resultado
|
|
341
469
|
|
|
342
|
-
|
|
470
|
+
Use a macro `results do |on| ... end` para declarar quais tipos de resultado seu caso de uso pode produzir e quais chaves cada um deles exige. Chamadas que usam um tipo não declarado lançam `Micro::Case::Error::UnexpectedResultType`; chamadas que omitem uma chave obrigatória declarada lançam `Micro::Case::Error::MissingResultKeys`.
|
|
343
471
|
|
|
344
472
|
```ruby
|
|
345
|
-
class
|
|
346
|
-
|
|
473
|
+
class PublishPost < Micro::Case
|
|
474
|
+
attribute :post
|
|
347
475
|
|
|
348
476
|
results do |on|
|
|
349
|
-
on.failure(:
|
|
350
|
-
on.failure(:
|
|
477
|
+
on.failure(:already_published)
|
|
478
|
+
on.failure(:missing_content)
|
|
351
479
|
|
|
352
|
-
on.success(result: [:
|
|
480
|
+
on.success(result: [:post])
|
|
353
481
|
end
|
|
354
482
|
|
|
355
483
|
def call!
|
|
356
|
-
return Failure(:
|
|
357
|
-
return Failure(:
|
|
484
|
+
return Failure(:already_published) if post.published?
|
|
485
|
+
return Failure(:missing_content) if post.body.to_s.strip.empty?
|
|
358
486
|
|
|
359
|
-
|
|
487
|
+
post.update!(status: :published, published_at: Time.current)
|
|
488
|
+
Success result: { post: }
|
|
360
489
|
end
|
|
361
490
|
end
|
|
362
491
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
492
|
+
PublishPost.call(post: ready_post).data # => { post: #<Post ...> }
|
|
493
|
+
PublishPost.call(post: empty_post).type # => :missing_content
|
|
494
|
+
PublishPost.call(post: already_live_post).type # => :already_published
|
|
366
495
|
```
|
|
367
496
|
|
|
368
|
-
Um tipo
|
|
497
|
+
Um tipo passado sem `result:` é declarado sem chaves obrigatórias (qualquer payload — incluindo o `{ type => true }` implícito de `Failure(:my_type)` — é aceito). Com `result: [:key1, :key2]`, essas chaves precisam estar presentes no hash de resultado; chaves extras são permitidas.
|
|
369
498
|
|
|
370
499
|
```ruby
|
|
371
|
-
class
|
|
500
|
+
class CreateComment < Micro::Case
|
|
372
501
|
results do |on|
|
|
373
|
-
on.success(result: [:
|
|
374
|
-
on.failure(:
|
|
502
|
+
on.success(result: [:comment])
|
|
503
|
+
on.failure(:spam)
|
|
375
504
|
end
|
|
376
505
|
|
|
377
506
|
def call!
|
|
378
|
-
Success(:
|
|
379
|
-
# Success(result: {
|
|
380
|
-
# Failure(:
|
|
507
|
+
Success(:moderated, result: { comment: ... }) # lança Micro::Case::Error::UnexpectedResultType
|
|
508
|
+
# Success(result: { body: '...' }) # lança Micro::Case::Error::MissingResultKeys
|
|
509
|
+
# Failure(:rate_limited) # lança Micro::Case::Error::UnexpectedResultType
|
|
381
510
|
end
|
|
382
511
|
end
|
|
383
512
|
```
|
|
384
513
|
|
|
385
|
-
|
|
386
|
-
- Casos de uso sem o bloco `results` mantêm o comportamento anterior sem restrições — o contrato é opt-in.
|
|
387
|
-
- Subclasses herdam o contrato declarado na classe pai.
|
|
388
|
-
- Exceções capturadas em `Micro::Case::Safe` (que geram `Failure(result: exception)` automaticamente) são exemptas do contrato.
|
|
389
|
-
|
|
390
|
-
[⬆️ Voltar para o índice](#índice-)
|
|
514
|
+
Observações:
|
|
391
515
|
|
|
392
|
-
|
|
516
|
+
- Casos de uso sem um bloco `results` mantêm o comportamento irrestrito anterior — o contrato é opt-in.
|
|
517
|
+
- Subclasses herdam o contrato do pai.
|
|
518
|
+
- A auto-falha produzida pela validação de atributos via [`accept:` / `reject:`](#accept-e-reject-padrão) escapa do contrato — combinar `results` com validação de atributos **não** exige declarar `:invalid_attributes`.
|
|
519
|
+
- Exceções capturadas pelo [`Micro::Case::Safe`](#modo-seguro--capturando-exceções) (que produzem `Failure(result: exception)`) também escapam do contrato.
|
|
520
|
+
- Contratos são independentes de [hooks](#hooks-de-resultado) e [pattern matching](#pattern-matching): o contrato dispara no momento da chamada `Success(...)` / `Failure(...)`, dentro do `call!`. Uma vez que o `Result` existe, quem chama consome ele normalmente — não há enforcement no lado de quem chama.
|
|
393
521
|
|
|
394
|
-
|
|
395
|
-
`#on_success`, `on_failure`.
|
|
522
|
+
#### Hooks de resultado
|
|
396
523
|
|
|
397
|
-
|
|
524
|
+
`on_success` e `on_failure` ramificam em função do tipo do resultado. Passe um símbolo para casar com um tipo específico, ou nenhum argumento para casar com qualquer um:
|
|
398
525
|
|
|
399
526
|
```ruby
|
|
400
|
-
class
|
|
401
|
-
|
|
527
|
+
class ChangePassword < Micro::Case
|
|
528
|
+
attributes :user, :new_password
|
|
402
529
|
|
|
403
530
|
def call!
|
|
404
|
-
return Failure
|
|
405
|
-
return Failure
|
|
531
|
+
return Failure(:weak, result: { msg: 'password too short' }) unless new_password.is_a?(String) && new_password.length >= 8
|
|
532
|
+
return Failure(:reused, result: { msg: 'password recently used' }) if user.recently_used?(new_password)
|
|
406
533
|
|
|
407
|
-
|
|
534
|
+
user.update_password!(new_password)
|
|
535
|
+
Success result: { user: }
|
|
408
536
|
end
|
|
409
537
|
end
|
|
410
538
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
.
|
|
419
|
-
.on_failure
|
|
420
|
-
|
|
421
|
-
# O output será:
|
|
422
|
-
# 6
|
|
423
|
-
|
|
424
|
-
#===================================#
|
|
425
|
-
# Lançando um erro em caso de falha #
|
|
426
|
-
#===================================#
|
|
427
|
-
|
|
428
|
-
Double
|
|
429
|
-
.call(number: -1)
|
|
430
|
-
.on_success { |result| p result[:number] }
|
|
431
|
-
.on_failure { |_result, use_case| puts "#{use_case.class.name} was the use case responsible for the failure" }
|
|
432
|
-
.on_failure(:invalid) { |result| raise TypeError, result[:msg] }
|
|
433
|
-
.on_failure(:lte_zero) { |result| raise ArgumentError, result[:msg] }
|
|
434
|
-
|
|
435
|
-
# O output será:
|
|
436
|
-
#
|
|
437
|
-
# 1. Imprimirá a mensagem: Double was the use case responsible for the failure
|
|
438
|
-
# 2. Lançará a exception: ArgumentError (the number must be greater than 0)
|
|
439
|
-
|
|
440
|
-
# Nota:
|
|
441
|
-
# ----
|
|
442
|
-
# O caso de uso responsável estará sempre acessível como o segundo argumento do hook
|
|
539
|
+
ChangePassword
|
|
540
|
+
.call(user: ada, new_password: 'long-enough-1')
|
|
541
|
+
.on_success { audit "password updated for #{it[:user].id}" }
|
|
542
|
+
.on_failure(:weak) { raise ArgumentError, it[:msg] }
|
|
543
|
+
.on_failure(:reused) { raise ArgumentError, it[:msg] }
|
|
544
|
+
|
|
545
|
+
ChangePassword
|
|
546
|
+
.call(user: ada, new_password: 'short')
|
|
547
|
+
.on_failure { |_r, use_case| audit "#{use_case.class.name} failed" } # 1. ChangePassword failed
|
|
548
|
+
.on_failure(:weak) { raise ArgumentError, it[:msg] } # 2. ArgumentError
|
|
443
549
|
```
|
|
444
550
|
|
|
445
|
-
|
|
551
|
+
> O caso de uso responsável pelo resultado está sempre disponível como o segundo argumento do bloco do hook.
|
|
446
552
|
|
|
447
|
-
|
|
553
|
+
Sem um tipo explícito, o bloco recebe o resultado inteiro, então você pode ramificar com um `case`:
|
|
448
554
|
|
|
449
555
|
```ruby
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
def call!
|
|
454
|
-
return Failure(:invalid) unless number.is_a?(Numeric)
|
|
455
|
-
return Failure :lte_zero, result: attributes(:number) if number <= 0
|
|
456
|
-
|
|
457
|
-
Success result: { number: number * 2 }
|
|
458
|
-
end
|
|
459
|
-
end
|
|
460
|
-
|
|
461
|
-
Double
|
|
462
|
-
.call(number: -1)
|
|
556
|
+
ChangePassword
|
|
557
|
+
.call(user: ada, new_password: 'short')
|
|
463
558
|
.on_failure do |result, use_case|
|
|
464
559
|
case result.type
|
|
465
|
-
when :
|
|
466
|
-
when :
|
|
560
|
+
when :weak then raise ArgumentError, 'password too short'
|
|
561
|
+
when :reused then raise ArgumentError, 'password recently used'
|
|
467
562
|
else raise NotImplementedError
|
|
468
563
|
end
|
|
469
564
|
end
|
|
470
|
-
|
|
471
|
-
# O output será uma exception:
|
|
472
|
-
#
|
|
473
|
-
# ArgumentError (number `-1` must be greater than 0)
|
|
474
565
|
```
|
|
475
566
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
##### Usando decomposição para acessar os dados e tipo do resultado
|
|
479
|
-
|
|
480
|
-
A sintaxe para decompor um Array pode ser usada na declaração de variáveis e nos argumentos de métodos/blocos.
|
|
481
|
-
Se você não sabia disso, confira a [documentação do Ruby](https://ruby-doc.org/core-2.2.0/doc/syntax/assignment_rdoc.html#label-Array+Decomposition).
|
|
567
|
+
Se o mesmo hook for declarado múltiplas vezes, todo match dispara:
|
|
482
568
|
|
|
483
569
|
```ruby
|
|
484
|
-
|
|
570
|
+
calls = 0
|
|
571
|
+
result = ChangePassword.call(user: ada, new_password: 'long-enough-1')
|
|
485
572
|
|
|
486
|
-
|
|
487
|
-
.
|
|
488
|
-
.
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
when :lte_zero then raise ArgumentError, "number `#{data[:number]}` must be greater than 0"
|
|
492
|
-
else raise NotImplementedError
|
|
493
|
-
end
|
|
494
|
-
end
|
|
573
|
+
result
|
|
574
|
+
.on_success { calls += 1 }
|
|
575
|
+
.on_success { calls += 1 }
|
|
576
|
+
.on_success(:ok) { calls += 1 }
|
|
577
|
+
.on_success(:ok) { calls += 1 }
|
|
495
578
|
|
|
496
|
-
#
|
|
497
|
-
#
|
|
498
|
-
# ArgumentError (the number `-2` must be greater than 0)
|
|
579
|
+
calls # => 4
|
|
499
580
|
```
|
|
500
581
|
|
|
501
|
-
|
|
582
|
+
#### Pattern matching
|
|
502
583
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
##### Usando pattern matching para desestruturar um resultado
|
|
506
|
-
|
|
507
|
-
`Micro::Case::Result` implementa [`deconstruct`](https://docs.ruby-lang.org/en/3.4/syntax/pattern_matching_rdoc.html) e [`deconstruct_keys`](https://docs.ruby-lang.org/en/3.4/syntax/pattern_matching_rdoc.html), então o pattern matching do Ruby (`case`/`in`) funciona de forma nativa (requer Ruby `>= 2.7`).
|
|
584
|
+
`Micro::Case::Result` implementa [`deconstruct`](https://docs.ruby-lang.org/en/3.4/syntax/pattern_matching_rdoc.html) e [`deconstruct_keys`](https://docs.ruby-lang.org/en/3.4/syntax/pattern_matching_rdoc.html), então o `case`/`in` do Ruby funciona direto (requer Ruby ≥ 2.7):
|
|
508
585
|
|
|
509
586
|
```ruby
|
|
510
|
-
result = Divide.call(a: 10, b: 2)
|
|
511
|
-
|
|
512
587
|
case result
|
|
513
588
|
in { success: _, data: { number: Numeric => number } }
|
|
514
|
-
puts "
|
|
589
|
+
puts "got #{number}"
|
|
515
590
|
in { failure: :invalid_attributes, data: { invalid_attributes: errors } }
|
|
516
|
-
warn "
|
|
591
|
+
warn "bad input: #{errors.keys.join(", ")}"
|
|
517
592
|
in { failure: :exception, data: { exception: } }
|
|
518
593
|
warn "boom: #{exception.message}"
|
|
519
594
|
end
|
|
@@ -521,19 +596,17 @@ end
|
|
|
521
596
|
|
|
522
597
|
Os hash patterns expõem essas chaves:
|
|
523
598
|
|
|
524
|
-
| Chave
|
|
525
|
-
|
|
|
526
|
-
| `success:`
|
|
527
|
-
| `failure:`
|
|
528
|
-
| `type:`
|
|
529
|
-
| `data:`
|
|
530
|
-
| `result:`
|
|
531
|
-
| `use_case:`
|
|
532
|
-
| `transitions:`
|
|
533
|
-
|
|
534
|
-
> **Nota:** No lado de **leitura**, `Result#data` também é acessível como `Result#value` (apelido existente). No lado de **pattern matching**, a chave `data:` também é acessível como `result:` — ambas se referem ao mesmo payload.
|
|
599
|
+
| Chave | Presente em | Valor |
|
|
600
|
+
| -------------- | ------------- | ----------------------------------------------------------------------------------- |
|
|
601
|
+
| `success:` | só em sucesso | o `type` do resultado (ex. `:ok`) |
|
|
602
|
+
| `failure:` | só em falha | o `type` do resultado (ex. `:invalid_attributes`) |
|
|
603
|
+
| `type:` | sempre | o `type` do resultado |
|
|
604
|
+
| `data:` | sempre | o hash de `data` do resultado |
|
|
605
|
+
| `result:` | sempre | alias de `data:` (espelha a keyword `Success(result: …)` usada no local da criação) |
|
|
606
|
+
| `use_case:` | sempre | a instância do caso de uso que produziu o resultado |
|
|
607
|
+
| `transitions:` | sempre | o array de `transitions` do resultado |
|
|
535
608
|
|
|
536
|
-
`Result#deconstruct` retorna um array de três elementos `[status, type, data]
|
|
609
|
+
`Result#deconstruct` retorna um array de três elementos `[status, type, data]` onde `status` é `:success` ou `:failure`, então array patterns podem usar o status como discriminante — espelhando como bibliotecas com classes `Success` / `Failure` separadas são pattern-matched, mesmo que `Micro::Case::Result` seja uma única classe:
|
|
537
610
|
|
|
538
611
|
```ruby
|
|
539
612
|
case result
|
|
@@ -546,741 +619,581 @@ in [:failure, :exception, { exception: }]
|
|
|
546
619
|
end
|
|
547
620
|
```
|
|
548
621
|
|
|
549
|
-
>
|
|
622
|
+
> `Result#to_ary` continua igual e retorna `[data, type]` (usado em multi-assignment, ex. `data, type = result`). O pattern matching do Ruby usa `#deconstruct`, então os dois métodos intencionalmente retornam formatos diferentes.
|
|
550
623
|
|
|
551
|
-
|
|
624
|
+
#### Decomposição
|
|
552
625
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
Resposta: Se o tipo do resultado for identificado o hook será sempre executado.
|
|
626
|
+
Dentro de um hook sem tipo, o resultado também pode ser decomposto em array `[data, type]`:
|
|
556
627
|
|
|
557
628
|
```ruby
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
else
|
|
565
|
-
Failure :invalid, result: { msg: 'number must be a numeric value' }
|
|
629
|
+
ChangePassword
|
|
630
|
+
.call(user: ada, new_password: 'short')
|
|
631
|
+
.on_failure do |(data, type), use_case|
|
|
632
|
+
case type
|
|
633
|
+
when :weak then raise ArgumentError, data[:msg]
|
|
634
|
+
when :reused then raise ArgumentError, data[:msg]
|
|
635
|
+
else raise NotImplementedError
|
|
566
636
|
end
|
|
567
637
|
end
|
|
568
|
-
end
|
|
569
|
-
|
|
570
|
-
result = Double.call(number: 3)
|
|
571
|
-
result.data # { number: 6 }
|
|
572
|
-
result[:number] * 4 # 24
|
|
573
|
-
|
|
574
|
-
accum = 0
|
|
575
|
-
|
|
576
|
-
result
|
|
577
|
-
.on_success { |result| accum += result[:number] }
|
|
578
|
-
.on_success { |result| accum += result[:number] }
|
|
579
|
-
.on_success(:computed) { |result| accum += result[:number] }
|
|
580
|
-
.on_success(:computed) { |result| accum += result[:number] }
|
|
581
|
-
|
|
582
|
-
accum # 24
|
|
583
|
-
|
|
584
|
-
result[:number] * 4 == accum # true
|
|
585
638
|
```
|
|
586
639
|
|
|
587
|
-
####
|
|
640
|
+
#### Continuações dinâmicas com `Result#then`
|
|
588
641
|
|
|
589
|
-
|
|
642
|
+
`Result#then` aplica outro caso de uso (ou callable) a um resultado de sucesso — `Failure` curto-circuita. Use para construir continuações dinâmicas a partir de um resultado que já existe:
|
|
590
643
|
|
|
591
644
|
```ruby
|
|
592
|
-
class
|
|
593
|
-
attribute :
|
|
645
|
+
class FindActiveUser < Micro::Case
|
|
646
|
+
attribute :email
|
|
594
647
|
|
|
595
648
|
def call!
|
|
596
|
-
|
|
649
|
+
user = User.active.find_by(email:)
|
|
650
|
+
|
|
651
|
+
return Success result: { user: } if user
|
|
597
652
|
|
|
598
|
-
Failure result:
|
|
653
|
+
Failure result: { email: }
|
|
599
654
|
end
|
|
600
655
|
end
|
|
601
656
|
|
|
602
|
-
class
|
|
603
|
-
attribute :
|
|
657
|
+
class GenerateInviteToken < Micro::Case
|
|
658
|
+
attribute :user
|
|
604
659
|
|
|
605
660
|
def call!
|
|
606
|
-
Success result: {
|
|
661
|
+
Success result: { user:, token: SecureRandom.hex(16) }
|
|
607
662
|
end
|
|
608
663
|
end
|
|
609
664
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
.then(Add3)
|
|
614
|
-
|
|
615
|
-
result1.data # {'number' => -1}
|
|
616
|
-
result1.failure? # true
|
|
617
|
-
|
|
618
|
-
# ---
|
|
619
|
-
|
|
620
|
-
result2 =
|
|
621
|
-
ForbidNegativeNumber
|
|
622
|
-
.call(number: 1)
|
|
623
|
-
.then(Add3)
|
|
624
|
-
|
|
625
|
-
result2.data # {'number' => 4}
|
|
626
|
-
result2.success? # true
|
|
665
|
+
FindActiveUser.call(email: 'unknown@example.com').then(GenerateInviteToken).failure? # => true
|
|
666
|
+
FindActiveUser.call(email: 'ada@example.com').then(GenerateInviteToken).data
|
|
667
|
+
# => { user: #<User ...>, token: "9f2b…" }
|
|
627
668
|
```
|
|
628
669
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
[⬆️ Voltar para o índice](#índice-)
|
|
632
|
-
|
|
633
|
-
##### O que acontece quando um `Micro::Case::Result#then` recebe um bloco?
|
|
634
|
-
|
|
635
|
-
Ele passará o próprio resultado (uma instância do `Micro::Case::Result`) como argumento do bloco, e retornará o output do bloco ao invés dele mesmo. e.g:
|
|
670
|
+
Passar um bloco yielda `self` (um `Micro::Case::Result`) e retorna o valor do bloco — útil para desembrulhar em um tipo não-Result:
|
|
636
671
|
|
|
637
672
|
```ruby
|
|
638
|
-
class
|
|
639
|
-
|
|
673
|
+
class FindUser < Micro::Case
|
|
674
|
+
attribute :email
|
|
640
675
|
|
|
641
676
|
def call!
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
Failure(:attributes_arent_numbers)
|
|
646
|
-
end
|
|
677
|
+
user = User.find_by(email:)
|
|
678
|
+
|
|
679
|
+
user ? Success(result: { user: }) : Failure(:not_found)
|
|
647
680
|
end
|
|
648
681
|
end
|
|
649
682
|
|
|
650
|
-
#
|
|
651
|
-
|
|
652
|
-
success_result =
|
|
653
|
-
Add
|
|
654
|
-
.call(a: 2, b: 2)
|
|
655
|
-
.then { |result| result.success? ? result[:sum] : 0 }
|
|
656
|
-
|
|
657
|
-
puts success_result # 4
|
|
658
|
-
|
|
659
|
-
# --
|
|
660
|
-
|
|
661
|
-
failure_result =
|
|
662
|
-
Add
|
|
663
|
-
.call(a: 2, b: '2')
|
|
664
|
-
.then { |result| result.success? ? result[:sum] : 0 }
|
|
665
|
-
|
|
666
|
-
puts failure_result # 0
|
|
683
|
+
FindUser.call(email: 'ada@example.com').then { it.success? ? it[:user].id : nil } # => 42
|
|
684
|
+
FindUser.call(email: 'unknown@example.com').then { it.success? ? it[:user].id : nil } # => nil
|
|
667
685
|
```
|
|
668
686
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
##### Como fazer injeção de dependência usando este recurso?
|
|
672
|
-
|
|
673
|
-
Passe um `Hash` como segundo argumento do método `Micro::Case::Result#then`.
|
|
687
|
+
Passe um `Hash` extra para injetar atributos no próximo caso de uso:
|
|
674
688
|
|
|
675
689
|
```ruby
|
|
676
690
|
Todo::FindAllForUser
|
|
677
691
|
.call(user: current_user, params: params)
|
|
678
692
|
.then(Paginate)
|
|
679
693
|
.then(Serialize::PaginatedRelationAsJson, serializer: Todo::Serializer)
|
|
680
|
-
.on_success {
|
|
694
|
+
.on_success { render_json(200, data: it[:todos]) }
|
|
681
695
|
```
|
|
682
696
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
#### Steps internos — construindo um flow inline dentro do `call!`
|
|
686
|
-
|
|
687
|
-
`Result#then` (e seu alias `|`) é a **terceira forma de compor um
|
|
688
|
-
flow** no u-case, lado a lado com `Micro::Cases.flow(...)` e a macro
|
|
689
|
-
de nível de classe `flow ...`. Em vez de ligar casos de uso entre si,
|
|
690
|
-
você mantém o encadeamento *dentro* do `call!` de um único caso de
|
|
691
|
-
uso: cada elo é um método, lambda ou outra classe de caso de uso;
|
|
692
|
-
cada elo retorna um `Micro::Case::Result`; os dados do `Success` de
|
|
693
|
-
cada elo viram os argumentos nomeados do próximo; e cada elo
|
|
694
|
-
contribui com uma linha em `result.transitions` — exatamente como um
|
|
695
|
-
step em um flow de nível superior.
|
|
696
|
-
|
|
697
|
-
##### O que `Result#then` (e `|`) aceitam
|
|
698
|
-
|
|
699
|
-
| Formato | Exemplo |
|
|
700
|
-
| --- | --- |
|
|
701
|
-
| `Symbol` (nome de método) | `result.then(:sum_a_and_b)` |
|
|
702
|
-
| Objeto `Method` ligado | `result.then(method(:sum_a_and_b))` |
|
|
703
|
-
| `Lambda` / `Proc` | `result.then(-> data { sum_a_and_b(**data) })` |
|
|
704
|
-
| Classe de caso de uso | `result.then(SumHalf)` |
|
|
705
|
-
| `Symbol` + Hash de defaults | `result.then(:add, number: 3)` |
|
|
706
|
-
| Bloco | `result.then { \|r\| r.success? ? r[:sum] : 0 }` |
|
|
707
|
-
|
|
708
|
-
O método conectado **precisa** retornar um `Micro::Case::Result`.
|
|
709
|
-
Qualquer outro retorno levanta `Micro::Case::Error::UnexpectedResult`
|
|
710
|
-
— por exemplo um método que devolve um `Hash` será rejeitado com uma
|
|
711
|
-
mensagem do tipo `MeuCase#method(:foo) must return an instance of
|
|
712
|
-
Micro::Case::Result`.
|
|
697
|
+
> `Result#then` também aceita um `Symbol`, um objeto `Method`, ou uma `Lambda` — veja [Steps internos](#steps-internos--cadeias-com-resultthen).
|
|
713
698
|
|
|
714
|
-
|
|
699
|
+
[⬆️ Voltar ao topo](#índice-)
|
|
715
700
|
|
|
716
|
-
|
|
717
|
-
class SumHalf < Micro::Case
|
|
718
|
-
attribute :sum
|
|
701
|
+
### Validando atributos
|
|
719
702
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
end
|
|
703
|
+
#### `accept:` e `reject:` (padrão)
|
|
704
|
+
|
|
705
|
+
Desde a 5.2.0, todo caso de uso inclui a [extensão `accept` do `u-attributes`](https://github.com/serradura/u-attributes). Declare uma expectativa de tipo (ou qualquer predicado) no atributo, e o caso de uso falha automaticamente com `type: :invalid_attributes` quando um atributo é rejeitado — sem precisar validar dentro do `call!`:
|
|
724
706
|
|
|
725
|
-
|
|
726
|
-
|
|
707
|
+
```ruby
|
|
708
|
+
class CreateUser < Micro::Case
|
|
709
|
+
attribute :name, accept: String
|
|
710
|
+
attribute :email, accept: ->(v) { v.is_a?(String) && v.include?('@') }
|
|
711
|
+
attribute :age, accept: Integer, allow_nil: true
|
|
727
712
|
|
|
728
713
|
def call!
|
|
729
|
-
|
|
730
|
-
.then(:sum_a_and_b)
|
|
731
|
-
.then(:add, number: 3)
|
|
732
|
-
.then(SumHalf)
|
|
714
|
+
Success result: { user: User.create!(attributes) }
|
|
733
715
|
end
|
|
716
|
+
end
|
|
734
717
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
def validate_numbers
|
|
738
|
-
Kind.of?(Numeric, a, b) ? Success(:valid) : Failure()
|
|
739
|
-
end
|
|
718
|
+
CreateUser.call(name: 'Bob', email: 'bob@example.com')
|
|
719
|
+
# => #<Success type=:ok ...>
|
|
740
720
|
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
721
|
+
CreateUser.call(name: 42, email: 'not-an-email')
|
|
722
|
+
# => #<Failure type=:invalid_attributes data={
|
|
723
|
+
# errors: {
|
|
724
|
+
# "name" => "expected to be a kind of String",
|
|
725
|
+
# "email" => "is invalid"
|
|
726
|
+
# }
|
|
727
|
+
# }>
|
|
728
|
+
```
|
|
744
729
|
|
|
745
|
-
|
|
746
|
-
Success :second_sum, result: { sum: sum + number }
|
|
747
|
-
end
|
|
748
|
-
end
|
|
730
|
+
O tipo da falha segue a mesma configuração usada pela integração com ActiveModel — veja `set_activemodel_validation_errors_failure` em [Configuração](#configuração).
|
|
749
731
|
|
|
750
|
-
|
|
732
|
+
#### Integração com ActiveModel (opt-in)
|
|
751
733
|
|
|
752
|
-
|
|
753
|
-
result.data # { sum: 6.5 }
|
|
754
|
-
result.transitions # 4 entradas — veja abaixo
|
|
755
|
-
```
|
|
734
|
+
Você pode sobrepor regras estilo Rails (`validates`) em cima de `accept:` / `reject:` para validações mais ricas (`presence`, `numericality`, `format`, validators customizados…). Requer [`activemodel >= 6.0`](https://rubygems.org/gems/activemodel) na sua aplicação.
|
|
756
735
|
|
|
757
|
-
`
|
|
736
|
+
A forma mais simples — `validates` está disponível em todo caso de uso, e você falha manualmente:
|
|
758
737
|
|
|
759
738
|
```ruby
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
success: { type: :valid, result: { valid: true } },
|
|
763
|
-
accessible_attributes: [:a, :b] },
|
|
739
|
+
class CreatePost < Micro::Case
|
|
740
|
+
attributes :title, :body
|
|
764
741
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
accessible_attributes: [:a, :b, :valid] },
|
|
742
|
+
validates :title, :body, presence: true
|
|
743
|
+
validates :title, length: { maximum: 120 }
|
|
768
744
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
accessible_attributes: [:a, :b, :valid, :number, :sum] },
|
|
745
|
+
def call!
|
|
746
|
+
return Failure :invalid_attributes, result: { errors: self.errors } if invalid?
|
|
772
747
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
]
|
|
748
|
+
Success result: { post: Post.create!(title:, body:) }
|
|
749
|
+
end
|
|
750
|
+
end
|
|
777
751
|
```
|
|
778
752
|
|
|
779
|
-
|
|
780
|
-
uso hospedeiro**, portanto as três primeiras transições reportam
|
|
781
|
-
`class: DoSomeSum`. Apenas o elo `SumHalf`, que é outra classe de
|
|
782
|
-
caso de uso, contribui com uma transição com `use_case.class`
|
|
783
|
-
diferente. O `accessible_attributes` cresce conforme o `Success` de
|
|
784
|
-
cada elo é mesclado nos dados acumulados.
|
|
785
|
-
|
|
786
|
-
##### O alias `|` (pipe)
|
|
787
|
-
|
|
788
|
-
`|` é açúcar para `.then(...)`. O exemplo anterior fica:
|
|
753
|
+
Para fazer casos de uso **falharem automaticamente** quando `invalid?` é `true`, require o entry point de auto-validação:
|
|
789
754
|
|
|
790
755
|
```ruby
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
end
|
|
756
|
+
# Gemfile
|
|
757
|
+
gem 'u-case', require: 'u-case/with_activemodel_validation'
|
|
794
758
|
```
|
|
795
759
|
|
|
796
|
-
|
|
797
|
-
idênticos.
|
|
760
|
+
…ou habilite via [Configuração](#configuração). O exemplo então colapsa:
|
|
798
761
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
> bloco/lambda, é possível escrever uma cadeia que se lê quase
|
|
802
|
-
> exatamente como o operador `|>` do Elixir. Cada lambda recebe o
|
|
803
|
-
> hash de dados acumulados como `it` e ainda precisa terminar em
|
|
804
|
-
> uma chamada `Success(...)` / `Failure(...)`:
|
|
805
|
-
>
|
|
806
|
-
> ```ruby
|
|
807
|
-
> def call!
|
|
808
|
-
> validate_something \
|
|
809
|
-
> | -> { do_something_with(**it) } \
|
|
810
|
-
> | -> { and_another_thing_with(**it) }
|
|
811
|
-
> end
|
|
812
|
-
> ```
|
|
813
|
-
>
|
|
814
|
-
> No Ruby 2.7 – 3.3 (onde `it` é apenas um identificador
|
|
815
|
-
> indefinido), use a forma explícita portátil
|
|
816
|
-
> `->(data) { do_something_with(**data) }` mostrada na próxima seção.
|
|
762
|
+
```ruby
|
|
763
|
+
require 'u-case/with_activemodel_validation'
|
|
817
764
|
|
|
818
|
-
|
|
765
|
+
class CreatePost < Micro::Case
|
|
766
|
+
attributes :title, :body
|
|
819
767
|
|
|
820
|
-
|
|
821
|
-
|
|
768
|
+
validates :title, :body, presence: true
|
|
769
|
+
validates :title, length: { maximum: 120 }
|
|
822
770
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
.then(method(:sum_a_and_b))
|
|
827
|
-
.then(->(data) { add(**data, number: 3) })
|
|
828
|
-
.then(SumHalf)
|
|
771
|
+
def call!
|
|
772
|
+
Success result: { post: Post.create!(title:, body:) }
|
|
773
|
+
end
|
|
829
774
|
end
|
|
830
775
|
```
|
|
831
776
|
|
|
832
|
-
|
|
777
|
+
Quando tanto `accept:` quanto validações do ActiveModel estão presentes, a ordem de execução é:
|
|
833
778
|
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
são invocados, e o `result` final é a falha:
|
|
779
|
+
1. `u-attributes` resolve o default de cada atributo.
|
|
780
|
+
2. `u-attributes` roda as checagens de `accept:` / `reject:`.
|
|
781
|
+
3. `u-case` roda as validações do ActiveModel **apenas se** todos os atributos foram aceitos.
|
|
838
782
|
|
|
839
|
-
|
|
840
|
-
DoSomeSum.call(a: 1, b: '2')
|
|
841
|
-
|
|
842
|
-
# validate_numbers retorna Failure() → :sum_a_and_b, :add e SumHalf
|
|
843
|
-
# nunca rodam. result.failure? == true, result.transitions tem 1
|
|
844
|
-
# entrada.
|
|
845
|
-
```
|
|
783
|
+
> A auto-validação também é herdada por `Micro::Case::Strict` e `Micro::Case::Safe`.
|
|
846
784
|
|
|
847
|
-
#####
|
|
785
|
+
##### Desabilitando a auto-validação em um caso específico
|
|
848
786
|
|
|
849
|
-
|
|
850
|
-
sendo apenas um caso de uso, portanto pode ser colocado em qualquer
|
|
851
|
-
construtor de flow:
|
|
787
|
+
Use a macro `disable_auto_validation`:
|
|
852
788
|
|
|
853
789
|
```ruby
|
|
854
|
-
|
|
855
|
-
NormalizeParams,
|
|
856
|
-
DoSomeSum, # ← usa .then(:method) internamente
|
|
857
|
-
EnqueueIndexingJob
|
|
858
|
-
])
|
|
859
|
-
```
|
|
860
|
-
|
|
861
|
-
As transições internas da classe hospedeira são intercaladas com as
|
|
862
|
-
transições dos steps externos na ordem de execução. Se `DoSomeSum`
|
|
863
|
-
produz 4 transições internas e o flow externo tem 2 outros steps,
|
|
864
|
-
`result.transitions` final tem 6 entradas.
|
|
865
|
-
|
|
866
|
-
##### Steps internos **sem** transações
|
|
790
|
+
require 'u-case/with_activemodel_validation'
|
|
867
791
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
como qualquer outro código em `call!`: efeitos colaterais feitos por
|
|
871
|
-
elos anteriores **persistem** mesmo se um elo posterior retornar
|
|
872
|
-
`Failure`. A cadeia é interrompida, mas tudo que já foi escrito no
|
|
873
|
-
banco permanece escrito:
|
|
792
|
+
class CountPosts < Micro::Case
|
|
793
|
+
disable_auto_validation
|
|
874
794
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
attributes :name, :info
|
|
795
|
+
attribute :user
|
|
796
|
+
validates :user, presence: true
|
|
878
797
|
|
|
879
798
|
def call!
|
|
880
|
-
|
|
881
|
-
.then(:create_profile)
|
|
799
|
+
Success result: { count: user.posts.count }
|
|
882
800
|
end
|
|
801
|
+
end
|
|
883
802
|
|
|
884
|
-
|
|
803
|
+
CountPosts.call(user: nil)
|
|
804
|
+
# => NoMethodError (undefined method `posts' for nil:NilClass)
|
|
805
|
+
```
|
|
885
806
|
|
|
886
|
-
|
|
887
|
-
user = User.create(name: name)
|
|
888
|
-
Success result: { user: user }
|
|
889
|
-
end
|
|
807
|
+
##### `Kind::Validator`
|
|
890
808
|
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
809
|
+
A [gem `kind`](https://github.com/serradura/kind) traz um [`Kind::Validator`](https://github.com/serradura/kind#kindvalidator-activemodelvalidations) para o ActiveModel que valida tipos usando seu sistema de tipos em runtime. Requerer `'u-case/with_activemodel_validation'` também carrega o `Kind::Validator`:
|
|
810
|
+
|
|
811
|
+
```ruby
|
|
812
|
+
class Todo::List::AddItem < Micro::Case
|
|
813
|
+
attributes :user, :params
|
|
814
|
+
|
|
815
|
+
validates :user, kind: User
|
|
816
|
+
validates :params, kind: ActionController::Parameters
|
|
817
|
+
|
|
818
|
+
def call!
|
|
819
|
+
todo_params = params.require(:todo).permit(:title, :due_at)
|
|
820
|
+
todo = user.todos.create(todo_params)
|
|
894
821
|
|
|
895
|
-
Success result: {
|
|
822
|
+
Success result: { todo: todo }
|
|
823
|
+
rescue ActionController::ParameterMissing => e
|
|
824
|
+
Failure :parameter_missing, result: { message: e.message }
|
|
896
825
|
end
|
|
897
826
|
end
|
|
898
|
-
|
|
899
|
-
CreateUserWithProfileInline.call(name: 'Rodrigo', info: '')
|
|
900
|
-
# create_user já INSERIU a linha do user; create_profile falhou.
|
|
901
|
-
# user está persistido; profile não. Não há rollback automático.
|
|
902
827
|
```
|
|
903
828
|
|
|
904
|
-
|
|
905
|
-
envolva a cadeia em uma transação. Como steps internos são apenas
|
|
906
|
-
outra forma de expressar um flow (um flow *interno*), a história
|
|
907
|
-
transacional é exatamente a que já está documentada em
|
|
908
|
-
[Como executar um caso de uso ou flow dentro de uma transação de banco de dados?](#como-executar-um-caso-de-uso-ou-flow-dentro-de-uma-transação-de-banco-de-dados)
|
|
909
|
-
abaixo — a subseção "Flows com steps internos sob transações" lá
|
|
910
|
-
percorre tanto a forma inline `transaction { ... }` quanto a forma
|
|
911
|
-
com `transaction: true` para um caso hospedeiro de steps internos.
|
|
829
|
+
[⬆️ Voltar ao topo](#índice-)
|
|
912
830
|
|
|
913
|
-
|
|
914
|
-
> `with_methods_test.rb` e `with_lambdas_test.rb` para exemplos
|
|
915
|
-
> completos de cada forma, e
|
|
916
|
-
> `test/micro/cases/flow/internal_steps_in_flows_test.rb` para a
|
|
917
|
-
> interação com flows e transações (acumulação, transições e
|
|
918
|
-
> rollback em todos os níveis de aninhamento).
|
|
831
|
+
### Compondo casos de uso
|
|
919
832
|
|
|
920
|
-
[
|
|
833
|
+
Uma composição encadeia casos de uso de forma que os dados do `Success` de cada step alimentam a entrada do próximo step. Há duas formas de compor: [Flows](#flows) — que cobrem tanto `Micro::Cases.flow(...)` quanto a macro `flow ...` no nível da classe — e [Steps internos](#steps-internos--cadeias-com-resultthen) (a cadeia `Result#then` / `|` dentro de um único `call!`). Qualquer uma das formas pode ser envolvida em uma [Transação](#transações).
|
|
921
834
|
|
|
922
|
-
|
|
835
|
+
#### Flows
|
|
923
836
|
|
|
924
|
-
|
|
837
|
+
Um `Micro::Cases::Flow` é uma composição independente. Construa um com `Micro::Cases.flow([...])` ou com a macro `flow ...` no nível da classe:
|
|
925
838
|
|
|
926
839
|
```ruby
|
|
927
840
|
module Steps
|
|
928
|
-
class
|
|
929
|
-
attribute :
|
|
841
|
+
class ParseTags < Micro::Case
|
|
842
|
+
attribute :tags
|
|
930
843
|
|
|
931
844
|
def call!
|
|
932
|
-
if
|
|
933
|
-
Success result: {
|
|
845
|
+
if tags.is_a?(String)
|
|
846
|
+
Success result: { tags: tags.split(',').map(&:strip) }
|
|
934
847
|
else
|
|
935
|
-
Failure result: { message: '
|
|
848
|
+
Failure result: { message: 'tags must be a comma-separated String' }
|
|
936
849
|
end
|
|
937
850
|
end
|
|
938
851
|
end
|
|
939
852
|
|
|
940
|
-
class
|
|
941
|
-
attribute :
|
|
942
|
-
|
|
943
|
-
def call!
|
|
944
|
-
Success result: { numbers: numbers.map { |number| number + 2 } }
|
|
945
|
-
end
|
|
853
|
+
class Downcase < Micro::Case::Strict
|
|
854
|
+
attribute :tags
|
|
855
|
+
def call!; Success result: { tags: tags.map(&:downcase) }; end
|
|
946
856
|
end
|
|
947
857
|
|
|
948
|
-
class
|
|
949
|
-
attribute :
|
|
950
|
-
|
|
951
|
-
def call!
|
|
952
|
-
Success result: { numbers: numbers.map { |number| number * 2 } }
|
|
953
|
-
end
|
|
858
|
+
class StripHashPrefix < Micro::Case::Strict
|
|
859
|
+
attribute :tags
|
|
860
|
+
def call!; Success result: { tags: tags.map { it.sub(/\A#/, '') } }; end
|
|
954
861
|
end
|
|
955
862
|
|
|
956
|
-
class
|
|
957
|
-
attribute :
|
|
958
|
-
|
|
959
|
-
def call!
|
|
960
|
-
Success result: { numbers: numbers.map { |number| number * number } }
|
|
961
|
-
end
|
|
863
|
+
class RemoveDuplicates < Micro::Case::Strict
|
|
864
|
+
attribute :tags
|
|
865
|
+
def call!; Success result: { tags: tags.uniq }; end
|
|
962
866
|
end
|
|
963
867
|
end
|
|
964
868
|
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
Add2ToAllNumbers = Micro::Cases.flow([
|
|
970
|
-
Steps::ConvertTextToNumbers,
|
|
971
|
-
Steps::Add2
|
|
869
|
+
# Usando o construtor a nível de módulo:
|
|
870
|
+
DowncaseTags = Micro::Cases.flow([
|
|
871
|
+
Steps::ParseTags,
|
|
872
|
+
Steps::Downcase
|
|
972
873
|
])
|
|
973
874
|
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
result.success? # true
|
|
977
|
-
result.data # {:numbers => [3, 3, 4, 4, 5, 6]}
|
|
978
|
-
|
|
979
|
-
#--------------------------------#
|
|
980
|
-
# Criando um flow usando classes #
|
|
981
|
-
#--------------------------------#
|
|
875
|
+
DowncaseTags.call(tags: 'Ruby, Rails, RUBY').data
|
|
876
|
+
# => { tags: ["ruby", "rails", "ruby"] }
|
|
982
877
|
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
878
|
+
# Usando uma classe:
|
|
879
|
+
class NormalizeTags < Micro::Case
|
|
880
|
+
flow Steps::ParseTags,
|
|
881
|
+
Steps::Downcase,
|
|
882
|
+
Steps::StripHashPrefix,
|
|
883
|
+
Steps::RemoveDuplicates
|
|
986
884
|
end
|
|
987
885
|
|
|
988
|
-
|
|
989
|
-
call(
|
|
990
|
-
on_failure {
|
|
886
|
+
NormalizeTags
|
|
887
|
+
.call(tags: 42)
|
|
888
|
+
.on_failure { puts it[:message] }
|
|
889
|
+
# => "tags must be a comma-separated String"
|
|
991
890
|
```
|
|
992
891
|
|
|
993
|
-
|
|
892
|
+
Quando um flow falha, `Result#use_case` aponta para o step responsável:
|
|
994
893
|
|
|
995
894
|
```ruby
|
|
996
|
-
result =
|
|
895
|
+
result = NormalizeTags.call(tags: 42)
|
|
896
|
+
result.failure? # => true
|
|
897
|
+
result.use_case.is_a?(Steps::ParseTags) # => true
|
|
898
|
+
```
|
|
997
899
|
|
|
998
|
-
|
|
999
|
-
result.use_case.is_a?(Steps::ConvertTextToNumbers) # true
|
|
900
|
+
##### Compondo flows entre si
|
|
1000
901
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
902
|
+
Flows podem ser steps dentro de outros flows. Misture qualquer um dos três estilos de composição:
|
|
903
|
+
|
|
904
|
+
```ruby
|
|
905
|
+
DowncaseTags = Micro::Cases.flow([Steps::ParseTags, Steps::Downcase])
|
|
906
|
+
DedupedTags = Micro::Cases.flow([Steps::ParseTags, Steps::RemoveDuplicates])
|
|
907
|
+
DowncaseAndDedupedTags = Micro::Cases.flow([DowncaseTags, Steps::RemoveDuplicates])
|
|
908
|
+
StrippedAndDeduped = Micro::Cases.flow([Steps::ParseTags, Steps::StripHashPrefix, Steps::RemoveDuplicates])
|
|
909
|
+
|
|
910
|
+
DowncaseAndDedupedTags
|
|
911
|
+
.call(tags: 'Ruby, Rails, RUBY')
|
|
912
|
+
.on_success { p it[:tags] } # => ["ruby", "rails"]
|
|
1004
913
|
```
|
|
1005
914
|
|
|
1006
|
-
|
|
915
|
+
> Veja [`test/micro/cases/flow/blend_test.rb`](https://github.com/serradura/u-case/blob/main/test/micro/cases/flow/blend_test.rb) para todas as combinações possíveis.
|
|
1007
916
|
|
|
1008
|
-
|
|
917
|
+
##### Acumulação de dados através de um flow
|
|
1009
918
|
|
|
1010
|
-
|
|
919
|
+
A saída de `Success` de cada step é mesclada em um hash de atributos corrente, que se torna a entrada do próximo step. Os steps não precisam encadear inputs manualmente — eles apenas declaram o que precisam:
|
|
1011
920
|
|
|
1012
921
|
```ruby
|
|
1013
|
-
module
|
|
1014
|
-
class
|
|
1015
|
-
attribute :
|
|
922
|
+
module Users
|
|
923
|
+
class FindByEmail < Micro::Case
|
|
924
|
+
attribute :email
|
|
1016
925
|
|
|
1017
926
|
def call!
|
|
1018
|
-
|
|
1019
|
-
Success result: { numbers: numbers.map(&:to_i) }
|
|
1020
|
-
else
|
|
1021
|
-
Failure result: { message: 'numbers must contain only numeric types' }
|
|
1022
|
-
end
|
|
1023
|
-
end
|
|
1024
|
-
end
|
|
927
|
+
user = User.find_by(email:)
|
|
1025
928
|
|
|
1026
|
-
|
|
1027
|
-
attribute :numbers
|
|
929
|
+
return Success result: { user: } if user
|
|
1028
930
|
|
|
1029
|
-
|
|
1030
|
-
Success result: { numbers: numbers.map { |number| number + 2 } }
|
|
931
|
+
Failure(:user_not_found)
|
|
1031
932
|
end
|
|
1032
933
|
end
|
|
1033
934
|
|
|
1034
|
-
class
|
|
1035
|
-
|
|
935
|
+
class ValidatePassword < Micro::Case::Strict
|
|
936
|
+
attributes :user, :password
|
|
1036
937
|
|
|
1037
938
|
def call!
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
end
|
|
1041
|
-
|
|
1042
|
-
class Square < Micro::Case::Strict
|
|
1043
|
-
attribute :numbers
|
|
939
|
+
return Failure(:user_must_be_persisted) if user.new_record?
|
|
940
|
+
return Failure(:wrong_password) if user.wrong_password?(password)
|
|
1044
941
|
|
|
1045
|
-
|
|
1046
|
-
Success result: { numbers: numbers.map { |number| number * number } }
|
|
942
|
+
Success result: attributes(:user)
|
|
1047
943
|
end
|
|
1048
944
|
end
|
|
945
|
+
|
|
946
|
+
Authenticate = Micro::Cases.flow([FindByEmail, ValidatePassword])
|
|
1049
947
|
end
|
|
1050
948
|
|
|
1051
|
-
|
|
1052
|
-
|
|
949
|
+
Users::Authenticate
|
|
950
|
+
.call(email: 'somebody@test.com', password: 'password')
|
|
951
|
+
.on_success { sign_in(it[:user]) }
|
|
952
|
+
.on_failure(:wrong_password) { render status: 401 }
|
|
953
|
+
.on_failure(:user_not_found) { render status: 404 }
|
|
954
|
+
```
|
|
1053
955
|
|
|
1054
|
-
|
|
1055
|
-
Micro::Cases.flow([Steps::ConvertTextToNumbers, Steps::Square])
|
|
956
|
+
`ValidatePassword` declara `:user` como um dos seus atributos mas não recebe ele explicitamente — herda do resultado de sucesso de `FindByEmail`. Esse é o contrato de acumulação: saída → entrada.
|
|
1056
957
|
|
|
1057
|
-
|
|
1058
|
-
Micro::Cases.flow([DoubleAllNumbers, Steps::Add2])
|
|
958
|
+
##### Inspecionando a execução com `result.transitions`
|
|
1059
959
|
|
|
1060
|
-
|
|
1061
|
-
Micro::Cases.flow([SquareAllNumbers, Steps::Add2])
|
|
960
|
+
Cada caso de uso (e cada step interno) contribui com uma entrada para `result.transitions`. Use para debugar, rastrear ou testar a execução de um flow:
|
|
1062
961
|
|
|
1063
|
-
|
|
1064
|
-
|
|
962
|
+
```ruby
|
|
963
|
+
user_authenticated = Users::Authenticate.call(email: 'rodrigo@test.com', password: '...')
|
|
1065
964
|
|
|
1066
|
-
|
|
1067
|
-
|
|
965
|
+
user_authenticated.transitions
|
|
966
|
+
# => [
|
|
967
|
+
# {
|
|
968
|
+
# use_case: {
|
|
969
|
+
# class: Users::FindByEmail,
|
|
970
|
+
# attributes: { email: 'rodrigo@test.com' }
|
|
971
|
+
# },
|
|
972
|
+
# success: { type: :ok, result: { user: #<User ...> } },
|
|
973
|
+
# accessible_attributes: [ :email, :password ]
|
|
974
|
+
# },
|
|
975
|
+
# {
|
|
976
|
+
# use_case: {
|
|
977
|
+
# class: Users::ValidatePassword,
|
|
978
|
+
# attributes: { user: #<User ...>, password: '...' }
|
|
979
|
+
# },
|
|
980
|
+
# success: { type: :ok, result: { user: #<User ...> } },
|
|
981
|
+
# accessible_attributes: [ :email, :password, :user ]
|
|
982
|
+
# }
|
|
983
|
+
# ]
|
|
984
|
+
```
|
|
1068
985
|
|
|
1069
|
-
|
|
1070
|
-
.call(numbers: %w[1 1 2 2 3 4])
|
|
1071
|
-
.on_success { |result| p result[:numbers] } # [6, 6, 12, 12, 22, 36]
|
|
986
|
+
Schema:
|
|
1072
987
|
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
988
|
+
```ruby
|
|
989
|
+
[
|
|
990
|
+
{
|
|
991
|
+
use_case: {
|
|
992
|
+
class: <Micro::Case>, # o caso de uso executado
|
|
993
|
+
attributes: <Hash> # entrada
|
|
994
|
+
},
|
|
995
|
+
[success:, failure:] => { # saída (um dos dois)
|
|
996
|
+
type: <Symbol>, # :ok / :error / :exception / customizado
|
|
997
|
+
result: <Hash> # data
|
|
998
|
+
},
|
|
999
|
+
accessible_attributes: <Array> # atributos acessíveis neste step
|
|
1000
|
+
# (cresce a cada sucesso)
|
|
1001
|
+
}
|
|
1002
|
+
]
|
|
1076
1003
|
```
|
|
1077
1004
|
|
|
1078
|
-
|
|
1005
|
+
`accessible_attributes` cresce conforme a saída de `Success` de cada step é mesclada nos dados correntes. [`Result#then`](#continuações-dinâmicas-com-resultthen) também contribui com uma transition.
|
|
1079
1006
|
|
|
1080
|
-
|
|
1007
|
+
Para desabilitar transitions globalmente (economiza um hash por step), veja [Configuração](#configuração).
|
|
1081
1008
|
|
|
1082
|
-
|
|
1009
|
+
##### Compondo um flow que inclui a si mesmo
|
|
1083
1010
|
|
|
1084
|
-
|
|
1011
|
+
Uma classe pode usar ela mesma como um step na sua própria declaração de `flow` via `self.call!`:
|
|
1085
1012
|
|
|
1086
1013
|
```ruby
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1014
|
+
class ParseTagsString < Micro::Case
|
|
1015
|
+
attribute :input
|
|
1016
|
+
def call!; Success result: { tags: input.split(',').map(&:strip) }; end
|
|
1017
|
+
end
|
|
1090
1018
|
|
|
1091
|
-
|
|
1092
|
-
|
|
1019
|
+
class JoinTagsArray < Micro::Case
|
|
1020
|
+
attribute :tags
|
|
1021
|
+
def call!; Success result: { input: tags.join(', ') }; end
|
|
1022
|
+
end
|
|
1093
1023
|
|
|
1094
|
-
|
|
1024
|
+
class CleanTags < Micro::Case
|
|
1025
|
+
flow ParseTagsString,
|
|
1026
|
+
self.call!,
|
|
1027
|
+
JoinTagsArray
|
|
1095
1028
|
|
|
1096
|
-
|
|
1097
|
-
|
|
1029
|
+
attribute :tags
|
|
1030
|
+
|
|
1031
|
+
def call!
|
|
1032
|
+
Success result: { tags: tags.map(&:downcase).uniq }
|
|
1098
1033
|
end
|
|
1099
1034
|
end
|
|
1100
1035
|
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
attributes :user, :password
|
|
1036
|
+
CleanTags.call(input: 'Ruby, RUBY, Rails').data[:input] # => "ruby, rails"
|
|
1037
|
+
```
|
|
1104
1038
|
|
|
1105
|
-
|
|
1106
|
-
return Failure(:user_must_be_persisted) if user.new_record?
|
|
1107
|
-
return Failure(:wrong_password) if user.wrong_password?(password)
|
|
1039
|
+
Funciona com `Micro::Case::Safe` também — veja [`test/micro/case/safe/with_inner_flow_test.rb`](https://github.com/serradura/u-case/blob/main/test/micro/case/safe/with_inner_flow_test.rb).
|
|
1108
1040
|
|
|
1109
|
-
|
|
1110
|
-
end
|
|
1111
|
-
end
|
|
1112
|
-
end
|
|
1041
|
+
#### Steps internos — cadeias com `Result#then`
|
|
1113
1042
|
|
|
1114
|
-
|
|
1115
|
-
Authenticate = Micro::Cases.flow([
|
|
1116
|
-
FindByEmail,
|
|
1117
|
-
ValidatePassword
|
|
1118
|
-
])
|
|
1119
|
-
end
|
|
1043
|
+
`Result#then` (e seu alias `|` pipe) é a **terceira forma de compor um flow** do u-case — ao lado de `Micro::Cases.flow(...)` e da macro `flow ...` no nível da classe. Em vez de conectar casos de uso irmãos, você mantém a cadeia _dentro_ do `call!` de um único caso de uso. Cada elo é um método, lambda, ou outra classe de caso de uso; cada elo retorna um `Micro::Case::Result`; os dados de `Success` de cada elo viram os keyword arguments do próximo; cada elo contribui com uma linha em `result.transitions`.
|
|
1120
1044
|
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1045
|
+
##### Formas aceitas de elo
|
|
1046
|
+
|
|
1047
|
+
| Formato do argumento | Exemplo |
|
|
1048
|
+
| --------------------------- | ------------------------------------------------ |
|
|
1049
|
+
| `Symbol` (nome de método) | `result.then(:strip_title)` |
|
|
1050
|
+
| Objeto `Method` bound | `result.then(method(:strip_title))` |
|
|
1051
|
+
| `Lambda` / `Proc` | `result.then(-> data { strip_title(**data) })` |
|
|
1052
|
+
| Classe de caso de uso | `result.then(CapitalizeTitle)` |
|
|
1053
|
+
| `Symbol` + Hash de defaults | `result.then(:add, number: 3)` |
|
|
1054
|
+
| Bloco | `result.then { \|r\| r.success? ? r[:sum] : 0 }` |
|
|
1127
1055
|
|
|
1128
|
-
|
|
1056
|
+
O método conectado **precisa** retornar um `Micro::Case::Result`. Qualquer outra coisa levanta `Micro::Case::Error::UnexpectedResult` (ex. um método que retorna um `Hash` simples é rejeitado com `MyCase#method(:foo) must return an instance of Micro::Case::Result`).
|
|
1057
|
+
|
|
1058
|
+
##### Um exemplo mínimo
|
|
1129
1059
|
|
|
1130
1060
|
```ruby
|
|
1131
|
-
class
|
|
1132
|
-
attribute :
|
|
1061
|
+
class CapitalizeTitle < Micro::Case
|
|
1062
|
+
attribute :title
|
|
1063
|
+
|
|
1064
|
+
def call!
|
|
1065
|
+
Success :capitalized, result: { title: title.split.map(&:capitalize).join(' ') }
|
|
1066
|
+
end
|
|
1133
1067
|
end
|
|
1134
1068
|
|
|
1135
|
-
class
|
|
1136
|
-
attributes :
|
|
1069
|
+
class CreateBlogPost < Micro::Case
|
|
1070
|
+
attributes :raw_title, :body
|
|
1071
|
+
|
|
1072
|
+
def call!
|
|
1073
|
+
validate_input
|
|
1074
|
+
.then(:strip_title)
|
|
1075
|
+
.then(:slugify, separator: '-')
|
|
1076
|
+
.then(CapitalizeTitle)
|
|
1077
|
+
end
|
|
1078
|
+
|
|
1079
|
+
private
|
|
1080
|
+
|
|
1081
|
+
def validate_input
|
|
1082
|
+
Kind.of?(String, raw_title, body) ? Success(:valid) : Failure()
|
|
1083
|
+
end
|
|
1084
|
+
|
|
1085
|
+
def strip_title
|
|
1086
|
+
Success :stripped, result: { title: raw_title.strip }
|
|
1087
|
+
end
|
|
1088
|
+
|
|
1089
|
+
def slugify(title:, separator:, **)
|
|
1090
|
+
slug = title.downcase.gsub(/[^a-z0-9]+/, separator)
|
|
1091
|
+
Success :slugified, result: { title:, slug: }
|
|
1092
|
+
end
|
|
1137
1093
|
end
|
|
1094
|
+
|
|
1095
|
+
CreateBlogPost.call(raw_title: ' hello world ', body: 'lorem ipsum').data
|
|
1096
|
+
# => { title: "Hello World" }
|
|
1138
1097
|
```
|
|
1139
1098
|
|
|
1140
|
-
|
|
1141
|
-
R: Ele recebe o usuário do resultado de sucesso `Users::FindByEmail`!
|
|
1099
|
+
Elos baseados em símbolos, métodos e lambdas todos rodam **como o caso de uso hospedeiro**, então eles reportam `class: CreateBlogPost` em `result.transitions`. Só o elo `CapitalizeTitle` (outra classe de caso de uso) contribui com uma transition com `use_case.class` diferente. `accessible_attributes` cresce conforme a saída de `Success` de cada elo é mesclada nos dados correntes — quando `CapitalizeTitle` roda, `slug` também já está acessível upstream.
|
|
1142
1100
|
|
|
1143
|
-
|
|
1101
|
+
##### Alias `|` (pipe)
|
|
1144
1102
|
|
|
1145
|
-
|
|
1103
|
+
`|` é açúcar sintático para `.then(...)`. O exemplo anterior fica:
|
|
1146
1104
|
|
|
1147
|
-
|
|
1105
|
+
```ruby
|
|
1106
|
+
def call!
|
|
1107
|
+
validate_input | :strip_title | :slugify | CapitalizeTitle
|
|
1108
|
+
end
|
|
1109
|
+
```
|
|
1148
1110
|
|
|
1149
|
-
|
|
1111
|
+
As duas formas produzem o mesmo `result.data` e o mesmo `result.transitions`.
|
|
1150
1112
|
|
|
1151
|
-
|
|
1113
|
+
> **Cadeias estilo Elixir com `it` (Ruby ≥ 3.4):** o Ruby 3.4 expõe `it` como o primeiro parâmetro implícito do corpo de um bloco/lambda, então uma cadeia pode ficar quase idêntica ao `|>` do Elixir. Cada lambda recebe o hash de dados acumulado como `it` e ainda precisa terminar em `Success(...)` / `Failure(...)`:
|
|
1114
|
+
>
|
|
1115
|
+
> ```ruby
|
|
1116
|
+
> def call!
|
|
1117
|
+
> validate_something \
|
|
1118
|
+
> | -> { do_something_with(**it) } \
|
|
1119
|
+
> | -> { and_another_thing_with(**it) }
|
|
1120
|
+
> end
|
|
1121
|
+
> ```
|
|
1122
|
+
>
|
|
1123
|
+
> No Ruby 2.7 – 3.3 (onde `it` é só um identificador indefinido), use a forma explícita `->(data) { do_something_with(**data) }`.
|
|
1152
1124
|
|
|
1153
|
-
|
|
1125
|
+
##### Formas Lambda / `Method`
|
|
1154
1126
|
|
|
1155
|
-
|
|
1127
|
+
Lambdas (e objetos `Method` bound) recebem os dados acumulados **posicionalmente** como um único Hash:
|
|
1156
1128
|
|
|
1157
1129
|
```ruby
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
:use_case => {
|
|
1165
|
-
:class => Users::FindByEmail,
|
|
1166
|
-
:attributes => { :email => "rodrigo@test.com" }
|
|
1167
|
-
},
|
|
1168
|
-
:success => {
|
|
1169
|
-
:type => :ok,
|
|
1170
|
-
:result => {
|
|
1171
|
-
:user => #<User:0x00007fb57b1c5f88 @email="rodrigo@test.com" ...>
|
|
1172
|
-
}
|
|
1173
|
-
},
|
|
1174
|
-
:accessible_attributes => [ :email, :password ]
|
|
1175
|
-
},
|
|
1176
|
-
{
|
|
1177
|
-
:use_case => {
|
|
1178
|
-
:class => Users::ValidatePassword,
|
|
1179
|
-
:attributes => {
|
|
1180
|
-
:user => #<User:0x00007fb57b1c5f88 @email="rodrigo@test.com" ...>
|
|
1181
|
-
:password => "123456"
|
|
1182
|
-
}
|
|
1183
|
-
},
|
|
1184
|
-
:success => {
|
|
1185
|
-
:type => :ok,
|
|
1186
|
-
:result => {
|
|
1187
|
-
:user => #<User:0x00007fb57b1c5f88 @email="rodrigo@test.com" ...>
|
|
1188
|
-
}
|
|
1189
|
-
},
|
|
1190
|
-
:accessible_attributes => [ :email, :password, :user ]
|
|
1191
|
-
}
|
|
1192
|
-
]
|
|
1130
|
+
def call!
|
|
1131
|
+
validate_input
|
|
1132
|
+
.then(method(:strip_title))
|
|
1133
|
+
.then(->(data) { slugify(**data, separator: '-') })
|
|
1134
|
+
.then(CapitalizeTitle)
|
|
1135
|
+
end
|
|
1193
1136
|
```
|
|
1194
1137
|
|
|
1195
|
-
|
|
1196
|
-
Com ele é possível analisar a ordem de execução dos casos de uso e quais foram os `inputs` fornecidos (`[:attributes]`) e `outputs` (`[:success][:result]`) em toda a execução.
|
|
1138
|
+
##### `Failure` interrompe a cadeia
|
|
1197
1139
|
|
|
1198
|
-
|
|
1140
|
+
Retornar `Failure(...)` de qualquer elo interrompe o resto da cadeia imediatamente — exatamente como um step em um flow top-level retornando uma falha. Os `.then(...)` / `|` restantes não são invocados; o `result` final é a falha.
|
|
1199
1141
|
|
|
1200
|
-
|
|
1142
|
+
##### Usando um caso com steps internos dentro de um flow externo
|
|
1201
1143
|
|
|
1202
|
-
|
|
1203
|
-
```ruby
|
|
1204
|
-
[
|
|
1205
|
-
{
|
|
1206
|
-
use_case: {
|
|
1207
|
-
class: <Micro::Case>,# Caso de uso que será executado
|
|
1208
|
-
attributes: <Hash> # (Input) Os atributos do caso de uso
|
|
1209
|
-
},
|
|
1210
|
-
[success:, failure:] => { # (Output)
|
|
1211
|
-
type: <Symbol>, # Tipo do resultado. Padrões:
|
|
1212
|
-
# Success = :ok, Failure = :error or :exception
|
|
1213
|
-
result: <Hash> # Os dados retornados pelo resultado do use case
|
|
1214
|
-
},
|
|
1215
|
-
accessible_attributes: <Array>, # Propriedades que podem ser acessadas pelos atributos do caso de uso,
|
|
1216
|
-
# começando com Hash usado para invocá-lo e que são incrementados
|
|
1217
|
-
# com os valores de resultado de cada caso de uso do fluxo.
|
|
1218
|
-
}
|
|
1219
|
-
]
|
|
1144
|
+
Um caso de uso que compõe internamente é só um caso de uso, então cabe em qualquer flow:
|
|
1220
1145
|
|
|
1146
|
+
```ruby
|
|
1147
|
+
PublishWorkflow = Micro::Cases.flow([
|
|
1148
|
+
AuthorizePublisher,
|
|
1149
|
+
CreateBlogPost, # ← usa .then(:método) internamente
|
|
1150
|
+
EnqueueIndexingJob
|
|
1151
|
+
])
|
|
1221
1152
|
```
|
|
1222
1153
|
|
|
1223
|
-
|
|
1154
|
+
As transitions internas do hospedeiro são intercaladas com as transitions folha do flow externo na ordem de execução. Se `CreateBlogPost` produz 4 transitions internas e o flow externo tem 2 outros steps folha, o `result.transitions` final tem 6 entradas.
|
|
1224
1155
|
|
|
1225
|
-
|
|
1156
|
+
##### Persistência sem transação
|
|
1226
1157
|
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
Resposta: Sim! Você pode usar a macro `self` ou `self.call!`. Exemplo:
|
|
1158
|
+
Por padrão — quando nem a classe hospedeira nem o flow externo usam `transaction: true` — steps internos se comportam como qualquer outro código em `call!`: efeitos colaterais de elos anteriores **persistem** mesmo se um elo posterior retornar `Failure`. A cadeia para, mas o que já foi escrito fica escrito:
|
|
1230
1159
|
|
|
1231
1160
|
```ruby
|
|
1232
|
-
class
|
|
1233
|
-
|
|
1161
|
+
class CreateUserWithProfileInline < Micro::Case
|
|
1162
|
+
attributes :name, :info
|
|
1234
1163
|
|
|
1235
1164
|
def call!
|
|
1236
|
-
|
|
1165
|
+
create_user.then(:create_profile)
|
|
1237
1166
|
end
|
|
1238
|
-
end
|
|
1239
1167
|
|
|
1240
|
-
|
|
1241
|
-
attribute :number
|
|
1168
|
+
private
|
|
1242
1169
|
|
|
1243
|
-
def
|
|
1244
|
-
|
|
1170
|
+
def create_user
|
|
1171
|
+
user = User.create(name:)
|
|
1172
|
+
Success result: { user: }
|
|
1245
1173
|
end
|
|
1246
|
-
end
|
|
1247
1174
|
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
ConvertNumberToText
|
|
1252
|
-
|
|
1253
|
-
attribute :number
|
|
1175
|
+
def create_profile(user:, **)
|
|
1176
|
+
profile = UserProfile.create(user_id: user.id, info:)
|
|
1177
|
+
return Failure(:invalid_profile) if profile.errors.any?
|
|
1254
1178
|
|
|
1255
|
-
|
|
1256
|
-
Success result: { number: number * 2 }
|
|
1179
|
+
Success result: { user:, profile: }
|
|
1257
1180
|
end
|
|
1258
1181
|
end
|
|
1259
1182
|
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
result[:number] # "8"
|
|
1183
|
+
CreateUserWithProfileInline.call(name: 'Rodrigo', info: '')
|
|
1184
|
+
# create_user já fez INSERT na linha do user; create_profile falhou.
|
|
1185
|
+
# user está persistido; profile não. Sem rollback automático.
|
|
1264
1186
|
```
|
|
1265
1187
|
|
|
1266
|
-
|
|
1188
|
+
Para reverter os writes parciais, envolva a cadeia em uma [transação](#transações).
|
|
1267
1189
|
|
|
1268
|
-
|
|
1190
|
+
#### Transações
|
|
1269
1191
|
|
|
1270
|
-
|
|
1192
|
+
O `u-case` traz dois helpers complementares para envolver trabalho em uma `ActiveRecord::Base.transaction`. Ambos são opt-in — `active_record` **não** é requerido pela gem, então você carrega o ActiveRecord por conta própria (aplicações Rails já fazem isso).
|
|
1271
1193
|
|
|
1272
|
-
|
|
1273
|
-
um `ActiveRecord::Base.transaction`. Ambos são opt-in — a gem **não**
|
|
1274
|
-
requer `active_record` automaticamente, então você precisa carregar o
|
|
1275
|
-
ActiveRecord por conta própria (aplicações Rails já o fazem).
|
|
1194
|
+
##### `transaction { ... }` inline dentro do `call!`
|
|
1276
1195
|
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
`Micro::Case#transaction` (e `Micro::Case::Safe#transaction`) é um helper
|
|
1280
|
-
privado de instância que envolve um bloco em uma transação de banco e
|
|
1281
|
-
dispara um `ActiveRecord::Rollback` sempre que o resultado do bloco for
|
|
1282
|
-
um `Failure`. O resultado original é devolvido nos dois casos, permitindo
|
|
1283
|
-
continuar encadeando com `Result#then`:
|
|
1196
|
+
`Micro::Case#transaction` (e `Micro::Case::Safe#transaction`) é um helper de instância privado que envolve um bloco em uma transação de banco e dispara `ActiveRecord::Rollback` sempre que o resultado do bloco é um `Failure`. O resultado original é retornado de qualquer forma, então você pode continuar encadeando com `Result#then`:
|
|
1284
1197
|
|
|
1285
1198
|
```ruby
|
|
1286
1199
|
class CreateUserWithAProfile < Micro::Case
|
|
@@ -1292,11 +1205,7 @@ class CreateUserWithAProfile < Micro::Case
|
|
|
1292
1205
|
end
|
|
1293
1206
|
```
|
|
1294
1207
|
|
|
1295
|
-
Se o bloco
|
|
1296
|
-
gravadas dentro do bloco serão revertidas. O helper aceita um kwarg
|
|
1297
|
-
opcional `with:` para escolher a classe ActiveRecord sobre a qual
|
|
1298
|
-
`.transaction` é aberta — útil em aplicações Rails com múltiplos bancos
|
|
1299
|
-
(`ApplicationRecord`, `AnalyticsRecord`, `BillingRecord`, …):
|
|
1208
|
+
Se o bloco retorna uma falha (ou levanta), todas as linhas escritas dentro do bloco são revertidas. O helper aceita `with:` para escolher a classe ActiveRecord na qual `.transaction` é aberta — útil para aplicações Rails com multi-database (`ApplicationRecord`, `AnalyticsRecord`, `BillingRecord`, …):
|
|
1300
1209
|
|
|
1301
1210
|
```ruby
|
|
1302
1211
|
class CreateAuditEntry < Micro::Case
|
|
@@ -1308,29 +1217,15 @@ class CreateAuditEntry < Micro::Case
|
|
|
1308
1217
|
end
|
|
1309
1218
|
```
|
|
1310
1219
|
|
|
1311
|
-
Quando `with:` é omitido, o helper cai
|
|
1312
|
-
(`transaction with: …`) e depois no callback global padrão (veja abaixo),
|
|
1313
|
-
que vem com `-> { ::ActiveRecord::Base }`.
|
|
1314
|
-
|
|
1315
|
-
> **Nota:** qualquer classe passada via `with:` (aqui, no macro de classe ou
|
|
1316
|
-
> no kwarg `transaction:` de um flow) **precisa ser uma subclasse de
|
|
1317
|
-
> `ActiveRecord::Base`**. Classes não-AR são rejeitadas com `ArgumentError`.
|
|
1318
|
-
> A validação do macro de classe roda em tempo de class-eval quando o
|
|
1319
|
-
> ActiveRecord já está carregado (caso típico de apps Rails); caso
|
|
1320
|
-
> contrário, é adiada para runtime, então a ordem de carregamento de
|
|
1321
|
-
> initializers não quebra declarações.
|
|
1220
|
+
Quando `with:` é omitido, o helper cai para a macro de classe (`transaction with: …`) e depois para o callback global de padrão.
|
|
1322
1221
|
|
|
1323
|
-
> **
|
|
1324
|
-
>
|
|
1325
|
-
> `transaction { ... }
|
|
1326
|
-
> `ArgumentError` — o helper antigo aceitava apenas `:activerecord`.
|
|
1222
|
+
> Qualquer classe passada via `with:` (helper inline, macro de classe ou kwarg de flow) **precisa ser uma subclasse de `ActiveRecord::Base`**. Classes que não sejam AR são rejeitadas com `ArgumentError`.
|
|
1223
|
+
>
|
|
1224
|
+
> **Retrocompatibilidade:** a forma posicional pré-5.6.0 `transaction(:activerecord) { ... }` continua funcionando como alias de `transaction { ... }`; qualquer outro valor posicional levanta `ArgumentError`.
|
|
1327
1225
|
|
|
1328
1226
|
##### `transaction with: …` — declarando o padrão para um caso
|
|
1329
1227
|
|
|
1330
|
-
|
|
1331
|
-
ActiveRecord deve ser dona de suas transações, para que nem o helper
|
|
1332
|
-
inline nem qualquer flow que envolva o caso precise especificá-la em cada
|
|
1333
|
-
ponto de chamada. A declaração é herdada por subclasses:
|
|
1228
|
+
Uma macro de classe permite que um caso declare qual classe ActiveRecord deve dona das transações dele, então nem o helper inline nem nenhum flow que envolve o caso precisam soletrar isso. A declaração é herdada:
|
|
1334
1229
|
|
|
1335
1230
|
```ruby
|
|
1336
1231
|
class ApplicationUseCase < Micro::Case
|
|
@@ -1339,36 +1234,30 @@ end
|
|
|
1339
1234
|
|
|
1340
1235
|
class CreateUserWithAProfile < ApplicationUseCase
|
|
1341
1236
|
flow(transaction: true, steps: [CreateUser, CreateUserProfile])
|
|
1342
|
-
# transaction: true resolve para ApplicationRecord
|
|
1343
|
-
# a classe hospedeira declarou via `transaction with:`.
|
|
1237
|
+
# transaction: true resolve para ApplicationRecord (herdado).
|
|
1344
1238
|
end
|
|
1345
1239
|
|
|
1346
1240
|
class BillingCase < ApplicationUseCase
|
|
1347
1241
|
transaction with: BillingRecord
|
|
1348
|
-
# sobrescreve a declaração herdada para este ramo da
|
|
1242
|
+
# sobrescreve a declaração herdada para este ramo da árvore
|
|
1349
1243
|
end
|
|
1350
1244
|
```
|
|
1351
1245
|
|
|
1352
|
-
#####
|
|
1246
|
+
##### Transações no nível do flow
|
|
1353
1247
|
|
|
1354
|
-
Passe `transaction:` junto com `steps:` para envolver um flow inteiro em
|
|
1355
|
-
uma única transação. Se qualquer step retornar uma falha (ou levantar uma
|
|
1356
|
-
exceção, no caso de `safe_flow`), todas as escritas realizadas no banco
|
|
1357
|
-
durante o flow serão revertidas. O kwarg aceita três formas:
|
|
1248
|
+
Passe `transaction:` junto com `steps:` para envolver um flow inteiro em uma única transação. Se qualquer step retorna uma falha (ou levanta, num `safe_flow`), todo write de banco feito durante o flow é revertido. Três formas:
|
|
1358
1249
|
|
|
1359
1250
|
```ruby
|
|
1360
|
-
# Usa
|
|
1361
|
-
# o padrão global (`ActiveRecord::Base` salvo configuração).
|
|
1251
|
+
# Usa a macro de classe (se a classe hospedeira declarou uma) ou o padrão global.
|
|
1362
1252
|
Micro::Cases.flow(transaction: true, steps: [CreateUser, CreateUserProfile])
|
|
1363
1253
|
|
|
1364
|
-
# Escolhe uma classe ActiveRecord explícita só para este flow — mesmo
|
|
1365
|
-
# vocabulário `with:` usado pelo helper inline e pelo macro de classe.
|
|
1254
|
+
# Escolhe uma classe ActiveRecord explícita só para este flow — mesmo vocabulário `with:`.
|
|
1366
1255
|
Micro::Cases.flow(transaction: { with: AnalyticsRecord }, steps: [
|
|
1367
1256
|
WriteAuditLog,
|
|
1368
1257
|
BumpCounter
|
|
1369
1258
|
])
|
|
1370
1259
|
|
|
1371
|
-
# safe_flow
|
|
1260
|
+
# safe_flow reverte em falhas E em exceções inesperadas.
|
|
1372
1261
|
Micro::Cases.safe_flow(transaction: { with: ApplicationRecord }, steps: [
|
|
1373
1262
|
CreateUser,
|
|
1374
1263
|
CreateUserProfile
|
|
@@ -1380,9 +1269,7 @@ class CreateUserWithAProfile < Micro::Case
|
|
|
1380
1269
|
end
|
|
1381
1270
|
```
|
|
1382
1271
|
|
|
1383
|
-
Para aninhar um flow transacional dentro de outro flow, envolva
|
|
1384
|
-
classe de caso de uso — `Micro::Cases.flow([...])` achata instâncias de
|
|
1385
|
-
`Flow` passadas como steps, mas **não** achata classes:
|
|
1272
|
+
Para aninhar um flow transacional dentro de outro flow, envolva ele em uma classe de caso de uso — `Micro::Cases.flow([...])` achata instâncias de `Flow` passadas como steps, mas **não** achata classes:
|
|
1386
1273
|
|
|
1387
1274
|
```ruby
|
|
1388
1275
|
class CreateUserAndProfile < Micro::Case
|
|
@@ -1397,17 +1284,11 @@ SignUpFlow = Micro::Cases.flow([
|
|
|
1397
1284
|
])
|
|
1398
1285
|
```
|
|
1399
1286
|
|
|
1400
|
-
Se `transaction: true` for usado
|
|
1401
|
-
carregado, o flow levantará `Micro::Cases::Error::TransactionAdapterMissing`
|
|
1402
|
-
na primeira chamada, sinalizando a configuração incorreta imediatamente.
|
|
1403
|
-
Passar `transaction: { with: SomeClass }` pula essa verificação —
|
|
1404
|
-
`SomeClass` é considerada confiável e basta responder a `.transaction`.
|
|
1287
|
+
Se `transaction: true` for usado enquanto `ActiveRecord::Base` não está carregado, o flow levanta `Micro::Cases::Error::TransactionAdapterMissing` na primeira chamada para que a configuração errada apareça imediatamente. Passar `transaction: { with: SomeClass }` pula essa checagem — `SomeClass` é confiado a responder a `.transaction`.
|
|
1405
1288
|
|
|
1406
|
-
##### `config.default_transaction_class { … }`
|
|
1289
|
+
##### Padrão global — `config.default_transaction_class { … }`
|
|
1407
1290
|
|
|
1408
|
-
Para aplicações Rails que usam um único
|
|
1409
|
-
(`ApplicationRecord`), configure-o uma vez em um initializer em vez de
|
|
1410
|
-
declará-lo em cada caso ou flow:
|
|
1291
|
+
Para aplicações Rails que usam um único record abstrato (`ApplicationRecord`), configure-o uma vez em um initializer em vez de declarar em cada caso ou flow:
|
|
1411
1292
|
|
|
1412
1293
|
```ruby
|
|
1413
1294
|
# config/initializers/u_case.rb
|
|
@@ -1416,65 +1297,44 @@ Micro::Case.config do |config|
|
|
|
1416
1297
|
end
|
|
1417
1298
|
```
|
|
1418
1299
|
|
|
1419
|
-
O callback (
|
|
1420
|
-
— sem memoização — então é seguro fazer o valor de retorno depender de
|
|
1421
|
-
estado em tempo de execução (roteamento por tenant, etc.). O padrão é
|
|
1422
|
-
`-> { ::ActiveRecord::Base }`. Ordem de resolução quando uma transação
|
|
1423
|
-
abre:
|
|
1300
|
+
O callback (bloco ou lambda) é invocado **toda vez** que uma transação abre — sem memoização — então o valor de retorno pode depender de estado em runtime (roteamento por tenant, etc.). O padrão é `-> { ::ActiveRecord::Base }`.
|
|
1424
1301
|
|
|
1425
|
-
|
|
1426
|
-
kwarg do flow, ou `transaction(with: X) { ... }` no helper inline.
|
|
1427
|
-
2. **Macro `transaction with: X` da classe hospedeira** (sobe pela
|
|
1428
|
-
hierarquia).
|
|
1429
|
-
3. **`Micro::Case.config.default_transaction_class.call`** — o callback
|
|
1430
|
-
global (padrão `ActiveRecord::Base`).
|
|
1302
|
+
Ordem de resolução, quando uma transação abre:
|
|
1431
1303
|
|
|
1432
|
-
|
|
1433
|
-
`
|
|
1434
|
-
|
|
1435
|
-
|
|
1304
|
+
1. **Override no local de chamada** — `transaction: { with: X }` em um kwarg de flow, ou `transaction(with: X) { ... }` no helper inline.
|
|
1305
|
+
2. **Macro `transaction with: X` da classe hospedeira** (caminha pelos ancestrais).
|
|
1306
|
+
3. **`Micro::Case.config.default_transaction_class.call`** — o callback global (padrão é `ActiveRecord::Base`).
|
|
1307
|
+
|
|
1308
|
+
Uma atribuição não-callable em `default_transaction_class=` levanta `ArgumentError` na hora da configuração para que typos como `config.default_transaction_class = 'ApplicationRecord'` falhem barulhentamente em vez de crasharem na primeira transação.
|
|
1436
1309
|
|
|
1437
1310
|
##### Flows com steps internos sob transações
|
|
1438
1311
|
|
|
1439
|
-
|
|
1440
|
-
(a forma `Result#then(:symbol)` / `|` construída inline dentro de um
|
|
1441
|
-
único `call!`) são a terceira forma do u-case de compor um flow —
|
|
1442
|
-
um flow *interno*. Por padrão, um flow interno **não tem rollback
|
|
1443
|
-
transacional**: efeitos colaterais de elos `.then(:método)`
|
|
1444
|
-
anteriores persistem mesmo quando um elo posterior retorna
|
|
1445
|
-
`Failure`.
|
|
1312
|
+
[Steps internos](#steps-internos--cadeias-com-resultthen) — a forma `Result#then(:symbol)` / `|` construída inline dentro de um único `call!` — são um flow _interno_. Por padrão eles **não têm rollback transacional**: efeitos colaterais de elos `.then(:method)` anteriores persistem mesmo quando um elo posterior retorna `Failure`.
|
|
1446
1313
|
|
|
1447
|
-
|
|
1448
|
-
interno. Ambas reutilizam os helpers já documentados acima:
|
|
1314
|
+
Duas formas naturais de dar rollback:
|
|
1449
1315
|
|
|
1450
|
-
**1.
|
|
1451
|
-
Esta é a forma recomendada assim que o caso hospedeiro é composto
|
|
1452
|
-
com o resto do pipeline. A transação cobre a chamada inteira do flow,
|
|
1453
|
-
então um `Failure` *em qualquer ponto* — incluindo de qualquer elo
|
|
1454
|
-
`.then(:método)` interno — reverte todas as escritas de banco feitas
|
|
1455
|
-
durante a chamada:
|
|
1316
|
+
**1. Envolva o caso hospedeiro em um flow `transaction: true`.** Recomendado uma vez que o caso hospedeiro está dentro de um pipeline maior. A transação cobre a chamada inteira do flow, então uma `Failure` _em qualquer lugar_ — incluindo de qualquer elo interno `.then(:method)` — reverte todo write de banco:
|
|
1456
1317
|
|
|
1457
1318
|
```ruby
|
|
1458
1319
|
class CreateUserWithProfileInline < Micro::Case
|
|
1459
1320
|
attributes :name, :info
|
|
1460
1321
|
|
|
1461
1322
|
def call!
|
|
1462
|
-
create_user
|
|
1463
|
-
.then(:create_profile)
|
|
1323
|
+
create_user.then(:create_profile)
|
|
1464
1324
|
end
|
|
1465
1325
|
|
|
1466
1326
|
private
|
|
1467
1327
|
|
|
1468
1328
|
def create_user
|
|
1469
|
-
user = User.create(name:
|
|
1470
|
-
Success result: { user:
|
|
1329
|
+
user = User.create(name:)
|
|
1330
|
+
Success result: { user: }
|
|
1471
1331
|
end
|
|
1472
1332
|
|
|
1473
1333
|
def create_profile(user:, **)
|
|
1474
|
-
profile = UserProfile.create(user_id: user.id, info:
|
|
1334
|
+
profile = UserProfile.create(user_id: user.id, info:)
|
|
1475
1335
|
return Failure(:invalid_profile) if profile.errors.any?
|
|
1476
1336
|
|
|
1477
|
-
Success result: { user
|
|
1337
|
+
Success result: { user:, profile: }
|
|
1478
1338
|
end
|
|
1479
1339
|
end
|
|
1480
1340
|
|
|
@@ -1483,694 +1343,303 @@ SignUp = Micro::Cases.flow(transaction: true, steps: [
|
|
|
1483
1343
|
CreateUserWithProfileInline, # ← falha interna agora reverte
|
|
1484
1344
|
EnqueueIndexingJob
|
|
1485
1345
|
])
|
|
1486
|
-
|
|
1487
|
-
# Ou no nível de classe:
|
|
1488
|
-
class SignUp < Micro::Case
|
|
1489
|
-
flow(transaction: true, steps: [
|
|
1490
|
-
NormalizeParams,
|
|
1491
|
-
CreateUserWithProfileInline,
|
|
1492
|
-
EnqueueIndexingJob
|
|
1493
|
-
])
|
|
1494
|
-
end
|
|
1495
1346
|
```
|
|
1496
1347
|
|
|
1497
|
-
Se `create_profile`
|
|
1498
|
-
`Failure(:invalid_profile)`, a linha de `User` inserida antes por
|
|
1499
|
-
`create_user` é revertida como parte da mesma
|
|
1500
|
-
`ActiveRecord::Base.transaction`. O resultado ainda expõe o tipo da
|
|
1501
|
-
falha e as transições parciais, mas nenhuma linha permanece no banco.
|
|
1348
|
+
Se `create_profile` retorna `Failure(:invalid_profile)`, a linha de `User` inserida antes é revertida como parte da mesma `ActiveRecord::Base.transaction`. O resultado ainda surfaceia o tipo de falha e as transitions parciais, mas nenhuma linha fica para trás.
|
|
1502
1349
|
|
|
1503
|
-
**2.
|
|
1504
|
-
rollback a um único `call!` sem envolver um flow externo:
|
|
1350
|
+
**2. Use o helper inline `transaction { ... }`** para escopar o rollback a um único `call!` sem envolver um flow externo:
|
|
1505
1351
|
|
|
1506
1352
|
```ruby
|
|
1507
1353
|
class CreateUserWithProfileInline < Micro::Case
|
|
1508
1354
|
def call!
|
|
1509
1355
|
transaction {
|
|
1510
|
-
create_user
|
|
1511
|
-
.then(:create_profile)
|
|
1356
|
+
create_user.then(:create_profile)
|
|
1512
1357
|
}
|
|
1513
1358
|
end
|
|
1514
1359
|
end
|
|
1515
1360
|
```
|
|
1516
1361
|
|
|
1517
|
-
|
|
1518
|
-
um flow) e você ainda quer que o flow interno seja atômico. O bloco
|
|
1519
|
-
`transaction` retorna o `Result` da cadeia como está, então você pode
|
|
1520
|
-
continuar compondo com `Result#then` depois dele.
|
|
1521
|
-
|
|
1522
|
-
As duas abordagens **se compõem**. Se você colocar
|
|
1523
|
-
`CreateUserWithProfileInline` (que já usa `transaction { ... }`
|
|
1524
|
-
inline) dentro de um flow externo com `transaction: true`, o
|
|
1525
|
-
ActiveRecord junta a transação interna à externa por padrão — uma
|
|
1526
|
-
falha externa reverte também as escritas internas. Veja as
|
|
1527
|
-
**Observações de comportamento** abaixo para as regras completas de
|
|
1528
|
-
aninhamento / achatamento.
|
|
1362
|
+
As duas abordagens compõem. Se `CreateUserWithProfileInline` (usando `transaction { ... }` inline) está dentro de um flow externo `transaction: true`, o ActiveRecord junta a transação interna na externa por padrão — uma falha externa reverte os writes da interna também.
|
|
1529
1363
|
|
|
1530
1364
|
##### Observações de comportamento
|
|
1531
1365
|
|
|
1532
|
-
- **O resultado não é afetado.** `transaction: true` afeta
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
-
|
|
1537
|
-
Outro])` achata `flow_interno` em seus steps internos, o que faz com
|
|
1538
|
-
que uma instância de `Flow` transacional passada dessa forma **perca
|
|
1539
|
-
sua transação**. Envolva flows transacionais reutilizáveis em uma
|
|
1540
|
-
classe de caso de uso (como no snippet acima) para preservar a
|
|
1541
|
-
transação ao aninhar.
|
|
1542
|
-
- **Transações aninhadas se unem à transação externa.** Quando um flow
|
|
1543
|
-
transacional é aninhado dentro de outro flow transacional, o
|
|
1544
|
-
ActiveRecord as une por padrão (sem `requires_new: true`). Uma falha
|
|
1545
|
-
em qualquer ponto da cadeia reverte **tudo** que foi escrito dentro
|
|
1546
|
-
da transação mais externa — incluindo escritas feitas pelo flow
|
|
1547
|
-
interno.
|
|
1548
|
-
- **Um externo não-transacional comita o interno.** Se o flow externo
|
|
1549
|
-
não for transacional e o flow transacional interno tiver sucesso, as
|
|
1550
|
-
escritas do interno são comitadas ao final daquele step. Uma falha
|
|
1551
|
-
em um step posterior (não-transacional) **não** desfaz essas
|
|
1552
|
-
escritas.
|
|
1553
|
-
- **`Micro::Cases.flow(transaction: true, ...)` simples re-lança
|
|
1554
|
-
exceções.** A transação ainda é revertida, mas o chamador precisa
|
|
1555
|
-
fazer rescue. Use `Micro::Cases.safe_flow(transaction: true, ...)`
|
|
1556
|
-
(ou a forma de classe com `Micro::Case::Safe`) para capturar a
|
|
1557
|
-
exceção como uma falha do tipo `:exception`.
|
|
1558
|
-
|
|
1559
|
-
[⬆️ Voltar para o índice](#índice-)
|
|
1560
|
-
|
|
1561
|
-
### `Micro::Case::Strict` - O que é um caso de uso estrito?
|
|
1562
|
-
|
|
1563
|
-
Resposta: é um tipo de caso de uso que exigirá todas as palavras-chave (atributos) em sua inicialização.
|
|
1366
|
+
- **O resultado não é afetado.** `transaction: true` só afeta efeitos colaterais de banco. `result.data`, `result.type`, `result.transitions` e `result.accessible_attributes` são idênticos aos de um flow não-transacional equivalente.
|
|
1367
|
+
- **Instâncias de `Flow` são achatadas.** `Micro::Cases.flow([inner_flow, Other])` achata `inner_flow` para seus steps folha — uma instância transacional de `Flow` passada assim **perde sua transação**. Envolva flows transacionais reutilizáveis em uma classe de caso de uso para preservar a transação quando aninhados.
|
|
1368
|
+
- **Transações aninhadas se juntam à externa.** O ActiveRecord junta elas por padrão (sem `requires_new: true`). Uma falha em qualquer lugar na cadeia reverte **tudo** escrito dentro da transação mais externa.
|
|
1369
|
+
- **Um externo não-transacional commita o interno.** Se o flow externo não é transacional e o flow transacional interno sucede, os writes do interno commitam no final do step interno. Uma falha em um step posterior (não-transacional) **não** desfaz esses writes.
|
|
1370
|
+
- **`Micro::Cases.flow(transaction: true, ...)` puro relança exceções.** A transação ainda reverte, mas quem chamou tem que dar `rescue`. Use `Micro::Cases.safe_flow(transaction: true, ...)` (ou a forma a nível de classe com `Micro::Case::Safe`) para capturar a exceção como uma falha `:exception`.
|
|
1564
1371
|
|
|
1565
|
-
|
|
1566
|
-
class Double < Micro::Case::Strict
|
|
1567
|
-
attribute :numbers
|
|
1372
|
+
[⬆️ Voltar ao topo](#índice-)
|
|
1568
1373
|
|
|
1569
|
-
|
|
1570
|
-
Success result: { numbers: numbers.map { |number| number * 2 } }
|
|
1571
|
-
end
|
|
1572
|
-
end
|
|
1573
|
-
|
|
1574
|
-
Double.call({})
|
|
1575
|
-
|
|
1576
|
-
# O output será:
|
|
1577
|
-
# ArgumentError (missing keyword: :numbers)
|
|
1578
|
-
```
|
|
1579
|
-
|
|
1580
|
-
[⬆️ Voltar para o índice](#índice-)
|
|
1581
|
-
|
|
1582
|
-
### `Micro::Case::Safe` - Existe algum recurso para lidar automaticamente com exceções dentro de um caso de uso ou fluxo?
|
|
1583
|
-
|
|
1584
|
-
Sim, assim como `Micro::Case::Strict`, o `Micro::Case::Safe` é outro tipo de caso de uso. Ele tem a capacidade de interceptar automaticamente qualquer exceção como um resultado de falha. Exemplo:
|
|
1585
|
-
|
|
1586
|
-
```ruby
|
|
1587
|
-
require 'logger'
|
|
1588
|
-
|
|
1589
|
-
AppLogger = Logger.new(STDOUT)
|
|
1590
|
-
|
|
1591
|
-
class Divide < Micro::Case::Safe
|
|
1592
|
-
attributes :a, :b
|
|
1593
|
-
|
|
1594
|
-
def call!
|
|
1595
|
-
if a.is_a?(Integer) && b.is_a?(Integer)
|
|
1596
|
-
Success result: { number: a / b}
|
|
1597
|
-
else
|
|
1598
|
-
Failure(:not_an_integer)
|
|
1599
|
-
end
|
|
1600
|
-
end
|
|
1601
|
-
end
|
|
1602
|
-
|
|
1603
|
-
result = Divide.call(a: 2, b: 0)
|
|
1604
|
-
result.type == :exception # true
|
|
1605
|
-
result.data # { exception: #<ZeroDivisionError...> }
|
|
1606
|
-
result[:exception].is_a?(ZeroDivisionError) # true
|
|
1607
|
-
|
|
1608
|
-
result.on_failure(:exception) do |result|
|
|
1609
|
-
AppLogger.error(result[:exception].message) # E, [2019-08-21T00:05:44.195506 #9532] ERROR -- : divided by 0
|
|
1610
|
-
end
|
|
1611
|
-
```
|
|
1374
|
+
## Configuração
|
|
1612
1375
|
|
|
1613
|
-
|
|
1376
|
+
`Micro::Case.config` expõe as toggles da gem. Configure uma vez — tipicamente em um initializer do Rails:
|
|
1614
1377
|
|
|
1615
1378
|
```ruby
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
else AppLogger.debug("#{use_case.class.name} was the use case responsible for the exception")
|
|
1620
|
-
end
|
|
1621
|
-
end
|
|
1622
|
-
```
|
|
1623
|
-
|
|
1624
|
-
> **Note:** É possível resgatar uma exceção mesmo quando é um caso de uso seguro. Exemplos: https://github.com/serradura/u-case/blob/714c6b658fc6aa02617e6833ddee09eddc760f2a/test/micro/case/safe_test.rb#L90-L118
|
|
1379
|
+
Micro::Case.config do |config|
|
|
1380
|
+
# Falha automaticamente casos de uso em erros de validação do ActiveModel.
|
|
1381
|
+
config.enable_activemodel_validation = false
|
|
1625
1382
|
|
|
1383
|
+
# Símbolo de tipo usado pela auto-falha quando a validação do ActiveModel
|
|
1384
|
+
# rejeita um atributo (compartilhado com a falha de rejeição de accept:/reject:).
|
|
1385
|
+
# Padrão é :invalid_attributes.
|
|
1386
|
+
config.set_activemodel_validation_errors_failure = :invalid_attributes
|
|
1626
1387
|
|
|
1627
|
-
|
|
1388
|
+
# Registra Micro::Case::Result#transitions em cada step do flow.
|
|
1389
|
+
# Configure para false para economizar a alocação do hash por step em hot paths.
|
|
1390
|
+
config.enable_transitions = true
|
|
1628
1391
|
|
|
1629
|
-
|
|
1392
|
+
# Proíbe as APIs Safe para impor uma única convenção de tratamento de
|
|
1393
|
+
# exceções (apenas `rescue` dentro dos casos de uso). Quando true, os itens
|
|
1394
|
+
# abaixo levantam Micro::Case::Error::SafeFeaturesDisabled:
|
|
1395
|
+
# - herdar de Micro::Case::Safe
|
|
1396
|
+
# - chamar Micro::Cases.safe_flow(...)
|
|
1397
|
+
# - chamar Micro::Case::Result#on_exception
|
|
1398
|
+
config.disable_safe_features = false
|
|
1630
1399
|
|
|
1631
|
-
|
|
1400
|
+
# Pula os checks internos de argumento/contrato da gem para um pequeno ganho
|
|
1401
|
+
# de performance em produção uma vez que seu test suite tenha exercitado os
|
|
1402
|
+
# code paths. Usos incorretos vão aparecer como erros downstream em vez dos
|
|
1403
|
+
# erros curados da gem.
|
|
1404
|
+
config.disable_runtime_checks = false
|
|
1632
1405
|
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
ValidateParams,
|
|
1638
|
-
Persist,
|
|
1639
|
-
SendToCRM
|
|
1640
|
-
])
|
|
1406
|
+
# A classe ActiveRecord usada por `transaction: true`. Passe um bloco (ou lambda).
|
|
1407
|
+
# O padrão é `-> { ::ActiveRecord::Base }`. Sobrescreva para usar um record
|
|
1408
|
+
# abstrato por aplicação como ApplicationRecord.
|
|
1409
|
+
config.default_transaction_class { ApplicationRecord }
|
|
1641
1410
|
end
|
|
1642
1411
|
```
|
|
1643
1412
|
|
|
1644
|
-
|
|
1413
|
+
Todos os checks internos vivem em `Micro::Case::Check::Enabled` (o padrão). Ativar `disable_runtime_checks = true` troca `Micro::Case.check` para `Micro::Case::Check::Disabled`, cujos métodos são no-ops — as validações em si param de rodar a cada chamada.
|
|
1645
1414
|
|
|
1646
|
-
|
|
1647
|
-
module Users
|
|
1648
|
-
class Create < Micro::Case::Safe
|
|
1649
|
-
flow ProcessParams,
|
|
1650
|
-
ValidateParams,
|
|
1651
|
-
Persist,
|
|
1652
|
-
SendToCRM
|
|
1653
|
-
end
|
|
1654
|
-
end
|
|
1655
|
-
```
|
|
1415
|
+
[⬆️ Voltar ao topo](#índice-)
|
|
1656
1416
|
|
|
1657
|
-
|
|
1417
|
+
## Performance
|
|
1658
1418
|
|
|
1659
|
-
|
|
1419
|
+
Em benchmarks contra abstrações comparáveis, `Micro::Case` é o mais rápido depois do `Dry::Monads`:
|
|
1660
1420
|
|
|
1661
|
-
|
|
1421
|
+
| Gem / Abstração | Success (i/s) | Failure (i/s) |
|
|
1422
|
+
| ---------------------- | ------------: | ------------: |
|
|
1423
|
+
| Dry::Monads | 315,635.1 | 135,386.9 |
|
|
1424
|
+
| **Micro::Case** | 75,837.7 | 73,489.3 |
|
|
1425
|
+
| Interactor | 59,745.5 | 27,037.0 |
|
|
1426
|
+
| Trailblazer::Operation | 28,423.9 | 29,016.4 |
|
|
1427
|
+
| Dry::Transaction | 10,130.9 | 8,988.6 |
|
|
1662
1428
|
|
|
1663
|
-
Para
|
|
1429
|
+
Para flows, o alias `|` pipe é o estilo de composição mais rápido:
|
|
1664
1430
|
|
|
1665
|
-
|
|
1431
|
+
| Estilo de composição | Success | Failure |
|
|
1432
|
+
| ---------------------------- | -----------: | -----------: |
|
|
1433
|
+
| `Result#\|` (pipe) | 80,936.2 | 78,280.4 |
|
|
1434
|
+
| `Micro::Cases.flow(...)` | same-ish | same-ish |
|
|
1435
|
+
| `Result#then` | same-ish | same-ish |
|
|
1436
|
+
| Classe com `flow` interno | 1.72× slower | 1.68× slower |
|
|
1437
|
+
| Classe que inclui a si mesma | 1.93× slower | 1.87× slower |
|
|
1438
|
+
| `Interactor::Organizer` | 3.33× slower | 3.22× slower |
|
|
1666
1439
|
|
|
1667
|
-
|
|
1440
|
+
> `Dry::Monads`, `Dry::Transaction` e `Trailblazer::Operation` não têm uma feature equivalente a flow e ficam fora da tabela de flow.
|
|
1668
1441
|
|
|
1669
|
-
|
|
1670
|
-
class Divide < Micro::Case::Safe
|
|
1671
|
-
attributes :a, :b
|
|
1442
|
+
### Executando os benchmarks
|
|
1672
1443
|
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1444
|
+
```sh
|
|
1445
|
+
# Casos de uso
|
|
1446
|
+
ruby benchmarks/perfomance/use_case/success_results.rb
|
|
1447
|
+
ruby benchmarks/perfomance/use_case/failure_results.rb
|
|
1677
1448
|
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
.on_exception(TypeError) { puts 'Please, use only numeric attributes.' }
|
|
1682
|
-
.on_exception(ZeroDivisionError) { |_error| puts "Can't divide a number by 0." }
|
|
1683
|
-
.on_exception { |_error, _use_case| puts 'Oh no, something went wrong!' }
|
|
1684
|
-
|
|
1685
|
-
# Output:
|
|
1686
|
-
# -------
|
|
1687
|
-
# Can't divide a number by 0
|
|
1688
|
-
# Oh no, something went wrong!
|
|
1689
|
-
|
|
1690
|
-
Divide
|
|
1691
|
-
.call(a: 2, b: '2')
|
|
1692
|
-
.on_success { |result| puts result[:division] }
|
|
1693
|
-
.on_exception(TypeError) { puts 'Please, use only numeric attributes.' }
|
|
1694
|
-
.on_exception(ZeroDivisionError) { |_error| puts "Can't divide a number by 0." }
|
|
1695
|
-
.on_exception { |_error, _use_case| puts 'Oh no, something went wrong!' }
|
|
1696
|
-
|
|
1697
|
-
# Output:
|
|
1698
|
-
# -------
|
|
1699
|
-
# Please, use only numeric attributes.
|
|
1700
|
-
# Oh no, something went wrong!
|
|
1449
|
+
# Flows
|
|
1450
|
+
ruby benchmarks/perfomance/flow/success_results.rb
|
|
1451
|
+
ruby benchmarks/perfomance/flow/failure_results.rb
|
|
1701
1452
|
```
|
|
1702
1453
|
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
[⬆️ Voltar para o índice](#índice-)
|
|
1454
|
+
Memory profiling:
|
|
1706
1455
|
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
```ruby
|
|
1714
|
-
Micro::Case.config do |config|
|
|
1715
|
-
config.disable_safe_features = true
|
|
1716
|
-
end
|
|
1456
|
+
```sh
|
|
1457
|
+
./benchmarks/memory/use_case/success/with_transitions/analyze.sh
|
|
1458
|
+
./benchmarks/memory/use_case/success/without_transitions/analyze.sh
|
|
1459
|
+
./benchmarks/memory/flow/success/with_transitions/analyze.sh
|
|
1460
|
+
./benchmarks/memory/flow/success/without_transitions/analyze.sh
|
|
1717
1461
|
```
|
|
1718
1462
|
|
|
1719
|
-
|
|
1463
|
+
### Desabilitando os checks em runtime
|
|
1720
1464
|
|
|
1721
|
-
|
|
1722
|
-
- Chamar `Micro::Cases.safe_flow(...)`
|
|
1723
|
-
- Chamar `Micro::Case::Result#on_exception`
|
|
1724
|
-
|
|
1725
|
-
Veja [`Micro::Case.config`](#microcaseconfig) para a lista completa de configurações disponíveis.
|
|
1726
|
-
|
|
1727
|
-
[⬆️ Voltar para o índice](#índice-)
|
|
1728
|
-
|
|
1729
|
-
### Validando atributos com `accept:` / `reject:`
|
|
1730
|
-
|
|
1731
|
-
Desde a versão `5.2.0` do `u-case`, todo caso de uso já inclui a [extensão `accept`](https://github.com/serradura/u-attributes#accept-extension) do [`u-attributes`](https://github.com/serradura/u-attributes) (requer `u-attributes >= 2.8`). Você pode declarar a expectativa de tipo (ou qualquer outra verificação) diretamente no atributo, e o caso de uso falhará automaticamente com o tipo `:invalid_attributes` quando algum atributo for rejeitado — sem precisar validar dentro do `call!`.
|
|
1465
|
+
Configure `disable_runtime_checks = true` para um pequeno ganho de alguns por cento em produção uma vez que seu test suite tenha exercitado os code paths:
|
|
1732
1466
|
|
|
1733
1467
|
```ruby
|
|
1734
|
-
|
|
1735
|
-
attribute :name, accept: String
|
|
1736
|
-
attribute :email, accept: ->(value) { value.is_a?(String) && value.include?('@') }
|
|
1737
|
-
attribute :age, accept: Integer, allow_nil: true
|
|
1738
|
-
|
|
1739
|
-
def call!
|
|
1740
|
-
Success result: { user: User.create!(attributes) }
|
|
1741
|
-
end
|
|
1742
|
-
end
|
|
1743
|
-
|
|
1744
|
-
CreateUser.call(name: 'Bob', email: 'bob@example.com')
|
|
1745
|
-
# => #<Success type=:ok ...>
|
|
1746
|
-
|
|
1747
|
-
CreateUser.call(name: 42, email: 'not-an-email')
|
|
1748
|
-
# => #<Failure type=:invalid_attributes data={
|
|
1749
|
-
# errors: {
|
|
1750
|
-
# "name" => "expected to be a kind of String",
|
|
1751
|
-
# "email" => "is invalid"
|
|
1752
|
-
# }
|
|
1753
|
-
# }>
|
|
1468
|
+
Micro::Case.config { it.disable_runtime_checks = true }
|
|
1754
1469
|
```
|
|
1755
1470
|
|
|
1756
|
-
|
|
1471
|
+
Os ganhos medidos (veja [`benchmarks/perfomance/runtime_checks/compare.rb`](https://github.com/serradura/u-case/blob/main/benchmarks/perfomance/runtime_checks/compare.rb)) dependem do JIT: dentro do ruído no Ruby puro, ~3–5% no Ruby 3.2 +YJIT, ~4–7% no Ruby 4.0 +PRISM.
|
|
1757
1472
|
|
|
1758
|
-
|
|
1473
|
+
### Comparações
|
|
1759
1474
|
|
|
1760
|
-
|
|
1761
|
-
2. O `u-attributes` executa as verificações de `accept:` / `reject:`.
|
|
1762
|
-
3. O `u-case` executa as validações do `ActiveModel` **apenas se** todos os atributos forem aceitos.
|
|
1475
|
+
Implementações lado a lado do mesmo caso de uso em outras bibliotecas:
|
|
1763
1476
|
|
|
1764
|
-
[
|
|
1477
|
+
- [Interactor](https://github.com/serradura/u-case/blob/main/comparisons/interactor.rb)
|
|
1478
|
+
- [u-case](https://github.com/serradura/u-case/blob/main/comparisons/u-case.rb)
|
|
1765
1479
|
|
|
1766
|
-
|
|
1480
|
+
[⬆️ Voltar ao topo](#índice-)
|
|
1767
1481
|
|
|
1768
|
-
|
|
1482
|
+
## Exemplos
|
|
1769
1483
|
|
|
1770
|
-
|
|
1484
|
+
### Um flow completo de cadastro
|
|
1771
1485
|
|
|
1772
|
-
|
|
1486
|
+
Três casos de uso compostos em um flow transacional, usando validação `accept:`, contratos de resultado e hooks:
|
|
1773
1487
|
|
|
1774
1488
|
```ruby
|
|
1775
|
-
class
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
validates :a, :b, presence: true, numericality: true
|
|
1489
|
+
class NormalizeParams < Micro::Case
|
|
1490
|
+
attribute :params, accept: Hash
|
|
1779
1491
|
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
Success result: { number: a * b }
|
|
1492
|
+
results do |on|
|
|
1493
|
+
on.success(result: [:name, :email])
|
|
1494
|
+
on.failure(:invalid_params)
|
|
1784
1495
|
end
|
|
1785
|
-
end
|
|
1786
|
-
```
|
|
1787
|
-
|
|
1788
|
-
Mas se você deseja uma maneira automática de falhar seus casos de uso em erros de validação, você poderá fazer:
|
|
1789
|
-
|
|
1790
|
-
1. **require 'u-case/with_activemodel_validation'** no Gemfile
|
|
1791
|
-
|
|
1792
|
-
```ruby
|
|
1793
|
-
gem 'u-case', require: 'u-case/with_activemodel_validation'
|
|
1794
|
-
```
|
|
1795
|
-
|
|
1796
|
-
2. Usar o `Micro::Case.config` para habilitar ele. [Link para](#microcaseconfig) essa seção.
|
|
1797
|
-
|
|
1798
|
-
Usando essa abordagem, você pode reescrever o exemplo anterior com menos código. Exemplo:
|
|
1799
|
-
|
|
1800
|
-
```ruby
|
|
1801
|
-
require 'u-case/with_activemodel_validation'
|
|
1802
|
-
|
|
1803
|
-
class Multiply < Micro::Case
|
|
1804
|
-
attributes :a, :b
|
|
1805
|
-
|
|
1806
|
-
validates :a, :b, presence: true, numericality: true
|
|
1807
1496
|
|
|
1808
1497
|
def call!
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
end
|
|
1812
|
-
```
|
|
1813
|
-
|
|
1814
|
-
> **Nota:** Após habilitar o modo de validação, as classes `Micro::Case::Strict` e `Micro::Case::Safe` irão herdar este novo comportamento.
|
|
1815
|
-
|
|
1816
|
-
#### Se eu habilitei a validação automática, é possível desabilitá-la apenas em casos de uso específicos?
|
|
1817
|
-
|
|
1818
|
-
Resposta: Sim, é possível. Para fazer isso, você só precisará usar a macro `disable_auto_validation`. Exemplo:
|
|
1819
|
-
|
|
1820
|
-
```ruby
|
|
1821
|
-
require 'u-case/with_activemodel_validation'
|
|
1498
|
+
name = params[:name].to_s.strip
|
|
1499
|
+
email = params[:email].to_s.strip.downcase
|
|
1822
1500
|
|
|
1823
|
-
|
|
1824
|
-
disable_auto_validation
|
|
1825
|
-
|
|
1826
|
-
attribute :a
|
|
1827
|
-
attribute :b
|
|
1828
|
-
validates :a, :b, presence: true, numericality: true
|
|
1501
|
+
return Failure(:invalid_params) if name.empty? || email.empty?
|
|
1829
1502
|
|
|
1830
|
-
|
|
1831
|
-
Success result: { number: a * b }
|
|
1503
|
+
Success result: { name:, email: }
|
|
1832
1504
|
end
|
|
1833
1505
|
end
|
|
1834
1506
|
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
# O output será:
|
|
1838
|
-
# TypeError (String can't be coerced into Integer)
|
|
1839
|
-
```
|
|
1840
|
-
|
|
1841
|
-
[⬆️ Voltar para o índice](#índice-)
|
|
1842
|
-
|
|
1843
|
-
#### `Kind::Validator`
|
|
1844
|
-
|
|
1845
|
-
A [gem kind](https://github.com/serradura/kind) possui um módulo para habilitar a validação do tipo de dados através do [`ActiveModel validations`](https://guides.rubyonrails.org/active_model_basics.html#validations). Então, quando você fizer o require do `'u-case/with_activemodel_validation'`, este módulo também irá fazer o require do [`Kind::Validator`](https://github.com/serradura/kind#kindvalidator-activemodelvalidations).
|
|
1846
|
-
|
|
1847
|
-
O exemplo abaixo mostra como validar os tipos de atributos.
|
|
1848
|
-
|
|
1849
|
-
```ruby
|
|
1850
|
-
class Todo::List::AddItem < Micro::Case
|
|
1851
|
-
attributes :user, :params
|
|
1507
|
+
class CreateUser < Micro::Case
|
|
1508
|
+
attributes :name, :email
|
|
1852
1509
|
|
|
1853
|
-
|
|
1854
|
-
|
|
1510
|
+
results do |on|
|
|
1511
|
+
on.success(result: [:user])
|
|
1512
|
+
on.failure(:invalid_user)
|
|
1513
|
+
end
|
|
1855
1514
|
|
|
1856
1515
|
def call!
|
|
1857
|
-
|
|
1516
|
+
user = User.create(name:, email:)
|
|
1858
1517
|
|
|
1859
|
-
|
|
1518
|
+
return Failure(:invalid_user, result: { errors: user.errors }) if user.errors.any?
|
|
1860
1519
|
|
|
1861
|
-
Success result: {
|
|
1862
|
-
rescue ActionController::ParameterMissing => e
|
|
1863
|
-
Failure :parameter_missing, result: { message: e.message }
|
|
1520
|
+
Success result: { user: }
|
|
1864
1521
|
end
|
|
1865
1522
|
end
|
|
1866
|
-
```
|
|
1867
|
-
|
|
1868
|
-
[⬆️ Voltar para o índice](#índice-)
|
|
1869
|
-
|
|
1870
|
-
## `Micro::Case.config`
|
|
1871
|
-
|
|
1872
|
-
A ideia deste recurso é permitir a configuração de algumas funcionalidades/módulos do `u-case`.
|
|
1873
|
-
Eu recomendo que você use apenas uma vez em sua base de código. Exemplo: Em um inicializador do Rails.
|
|
1874
1523
|
|
|
1875
|
-
|
|
1524
|
+
class CreateProfile < Micro::Case
|
|
1525
|
+
attributes :user
|
|
1876
1526
|
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1527
|
+
results do |on|
|
|
1528
|
+
on.success(result: [:profile])
|
|
1529
|
+
on.failure(:invalid_profile)
|
|
1530
|
+
end
|
|
1881
1531
|
|
|
1882
|
-
|
|
1883
|
-
|
|
1532
|
+
def call!
|
|
1533
|
+
profile = Profile.create(user_id: user.id)
|
|
1884
1534
|
|
|
1885
|
-
|
|
1886
|
-
# exceções (via `rescue` padrão). Quando `true`, os itens abaixo levantarão
|
|
1887
|
-
# `Micro::Case::Error::SafeFeaturesDisabled`:
|
|
1888
|
-
# - Herdar de `Micro::Case::Safe`
|
|
1889
|
-
# - Chamar `Micro::Cases.safe_flow(...)`
|
|
1890
|
-
# - Chamar `Micro::Case::Result#on_exception`
|
|
1891
|
-
config.disable_safe_features = false
|
|
1535
|
+
return Failure(:invalid_profile, result: { errors: profile.errors }) if profile.errors.any?
|
|
1892
1536
|
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
# "o use case é um tipo de Micro::Case?"). Defina `true` em produção para
|
|
1896
|
-
# um pequeno ganho de performance depois que seus caminhos de código já
|
|
1897
|
-
# estiverem cobertos pela sua suíte de testes. O custo é que usos
|
|
1898
|
-
# incorretos vão aparecer como erros confusos mais à frente, em vez dos
|
|
1899
|
-
# erros curados pela gem (ex.: `Micro::Case::Error::InvalidUseCase`).
|
|
1900
|
-
config.disable_runtime_checks = false
|
|
1537
|
+
Success result: { profile: }
|
|
1538
|
+
end
|
|
1901
1539
|
end
|
|
1902
|
-
```
|
|
1903
|
-
|
|
1904
|
-
Todas as verificações estão consolidadas em `Micro::Case::Check::Enabled` (o
|
|
1905
|
-
padrão). Definir `disable_runtime_checks = true` troca `Micro::Case.check` por
|
|
1906
|
-
`Micro::Case::Check::Disabled` — um módulo com a mesma assinatura cujos
|
|
1907
|
-
métodos não fazem nada — de forma que as validações não são executadas a
|
|
1908
|
-
cada chamada.
|
|
1909
|
-
|
|
1910
|
-
[⬆️ Voltar para o índice](#índice-)
|
|
1911
1540
|
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
| Gem / Abstração | Iterações por segundo | Comparação |
|
|
1919
|
-
| ----------------- | --------------------: | -------------------: |
|
|
1920
|
-
| Dry::Monads | 315635.1 | _**O mais rápido**_ |
|
|
1921
|
-
| **Micro::Case** | 75837.7 | 4.16x mais lento |
|
|
1922
|
-
| Interactor | 59745.5 | 5.28x mais lento |
|
|
1923
|
-
| Trailblazer::Operation | 28423.9 | 11.10x mais lento |
|
|
1924
|
-
| Dry::Transaction | 10130.9 | 31.16x mais lento |
|
|
1925
|
-
|
|
1926
|
-
<details>
|
|
1927
|
-
<summary>Show the full <a href="https://github.com/evanphx/benchmark-ips">benchmark/ips</a> results.</summary>
|
|
1928
|
-
|
|
1929
|
-
```ruby
|
|
1930
|
-
# Warming up --------------------------------------
|
|
1931
|
-
# Interactor 5.711k i/100ms
|
|
1932
|
-
# Trailblazer::Operation
|
|
1933
|
-
# 2.283k i/100ms
|
|
1934
|
-
# Dry::Monads 31.130k i/100ms
|
|
1935
|
-
# Dry::Transaction 994.000 i/100ms
|
|
1936
|
-
# Micro::Case 7.911k i/100ms
|
|
1937
|
-
# Micro::Case::Safe 7.911k i/100ms
|
|
1938
|
-
# Micro::Case::Strict 6.248k i/100ms
|
|
1939
|
-
|
|
1940
|
-
# Calculating -------------------------------------
|
|
1941
|
-
# Interactor 59.746k (±29.9%) i/s - 274.128k in 5.049901s
|
|
1942
|
-
# Trailblazer::Operation
|
|
1943
|
-
# 28.424k (±15.8%) i/s - 141.546k in 5.087882s
|
|
1944
|
-
# Dry::Monads 315.635k (± 6.1%) i/s - 1.588M in 5.048914s
|
|
1945
|
-
# Dry::Transaction 10.131k (± 6.4%) i/s - 50.694k in 5.025150s
|
|
1946
|
-
# Micro::Case 75.838k (± 9.7%) i/s - 379.728k in 5.052573s
|
|
1947
|
-
# Micro::Case::Safe 75.461k (±10.1%) i/s - 379.728k in 5.079238s
|
|
1948
|
-
# Micro::Case::Strict 64.235k (± 9.0%) i/s - 324.896k in 5.097028s
|
|
1949
|
-
|
|
1950
|
-
# Comparison:
|
|
1951
|
-
# Dry::Monads: 315635.1 i/s
|
|
1952
|
-
# Micro::Case: 75837.7 i/s - 4.16x (± 0.00) slower
|
|
1953
|
-
# Micro::Case::Safe: 75461.3 i/s - 4.18x (± 0.00) slower
|
|
1954
|
-
# Micro::Case::Strict: 64234.9 i/s - 4.91x (± 0.00) slower
|
|
1955
|
-
# Interactor: 59745.5 i/s - 5.28x (± 0.00) slower
|
|
1956
|
-
# Trailblazer::Operation: 28423.9 i/s - 11.10x (± 0.00) slower
|
|
1957
|
-
# Dry::Transaction: 10130.9 i/s - 31.16x (± 0.00) slower
|
|
1958
|
-
```
|
|
1959
|
-
</details>
|
|
1960
|
-
|
|
1961
|
-
https://github.com/serradura/u-case/blob/main/benchmarks/perfomance/use_case/success_results.
|
|
1962
|
-
|
|
1963
|
-
#### Failure results
|
|
1964
|
-
|
|
1965
|
-
| Gem / Abstração | Iterações por segundo | Comparação |
|
|
1966
|
-
| ----------------- | --------------------: | -------------------: |
|
|
1967
|
-
| Dry::Monads | 135386.9 | _**O mais rápido**_ |
|
|
1968
|
-
| **Micro::Case** | 73489.3 | 1.85x mais lento |
|
|
1969
|
-
| Trailblazer::Operation | 29016.4 | 4.67x mais lento |
|
|
1970
|
-
| Interactor | 27037.0 | 5.01x mais lento |
|
|
1971
|
-
| Dry::Transaction | 8988.6 | 15.06x mais lento |
|
|
1972
|
-
|
|
1973
|
-
<details>
|
|
1974
|
-
<summary>Mostrar o resultado completo do <a href="https://github.com/evanphx/benchmark-ips">benchmark/ips</a>.</summary>
|
|
1541
|
+
SignUp = Micro::Cases.flow(transaction: true, steps: [
|
|
1542
|
+
NormalizeParams,
|
|
1543
|
+
CreateUser,
|
|
1544
|
+
CreateProfile
|
|
1545
|
+
])
|
|
1975
1546
|
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
# Micro::Case 7.603k i/100ms
|
|
1983
|
-
# Micro::Case::Safe 7.598k i/100ms
|
|
1984
|
-
# Micro::Case::Strict 6.178k i/100ms
|
|
1985
|
-
|
|
1986
|
-
# Calculating -------------------------------------
|
|
1987
|
-
# Interactor 27.037k (±24.9%) i/s - 128.674k in 5.102133s
|
|
1988
|
-
# Trailblazer::Operation 29.016k (±12.4%) i/s - 145.266k in 5.074991s
|
|
1989
|
-
# Dry::Monads 135.387k (±15.1%) i/s - 669.300k in 5.055356s
|
|
1990
|
-
# Dry::Transaction 8.989k (± 9.2%) i/s - 45.136k in 5.084820s
|
|
1991
|
-
# Micro::Case 73.247k (± 9.9%) i/s - 364.944k in 5.030449s
|
|
1992
|
-
# Micro::Case::Safe 73.489k (± 9.6%) i/s - 364.704k in 5.007282s
|
|
1993
|
-
# Micro::Case::Strict 61.980k (± 8.0%) i/s - 308.900k in 5.014821s
|
|
1994
|
-
|
|
1995
|
-
# Comparison:
|
|
1996
|
-
# Dry::Monads: 135386.9 i/s
|
|
1997
|
-
# Micro::Case::Safe: 73489.3 i/s - 1.84x (± 0.00) slower
|
|
1998
|
-
# Micro::Case: 73246.6 i/s - 1.85x (± 0.00) slower
|
|
1999
|
-
# Micro::Case::Strict: 61979.7 i/s - 2.18x (± 0.00) slower
|
|
2000
|
-
# Trailblazer::Operation: 29016.4 i/s - 4.67x (± 0.00) slower
|
|
2001
|
-
# Interactor: 27037.0 i/s - 5.01x (± 0.00) slower
|
|
2002
|
-
# Dry::Transaction: 8988.6 i/s - 15.06x (± 0.00) slower
|
|
1547
|
+
SignUp
|
|
1548
|
+
.call(params: { name: 'Ada', email: 'ADA@EXAMPLE.com' })
|
|
1549
|
+
.on_success { render json: { user_id: it[:user].id } }
|
|
1550
|
+
.on_failure(:invalid_params) { render status: 422 }
|
|
1551
|
+
.on_failure(:invalid_user) { render status: 422, json: { errors: it[:errors] } }
|
|
1552
|
+
.on_failure(:invalid_profile) { render status: 422, json: { errors: it[:errors] } }
|
|
2003
1553
|
```
|
|
2004
|
-
</details>
|
|
2005
1554
|
|
|
2006
|
-
|
|
1555
|
+
Se `CreateProfile` falha, a linha de `User` inserida por `CreateUser` é revertida — esse é o `transaction: true` fazendo seu trabalho. O resultado surfaceia `:invalid_profile`, o hook dispara, e o banco fica limpo.
|
|
2007
1556
|
|
|
2008
|
-
|
|
1557
|
+
### Mais exemplos
|
|
2009
1558
|
|
|
2010
|
-
|
|
1559
|
+
- **[Flow de criação de usuários](https://github.com/serradura/u-case/blob/main/examples/users_creation)** — sanitiza, valida, persiste; demonstra todos os estilos de composição.
|
|
1560
|
+
- **[Aplicação Rails (API)](https://github.com/serradura/from-fat-controllers-to-use-cases)** — arquiteturas diferentes em commits diferentes; o último usa `Micro::Case` para a regra de negócio.
|
|
1561
|
+
- **[Calculadora CLI](https://github.com/serradura/u-case/tree/main/examples/calculator)** — Rake tasks demonstrando manipulação de input do usuário e fluxo de controle baseado em tipos de falha.
|
|
1562
|
+
- **[Capturando exceções](https://github.com/serradura/u-case/blob/main/examples/rescuing_exceptions.rb)** — padrões para tratamento de exceções dentro de casos de uso.
|
|
2011
1563
|
|
|
2012
|
-
|
|
2013
|
-
| ------------------------------------------- | ----------------: | ----------------: |
|
|
2014
|
-
| Micro::Case::Result `pipe` method | 80936.2 i/s | 78280.4 i/s |
|
|
2015
|
-
| Micro::Case::Result `then` method | 0x mais lento | 0x mais lento |
|
|
2016
|
-
| Micro::Cases.flow | 0x mais lento | 0x mais lento |
|
|
2017
|
-
| Micro::Case class with an inner flow | 1.72x mais lento | 1.68x mais lento |
|
|
2018
|
-
| Micro::Case class including itself as a step| 1.93x mais lento | 1.87x mais lento |
|
|
2019
|
-
| Interactor::Organizer | 3.33x mais lento | 3.22x mais lento |
|
|
1564
|
+
[⬆️ Voltar ao topo](#índice-)
|
|
2020
1565
|
|
|
2021
|
-
|
|
1566
|
+
## Indo além com `u-attributes`
|
|
2022
1567
|
|
|
2023
|
-
|
|
2024
|
-
<summary><strong>Resultados de sucesso</strong> - Mostrar o resultado completo do benchmark/ips.</summary>
|
|
1568
|
+
As macros `attribute` / `attributes` do `Micro::Case` vêm do [`u-attributes`](https://github.com/serradura/u-attributes), e todo recurso que aquela gem suporta está disponível em todo caso de uso. Dois padrões que vale conhecer — **ambos requerem [`u-attributes >= 3.1`](https://github.com/serradura/u-attributes)**:
|
|
2025
1569
|
|
|
2026
|
-
|
|
2027
|
-
# Warming up --------------------------------------
|
|
2028
|
-
# Interactor::Organizer 1.809k i/100ms
|
|
2029
|
-
# Micro::Cases.flow([]) 7.808k i/100ms
|
|
2030
|
-
# Micro::Case flow in a class 4.816k i/100ms
|
|
2031
|
-
# Micro::Case including the class 4.094k i/100ms
|
|
2032
|
-
# Micro::Case::Result#| 7.656k i/100ms
|
|
2033
|
-
# Micro::Case::Result#then 7.138k i/100ms
|
|
2034
|
-
|
|
2035
|
-
# Calculating -------------------------------------
|
|
2036
|
-
# Interactor::Organizer 24.290k (±24.0%) i/s - 113.967k in 5.032825s
|
|
2037
|
-
# Micro::Cases.flow([]) 74.790k (±11.1%) i/s - 374.784k in 5.071740s
|
|
2038
|
-
# Micro::Case flow in a class 47.043k (± 8.0%) i/s - 235.984k in 5.047477s
|
|
2039
|
-
# Micro::Case including the class 42.030k (± 8.5%) i/s - 208.794k in 5.002138s
|
|
2040
|
-
# Micro::Case::Result#| 80.936k (±15.9%) i/s - 398.112k in 5.052531s
|
|
2041
|
-
# Micro::Case::Result#then 71.459k (± 8.8%) i/s - 356.900k in 5.030526s
|
|
2042
|
-
|
|
2043
|
-
# Comparison:
|
|
2044
|
-
# Micro::Case::Result#|: 80936.2 i/s
|
|
2045
|
-
# Micro::Cases.flow([]): 74790.1 i/s - same-ish: difference falls within error
|
|
2046
|
-
# Micro::Case::Result#then: 71459.5 i/s - same-ish: difference falls within error
|
|
2047
|
-
# Micro::Case flow in a class: 47042.6 i/s - 1.72x (± 0.00) slower
|
|
2048
|
-
# Micro::Case including the class: 42030.2 i/s - 1.93x (± 0.00) slower
|
|
2049
|
-
# Interactor::Organizer: 24290.3 i/s - 3.33x (± 0.00) slower
|
|
2050
|
-
```
|
|
2051
|
-
</details>
|
|
1570
|
+
### Atributos aninhados (forma com bloco)
|
|
2052
1571
|
|
|
2053
|
-
|
|
2054
|
-
<summary><strong>Resultados de falha</strong> - Mostrar o resultado completo do benchmark/ips.</summary>
|
|
1572
|
+
Declare um atributo que tem atributos por dentro — útil quando seu input é um objeto estruturado em vez de um hash plano. O `accept:` nos atributos internos ainda participa da falha `:invalid_attributes` do pai:
|
|
2055
1573
|
|
|
2056
1574
|
```ruby
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
# Micro::Cases.flow([]) 7.515k i/100ms
|
|
2060
|
-
# Micro::Case flow in a class 4.636k i/100ms
|
|
2061
|
-
# Micro::Case including the class 4.114k i/100ms
|
|
2062
|
-
# Micro::Case::Result#| 7.588k i/100ms
|
|
2063
|
-
# Micro::Case::Result#then 6.681k i/100ms
|
|
2064
|
-
|
|
2065
|
-
# Calculating -------------------------------------
|
|
2066
|
-
# Interactor::Organizer 24.280k (±24.5%) i/s - 112.710k in 5.013334s
|
|
2067
|
-
# Micro::Cases.flow([]) 74.999k (± 9.8%) i/s - 375.750k in 5.055777s
|
|
2068
|
-
# Micro::Case flow in a class 46.681k (± 9.3%) i/s - 236.436k in 5.105105s
|
|
2069
|
-
# Micro::Case including the class 41.921k (± 8.9%) i/s - 209.814k in 5.043622s
|
|
2070
|
-
# Micro::Case::Result#| 78.280k (±12.6%) i/s - 386.988k in 5.022146s
|
|
2071
|
-
# Micro::Case::Result#then 68.898k (± 8.8%) i/s - 347.412k in 5.080116s
|
|
2072
|
-
|
|
2073
|
-
# Comparison:
|
|
2074
|
-
# Micro::Case::Result#|: 78280.4 i/s
|
|
2075
|
-
# Micro::Cases.flow([]): 74999.4 i/s - same-ish: difference falls within error
|
|
2076
|
-
# Micro::Case::Result#then: 68898.4 i/s - same-ish: difference falls within error
|
|
2077
|
-
# Micro::Case flow in a class: 46681.0 i/s - 1.68x (± 0.00) slower
|
|
2078
|
-
# Micro::Case including the class: 41920.8 i/s - 1.87x (± 0.00) slower
|
|
2079
|
-
# Interactor::Organizer: 24280.0 i/s - 3.22x (± 0.00) slower
|
|
2080
|
-
```
|
|
2081
|
-
</details>
|
|
1575
|
+
class CreateOrder < Micro::Case
|
|
1576
|
+
attribute :id, accept: Integer
|
|
2082
1577
|
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
### Execuntando os benchmarks
|
|
2088
|
-
|
|
2089
|
-
#### Performance (Benchmarks IPS)
|
|
1578
|
+
attribute :customer do
|
|
1579
|
+
attribute :name, accept: String
|
|
1580
|
+
attribute :email, accept: String
|
|
1581
|
+
end
|
|
2090
1582
|
|
|
2091
|
-
|
|
1583
|
+
def call!
|
|
1584
|
+
Success result: { order: Order.create!(id:, customer_id: customer.id) }
|
|
1585
|
+
end
|
|
1586
|
+
end
|
|
2092
1587
|
|
|
2093
|
-
|
|
1588
|
+
CreateOrder
|
|
1589
|
+
.call(id: 42, customer: { name: 'Ada', email: 'ada@example.com' })
|
|
1590
|
+
.success? # => true
|
|
2094
1591
|
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
1592
|
+
CreateOrder
|
|
1593
|
+
.call(id: 42, customer: { name: 42, email: 'ada@example.com' })
|
|
1594
|
+
.type # => :invalid_attributes
|
|
2098
1595
|
```
|
|
2099
1596
|
|
|
2100
|
-
|
|
1597
|
+
O hash aninhado é acessível como `customer.name`, `customer.email`.
|
|
2101
1598
|
|
|
2102
|
-
|
|
2103
|
-
ruby benchmarks/perfomance/flow/failure_results.rb
|
|
2104
|
-
ruby benchmarks/perfomance/flow/success_results.rb
|
|
2105
|
-
```
|
|
1599
|
+
### Aceitando outra classe de atributos
|
|
2106
1600
|
|
|
2107
|
-
|
|
1601
|
+
`accept:` pode apontar para outra classe — hashes que chegam são automaticamente convertidos em instâncias dela:
|
|
2108
1602
|
|
|
2109
|
-
|
|
1603
|
+
```ruby
|
|
1604
|
+
class CreateProfile < Micro::Case
|
|
1605
|
+
Address = Micro::Attributes.new do
|
|
1606
|
+
attribute :city, accept: String
|
|
1607
|
+
attribute :postal, accept: String
|
|
1608
|
+
end
|
|
2110
1609
|
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
./benchmarks/memory/use_case/success/without_transitions/analyze.sh
|
|
2114
|
-
```
|
|
1610
|
+
attribute :name, accept: String
|
|
1611
|
+
attribute :address, accept: Address
|
|
2115
1612
|
|
|
2116
|
-
|
|
1613
|
+
def call!
|
|
1614
|
+
Success result: { profile: Profile.create!(name:, address: address.to_h) }
|
|
1615
|
+
end
|
|
1616
|
+
end
|
|
2117
1617
|
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
1618
|
+
CreateProfile.call(
|
|
1619
|
+
name: 'Rodrigo',
|
|
1620
|
+
address: { city: 'Rio', postal: '20000-000' }
|
|
1621
|
+
)
|
|
1622
|
+
# => Success — `address` é uma instância de Address dentro de `call!`
|
|
2121
1623
|
```
|
|
2122
1624
|
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
### Comparações
|
|
2126
|
-
|
|
2127
|
-
Confira as implementações do mesmo caso de uso com diferentes gems/abstrações.
|
|
2128
|
-
|
|
2129
|
-
* [interactor](https://github.com/serradura/u-case/blob/main/comparisons/interactor.rb)
|
|
2130
|
-
* [u-case](https://github.com/serradura/u-case/blob/main/comparisons/u-case.rb)
|
|
2131
|
-
|
|
2132
|
-
[⬆️ Voltar para o índice](#índice-)
|
|
2133
|
-
|
|
2134
|
-
## Exemplos
|
|
2135
|
-
|
|
2136
|
-
### 1️⃣ Criação de usuários
|
|
2137
|
-
|
|
2138
|
-
> Um exemplo de fluxo que define etapas para higienizar, validar e persistir seus dados de entrada. Ele tem todas as abordagens possíveis para representar casos de uso com a gem `u-case`.
|
|
2139
|
-
>
|
|
2140
|
-
> Link: https://github.com/serradura/u-case/blob/main/examples/users_creation
|
|
2141
|
-
|
|
2142
|
-
### 2️⃣ Rails App (API)
|
|
2143
|
-
|
|
2144
|
-
> Este projeto mostra diferentes tipos de arquitetura (uma por commit), e na última, como usar a gem `Micro::Case` para lidar com a lógica de negócios da aplicação.
|
|
2145
|
-
>
|
|
2146
|
-
> Link: https://github.com/serradura/from-fat-controllers-to-use-cases
|
|
2147
|
-
|
|
2148
|
-
### 3️⃣ CLI calculator
|
|
2149
|
-
|
|
2150
|
-
> Rake tasks para demonstrar como lidar com os dados do usuário e como usar diferentes tipos de falha para controlar o fluxo do programa.
|
|
2151
|
-
>
|
|
2152
|
-
> Link: https://github.com/serradura/u-case/tree/main/examples/calculator
|
|
2153
|
-
|
|
2154
|
-
### 4️⃣ Interceptando exceções dentro dos casos de uso
|
|
2155
|
-
|
|
2156
|
-
> Link: https://github.com/serradura/u-case/blob/main/examples/rescuing_exceptions.rb
|
|
1625
|
+
Para defaults, `allow_nil:`, validators customizados e o resto do conjunto de recursos, veja o README do [`u-attributes`](https://github.com/serradura/u-attributes).
|
|
2157
1626
|
|
|
2158
|
-
[⬆️ Voltar
|
|
1627
|
+
[⬆️ Voltar ao topo](#índice-)
|
|
2159
1628
|
|
|
2160
1629
|
## Desenvolvimento
|
|
2161
1630
|
|
|
2162
|
-
|
|
1631
|
+
Depois de clonar o repo, rode `bin/setup` para instalar as dependências e atualizar os appraisals. Então `bundle exec rake test` roda a suíte padrão, `bundle exec appraisal <nome> rake test` roda um appraisal específico do Rails (veja `Appraisals`), e `bundle exec rake matrix` roda a matriz local completa para o Ruby ativo. `bin/console` abre um prompt interativo.
|
|
2163
1632
|
|
|
2164
|
-
Para instalar
|
|
1633
|
+
Para instalar na sua máquina, rode `bundle exec rake install`. Para lançar uma nova versão, atualize `lib/micro/case/version.rb` e então rode `bundle exec rake release` (cria a tag git, faz push dos commits e tags, e dá push do `.gem` para o [rubygems.org](https://rubygems.org)).
|
|
2165
1634
|
|
|
2166
1635
|
## Contribuindo
|
|
2167
1636
|
|
|
2168
|
-
|
|
1637
|
+
Bug reports e pull requests são bem-vindos no GitHub em https://github.com/serradura/u-case. Este projeto pretende ser um espaço seguro e acolhedor para colaboração, e os contribuidores devem aderir ao código de conduta do [Contributor Covenant](https://contributor-covenant.org).
|
|
2169
1638
|
|
|
2170
1639
|
## Licença
|
|
2171
1640
|
|
|
2172
|
-
|
|
1641
|
+
Disponível como open source sob os termos da [MIT License](https://opensource.org/licenses/MIT).
|
|
2173
1642
|
|
|
2174
1643
|
## Código de conduta
|
|
2175
1644
|
|
|
2176
|
-
|
|
1645
|
+
Todos que interagem com a codebase, issue trackers, salas de chat e listas de email do projeto Micro::Case devem seguir o [código de conduta](https://github.com/serradura/u-case/blob/main/CODE_OF_CONDUCT.md).
|