u-case 5.6.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 +16 -0
- data/README.md +1016 -1464
- data/README.pt-BR.md +1008 -1490
- data/lib/micro/case/result.rb +26 -0
- 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,76 +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
|
-
|
|
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)
|
|
78
182
|
- [Comparações](#comparações)
|
|
79
183
|
- [Exemplos](#exemplos)
|
|
80
|
-
- [
|
|
81
|
-
- [
|
|
82
|
-
|
|
83
|
-
- [
|
|
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)
|
|
84
189
|
- [Desenvolvimento](#desenvolvimento)
|
|
85
190
|
- [Contribuindo](#contribuindo)
|
|
86
191
|
- [Licença](#licença)
|
|
@@ -88,17 +193,16 @@ unreleased| https://github.com/serradura/u-case/blob/main/README.md
|
|
|
88
193
|
|
|
89
194
|
## Compatibilidade
|
|
90
195
|
|
|
91
|
-
| u-case
|
|
92
|
-
|
|
|
93
|
-
| unreleased
|
|
94
|
-
| 5.
|
|
95
|
-
| 5.
|
|
96
|
-
| 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 |
|
|
97
201
|
|
|
98
202
|
Esta biblioteca é testada (matriz de CI) contra:
|
|
99
203
|
|
|
100
204
|
| Ruby / Rails | 6.0 | 6.1 | 7.0 | 7.1 | 7.2 | 8.0 | 8.1 | Edge |
|
|
101
|
-
|
|
205
|
+
| ------------ | --- | --- | --- | --- | --- | --- | --- | ---- |
|
|
102
206
|
| 2.7 | ✅ | ✅ | ✅ | ✅ | | | | |
|
|
103
207
|
| 3.0 | ✅ | ✅ | ✅ | ✅ | | | | |
|
|
104
208
|
| 3.1 | | | ✅ | ✅ | ✅ | | | |
|
|
@@ -108,19 +212,12 @@ Esta biblioteca é testada (matriz de CI) contra:
|
|
|
108
212
|
| 4.x | | | | | | | ✅ | ✅ |
|
|
109
213
|
| Head | | | | | | | ✅ | ✅ |
|
|
110
214
|
|
|
111
|
-
>
|
|
215
|
+
> ActiveModel é uma dependência opcional — habilite [`u-case/with_activemodel_validation`](#integração-com-activemodel-opt-in) apenas se quiser.
|
|
112
216
|
|
|
113
217
|
## Dependências
|
|
114
218
|
|
|
115
|
-
1.
|
|
116
|
-
|
|
117
|
-
Sistema de tipos simples (em runtime) para Ruby.
|
|
118
|
-
|
|
119
|
-
É 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))).
|
|
120
|
-
2. [`u-attributes`](https://github.com/serradura/u-attributes) gem.
|
|
121
|
-
|
|
122
|
-
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.
|
|
123
|
-
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.
|
|
124
221
|
|
|
125
222
|
## Instalação
|
|
126
223
|
|
|
@@ -130,1108 +227,973 @@ Adicione essa linha ao Gemfile da sua aplicação:
|
|
|
130
227
|
gem 'u-case', '~> 5.0'
|
|
131
228
|
```
|
|
132
229
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
$ bundle
|
|
136
|
-
|
|
137
|
-
Ou instale manualmente:
|
|
138
|
-
|
|
139
|
-
$ gem install u-case
|
|
230
|
+
Então execute `bundle`, ou instale manualmente com `gem install u-case`.
|
|
140
231
|
|
|
141
232
|
## Uso
|
|
142
233
|
|
|
143
|
-
###
|
|
234
|
+
### Definindo um caso de uso
|
|
235
|
+
|
|
236
|
+
#### O básico
|
|
144
237
|
|
|
145
238
|
```ruby
|
|
146
|
-
class
|
|
147
|
-
# 1.
|
|
148
|
-
|
|
239
|
+
class ValidateEmail < Micro::Case
|
|
240
|
+
# 1. Declare a entrada como atributos
|
|
241
|
+
attribute :address
|
|
149
242
|
|
|
150
|
-
# 2.
|
|
243
|
+
# 2. Implemente call! com a regra de negócio
|
|
151
244
|
def call!
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
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 }
|
|
156
248
|
else
|
|
157
|
-
Failure result: { message: '`
|
|
249
|
+
Failure result: { message: '`address` must be a valid email' }
|
|
158
250
|
end
|
|
159
251
|
end
|
|
160
252
|
end
|
|
161
253
|
|
|
162
|
-
|
|
163
|
-
#
|
|
164
|
-
|
|
254
|
+
result = ValidateEmail.call(address: 'Ada@Example.com')
|
|
255
|
+
result.success? # => true
|
|
256
|
+
result.data # => { address: "ada@example.com" }
|
|
165
257
|
|
|
166
|
-
|
|
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
|
+
```
|
|
167
262
|
|
|
168
|
-
|
|
263
|
+
O objeto retornado por `.call` é um [`Micro::Case::Result`](#trabalhando-com-resultados) — assunto da próxima seção.
|
|
169
264
|
|
|
170
|
-
|
|
171
|
-
result.data # { number: 4 }
|
|
265
|
+
#### Modo estrito — atributos obrigatórios
|
|
172
266
|
|
|
173
|
-
|
|
267
|
+
`Micro::Case::Strict` exige que todos os atributos declarados sejam passados em `.call`. Keywords faltantes lançam `ArgumentError`:
|
|
174
268
|
|
|
175
|
-
|
|
269
|
+
```ruby
|
|
270
|
+
class FormatGreeting < Micro::Case::Strict
|
|
271
|
+
attributes :name, :time_of_day
|
|
176
272
|
|
|
177
|
-
|
|
178
|
-
|
|
273
|
+
def call!
|
|
274
|
+
Success result: { message: "Good #{time_of_day}, #{name}!" }
|
|
275
|
+
end
|
|
276
|
+
end
|
|
179
277
|
|
|
180
|
-
|
|
181
|
-
#
|
|
182
|
-
# 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)
|
|
183
280
|
```
|
|
184
281
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
### `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.
|
|
188
283
|
|
|
189
|
-
|
|
190
|
-
- `#success?` retorna `true` se for um resultado de sucesso.
|
|
191
|
-
- `#failure?` retorna `true` se for um resultado de falha.
|
|
192
|
-
- `#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).
|
|
193
|
-
- `#type` retorna um Symbol que dá significado ao resultado, isso é útil para declarar diferentes tipos de falha e sucesso.
|
|
194
|
-
- `#data` os dados do resultado (um `Hash`).
|
|
195
|
-
- `#[]` e `#values_at` são atalhos para acessar as propriedades do `#data`.
|
|
196
|
-
- `#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`.
|
|
197
|
-
- `#keys` retorna uma array com as chaves presentes no resultado.
|
|
198
|
-
- `#key?` retorna `true` se a chave estiver presente no `#data`.
|
|
199
|
-
- `#value?` retorna `true` se o valor estiver presente no `#data`.
|
|
200
|
-
- `#slice` retorna um novo `Hash` que inclui apenas as chaves fornecidas. Se as chaves fornecidas não existirem, um `Hash` vazio será retornado.
|
|
201
|
-
- `#on_success` or `#on_failure` são métodos de hooks que te auxiliam a definir o fluxo da aplicação.
|
|
202
|
-
- `#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.
|
|
203
|
-
- `#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
|
|
204
285
|
|
|
205
|
-
|
|
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]`:
|
|
206
287
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
288
|
+
```ruby
|
|
289
|
+
require 'json'
|
|
290
|
+
require 'logger'
|
|
210
291
|
|
|
211
|
-
|
|
212
|
-
- `:ok` em casos de sucesso;
|
|
213
|
-
- `:error` ou `:exception` em casos de falhas.
|
|
292
|
+
AppLogger = Logger.new(STDOUT)
|
|
214
293
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
attributes :a, :b
|
|
294
|
+
class ParseJsonPayload < Micro::Case::Safe
|
|
295
|
+
attribute :payload
|
|
218
296
|
|
|
219
297
|
def call!
|
|
220
|
-
if
|
|
221
|
-
Success result: { number: a / b }
|
|
222
|
-
else
|
|
223
|
-
Failure result: { invalid_attributes: invalid_attributes }
|
|
224
|
-
end
|
|
225
|
-
rescue => exception
|
|
226
|
-
Failure result: exception
|
|
227
|
-
end
|
|
298
|
+
return Failure(:blank_payload) if payload.to_s.empty?
|
|
228
299
|
|
|
229
|
-
|
|
230
|
-
attributes.select { |_key, value| !value.is_a?(Numeric) }
|
|
300
|
+
Success result: { data: JSON.parse(payload) }
|
|
231
301
|
end
|
|
232
302
|
end
|
|
233
303
|
|
|
234
|
-
|
|
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
|
|
235
308
|
|
|
236
|
-
result
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
result.success? # true
|
|
241
|
-
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
|
+
```
|
|
242
313
|
|
|
243
|
-
|
|
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:
|
|
244
315
|
|
|
245
|
-
|
|
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
|
+
```
|
|
246
324
|
|
|
247
|
-
|
|
248
|
-
bad_result.data # { invalid_attributes: { "b"=>"2" } }
|
|
249
|
-
bad_result.failure? # true
|
|
250
|
-
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).
|
|
251
326
|
|
|
252
|
-
|
|
327
|
+
##### Flows seguros
|
|
253
328
|
|
|
254
|
-
|
|
329
|
+
Um flow seguro intercepta exceções em qualquer um de seus steps:
|
|
255
330
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
331
|
+
```ruby
|
|
332
|
+
module Users
|
|
333
|
+
Create = Micro::Cases.safe_flow([
|
|
334
|
+
ProcessParams,
|
|
335
|
+
ValidateParams,
|
|
336
|
+
Persist,
|
|
337
|
+
SendToCRM
|
|
338
|
+
])
|
|
260
339
|
|
|
261
|
-
#
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
340
|
+
# Ou como uma classe:
|
|
341
|
+
class Create < Micro::Case::Safe
|
|
342
|
+
flow ProcessParams,
|
|
343
|
+
ValidateParams,
|
|
344
|
+
Persist,
|
|
345
|
+
SendToCRM
|
|
346
|
+
end
|
|
347
|
+
end
|
|
265
348
|
```
|
|
266
349
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
#### Como definir tipos customizados de resultados?
|
|
350
|
+
##### `Result#on_exception`
|
|
270
351
|
|
|
271
|
-
|
|
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:
|
|
272
353
|
|
|
273
354
|
```ruby
|
|
274
|
-
class
|
|
275
|
-
|
|
355
|
+
class ParseJsonPayload < Micro::Case::Safe
|
|
356
|
+
attribute :payload
|
|
276
357
|
|
|
277
358
|
def call!
|
|
278
|
-
|
|
279
|
-
Success result: { number: a * b }
|
|
280
|
-
else
|
|
281
|
-
Failure :invalid_data, result: {
|
|
282
|
-
attributes: attributes.reject { |_, input| input.is_a?(Numeric) }
|
|
283
|
-
}
|
|
284
|
-
end
|
|
359
|
+
Success result: { data: JSON.parse(payload) }
|
|
285
360
|
end
|
|
286
361
|
end
|
|
287
362
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
+
```
|
|
291
372
|
|
|
292
|
-
|
|
293
|
-
result.data # { number: 6 }
|
|
294
|
-
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)).
|
|
295
374
|
|
|
296
|
-
|
|
375
|
+
##### Desabilitando o Safe
|
|
297
376
|
|
|
298
|
-
|
|
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:
|
|
299
378
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
379
|
+
```ruby
|
|
380
|
+
Micro::Case.config do |config|
|
|
381
|
+
config.disable_safe_features = true
|
|
382
|
+
end
|
|
303
383
|
```
|
|
304
384
|
|
|
305
|
-
|
|
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:
|
|
306
413
|
|
|
307
|
-
|
|
414
|
+
- `:ok` — para `Success(...)`.
|
|
415
|
+
- `:error` — para `Failure(...)` cujo payload é um `Hash`.
|
|
416
|
+
- `:exception` — para `Failure(result: some_exception)` (uma instância de `Exception`).
|
|
308
417
|
|
|
309
|
-
|
|
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:
|
|
310
437
|
|
|
311
438
|
```ruby
|
|
312
|
-
class
|
|
313
|
-
attributes :
|
|
439
|
+
class MergeTags < Micro::Case
|
|
440
|
+
attributes :primary, :secondary
|
|
314
441
|
|
|
315
442
|
def call!
|
|
316
|
-
if
|
|
317
|
-
Success result: {
|
|
443
|
+
if primary.is_a?(Array) && secondary.is_a?(Array)
|
|
444
|
+
Success result: { tags: (primary + secondary).uniq }
|
|
318
445
|
else
|
|
319
|
-
Failure
|
|
446
|
+
Failure :invalid_input, result: {
|
|
447
|
+
attributes: attributes.reject { |_, v| v.is_a?(Array) }
|
|
448
|
+
}
|
|
320
449
|
end
|
|
321
450
|
end
|
|
322
451
|
end
|
|
323
452
|
|
|
324
|
-
|
|
453
|
+
MergeTags.call(primary: %w[ruby], secondary: 'rails').type # => :invalid_input
|
|
454
|
+
```
|
|
325
455
|
|
|
326
|
-
result
|
|
327
|
-
result.data # { :invalid_data => true }
|
|
328
|
-
result.type # :invalid_data
|
|
329
|
-
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:
|
|
330
457
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
# (este tópico será coberto em breve).
|
|
335
|
-
```
|
|
458
|
+
```ruby
|
|
459
|
+
def call!
|
|
460
|
+
return Failure(:invalid_input) unless primary.is_a?(Array) && secondary.is_a?(Array)
|
|
336
461
|
|
|
337
|
-
|
|
462
|
+
Success result: { tags: (primary + secondary).uniq }
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
# result.data => { invalid_input: true }
|
|
466
|
+
```
|
|
338
467
|
|
|
339
|
-
####
|
|
468
|
+
#### Contratos de resultado
|
|
340
469
|
|
|
341
|
-
|
|
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`.
|
|
342
471
|
|
|
343
472
|
```ruby
|
|
344
|
-
class
|
|
345
|
-
|
|
473
|
+
class PublishPost < Micro::Case
|
|
474
|
+
attribute :post
|
|
346
475
|
|
|
347
476
|
results do |on|
|
|
348
|
-
on.failure(:
|
|
349
|
-
on.failure(:
|
|
477
|
+
on.failure(:already_published)
|
|
478
|
+
on.failure(:missing_content)
|
|
350
479
|
|
|
351
|
-
on.success(result: [:
|
|
480
|
+
on.success(result: [:post])
|
|
352
481
|
end
|
|
353
482
|
|
|
354
483
|
def call!
|
|
355
|
-
return Failure(:
|
|
356
|
-
return Failure(:
|
|
484
|
+
return Failure(:already_published) if post.published?
|
|
485
|
+
return Failure(:missing_content) if post.body.to_s.strip.empty?
|
|
357
486
|
|
|
358
|
-
|
|
487
|
+
post.update!(status: :published, published_at: Time.current)
|
|
488
|
+
Success result: { post: }
|
|
359
489
|
end
|
|
360
490
|
end
|
|
361
491
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
|
365
495
|
```
|
|
366
496
|
|
|
367
|
-
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.
|
|
368
498
|
|
|
369
499
|
```ruby
|
|
370
|
-
class
|
|
500
|
+
class CreateComment < Micro::Case
|
|
371
501
|
results do |on|
|
|
372
|
-
on.success(result: [:
|
|
373
|
-
on.failure(:
|
|
502
|
+
on.success(result: [:comment])
|
|
503
|
+
on.failure(:spam)
|
|
374
504
|
end
|
|
375
505
|
|
|
376
506
|
def call!
|
|
377
|
-
Success(:
|
|
378
|
-
# Success(result: {
|
|
379
|
-
# 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
|
|
380
510
|
end
|
|
381
511
|
end
|
|
382
512
|
```
|
|
383
513
|
|
|
384
|
-
|
|
385
|
-
- Casos de uso sem o bloco `results` mantêm o comportamento anterior sem restrições — o contrato é opt-in.
|
|
386
|
-
- Subclasses herdam o contrato declarado na classe pai.
|
|
387
|
-
- Exceções capturadas em `Micro::Case::Safe` (que geram `Failure(result: exception)` automaticamente) são exemptas do contrato.
|
|
388
|
-
|
|
389
|
-
[⬆️ Voltar para o índice](#índice-)
|
|
514
|
+
Observações:
|
|
390
515
|
|
|
391
|
-
|
|
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.
|
|
392
521
|
|
|
393
|
-
|
|
394
|
-
`#on_success`, `on_failure`.
|
|
522
|
+
#### Hooks de resultado
|
|
395
523
|
|
|
396
|
-
|
|
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:
|
|
397
525
|
|
|
398
526
|
```ruby
|
|
399
|
-
class
|
|
400
|
-
|
|
527
|
+
class ChangePassword < Micro::Case
|
|
528
|
+
attributes :user, :new_password
|
|
401
529
|
|
|
402
530
|
def call!
|
|
403
|
-
return Failure
|
|
404
|
-
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)
|
|
405
533
|
|
|
406
|
-
|
|
534
|
+
user.update_password!(new_password)
|
|
535
|
+
Success result: { user: }
|
|
407
536
|
end
|
|
408
537
|
end
|
|
409
538
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
.
|
|
418
|
-
.on_failure
|
|
419
|
-
|
|
420
|
-
# O output será:
|
|
421
|
-
# 6
|
|
422
|
-
|
|
423
|
-
#===================================#
|
|
424
|
-
# Lançando um erro em caso de falha #
|
|
425
|
-
#===================================#
|
|
426
|
-
|
|
427
|
-
Double
|
|
428
|
-
.call(number: -1)
|
|
429
|
-
.on_success { |result| p result[:number] }
|
|
430
|
-
.on_failure { |_result, use_case| puts "#{use_case.class.name} was the use case responsible for the failure" }
|
|
431
|
-
.on_failure(:invalid) { |result| raise TypeError, result[:msg] }
|
|
432
|
-
.on_failure(:lte_zero) { |result| raise ArgumentError, result[:msg] }
|
|
433
|
-
|
|
434
|
-
# O output será:
|
|
435
|
-
#
|
|
436
|
-
# 1. Imprimirá a mensagem: Double was the use case responsible for the failure
|
|
437
|
-
# 2. Lançará a exception: ArgumentError (the number must be greater than 0)
|
|
438
|
-
|
|
439
|
-
# Nota:
|
|
440
|
-
# ----
|
|
441
|
-
# 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
|
|
442
549
|
```
|
|
443
550
|
|
|
444
|
-
|
|
551
|
+
> O caso de uso responsável pelo resultado está sempre disponível como o segundo argumento do bloco do hook.
|
|
445
552
|
|
|
446
|
-
|
|
553
|
+
Sem um tipo explícito, o bloco recebe o resultado inteiro, então você pode ramificar com um `case`:
|
|
447
554
|
|
|
448
555
|
```ruby
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
def call!
|
|
453
|
-
return Failure(:invalid) unless number.is_a?(Numeric)
|
|
454
|
-
return Failure :lte_zero, result: attributes(:number) if number <= 0
|
|
455
|
-
|
|
456
|
-
Success result: { number: number * 2 }
|
|
457
|
-
end
|
|
458
|
-
end
|
|
459
|
-
|
|
460
|
-
Double
|
|
461
|
-
.call(number: -1)
|
|
556
|
+
ChangePassword
|
|
557
|
+
.call(user: ada, new_password: 'short')
|
|
462
558
|
.on_failure do |result, use_case|
|
|
463
559
|
case result.type
|
|
464
|
-
when :
|
|
465
|
-
when :
|
|
560
|
+
when :weak then raise ArgumentError, 'password too short'
|
|
561
|
+
when :reused then raise ArgumentError, 'password recently used'
|
|
466
562
|
else raise NotImplementedError
|
|
467
563
|
end
|
|
468
564
|
end
|
|
469
|
-
|
|
470
|
-
# O output será uma exception:
|
|
471
|
-
#
|
|
472
|
-
# ArgumentError (number `-1` must be greater than 0)
|
|
473
565
|
```
|
|
474
566
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
##### Usando decomposição para acessar os dados e tipo do resultado
|
|
478
|
-
|
|
479
|
-
A sintaxe para decompor um Array pode ser usada na declaração de variáveis e nos argumentos de métodos/blocos.
|
|
480
|
-
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:
|
|
481
568
|
|
|
482
569
|
```ruby
|
|
483
|
-
|
|
570
|
+
calls = 0
|
|
571
|
+
result = ChangePassword.call(user: ada, new_password: 'long-enough-1')
|
|
484
572
|
|
|
485
|
-
|
|
486
|
-
.
|
|
487
|
-
.
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
when :lte_zero then raise ArgumentError, "number `#{data[:number]}` must be greater than 0"
|
|
491
|
-
else raise NotImplementedError
|
|
492
|
-
end
|
|
493
|
-
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 }
|
|
494
578
|
|
|
495
|
-
#
|
|
496
|
-
#
|
|
497
|
-
# ArgumentError (the number `-2` must be greater than 0)
|
|
579
|
+
calls # => 4
|
|
498
580
|
```
|
|
499
581
|
|
|
500
|
-
|
|
582
|
+
#### Pattern matching
|
|
501
583
|
|
|
502
|
-
|
|
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):
|
|
503
585
|
|
|
504
|
-
|
|
586
|
+
```ruby
|
|
587
|
+
case result
|
|
588
|
+
in { success: _, data: { number: Numeric => number } }
|
|
589
|
+
puts "got #{number}"
|
|
590
|
+
in { failure: :invalid_attributes, data: { invalid_attributes: errors } }
|
|
591
|
+
warn "bad input: #{errors.keys.join(", ")}"
|
|
592
|
+
in { failure: :exception, data: { exception: } }
|
|
593
|
+
warn "boom: #{exception.message}"
|
|
594
|
+
end
|
|
595
|
+
```
|
|
505
596
|
|
|
506
|
-
|
|
597
|
+
Os hash patterns expõem essas chaves:
|
|
507
598
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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 |
|
|
511
608
|
|
|
512
|
-
|
|
513
|
-
if number.is_a?(Numeric)
|
|
514
|
-
Success :computed, result: { number: number * 2 }
|
|
515
|
-
else
|
|
516
|
-
Failure :invalid, result: { msg: 'number must be a numeric value' }
|
|
517
|
-
end
|
|
518
|
-
end
|
|
519
|
-
end
|
|
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:
|
|
520
610
|
|
|
521
|
-
|
|
522
|
-
result
|
|
523
|
-
|
|
611
|
+
```ruby
|
|
612
|
+
case result
|
|
613
|
+
in [:success, :ok, { number: Integer => n }]
|
|
614
|
+
n
|
|
615
|
+
in [:failure, :invalid_attributes, { invalid_attributes: errors }]
|
|
616
|
+
# ...
|
|
617
|
+
in [:failure, :exception, { exception: }]
|
|
618
|
+
# ...
|
|
619
|
+
end
|
|
620
|
+
```
|
|
524
621
|
|
|
525
|
-
|
|
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.
|
|
526
623
|
|
|
527
|
-
|
|
528
|
-
.on_success { |result| accum += result[:number] }
|
|
529
|
-
.on_success { |result| accum += result[:number] }
|
|
530
|
-
.on_success(:computed) { |result| accum += result[:number] }
|
|
531
|
-
.on_success(:computed) { |result| accum += result[:number] }
|
|
624
|
+
#### Decomposição
|
|
532
625
|
|
|
533
|
-
|
|
626
|
+
Dentro de um hook sem tipo, o resultado também pode ser decomposto em array `[data, type]`:
|
|
534
627
|
|
|
535
|
-
|
|
628
|
+
```ruby
|
|
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
|
|
636
|
+
end
|
|
637
|
+
end
|
|
536
638
|
```
|
|
537
639
|
|
|
538
|
-
####
|
|
640
|
+
#### Continuações dinâmicas com `Result#then`
|
|
539
641
|
|
|
540
|
-
|
|
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:
|
|
541
643
|
|
|
542
644
|
```ruby
|
|
543
|
-
class
|
|
544
|
-
attribute :
|
|
645
|
+
class FindActiveUser < Micro::Case
|
|
646
|
+
attribute :email
|
|
545
647
|
|
|
546
648
|
def call!
|
|
547
|
-
|
|
649
|
+
user = User.active.find_by(email:)
|
|
548
650
|
|
|
549
|
-
|
|
651
|
+
return Success result: { user: } if user
|
|
652
|
+
|
|
653
|
+
Failure result: { email: }
|
|
550
654
|
end
|
|
551
655
|
end
|
|
552
656
|
|
|
553
|
-
class
|
|
554
|
-
attribute :
|
|
657
|
+
class GenerateInviteToken < Micro::Case
|
|
658
|
+
attribute :user
|
|
555
659
|
|
|
556
660
|
def call!
|
|
557
|
-
Success result: {
|
|
661
|
+
Success result: { user:, token: SecureRandom.hex(16) }
|
|
558
662
|
end
|
|
559
663
|
end
|
|
560
664
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
.then(Add3)
|
|
565
|
-
|
|
566
|
-
result1.data # {'number' => -1}
|
|
567
|
-
result1.failure? # true
|
|
568
|
-
|
|
569
|
-
# ---
|
|
570
|
-
|
|
571
|
-
result2 =
|
|
572
|
-
ForbidNegativeNumber
|
|
573
|
-
.call(number: 1)
|
|
574
|
-
.then(Add3)
|
|
575
|
-
|
|
576
|
-
result2.data # {'number' => 4}
|
|
577
|
-
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…" }
|
|
578
668
|
```
|
|
579
669
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
[⬆️ Voltar para o índice](#índice-)
|
|
583
|
-
|
|
584
|
-
##### O que acontece quando um `Micro::Case::Result#then` recebe um bloco?
|
|
585
|
-
|
|
586
|
-
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:
|
|
587
671
|
|
|
588
672
|
```ruby
|
|
589
|
-
class
|
|
590
|
-
|
|
673
|
+
class FindUser < Micro::Case
|
|
674
|
+
attribute :email
|
|
591
675
|
|
|
592
676
|
def call!
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
Failure(:attributes_arent_numbers)
|
|
597
|
-
end
|
|
677
|
+
user = User.find_by(email:)
|
|
678
|
+
|
|
679
|
+
user ? Success(result: { user: }) : Failure(:not_found)
|
|
598
680
|
end
|
|
599
681
|
end
|
|
600
682
|
|
|
601
|
-
#
|
|
602
|
-
|
|
603
|
-
success_result =
|
|
604
|
-
Add
|
|
605
|
-
.call(a: 2, b: 2)
|
|
606
|
-
.then { |result| result.success? ? result[:sum] : 0 }
|
|
607
|
-
|
|
608
|
-
puts success_result # 4
|
|
609
|
-
|
|
610
|
-
# --
|
|
611
|
-
|
|
612
|
-
failure_result =
|
|
613
|
-
Add
|
|
614
|
-
.call(a: 2, b: '2')
|
|
615
|
-
.then { |result| result.success? ? result[:sum] : 0 }
|
|
616
|
-
|
|
617
|
-
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
|
|
618
685
|
```
|
|
619
686
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
##### Como fazer injeção de dependência usando este recurso?
|
|
623
|
-
|
|
624
|
-
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:
|
|
625
688
|
|
|
626
689
|
```ruby
|
|
627
690
|
Todo::FindAllForUser
|
|
628
691
|
.call(user: current_user, params: params)
|
|
629
692
|
.then(Paginate)
|
|
630
693
|
.then(Serialize::PaginatedRelationAsJson, serializer: Todo::Serializer)
|
|
631
|
-
.on_success {
|
|
694
|
+
.on_success { render_json(200, data: it[:todos]) }
|
|
632
695
|
```
|
|
633
696
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
#### Steps internos — construindo um flow inline dentro do `call!`
|
|
637
|
-
|
|
638
|
-
`Result#then` (e seu alias `|`) é a **terceira forma de compor um
|
|
639
|
-
flow** no u-case, lado a lado com `Micro::Cases.flow(...)` e a macro
|
|
640
|
-
de nível de classe `flow ...`. Em vez de ligar casos de uso entre si,
|
|
641
|
-
você mantém o encadeamento *dentro* do `call!` de um único caso de
|
|
642
|
-
uso: cada elo é um método, lambda ou outra classe de caso de uso;
|
|
643
|
-
cada elo retorna um `Micro::Case::Result`; os dados do `Success` de
|
|
644
|
-
cada elo viram os argumentos nomeados do próximo; e cada elo
|
|
645
|
-
contribui com uma linha em `result.transitions` — exatamente como um
|
|
646
|
-
step em um flow de nível superior.
|
|
647
|
-
|
|
648
|
-
##### O que `Result#then` (e `|`) aceitam
|
|
649
|
-
|
|
650
|
-
| Formato | Exemplo |
|
|
651
|
-
| --- | --- |
|
|
652
|
-
| `Symbol` (nome de método) | `result.then(:sum_a_and_b)` |
|
|
653
|
-
| Objeto `Method` ligado | `result.then(method(:sum_a_and_b))` |
|
|
654
|
-
| `Lambda` / `Proc` | `result.then(-> data { sum_a_and_b(**data) })` |
|
|
655
|
-
| Classe de caso de uso | `result.then(SumHalf)` |
|
|
656
|
-
| `Symbol` + Hash de defaults | `result.then(:add, number: 3)` |
|
|
657
|
-
| Bloco | `result.then { \|r\| r.success? ? r[:sum] : 0 }` |
|
|
658
|
-
|
|
659
|
-
O método conectado **precisa** retornar um `Micro::Case::Result`.
|
|
660
|
-
Qualquer outro retorno levanta `Micro::Case::Error::UnexpectedResult`
|
|
661
|
-
— por exemplo um método que devolve um `Hash` será rejeitado com uma
|
|
662
|
-
mensagem do tipo `MeuCase#method(:foo) must return an instance of
|
|
663
|
-
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).
|
|
664
698
|
|
|
665
|
-
|
|
699
|
+
[⬆️ Voltar ao topo](#índice-)
|
|
666
700
|
|
|
667
|
-
|
|
668
|
-
class SumHalf < Micro::Case
|
|
669
|
-
attribute :sum
|
|
701
|
+
### Validando atributos
|
|
670
702
|
|
|
671
|
-
|
|
672
|
-
Success :third_sum, result: { sum: sum + 0.5 }
|
|
673
|
-
end
|
|
674
|
-
end
|
|
703
|
+
#### `accept:` e `reject:` (padrão)
|
|
675
704
|
|
|
676
|
-
|
|
677
|
-
|
|
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!`:
|
|
706
|
+
|
|
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
|
|
678
712
|
|
|
679
713
|
def call!
|
|
680
|
-
|
|
681
|
-
.then(:sum_a_and_b)
|
|
682
|
-
.then(:add, number: 3)
|
|
683
|
-
.then(SumHalf)
|
|
714
|
+
Success result: { user: User.create!(attributes) }
|
|
684
715
|
end
|
|
716
|
+
end
|
|
685
717
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
def validate_numbers
|
|
689
|
-
Kind.of?(Numeric, a, b) ? Success(:valid) : Failure()
|
|
690
|
-
end
|
|
718
|
+
CreateUser.call(name: 'Bob', email: 'bob@example.com')
|
|
719
|
+
# => #<Success type=:ok ...>
|
|
691
720
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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
|
+
```
|
|
695
729
|
|
|
696
|
-
|
|
697
|
-
Success :second_sum, result: { sum: sum + number }
|
|
698
|
-
end
|
|
699
|
-
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).
|
|
700
731
|
|
|
701
|
-
|
|
732
|
+
#### Integração com ActiveModel (opt-in)
|
|
702
733
|
|
|
703
|
-
|
|
704
|
-
result.data # { sum: 6.5 }
|
|
705
|
-
result.transitions # 4 entradas — veja abaixo
|
|
706
|
-
```
|
|
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.
|
|
707
735
|
|
|
708
|
-
`
|
|
736
|
+
A forma mais simples — `validates` está disponível em todo caso de uso, e você falha manualmente:
|
|
709
737
|
|
|
710
738
|
```ruby
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
success: { type: :valid, result: { valid: true } },
|
|
714
|
-
accessible_attributes: [:a, :b] },
|
|
739
|
+
class CreatePost < Micro::Case
|
|
740
|
+
attributes :title, :body
|
|
715
741
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
accessible_attributes: [:a, :b, :valid] },
|
|
742
|
+
validates :title, :body, presence: true
|
|
743
|
+
validates :title, length: { maximum: 120 }
|
|
719
744
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
accessible_attributes: [:a, :b, :valid, :number, :sum] },
|
|
745
|
+
def call!
|
|
746
|
+
return Failure :invalid_attributes, result: { errors: self.errors } if invalid?
|
|
723
747
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
]
|
|
748
|
+
Success result: { post: Post.create!(title:, body:) }
|
|
749
|
+
end
|
|
750
|
+
end
|
|
728
751
|
```
|
|
729
752
|
|
|
730
|
-
|
|
731
|
-
uso hospedeiro**, portanto as três primeiras transições reportam
|
|
732
|
-
`class: DoSomeSum`. Apenas o elo `SumHalf`, que é outra classe de
|
|
733
|
-
caso de uso, contribui com uma transição com `use_case.class`
|
|
734
|
-
diferente. O `accessible_attributes` cresce conforme o `Success` de
|
|
735
|
-
cada elo é mesclado nos dados acumulados.
|
|
736
|
-
|
|
737
|
-
##### O alias `|` (pipe)
|
|
738
|
-
|
|
739
|
-
`|` é 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:
|
|
740
754
|
|
|
741
755
|
```ruby
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
end
|
|
756
|
+
# Gemfile
|
|
757
|
+
gem 'u-case', require: 'u-case/with_activemodel_validation'
|
|
745
758
|
```
|
|
746
759
|
|
|
747
|
-
|
|
748
|
-
idênticos.
|
|
760
|
+
…ou habilite via [Configuração](#configuração). O exemplo então colapsa:
|
|
749
761
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
> bloco/lambda, é possível escrever uma cadeia que se lê quase
|
|
753
|
-
> exatamente como o operador `|>` do Elixir. Cada lambda recebe o
|
|
754
|
-
> hash de dados acumulados como `it` e ainda precisa terminar em
|
|
755
|
-
> uma chamada `Success(...)` / `Failure(...)`:
|
|
756
|
-
>
|
|
757
|
-
> ```ruby
|
|
758
|
-
> def call!
|
|
759
|
-
> validate_something \
|
|
760
|
-
> | -> { do_something_with(**it) } \
|
|
761
|
-
> | -> { and_another_thing_with(**it) }
|
|
762
|
-
> end
|
|
763
|
-
> ```
|
|
764
|
-
>
|
|
765
|
-
> No Ruby 2.7 – 3.3 (onde `it` é apenas um identificador
|
|
766
|
-
> indefinido), use a forma explícita portátil
|
|
767
|
-
> `->(data) { do_something_with(**data) }` mostrada na próxima seção.
|
|
762
|
+
```ruby
|
|
763
|
+
require 'u-case/with_activemodel_validation'
|
|
768
764
|
|
|
769
|
-
|
|
765
|
+
class CreatePost < Micro::Case
|
|
766
|
+
attributes :title, :body
|
|
770
767
|
|
|
771
|
-
|
|
772
|
-
|
|
768
|
+
validates :title, :body, presence: true
|
|
769
|
+
validates :title, length: { maximum: 120 }
|
|
773
770
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
.then(method(:sum_a_and_b))
|
|
778
|
-
.then(->(data) { add(**data, number: 3) })
|
|
779
|
-
.then(SumHalf)
|
|
771
|
+
def call!
|
|
772
|
+
Success result: { post: Post.create!(title:, body:) }
|
|
773
|
+
end
|
|
780
774
|
end
|
|
781
775
|
```
|
|
782
776
|
|
|
783
|
-
|
|
777
|
+
Quando tanto `accept:` quanto validações do ActiveModel estão presentes, a ordem de execução é:
|
|
778
|
+
|
|
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.
|
|
782
|
+
|
|
783
|
+
> A auto-validação também é herdada por `Micro::Case::Strict` e `Micro::Case::Safe`.
|
|
784
|
+
|
|
785
|
+
##### Desabilitando a auto-validação em um caso específico
|
|
784
786
|
|
|
785
|
-
|
|
786
|
-
cadeia imediatamente — exatamente como um step de um flow de nível
|
|
787
|
-
superior retornando uma falha. Os demais elos `.then(...)` / `|` não
|
|
788
|
-
são invocados, e o `result` final é a falha:
|
|
787
|
+
Use a macro `disable_auto_validation`:
|
|
789
788
|
|
|
790
789
|
```ruby
|
|
791
|
-
|
|
790
|
+
require 'u-case/with_activemodel_validation'
|
|
792
791
|
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
# entrada.
|
|
796
|
-
```
|
|
792
|
+
class CountPosts < Micro::Case
|
|
793
|
+
disable_auto_validation
|
|
797
794
|
|
|
798
|
-
|
|
795
|
+
attribute :user
|
|
796
|
+
validates :user, presence: true
|
|
799
797
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
798
|
+
def call!
|
|
799
|
+
Success result: { count: user.posts.count }
|
|
800
|
+
end
|
|
801
|
+
end
|
|
803
802
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
NormalizeParams,
|
|
807
|
-
DoSomeSum, # ← usa .then(:method) internamente
|
|
808
|
-
EnqueueIndexingJob
|
|
809
|
-
])
|
|
803
|
+
CountPosts.call(user: nil)
|
|
804
|
+
# => NoMethodError (undefined method `posts' for nil:NilClass)
|
|
810
805
|
```
|
|
811
806
|
|
|
812
|
-
|
|
813
|
-
transições dos steps externos na ordem de execução. Se `DoSomeSum`
|
|
814
|
-
produz 4 transições internas e o flow externo tem 2 outros steps,
|
|
815
|
-
`result.transitions` final tem 6 entradas.
|
|
816
|
-
|
|
817
|
-
##### Steps internos **sem** transações
|
|
807
|
+
##### `Kind::Validator`
|
|
818
808
|
|
|
819
|
-
|
|
820
|
-
externo usam `transaction: true` — os steps internos se comportam
|
|
821
|
-
como qualquer outro código em `call!`: efeitos colaterais feitos por
|
|
822
|
-
elos anteriores **persistem** mesmo se um elo posterior retornar
|
|
823
|
-
`Failure`. A cadeia é interrompida, mas tudo que já foi escrito no
|
|
824
|
-
banco permanece escrito:
|
|
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`:
|
|
825
810
|
|
|
826
811
|
```ruby
|
|
827
|
-
class
|
|
828
|
-
attributes :
|
|
829
|
-
|
|
830
|
-
def call!
|
|
831
|
-
create_user
|
|
832
|
-
.then(:create_profile)
|
|
833
|
-
end
|
|
834
|
-
|
|
835
|
-
private
|
|
812
|
+
class Todo::List::AddItem < Micro::Case
|
|
813
|
+
attributes :user, :params
|
|
836
814
|
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
Success result: { user: user }
|
|
840
|
-
end
|
|
815
|
+
validates :user, kind: User
|
|
816
|
+
validates :params, kind: ActionController::Parameters
|
|
841
817
|
|
|
842
|
-
def
|
|
843
|
-
|
|
844
|
-
|
|
818
|
+
def call!
|
|
819
|
+
todo_params = params.require(:todo).permit(:title, :due_at)
|
|
820
|
+
todo = user.todos.create(todo_params)
|
|
845
821
|
|
|
846
|
-
Success result: {
|
|
822
|
+
Success result: { todo: todo }
|
|
823
|
+
rescue ActionController::ParameterMissing => e
|
|
824
|
+
Failure :parameter_missing, result: { message: e.message }
|
|
847
825
|
end
|
|
848
826
|
end
|
|
849
|
-
|
|
850
|
-
CreateUserWithProfileInline.call(name: 'Rodrigo', info: '')
|
|
851
|
-
# create_user já INSERIU a linha do user; create_profile falhou.
|
|
852
|
-
# user está persistido; profile não. Não há rollback automático.
|
|
853
827
|
```
|
|
854
828
|
|
|
855
|
-
|
|
856
|
-
envolva a cadeia em uma transação. Como steps internos são apenas
|
|
857
|
-
outra forma de expressar um flow (um flow *interno*), a história
|
|
858
|
-
transacional é exatamente a que já está documentada em
|
|
859
|
-
[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)
|
|
860
|
-
abaixo — a subseção "Flows com steps internos sob transações" lá
|
|
861
|
-
percorre tanto a forma inline `transaction { ... }` quanto a forma
|
|
862
|
-
com `transaction: true` para um caso hospedeiro de steps internos.
|
|
829
|
+
[⬆️ Voltar ao topo](#índice-)
|
|
863
830
|
|
|
864
|
-
|
|
865
|
-
> `with_methods_test.rb` e `with_lambdas_test.rb` para exemplos
|
|
866
|
-
> completos de cada forma, e
|
|
867
|
-
> `test/micro/cases/flow/internal_steps_in_flows_test.rb` para a
|
|
868
|
-
> interação com flows e transações (acumulação, transições e
|
|
869
|
-
> rollback em todos os níveis de aninhamento).
|
|
831
|
+
### Compondo casos de uso
|
|
870
832
|
|
|
871
|
-
[
|
|
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).
|
|
872
834
|
|
|
873
|
-
|
|
835
|
+
#### Flows
|
|
874
836
|
|
|
875
|
-
|
|
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:
|
|
876
838
|
|
|
877
839
|
```ruby
|
|
878
840
|
module Steps
|
|
879
|
-
class
|
|
880
|
-
attribute :
|
|
841
|
+
class ParseTags < Micro::Case
|
|
842
|
+
attribute :tags
|
|
881
843
|
|
|
882
844
|
def call!
|
|
883
|
-
if
|
|
884
|
-
Success result: {
|
|
845
|
+
if tags.is_a?(String)
|
|
846
|
+
Success result: { tags: tags.split(',').map(&:strip) }
|
|
885
847
|
else
|
|
886
|
-
Failure result: { message: '
|
|
848
|
+
Failure result: { message: 'tags must be a comma-separated String' }
|
|
887
849
|
end
|
|
888
850
|
end
|
|
889
851
|
end
|
|
890
852
|
|
|
891
|
-
class
|
|
892
|
-
attribute :
|
|
893
|
-
|
|
894
|
-
def call!
|
|
895
|
-
Success result: { numbers: numbers.map { |number| number + 2 } }
|
|
896
|
-
end
|
|
853
|
+
class Downcase < Micro::Case::Strict
|
|
854
|
+
attribute :tags
|
|
855
|
+
def call!; Success result: { tags: tags.map(&:downcase) }; end
|
|
897
856
|
end
|
|
898
857
|
|
|
899
|
-
class
|
|
900
|
-
attribute :
|
|
901
|
-
|
|
902
|
-
def call!
|
|
903
|
-
Success result: { numbers: numbers.map { |number| number * 2 } }
|
|
904
|
-
end
|
|
858
|
+
class StripHashPrefix < Micro::Case::Strict
|
|
859
|
+
attribute :tags
|
|
860
|
+
def call!; Success result: { tags: tags.map { it.sub(/\A#/, '') } }; end
|
|
905
861
|
end
|
|
906
862
|
|
|
907
|
-
class
|
|
908
|
-
attribute :
|
|
909
|
-
|
|
910
|
-
def call!
|
|
911
|
-
Success result: { numbers: numbers.map { |number| number * number } }
|
|
912
|
-
end
|
|
863
|
+
class RemoveDuplicates < Micro::Case::Strict
|
|
864
|
+
attribute :tags
|
|
865
|
+
def call!; Success result: { tags: tags.uniq }; end
|
|
913
866
|
end
|
|
914
867
|
end
|
|
915
868
|
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
Add2ToAllNumbers = Micro::Cases.flow([
|
|
921
|
-
Steps::ConvertTextToNumbers,
|
|
922
|
-
Steps::Add2
|
|
869
|
+
# Usando o construtor a nível de módulo:
|
|
870
|
+
DowncaseTags = Micro::Cases.flow([
|
|
871
|
+
Steps::ParseTags,
|
|
872
|
+
Steps::Downcase
|
|
923
873
|
])
|
|
924
874
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
result.success? # true
|
|
928
|
-
result.data # {:numbers => [3, 3, 4, 4, 5, 6]}
|
|
875
|
+
DowncaseTags.call(tags: 'Ruby, Rails, RUBY').data
|
|
876
|
+
# => { tags: ["ruby", "rails", "ruby"] }
|
|
929
877
|
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
Steps::Double
|
|
878
|
+
# Usando uma classe:
|
|
879
|
+
class NormalizeTags < Micro::Case
|
|
880
|
+
flow Steps::ParseTags,
|
|
881
|
+
Steps::Downcase,
|
|
882
|
+
Steps::StripHashPrefix,
|
|
883
|
+
Steps::RemoveDuplicates
|
|
937
884
|
end
|
|
938
885
|
|
|
939
|
-
|
|
940
|
-
call(
|
|
941
|
-
on_failure {
|
|
886
|
+
NormalizeTags
|
|
887
|
+
.call(tags: 42)
|
|
888
|
+
.on_failure { puts it[:message] }
|
|
889
|
+
# => "tags must be a comma-separated String"
|
|
942
890
|
```
|
|
943
891
|
|
|
944
|
-
|
|
892
|
+
Quando um flow falha, `Result#use_case` aponta para o step responsável:
|
|
945
893
|
|
|
946
894
|
```ruby
|
|
947
|
-
result =
|
|
895
|
+
result = NormalizeTags.call(tags: 42)
|
|
896
|
+
result.failure? # => true
|
|
897
|
+
result.use_case.is_a?(Steps::ParseTags) # => true
|
|
898
|
+
```
|
|
948
899
|
|
|
949
|
-
|
|
950
|
-
result.use_case.is_a?(Steps::ConvertTextToNumbers) # true
|
|
900
|
+
##### Compondo flows entre si
|
|
951
901
|
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
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"]
|
|
955
913
|
```
|
|
956
914
|
|
|
957
|
-
|
|
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.
|
|
958
916
|
|
|
959
|
-
|
|
917
|
+
##### Acumulação de dados através de um flow
|
|
960
918
|
|
|
961
|
-
|
|
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:
|
|
962
920
|
|
|
963
921
|
```ruby
|
|
964
|
-
module
|
|
965
|
-
class
|
|
966
|
-
attribute :
|
|
922
|
+
module Users
|
|
923
|
+
class FindByEmail < Micro::Case
|
|
924
|
+
attribute :email
|
|
967
925
|
|
|
968
926
|
def call!
|
|
969
|
-
|
|
970
|
-
Success result: { numbers: numbers.map(&:to_i) }
|
|
971
|
-
else
|
|
972
|
-
Failure result: { message: 'numbers must contain only numeric types' }
|
|
973
|
-
end
|
|
974
|
-
end
|
|
975
|
-
end
|
|
927
|
+
user = User.find_by(email:)
|
|
976
928
|
|
|
977
|
-
|
|
978
|
-
attribute :numbers
|
|
929
|
+
return Success result: { user: } if user
|
|
979
930
|
|
|
980
|
-
|
|
981
|
-
Success result: { numbers: numbers.map { |number| number + 2 } }
|
|
931
|
+
Failure(:user_not_found)
|
|
982
932
|
end
|
|
983
933
|
end
|
|
984
934
|
|
|
985
|
-
class
|
|
986
|
-
|
|
935
|
+
class ValidatePassword < Micro::Case::Strict
|
|
936
|
+
attributes :user, :password
|
|
987
937
|
|
|
988
938
|
def call!
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
end
|
|
992
|
-
|
|
993
|
-
class Square < Micro::Case::Strict
|
|
994
|
-
attribute :numbers
|
|
939
|
+
return Failure(:user_must_be_persisted) if user.new_record?
|
|
940
|
+
return Failure(:wrong_password) if user.wrong_password?(password)
|
|
995
941
|
|
|
996
|
-
|
|
997
|
-
Success result: { numbers: numbers.map { |number| number * number } }
|
|
942
|
+
Success result: attributes(:user)
|
|
998
943
|
end
|
|
999
944
|
end
|
|
945
|
+
|
|
946
|
+
Authenticate = Micro::Cases.flow([FindByEmail, ValidatePassword])
|
|
1000
947
|
end
|
|
1001
948
|
|
|
1002
|
-
|
|
1003
|
-
|
|
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
|
+
```
|
|
1004
955
|
|
|
1005
|
-
|
|
1006
|
-
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.
|
|
1007
957
|
|
|
1008
|
-
|
|
1009
|
-
Micro::Cases.flow([DoubleAllNumbers, Steps::Add2])
|
|
958
|
+
##### Inspecionando a execução com `result.transitions`
|
|
1010
959
|
|
|
1011
|
-
|
|
1012
|
-
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:
|
|
1013
961
|
|
|
1014
|
-
|
|
1015
|
-
|
|
962
|
+
```ruby
|
|
963
|
+
user_authenticated = Users::Authenticate.call(email: 'rodrigo@test.com', password: '...')
|
|
1016
964
|
|
|
1017
|
-
|
|
1018
|
-
|
|
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
|
+
```
|
|
1019
985
|
|
|
1020
|
-
|
|
1021
|
-
.call(numbers: %w[1 1 2 2 3 4])
|
|
1022
|
-
.on_success { |result| p result[:numbers] } # [6, 6, 12, 12, 22, 36]
|
|
986
|
+
Schema:
|
|
1023
987
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
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
|
+
]
|
|
1027
1003
|
```
|
|
1028
1004
|
|
|
1029
|
-
|
|
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.
|
|
1030
1006
|
|
|
1031
|
-
|
|
1007
|
+
Para desabilitar transitions globalmente (economiza um hash por step), veja [Configuração](#configuração).
|
|
1032
1008
|
|
|
1033
|
-
|
|
1009
|
+
##### Compondo um flow que inclui a si mesmo
|
|
1034
1010
|
|
|
1035
|
-
|
|
1011
|
+
Uma classe pode usar ela mesma como um step na sua própria declaração de `flow` via `self.call!`:
|
|
1036
1012
|
|
|
1037
1013
|
```ruby
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1014
|
+
class ParseTagsString < Micro::Case
|
|
1015
|
+
attribute :input
|
|
1016
|
+
def call!; Success result: { tags: input.split(',').map(&:strip) }; end
|
|
1017
|
+
end
|
|
1041
1018
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1019
|
+
class JoinTagsArray < Micro::Case
|
|
1020
|
+
attribute :tags
|
|
1021
|
+
def call!; Success result: { input: tags.join(', ') }; end
|
|
1022
|
+
end
|
|
1044
1023
|
|
|
1045
|
-
|
|
1024
|
+
class CleanTags < Micro::Case
|
|
1025
|
+
flow ParseTagsString,
|
|
1026
|
+
self.call!,
|
|
1027
|
+
JoinTagsArray
|
|
1046
1028
|
|
|
1047
|
-
|
|
1048
|
-
|
|
1029
|
+
attribute :tags
|
|
1030
|
+
|
|
1031
|
+
def call!
|
|
1032
|
+
Success result: { tags: tags.map(&:downcase).uniq }
|
|
1049
1033
|
end
|
|
1050
1034
|
end
|
|
1051
1035
|
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
attributes :user, :password
|
|
1036
|
+
CleanTags.call(input: 'Ruby, RUBY, Rails').data[:input] # => "ruby, rails"
|
|
1037
|
+
```
|
|
1055
1038
|
|
|
1056
|
-
|
|
1057
|
-
return Failure(:user_must_be_persisted) if user.new_record?
|
|
1058
|
-
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).
|
|
1059
1040
|
|
|
1060
|
-
|
|
1061
|
-
end
|
|
1062
|
-
end
|
|
1063
|
-
end
|
|
1041
|
+
#### Steps internos — cadeias com `Result#then`
|
|
1064
1042
|
|
|
1065
|
-
|
|
1066
|
-
Authenticate = Micro::Cases.flow([
|
|
1067
|
-
FindByEmail,
|
|
1068
|
-
ValidatePassword
|
|
1069
|
-
])
|
|
1070
|
-
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`.
|
|
1071
1044
|
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
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 }` |
|
|
1078
1055
|
|
|
1079
|
-
|
|
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
|
|
1080
1059
|
|
|
1081
1060
|
```ruby
|
|
1082
|
-
class
|
|
1083
|
-
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
|
|
1084
1067
|
end
|
|
1085
1068
|
|
|
1086
|
-
class
|
|
1087
|
-
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
|
|
1088
1093
|
end
|
|
1094
|
+
|
|
1095
|
+
CreateBlogPost.call(raw_title: ' hello world ', body: 'lorem ipsum').data
|
|
1096
|
+
# => { title: "Hello World" }
|
|
1089
1097
|
```
|
|
1090
1098
|
|
|
1091
|
-
|
|
1092
|
-
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.
|
|
1093
1100
|
|
|
1094
|
-
|
|
1101
|
+
##### Alias `|` (pipe)
|
|
1095
1102
|
|
|
1096
|
-
|
|
1103
|
+
`|` é açúcar sintático para `.then(...)`. O exemplo anterior fica:
|
|
1097
1104
|
|
|
1098
|
-
|
|
1105
|
+
```ruby
|
|
1106
|
+
def call!
|
|
1107
|
+
validate_input | :strip_title | :slugify | CapitalizeTitle
|
|
1108
|
+
end
|
|
1109
|
+
```
|
|
1099
1110
|
|
|
1100
|
-
|
|
1111
|
+
As duas formas produzem o mesmo `result.data` e o mesmo `result.transitions`.
|
|
1101
1112
|
|
|
1102
|
-
|
|
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) }`.
|
|
1103
1124
|
|
|
1104
|
-
|
|
1125
|
+
##### Formas Lambda / `Method`
|
|
1105
1126
|
|
|
1106
|
-
|
|
1127
|
+
Lambdas (e objetos `Method` bound) recebem os dados acumulados **posicionalmente** como um único Hash:
|
|
1107
1128
|
|
|
1108
1129
|
```ruby
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
:use_case => {
|
|
1116
|
-
:class => Users::FindByEmail,
|
|
1117
|
-
:attributes => { :email => "rodrigo@test.com" }
|
|
1118
|
-
},
|
|
1119
|
-
:success => {
|
|
1120
|
-
:type => :ok,
|
|
1121
|
-
:result => {
|
|
1122
|
-
:user => #<User:0x00007fb57b1c5f88 @email="rodrigo@test.com" ...>
|
|
1123
|
-
}
|
|
1124
|
-
},
|
|
1125
|
-
:accessible_attributes => [ :email, :password ]
|
|
1126
|
-
},
|
|
1127
|
-
{
|
|
1128
|
-
:use_case => {
|
|
1129
|
-
:class => Users::ValidatePassword,
|
|
1130
|
-
:attributes => {
|
|
1131
|
-
:user => #<User:0x00007fb57b1c5f88 @email="rodrigo@test.com" ...>
|
|
1132
|
-
:password => "123456"
|
|
1133
|
-
}
|
|
1134
|
-
},
|
|
1135
|
-
:success => {
|
|
1136
|
-
:type => :ok,
|
|
1137
|
-
:result => {
|
|
1138
|
-
:user => #<User:0x00007fb57b1c5f88 @email="rodrigo@test.com" ...>
|
|
1139
|
-
}
|
|
1140
|
-
},
|
|
1141
|
-
:accessible_attributes => [ :email, :password, :user ]
|
|
1142
|
-
}
|
|
1143
|
-
]
|
|
1130
|
+
def call!
|
|
1131
|
+
validate_input
|
|
1132
|
+
.then(method(:strip_title))
|
|
1133
|
+
.then(->(data) { slugify(**data, separator: '-') })
|
|
1134
|
+
.then(CapitalizeTitle)
|
|
1135
|
+
end
|
|
1144
1136
|
```
|
|
1145
1137
|
|
|
1146
|
-
|
|
1147
|
-
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
|
|
1148
1139
|
|
|
1149
|
-
|
|
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.
|
|
1150
1141
|
|
|
1151
|
-
|
|
1142
|
+
##### Usando um caso com steps internos dentro de um flow externo
|
|
1152
1143
|
|
|
1153
|
-
|
|
1154
|
-
```ruby
|
|
1155
|
-
[
|
|
1156
|
-
{
|
|
1157
|
-
use_case: {
|
|
1158
|
-
class: <Micro::Case>,# Caso de uso que será executado
|
|
1159
|
-
attributes: <Hash> # (Input) Os atributos do caso de uso
|
|
1160
|
-
},
|
|
1161
|
-
[success:, failure:] => { # (Output)
|
|
1162
|
-
type: <Symbol>, # Tipo do resultado. Padrões:
|
|
1163
|
-
# Success = :ok, Failure = :error or :exception
|
|
1164
|
-
result: <Hash> # Os dados retornados pelo resultado do use case
|
|
1165
|
-
},
|
|
1166
|
-
accessible_attributes: <Array>, # Propriedades que podem ser acessadas pelos atributos do caso de uso,
|
|
1167
|
-
# começando com Hash usado para invocá-lo e que são incrementados
|
|
1168
|
-
# com os valores de resultado de cada caso de uso do fluxo.
|
|
1169
|
-
}
|
|
1170
|
-
]
|
|
1144
|
+
Um caso de uso que compõe internamente é só um caso de uso, então cabe em qualquer flow:
|
|
1171
1145
|
|
|
1146
|
+
```ruby
|
|
1147
|
+
PublishWorkflow = Micro::Cases.flow([
|
|
1148
|
+
AuthorizePublisher,
|
|
1149
|
+
CreateBlogPost, # ← usa .then(:método) internamente
|
|
1150
|
+
EnqueueIndexingJob
|
|
1151
|
+
])
|
|
1172
1152
|
```
|
|
1173
1153
|
|
|
1174
|
-
|
|
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.
|
|
1175
1155
|
|
|
1176
|
-
|
|
1156
|
+
##### Persistência sem transação
|
|
1177
1157
|
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
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:
|
|
1181
1159
|
|
|
1182
1160
|
```ruby
|
|
1183
|
-
class
|
|
1184
|
-
|
|
1161
|
+
class CreateUserWithProfileInline < Micro::Case
|
|
1162
|
+
attributes :name, :info
|
|
1185
1163
|
|
|
1186
1164
|
def call!
|
|
1187
|
-
|
|
1165
|
+
create_user.then(:create_profile)
|
|
1188
1166
|
end
|
|
1189
|
-
end
|
|
1190
1167
|
|
|
1191
|
-
|
|
1192
|
-
attribute :number
|
|
1168
|
+
private
|
|
1193
1169
|
|
|
1194
|
-
def
|
|
1195
|
-
|
|
1170
|
+
def create_user
|
|
1171
|
+
user = User.create(name:)
|
|
1172
|
+
Success result: { user: }
|
|
1196
1173
|
end
|
|
1197
|
-
end
|
|
1198
1174
|
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
ConvertNumberToText
|
|
1203
|
-
|
|
1204
|
-
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?
|
|
1205
1178
|
|
|
1206
|
-
|
|
1207
|
-
Success result: { number: number * 2 }
|
|
1179
|
+
Success result: { user:, profile: }
|
|
1208
1180
|
end
|
|
1209
1181
|
end
|
|
1210
1182
|
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
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.
|
|
1215
1186
|
```
|
|
1216
1187
|
|
|
1217
|
-
|
|
1188
|
+
Para reverter os writes parciais, envolva a cadeia em uma [transação](#transações).
|
|
1218
1189
|
|
|
1219
|
-
|
|
1190
|
+
#### Transações
|
|
1220
1191
|
|
|
1221
|
-
|
|
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).
|
|
1222
1193
|
|
|
1223
|
-
|
|
1224
|
-
um `ActiveRecord::Base.transaction`. Ambos são opt-in — a gem **não**
|
|
1225
|
-
requer `active_record` automaticamente, então você precisa carregar o
|
|
1226
|
-
ActiveRecord por conta própria (aplicações Rails já o fazem).
|
|
1194
|
+
##### `transaction { ... }` inline dentro do `call!`
|
|
1227
1195
|
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
`Micro::Case#transaction` (e `Micro::Case::Safe#transaction`) é um helper
|
|
1231
|
-
privado de instância que envolve um bloco em uma transação de banco e
|
|
1232
|
-
dispara um `ActiveRecord::Rollback` sempre que o resultado do bloco for
|
|
1233
|
-
um `Failure`. O resultado original é devolvido nos dois casos, permitindo
|
|
1234
|
-
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`:
|
|
1235
1197
|
|
|
1236
1198
|
```ruby
|
|
1237
1199
|
class CreateUserWithAProfile < Micro::Case
|
|
@@ -1243,11 +1205,7 @@ class CreateUserWithAProfile < Micro::Case
|
|
|
1243
1205
|
end
|
|
1244
1206
|
```
|
|
1245
1207
|
|
|
1246
|
-
Se o bloco
|
|
1247
|
-
gravadas dentro do bloco serão revertidas. O helper aceita um kwarg
|
|
1248
|
-
opcional `with:` para escolher a classe ActiveRecord sobre a qual
|
|
1249
|
-
`.transaction` é aberta — útil em aplicações Rails com múltiplos bancos
|
|
1250
|
-
(`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`, …):
|
|
1251
1209
|
|
|
1252
1210
|
```ruby
|
|
1253
1211
|
class CreateAuditEntry < Micro::Case
|
|
@@ -1259,29 +1217,15 @@ class CreateAuditEntry < Micro::Case
|
|
|
1259
1217
|
end
|
|
1260
1218
|
```
|
|
1261
1219
|
|
|
1262
|
-
Quando `with:` é omitido, o helper cai
|
|
1263
|
-
(`transaction with: …`) e depois no callback global padrão (veja abaixo),
|
|
1264
|
-
que vem com `-> { ::ActiveRecord::Base }`.
|
|
1265
|
-
|
|
1266
|
-
> **Nota:** qualquer classe passada via `with:` (aqui, no macro de classe ou
|
|
1267
|
-
> no kwarg `transaction:` de um flow) **precisa ser uma subclasse de
|
|
1268
|
-
> `ActiveRecord::Base`**. Classes não-AR são rejeitadas com `ArgumentError`.
|
|
1269
|
-
> A validação do macro de classe roda em tempo de class-eval quando o
|
|
1270
|
-
> ActiveRecord já está carregado (caso típico de apps Rails); caso
|
|
1271
|
-
> contrário, é adiada para runtime, então a ordem de carregamento de
|
|
1272
|
-
> 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.
|
|
1273
1221
|
|
|
1274
|
-
> **
|
|
1275
|
-
>
|
|
1276
|
-
> `transaction { ... }
|
|
1277
|
-
> `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`.
|
|
1278
1225
|
|
|
1279
1226
|
##### `transaction with: …` — declarando o padrão para um caso
|
|
1280
1227
|
|
|
1281
|
-
|
|
1282
|
-
ActiveRecord deve ser dona de suas transações, para que nem o helper
|
|
1283
|
-
inline nem qualquer flow que envolva o caso precise especificá-la em cada
|
|
1284
|
-
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:
|
|
1285
1229
|
|
|
1286
1230
|
```ruby
|
|
1287
1231
|
class ApplicationUseCase < Micro::Case
|
|
@@ -1290,36 +1234,30 @@ end
|
|
|
1290
1234
|
|
|
1291
1235
|
class CreateUserWithAProfile < ApplicationUseCase
|
|
1292
1236
|
flow(transaction: true, steps: [CreateUser, CreateUserProfile])
|
|
1293
|
-
# transaction: true resolve para ApplicationRecord
|
|
1294
|
-
# a classe hospedeira declarou via `transaction with:`.
|
|
1237
|
+
# transaction: true resolve para ApplicationRecord (herdado).
|
|
1295
1238
|
end
|
|
1296
1239
|
|
|
1297
1240
|
class BillingCase < ApplicationUseCase
|
|
1298
1241
|
transaction with: BillingRecord
|
|
1299
|
-
# sobrescreve a declaração herdada para este ramo da
|
|
1242
|
+
# sobrescreve a declaração herdada para este ramo da árvore
|
|
1300
1243
|
end
|
|
1301
1244
|
```
|
|
1302
1245
|
|
|
1303
|
-
#####
|
|
1246
|
+
##### Transações no nível do flow
|
|
1304
1247
|
|
|
1305
|
-
Passe `transaction:` junto com `steps:` para envolver um flow inteiro em
|
|
1306
|
-
uma única transação. Se qualquer step retornar uma falha (ou levantar uma
|
|
1307
|
-
exceção, no caso de `safe_flow`), todas as escritas realizadas no banco
|
|
1308
|
-
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:
|
|
1309
1249
|
|
|
1310
1250
|
```ruby
|
|
1311
|
-
# Usa
|
|
1312
|
-
# 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.
|
|
1313
1252
|
Micro::Cases.flow(transaction: true, steps: [CreateUser, CreateUserProfile])
|
|
1314
1253
|
|
|
1315
|
-
# Escolhe uma classe ActiveRecord explícita só para este flow — mesmo
|
|
1316
|
-
# 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:`.
|
|
1317
1255
|
Micro::Cases.flow(transaction: { with: AnalyticsRecord }, steps: [
|
|
1318
1256
|
WriteAuditLog,
|
|
1319
1257
|
BumpCounter
|
|
1320
1258
|
])
|
|
1321
1259
|
|
|
1322
|
-
# safe_flow
|
|
1260
|
+
# safe_flow reverte em falhas E em exceções inesperadas.
|
|
1323
1261
|
Micro::Cases.safe_flow(transaction: { with: ApplicationRecord }, steps: [
|
|
1324
1262
|
CreateUser,
|
|
1325
1263
|
CreateUserProfile
|
|
@@ -1331,9 +1269,7 @@ class CreateUserWithAProfile < Micro::Case
|
|
|
1331
1269
|
end
|
|
1332
1270
|
```
|
|
1333
1271
|
|
|
1334
|
-
Para aninhar um flow transacional dentro de outro flow, envolva
|
|
1335
|
-
classe de caso de uso — `Micro::Cases.flow([...])` achata instâncias de
|
|
1336
|
-
`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:
|
|
1337
1273
|
|
|
1338
1274
|
```ruby
|
|
1339
1275
|
class CreateUserAndProfile < Micro::Case
|
|
@@ -1348,17 +1284,11 @@ SignUpFlow = Micro::Cases.flow([
|
|
|
1348
1284
|
])
|
|
1349
1285
|
```
|
|
1350
1286
|
|
|
1351
|
-
Se `transaction: true` for usado
|
|
1352
|
-
carregado, o flow levantará `Micro::Cases::Error::TransactionAdapterMissing`
|
|
1353
|
-
na primeira chamada, sinalizando a configuração incorreta imediatamente.
|
|
1354
|
-
Passar `transaction: { with: SomeClass }` pula essa verificação —
|
|
1355
|
-
`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`.
|
|
1356
1288
|
|
|
1357
|
-
##### `config.default_transaction_class { … }`
|
|
1289
|
+
##### Padrão global — `config.default_transaction_class { … }`
|
|
1358
1290
|
|
|
1359
|
-
Para aplicações Rails que usam um único
|
|
1360
|
-
(`ApplicationRecord`), configure-o uma vez em um initializer em vez de
|
|
1361
|
-
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:
|
|
1362
1292
|
|
|
1363
1293
|
```ruby
|
|
1364
1294
|
# config/initializers/u_case.rb
|
|
@@ -1367,65 +1297,44 @@ Micro::Case.config do |config|
|
|
|
1367
1297
|
end
|
|
1368
1298
|
```
|
|
1369
1299
|
|
|
1370
|
-
O callback (
|
|
1371
|
-
— sem memoização — então é seguro fazer o valor de retorno depender de
|
|
1372
|
-
estado em tempo de execução (roteamento por tenant, etc.). O padrão é
|
|
1373
|
-
`-> { ::ActiveRecord::Base }`. Ordem de resolução quando uma transação
|
|
1374
|
-
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 }`.
|
|
1375
1301
|
|
|
1376
|
-
|
|
1377
|
-
kwarg do flow, ou `transaction(with: X) { ... }` no helper inline.
|
|
1378
|
-
2. **Macro `transaction with: X` da classe hospedeira** (sobe pela
|
|
1379
|
-
hierarquia).
|
|
1380
|
-
3. **`Micro::Case.config.default_transaction_class.call`** — o callback
|
|
1381
|
-
global (padrão `ActiveRecord::Base`).
|
|
1302
|
+
Ordem de resolução, quando uma transação abre:
|
|
1382
1303
|
|
|
1383
|
-
|
|
1384
|
-
`
|
|
1385
|
-
|
|
1386
|
-
|
|
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.
|
|
1387
1309
|
|
|
1388
1310
|
##### Flows com steps internos sob transações
|
|
1389
1311
|
|
|
1390
|
-
|
|
1391
|
-
(a forma `Result#then(:symbol)` / `|` construída inline dentro de um
|
|
1392
|
-
único `call!`) são a terceira forma do u-case de compor um flow —
|
|
1393
|
-
um flow *interno*. Por padrão, um flow interno **não tem rollback
|
|
1394
|
-
transacional**: efeitos colaterais de elos `.then(:método)`
|
|
1395
|
-
anteriores persistem mesmo quando um elo posterior retorna
|
|
1396
|
-
`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`.
|
|
1397
1313
|
|
|
1398
|
-
|
|
1399
|
-
interno. Ambas reutilizam os helpers já documentados acima:
|
|
1314
|
+
Duas formas naturais de dar rollback:
|
|
1400
1315
|
|
|
1401
|
-
**1.
|
|
1402
|
-
Esta é a forma recomendada assim que o caso hospedeiro é composto
|
|
1403
|
-
com o resto do pipeline. A transação cobre a chamada inteira do flow,
|
|
1404
|
-
então um `Failure` *em qualquer ponto* — incluindo de qualquer elo
|
|
1405
|
-
`.then(:método)` interno — reverte todas as escritas de banco feitas
|
|
1406
|
-
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:
|
|
1407
1317
|
|
|
1408
1318
|
```ruby
|
|
1409
1319
|
class CreateUserWithProfileInline < Micro::Case
|
|
1410
1320
|
attributes :name, :info
|
|
1411
1321
|
|
|
1412
1322
|
def call!
|
|
1413
|
-
create_user
|
|
1414
|
-
.then(:create_profile)
|
|
1323
|
+
create_user.then(:create_profile)
|
|
1415
1324
|
end
|
|
1416
1325
|
|
|
1417
1326
|
private
|
|
1418
1327
|
|
|
1419
1328
|
def create_user
|
|
1420
|
-
user = User.create(name:
|
|
1421
|
-
Success result: { user:
|
|
1329
|
+
user = User.create(name:)
|
|
1330
|
+
Success result: { user: }
|
|
1422
1331
|
end
|
|
1423
1332
|
|
|
1424
1333
|
def create_profile(user:, **)
|
|
1425
|
-
profile = UserProfile.create(user_id: user.id, info:
|
|
1334
|
+
profile = UserProfile.create(user_id: user.id, info:)
|
|
1426
1335
|
return Failure(:invalid_profile) if profile.errors.any?
|
|
1427
1336
|
|
|
1428
|
-
Success result: { user
|
|
1337
|
+
Success result: { user:, profile: }
|
|
1429
1338
|
end
|
|
1430
1339
|
end
|
|
1431
1340
|
|
|
@@ -1434,694 +1343,303 @@ SignUp = Micro::Cases.flow(transaction: true, steps: [
|
|
|
1434
1343
|
CreateUserWithProfileInline, # ← falha interna agora reverte
|
|
1435
1344
|
EnqueueIndexingJob
|
|
1436
1345
|
])
|
|
1437
|
-
|
|
1438
|
-
# Ou no nível de classe:
|
|
1439
|
-
class SignUp < Micro::Case
|
|
1440
|
-
flow(transaction: true, steps: [
|
|
1441
|
-
NormalizeParams,
|
|
1442
|
-
CreateUserWithProfileInline,
|
|
1443
|
-
EnqueueIndexingJob
|
|
1444
|
-
])
|
|
1445
|
-
end
|
|
1446
1346
|
```
|
|
1447
1347
|
|
|
1448
|
-
Se `create_profile`
|
|
1449
|
-
`Failure(:invalid_profile)`, a linha de `User` inserida antes por
|
|
1450
|
-
`create_user` é revertida como parte da mesma
|
|
1451
|
-
`ActiveRecord::Base.transaction`. O resultado ainda expõe o tipo da
|
|
1452
|
-
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.
|
|
1453
1349
|
|
|
1454
|
-
**2.
|
|
1455
|
-
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:
|
|
1456
1351
|
|
|
1457
1352
|
```ruby
|
|
1458
1353
|
class CreateUserWithProfileInline < Micro::Case
|
|
1459
1354
|
def call!
|
|
1460
1355
|
transaction {
|
|
1461
|
-
create_user
|
|
1462
|
-
.then(:create_profile)
|
|
1356
|
+
create_user.then(:create_profile)
|
|
1463
1357
|
}
|
|
1464
1358
|
end
|
|
1465
1359
|
end
|
|
1466
1360
|
```
|
|
1467
1361
|
|
|
1468
|
-
|
|
1469
|
-
um flow) e você ainda quer que o flow interno seja atômico. O bloco
|
|
1470
|
-
`transaction` retorna o `Result` da cadeia como está, então você pode
|
|
1471
|
-
continuar compondo com `Result#then` depois dele.
|
|
1472
|
-
|
|
1473
|
-
As duas abordagens **se compõem**. Se você colocar
|
|
1474
|
-
`CreateUserWithProfileInline` (que já usa `transaction { ... }`
|
|
1475
|
-
inline) dentro de um flow externo com `transaction: true`, o
|
|
1476
|
-
ActiveRecord junta a transação interna à externa por padrão — uma
|
|
1477
|
-
falha externa reverte também as escritas internas. Veja as
|
|
1478
|
-
**Observações de comportamento** abaixo para as regras completas de
|
|
1479
|
-
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.
|
|
1480
1363
|
|
|
1481
1364
|
##### Observações de comportamento
|
|
1482
1365
|
|
|
1483
|
-
- **O resultado não é afetado.** `transaction: true` afeta
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
-
|
|
1488
|
-
Outro])` achata `flow_interno` em seus steps internos, o que faz com
|
|
1489
|
-
que uma instância de `Flow` transacional passada dessa forma **perca
|
|
1490
|
-
sua transação**. Envolva flows transacionais reutilizáveis em uma
|
|
1491
|
-
classe de caso de uso (como no snippet acima) para preservar a
|
|
1492
|
-
transação ao aninhar.
|
|
1493
|
-
- **Transações aninhadas se unem à transação externa.** Quando um flow
|
|
1494
|
-
transacional é aninhado dentro de outro flow transacional, o
|
|
1495
|
-
ActiveRecord as une por padrão (sem `requires_new: true`). Uma falha
|
|
1496
|
-
em qualquer ponto da cadeia reverte **tudo** que foi escrito dentro
|
|
1497
|
-
da transação mais externa — incluindo escritas feitas pelo flow
|
|
1498
|
-
interno.
|
|
1499
|
-
- **Um externo não-transacional comita o interno.** Se o flow externo
|
|
1500
|
-
não for transacional e o flow transacional interno tiver sucesso, as
|
|
1501
|
-
escritas do interno são comitadas ao final daquele step. Uma falha
|
|
1502
|
-
em um step posterior (não-transacional) **não** desfaz essas
|
|
1503
|
-
escritas.
|
|
1504
|
-
- **`Micro::Cases.flow(transaction: true, ...)` simples re-lança
|
|
1505
|
-
exceções.** A transação ainda é revertida, mas o chamador precisa
|
|
1506
|
-
fazer rescue. Use `Micro::Cases.safe_flow(transaction: true, ...)`
|
|
1507
|
-
(ou a forma de classe com `Micro::Case::Safe`) para capturar a
|
|
1508
|
-
exceção como uma falha do tipo `:exception`.
|
|
1509
|
-
|
|
1510
|
-
[⬆️ Voltar para o índice](#índice-)
|
|
1511
|
-
|
|
1512
|
-
### `Micro::Case::Strict` - O que é um caso de uso estrito?
|
|
1513
|
-
|
|
1514
|
-
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`.
|
|
1515
1371
|
|
|
1516
|
-
|
|
1517
|
-
class Double < Micro::Case::Strict
|
|
1518
|
-
attribute :numbers
|
|
1372
|
+
[⬆️ Voltar ao topo](#índice-)
|
|
1519
1373
|
|
|
1520
|
-
|
|
1521
|
-
Success result: { numbers: numbers.map { |number| number * 2 } }
|
|
1522
|
-
end
|
|
1523
|
-
end
|
|
1524
|
-
|
|
1525
|
-
Double.call({})
|
|
1526
|
-
|
|
1527
|
-
# O output será:
|
|
1528
|
-
# ArgumentError (missing keyword: :numbers)
|
|
1529
|
-
```
|
|
1530
|
-
|
|
1531
|
-
[⬆️ Voltar para o índice](#índice-)
|
|
1532
|
-
|
|
1533
|
-
### `Micro::Case::Safe` - Existe algum recurso para lidar automaticamente com exceções dentro de um caso de uso ou fluxo?
|
|
1534
|
-
|
|
1535
|
-
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:
|
|
1536
|
-
|
|
1537
|
-
```ruby
|
|
1538
|
-
require 'logger'
|
|
1539
|
-
|
|
1540
|
-
AppLogger = Logger.new(STDOUT)
|
|
1541
|
-
|
|
1542
|
-
class Divide < Micro::Case::Safe
|
|
1543
|
-
attributes :a, :b
|
|
1544
|
-
|
|
1545
|
-
def call!
|
|
1546
|
-
if a.is_a?(Integer) && b.is_a?(Integer)
|
|
1547
|
-
Success result: { number: a / b}
|
|
1548
|
-
else
|
|
1549
|
-
Failure(:not_an_integer)
|
|
1550
|
-
end
|
|
1551
|
-
end
|
|
1552
|
-
end
|
|
1553
|
-
|
|
1554
|
-
result = Divide.call(a: 2, b: 0)
|
|
1555
|
-
result.type == :exception # true
|
|
1556
|
-
result.data # { exception: #<ZeroDivisionError...> }
|
|
1557
|
-
result[:exception].is_a?(ZeroDivisionError) # true
|
|
1558
|
-
|
|
1559
|
-
result.on_failure(:exception) do |result|
|
|
1560
|
-
AppLogger.error(result[:exception].message) # E, [2019-08-21T00:05:44.195506 #9532] ERROR -- : divided by 0
|
|
1561
|
-
end
|
|
1562
|
-
```
|
|
1374
|
+
## Configuração
|
|
1563
1375
|
|
|
1564
|
-
|
|
1376
|
+
`Micro::Case.config` expõe as toggles da gem. Configure uma vez — tipicamente em um initializer do Rails:
|
|
1565
1377
|
|
|
1566
1378
|
```ruby
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
else AppLogger.debug("#{use_case.class.name} was the use case responsible for the exception")
|
|
1571
|
-
end
|
|
1572
|
-
end
|
|
1573
|
-
```
|
|
1574
|
-
|
|
1575
|
-
> **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
|
|
1576
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
|
|
1577
1387
|
|
|
1578
|
-
|
|
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
|
|
1579
1391
|
|
|
1580
|
-
|
|
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
|
|
1581
1399
|
|
|
1582
|
-
|
|
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
|
|
1583
1405
|
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
ValidateParams,
|
|
1589
|
-
Persist,
|
|
1590
|
-
SendToCRM
|
|
1591
|
-
])
|
|
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 }
|
|
1592
1410
|
end
|
|
1593
1411
|
```
|
|
1594
1412
|
|
|
1595
|
-
|
|
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.
|
|
1596
1414
|
|
|
1597
|
-
|
|
1598
|
-
module Users
|
|
1599
|
-
class Create < Micro::Case::Safe
|
|
1600
|
-
flow ProcessParams,
|
|
1601
|
-
ValidateParams,
|
|
1602
|
-
Persist,
|
|
1603
|
-
SendToCRM
|
|
1604
|
-
end
|
|
1605
|
-
end
|
|
1606
|
-
```
|
|
1415
|
+
[⬆️ Voltar ao topo](#índice-)
|
|
1607
1416
|
|
|
1608
|
-
|
|
1417
|
+
## Performance
|
|
1609
1418
|
|
|
1610
|
-
|
|
1419
|
+
Em benchmarks contra abstrações comparáveis, `Micro::Case` é o mais rápido depois do `Dry::Monads`:
|
|
1611
1420
|
|
|
1612
|
-
|
|
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 |
|
|
1613
1428
|
|
|
1614
|
-
Para
|
|
1429
|
+
Para flows, o alias `|` pipe é o estilo de composição mais rápido:
|
|
1615
1430
|
|
|
1616
|
-
|
|
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 |
|
|
1617
1439
|
|
|
1618
|
-
|
|
1440
|
+
> `Dry::Monads`, `Dry::Transaction` e `Trailblazer::Operation` não têm uma feature equivalente a flow e ficam fora da tabela de flow.
|
|
1619
1441
|
|
|
1620
|
-
|
|
1621
|
-
class Divide < Micro::Case::Safe
|
|
1622
|
-
attributes :a, :b
|
|
1442
|
+
### Executando os benchmarks
|
|
1623
1443
|
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1444
|
+
```sh
|
|
1445
|
+
# Casos de uso
|
|
1446
|
+
ruby benchmarks/perfomance/use_case/success_results.rb
|
|
1447
|
+
ruby benchmarks/perfomance/use_case/failure_results.rb
|
|
1628
1448
|
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
.on_exception(TypeError) { puts 'Please, use only numeric attributes.' }
|
|
1633
|
-
.on_exception(ZeroDivisionError) { |_error| puts "Can't divide a number by 0." }
|
|
1634
|
-
.on_exception { |_error, _use_case| puts 'Oh no, something went wrong!' }
|
|
1635
|
-
|
|
1636
|
-
# Output:
|
|
1637
|
-
# -------
|
|
1638
|
-
# Can't divide a number by 0
|
|
1639
|
-
# Oh no, something went wrong!
|
|
1640
|
-
|
|
1641
|
-
Divide
|
|
1642
|
-
.call(a: 2, b: '2')
|
|
1643
|
-
.on_success { |result| puts result[:division] }
|
|
1644
|
-
.on_exception(TypeError) { puts 'Please, use only numeric attributes.' }
|
|
1645
|
-
.on_exception(ZeroDivisionError) { |_error| puts "Can't divide a number by 0." }
|
|
1646
|
-
.on_exception { |_error, _use_case| puts 'Oh no, something went wrong!' }
|
|
1647
|
-
|
|
1648
|
-
# Output:
|
|
1649
|
-
# -------
|
|
1650
|
-
# Please, use only numeric attributes.
|
|
1651
|
-
# Oh no, something went wrong!
|
|
1449
|
+
# Flows
|
|
1450
|
+
ruby benchmarks/perfomance/flow/success_results.rb
|
|
1451
|
+
ruby benchmarks/perfomance/flow/failure_results.rb
|
|
1652
1452
|
```
|
|
1653
1453
|
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
[⬆️ Voltar para o índice](#índice-)
|
|
1454
|
+
Memory profiling:
|
|
1657
1455
|
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
```ruby
|
|
1665
|
-
Micro::Case.config do |config|
|
|
1666
|
-
config.disable_safe_features = true
|
|
1667
|
-
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
|
|
1668
1461
|
```
|
|
1669
1462
|
|
|
1670
|
-
|
|
1463
|
+
### Desabilitando os checks em runtime
|
|
1671
1464
|
|
|
1672
|
-
|
|
1673
|
-
- Chamar `Micro::Cases.safe_flow(...)`
|
|
1674
|
-
- Chamar `Micro::Case::Result#on_exception`
|
|
1675
|
-
|
|
1676
|
-
Veja [`Micro::Case.config`](#microcaseconfig) para a lista completa de configurações disponíveis.
|
|
1677
|
-
|
|
1678
|
-
[⬆️ Voltar para o índice](#índice-)
|
|
1679
|
-
|
|
1680
|
-
### Validando atributos com `accept:` / `reject:`
|
|
1681
|
-
|
|
1682
|
-
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:
|
|
1683
1466
|
|
|
1684
1467
|
```ruby
|
|
1685
|
-
|
|
1686
|
-
attribute :name, accept: String
|
|
1687
|
-
attribute :email, accept: ->(value) { value.is_a?(String) && value.include?('@') }
|
|
1688
|
-
attribute :age, accept: Integer, allow_nil: true
|
|
1689
|
-
|
|
1690
|
-
def call!
|
|
1691
|
-
Success result: { user: User.create!(attributes) }
|
|
1692
|
-
end
|
|
1693
|
-
end
|
|
1694
|
-
|
|
1695
|
-
CreateUser.call(name: 'Bob', email: 'bob@example.com')
|
|
1696
|
-
# => #<Success type=:ok ...>
|
|
1697
|
-
|
|
1698
|
-
CreateUser.call(name: 42, email: 'not-an-email')
|
|
1699
|
-
# => #<Failure type=:invalid_attributes data={
|
|
1700
|
-
# errors: {
|
|
1701
|
-
# "name" => "expected to be a kind of String",
|
|
1702
|
-
# "email" => "is invalid"
|
|
1703
|
-
# }
|
|
1704
|
-
# }>
|
|
1468
|
+
Micro::Case.config { it.disable_runtime_checks = true }
|
|
1705
1469
|
```
|
|
1706
1470
|
|
|
1707
|
-
|
|
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.
|
|
1708
1472
|
|
|
1709
|
-
|
|
1473
|
+
### Comparações
|
|
1710
1474
|
|
|
1711
|
-
|
|
1712
|
-
2. O `u-attributes` executa as verificações de `accept:` / `reject:`.
|
|
1713
|
-
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:
|
|
1714
1476
|
|
|
1715
|
-
[
|
|
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)
|
|
1716
1479
|
|
|
1717
|
-
|
|
1480
|
+
[⬆️ Voltar ao topo](#índice-)
|
|
1718
1481
|
|
|
1719
|
-
|
|
1482
|
+
## Exemplos
|
|
1720
1483
|
|
|
1721
|
-
|
|
1484
|
+
### Um flow completo de cadastro
|
|
1722
1485
|
|
|
1723
|
-
|
|
1486
|
+
Três casos de uso compostos em um flow transacional, usando validação `accept:`, contratos de resultado e hooks:
|
|
1724
1487
|
|
|
1725
1488
|
```ruby
|
|
1726
|
-
class
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
validates :a, :b, presence: true, numericality: true
|
|
1489
|
+
class NormalizeParams < Micro::Case
|
|
1490
|
+
attribute :params, accept: Hash
|
|
1730
1491
|
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
Success result: { number: a * b }
|
|
1492
|
+
results do |on|
|
|
1493
|
+
on.success(result: [:name, :email])
|
|
1494
|
+
on.failure(:invalid_params)
|
|
1735
1495
|
end
|
|
1736
|
-
end
|
|
1737
|
-
```
|
|
1738
|
-
|
|
1739
|
-
Mas se você deseja uma maneira automática de falhar seus casos de uso em erros de validação, você poderá fazer:
|
|
1740
|
-
|
|
1741
|
-
1. **require 'u-case/with_activemodel_validation'** no Gemfile
|
|
1742
|
-
|
|
1743
|
-
```ruby
|
|
1744
|
-
gem 'u-case', require: 'u-case/with_activemodel_validation'
|
|
1745
|
-
```
|
|
1746
|
-
|
|
1747
|
-
2. Usar o `Micro::Case.config` para habilitar ele. [Link para](#microcaseconfig) essa seção.
|
|
1748
|
-
|
|
1749
|
-
Usando essa abordagem, você pode reescrever o exemplo anterior com menos código. Exemplo:
|
|
1750
|
-
|
|
1751
|
-
```ruby
|
|
1752
|
-
require 'u-case/with_activemodel_validation'
|
|
1753
|
-
|
|
1754
|
-
class Multiply < Micro::Case
|
|
1755
|
-
attributes :a, :b
|
|
1756
|
-
|
|
1757
|
-
validates :a, :b, presence: true, numericality: true
|
|
1758
1496
|
|
|
1759
1497
|
def call!
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
end
|
|
1763
|
-
```
|
|
1764
|
-
|
|
1765
|
-
> **Nota:** Após habilitar o modo de validação, as classes `Micro::Case::Strict` e `Micro::Case::Safe` irão herdar este novo comportamento.
|
|
1766
|
-
|
|
1767
|
-
#### Se eu habilitei a validação automática, é possível desabilitá-la apenas em casos de uso específicos?
|
|
1768
|
-
|
|
1769
|
-
Resposta: Sim, é possível. Para fazer isso, você só precisará usar a macro `disable_auto_validation`. Exemplo:
|
|
1770
|
-
|
|
1771
|
-
```ruby
|
|
1772
|
-
require 'u-case/with_activemodel_validation'
|
|
1498
|
+
name = params[:name].to_s.strip
|
|
1499
|
+
email = params[:email].to_s.strip.downcase
|
|
1773
1500
|
|
|
1774
|
-
|
|
1775
|
-
disable_auto_validation
|
|
1776
|
-
|
|
1777
|
-
attribute :a
|
|
1778
|
-
attribute :b
|
|
1779
|
-
validates :a, :b, presence: true, numericality: true
|
|
1501
|
+
return Failure(:invalid_params) if name.empty? || email.empty?
|
|
1780
1502
|
|
|
1781
|
-
|
|
1782
|
-
Success result: { number: a * b }
|
|
1503
|
+
Success result: { name:, email: }
|
|
1783
1504
|
end
|
|
1784
1505
|
end
|
|
1785
1506
|
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
# O output será:
|
|
1789
|
-
# TypeError (String can't be coerced into Integer)
|
|
1790
|
-
```
|
|
1791
|
-
|
|
1792
|
-
[⬆️ Voltar para o índice](#índice-)
|
|
1793
|
-
|
|
1794
|
-
#### `Kind::Validator`
|
|
1795
|
-
|
|
1796
|
-
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).
|
|
1797
|
-
|
|
1798
|
-
O exemplo abaixo mostra como validar os tipos de atributos.
|
|
1799
|
-
|
|
1800
|
-
```ruby
|
|
1801
|
-
class Todo::List::AddItem < Micro::Case
|
|
1802
|
-
attributes :user, :params
|
|
1507
|
+
class CreateUser < Micro::Case
|
|
1508
|
+
attributes :name, :email
|
|
1803
1509
|
|
|
1804
|
-
|
|
1805
|
-
|
|
1510
|
+
results do |on|
|
|
1511
|
+
on.success(result: [:user])
|
|
1512
|
+
on.failure(:invalid_user)
|
|
1513
|
+
end
|
|
1806
1514
|
|
|
1807
1515
|
def call!
|
|
1808
|
-
|
|
1516
|
+
user = User.create(name:, email:)
|
|
1809
1517
|
|
|
1810
|
-
|
|
1518
|
+
return Failure(:invalid_user, result: { errors: user.errors }) if user.errors.any?
|
|
1811
1519
|
|
|
1812
|
-
Success result: {
|
|
1813
|
-
rescue ActionController::ParameterMissing => e
|
|
1814
|
-
Failure :parameter_missing, result: { message: e.message }
|
|
1520
|
+
Success result: { user: }
|
|
1815
1521
|
end
|
|
1816
1522
|
end
|
|
1817
|
-
```
|
|
1818
|
-
|
|
1819
|
-
[⬆️ Voltar para o índice](#índice-)
|
|
1820
|
-
|
|
1821
|
-
## `Micro::Case.config`
|
|
1822
|
-
|
|
1823
|
-
A ideia deste recurso é permitir a configuração de algumas funcionalidades/módulos do `u-case`.
|
|
1824
|
-
Eu recomendo que você use apenas uma vez em sua base de código. Exemplo: Em um inicializador do Rails.
|
|
1825
1523
|
|
|
1826
|
-
|
|
1524
|
+
class CreateProfile < Micro::Case
|
|
1525
|
+
attributes :user
|
|
1827
1526
|
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1527
|
+
results do |on|
|
|
1528
|
+
on.success(result: [:profile])
|
|
1529
|
+
on.failure(:invalid_profile)
|
|
1530
|
+
end
|
|
1832
1531
|
|
|
1833
|
-
|
|
1834
|
-
|
|
1532
|
+
def call!
|
|
1533
|
+
profile = Profile.create(user_id: user.id)
|
|
1835
1534
|
|
|
1836
|
-
|
|
1837
|
-
# exceções (via `rescue` padrão). Quando `true`, os itens abaixo levantarão
|
|
1838
|
-
# `Micro::Case::Error::SafeFeaturesDisabled`:
|
|
1839
|
-
# - Herdar de `Micro::Case::Safe`
|
|
1840
|
-
# - Chamar `Micro::Cases.safe_flow(...)`
|
|
1841
|
-
# - Chamar `Micro::Case::Result#on_exception`
|
|
1842
|
-
config.disable_safe_features = false
|
|
1535
|
+
return Failure(:invalid_profile, result: { errors: profile.errors }) if profile.errors.any?
|
|
1843
1536
|
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
# "o use case é um tipo de Micro::Case?"). Defina `true` em produção para
|
|
1847
|
-
# um pequeno ganho de performance depois que seus caminhos de código já
|
|
1848
|
-
# estiverem cobertos pela sua suíte de testes. O custo é que usos
|
|
1849
|
-
# incorretos vão aparecer como erros confusos mais à frente, em vez dos
|
|
1850
|
-
# erros curados pela gem (ex.: `Micro::Case::Error::InvalidUseCase`).
|
|
1851
|
-
config.disable_runtime_checks = false
|
|
1537
|
+
Success result: { profile: }
|
|
1538
|
+
end
|
|
1852
1539
|
end
|
|
1853
|
-
```
|
|
1854
|
-
|
|
1855
|
-
Todas as verificações estão consolidadas em `Micro::Case::Check::Enabled` (o
|
|
1856
|
-
padrão). Definir `disable_runtime_checks = true` troca `Micro::Case.check` por
|
|
1857
|
-
`Micro::Case::Check::Disabled` — um módulo com a mesma assinatura cujos
|
|
1858
|
-
métodos não fazem nada — de forma que as validações não são executadas a
|
|
1859
|
-
cada chamada.
|
|
1860
|
-
|
|
1861
|
-
[⬆️ Voltar para o índice](#índice-)
|
|
1862
1540
|
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
| Gem / Abstração | Iterações por segundo | Comparação |
|
|
1870
|
-
| ----------------- | --------------------: | -------------------: |
|
|
1871
|
-
| Dry::Monads | 315635.1 | _**O mais rápido**_ |
|
|
1872
|
-
| **Micro::Case** | 75837.7 | 4.16x mais lento |
|
|
1873
|
-
| Interactor | 59745.5 | 5.28x mais lento |
|
|
1874
|
-
| Trailblazer::Operation | 28423.9 | 11.10x mais lento |
|
|
1875
|
-
| Dry::Transaction | 10130.9 | 31.16x mais lento |
|
|
1876
|
-
|
|
1877
|
-
<details>
|
|
1878
|
-
<summary>Show the full <a href="https://github.com/evanphx/benchmark-ips">benchmark/ips</a> results.</summary>
|
|
1879
|
-
|
|
1880
|
-
```ruby
|
|
1881
|
-
# Warming up --------------------------------------
|
|
1882
|
-
# Interactor 5.711k i/100ms
|
|
1883
|
-
# Trailblazer::Operation
|
|
1884
|
-
# 2.283k i/100ms
|
|
1885
|
-
# Dry::Monads 31.130k i/100ms
|
|
1886
|
-
# Dry::Transaction 994.000 i/100ms
|
|
1887
|
-
# Micro::Case 7.911k i/100ms
|
|
1888
|
-
# Micro::Case::Safe 7.911k i/100ms
|
|
1889
|
-
# Micro::Case::Strict 6.248k i/100ms
|
|
1890
|
-
|
|
1891
|
-
# Calculating -------------------------------------
|
|
1892
|
-
# Interactor 59.746k (±29.9%) i/s - 274.128k in 5.049901s
|
|
1893
|
-
# Trailblazer::Operation
|
|
1894
|
-
# 28.424k (±15.8%) i/s - 141.546k in 5.087882s
|
|
1895
|
-
# Dry::Monads 315.635k (± 6.1%) i/s - 1.588M in 5.048914s
|
|
1896
|
-
# Dry::Transaction 10.131k (± 6.4%) i/s - 50.694k in 5.025150s
|
|
1897
|
-
# Micro::Case 75.838k (± 9.7%) i/s - 379.728k in 5.052573s
|
|
1898
|
-
# Micro::Case::Safe 75.461k (±10.1%) i/s - 379.728k in 5.079238s
|
|
1899
|
-
# Micro::Case::Strict 64.235k (± 9.0%) i/s - 324.896k in 5.097028s
|
|
1900
|
-
|
|
1901
|
-
# Comparison:
|
|
1902
|
-
# Dry::Monads: 315635.1 i/s
|
|
1903
|
-
# Micro::Case: 75837.7 i/s - 4.16x (± 0.00) slower
|
|
1904
|
-
# Micro::Case::Safe: 75461.3 i/s - 4.18x (± 0.00) slower
|
|
1905
|
-
# Micro::Case::Strict: 64234.9 i/s - 4.91x (± 0.00) slower
|
|
1906
|
-
# Interactor: 59745.5 i/s - 5.28x (± 0.00) slower
|
|
1907
|
-
# Trailblazer::Operation: 28423.9 i/s - 11.10x (± 0.00) slower
|
|
1908
|
-
# Dry::Transaction: 10130.9 i/s - 31.16x (± 0.00) slower
|
|
1909
|
-
```
|
|
1910
|
-
</details>
|
|
1911
|
-
|
|
1912
|
-
https://github.com/serradura/u-case/blob/main/benchmarks/perfomance/use_case/success_results.
|
|
1913
|
-
|
|
1914
|
-
#### Failure results
|
|
1915
|
-
|
|
1916
|
-
| Gem / Abstração | Iterações por segundo | Comparação |
|
|
1917
|
-
| ----------------- | --------------------: | -------------------: |
|
|
1918
|
-
| Dry::Monads | 135386.9 | _**O mais rápido**_ |
|
|
1919
|
-
| **Micro::Case** | 73489.3 | 1.85x mais lento |
|
|
1920
|
-
| Trailblazer::Operation | 29016.4 | 4.67x mais lento |
|
|
1921
|
-
| Interactor | 27037.0 | 5.01x mais lento |
|
|
1922
|
-
| Dry::Transaction | 8988.6 | 15.06x mais lento |
|
|
1923
|
-
|
|
1924
|
-
<details>
|
|
1925
|
-
<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
|
+
])
|
|
1926
1546
|
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
# Micro::Case 7.603k i/100ms
|
|
1934
|
-
# Micro::Case::Safe 7.598k i/100ms
|
|
1935
|
-
# Micro::Case::Strict 6.178k i/100ms
|
|
1936
|
-
|
|
1937
|
-
# Calculating -------------------------------------
|
|
1938
|
-
# Interactor 27.037k (±24.9%) i/s - 128.674k in 5.102133s
|
|
1939
|
-
# Trailblazer::Operation 29.016k (±12.4%) i/s - 145.266k in 5.074991s
|
|
1940
|
-
# Dry::Monads 135.387k (±15.1%) i/s - 669.300k in 5.055356s
|
|
1941
|
-
# Dry::Transaction 8.989k (± 9.2%) i/s - 45.136k in 5.084820s
|
|
1942
|
-
# Micro::Case 73.247k (± 9.9%) i/s - 364.944k in 5.030449s
|
|
1943
|
-
# Micro::Case::Safe 73.489k (± 9.6%) i/s - 364.704k in 5.007282s
|
|
1944
|
-
# Micro::Case::Strict 61.980k (± 8.0%) i/s - 308.900k in 5.014821s
|
|
1945
|
-
|
|
1946
|
-
# Comparison:
|
|
1947
|
-
# Dry::Monads: 135386.9 i/s
|
|
1948
|
-
# Micro::Case::Safe: 73489.3 i/s - 1.84x (± 0.00) slower
|
|
1949
|
-
# Micro::Case: 73246.6 i/s - 1.85x (± 0.00) slower
|
|
1950
|
-
# Micro::Case::Strict: 61979.7 i/s - 2.18x (± 0.00) slower
|
|
1951
|
-
# Trailblazer::Operation: 29016.4 i/s - 4.67x (± 0.00) slower
|
|
1952
|
-
# Interactor: 27037.0 i/s - 5.01x (± 0.00) slower
|
|
1953
|
-
# 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] } }
|
|
1954
1553
|
```
|
|
1955
|
-
</details>
|
|
1956
1554
|
|
|
1957
|
-
|
|
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.
|
|
1958
1556
|
|
|
1959
|
-
|
|
1557
|
+
### Mais exemplos
|
|
1960
1558
|
|
|
1961
|
-
|
|
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.
|
|
1962
1563
|
|
|
1963
|
-
|
|
1964
|
-
| ------------------------------------------- | ----------------: | ----------------: |
|
|
1965
|
-
| Micro::Case::Result `pipe` method | 80936.2 i/s | 78280.4 i/s |
|
|
1966
|
-
| Micro::Case::Result `then` method | 0x mais lento | 0x mais lento |
|
|
1967
|
-
| Micro::Cases.flow | 0x mais lento | 0x mais lento |
|
|
1968
|
-
| Micro::Case class with an inner flow | 1.72x mais lento | 1.68x mais lento |
|
|
1969
|
-
| Micro::Case class including itself as a step| 1.93x mais lento | 1.87x mais lento |
|
|
1970
|
-
| Interactor::Organizer | 3.33x mais lento | 3.22x mais lento |
|
|
1564
|
+
[⬆️ Voltar ao topo](#índice-)
|
|
1971
1565
|
|
|
1972
|
-
|
|
1566
|
+
## Indo além com `u-attributes`
|
|
1973
1567
|
|
|
1974
|
-
|
|
1975
|
-
<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)**:
|
|
1976
1569
|
|
|
1977
|
-
|
|
1978
|
-
# Warming up --------------------------------------
|
|
1979
|
-
# Interactor::Organizer 1.809k i/100ms
|
|
1980
|
-
# Micro::Cases.flow([]) 7.808k i/100ms
|
|
1981
|
-
# Micro::Case flow in a class 4.816k i/100ms
|
|
1982
|
-
# Micro::Case including the class 4.094k i/100ms
|
|
1983
|
-
# Micro::Case::Result#| 7.656k i/100ms
|
|
1984
|
-
# Micro::Case::Result#then 7.138k i/100ms
|
|
1985
|
-
|
|
1986
|
-
# Calculating -------------------------------------
|
|
1987
|
-
# Interactor::Organizer 24.290k (±24.0%) i/s - 113.967k in 5.032825s
|
|
1988
|
-
# Micro::Cases.flow([]) 74.790k (±11.1%) i/s - 374.784k in 5.071740s
|
|
1989
|
-
# Micro::Case flow in a class 47.043k (± 8.0%) i/s - 235.984k in 5.047477s
|
|
1990
|
-
# Micro::Case including the class 42.030k (± 8.5%) i/s - 208.794k in 5.002138s
|
|
1991
|
-
# Micro::Case::Result#| 80.936k (±15.9%) i/s - 398.112k in 5.052531s
|
|
1992
|
-
# Micro::Case::Result#then 71.459k (± 8.8%) i/s - 356.900k in 5.030526s
|
|
1993
|
-
|
|
1994
|
-
# Comparison:
|
|
1995
|
-
# Micro::Case::Result#|: 80936.2 i/s
|
|
1996
|
-
# Micro::Cases.flow([]): 74790.1 i/s - same-ish: difference falls within error
|
|
1997
|
-
# Micro::Case::Result#then: 71459.5 i/s - same-ish: difference falls within error
|
|
1998
|
-
# Micro::Case flow in a class: 47042.6 i/s - 1.72x (± 0.00) slower
|
|
1999
|
-
# Micro::Case including the class: 42030.2 i/s - 1.93x (± 0.00) slower
|
|
2000
|
-
# Interactor::Organizer: 24290.3 i/s - 3.33x (± 0.00) slower
|
|
2001
|
-
```
|
|
2002
|
-
</details>
|
|
1570
|
+
### Atributos aninhados (forma com bloco)
|
|
2003
1571
|
|
|
2004
|
-
|
|
2005
|
-
<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:
|
|
2006
1573
|
|
|
2007
1574
|
```ruby
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
# Micro::Cases.flow([]) 7.515k i/100ms
|
|
2011
|
-
# Micro::Case flow in a class 4.636k i/100ms
|
|
2012
|
-
# Micro::Case including the class 4.114k i/100ms
|
|
2013
|
-
# Micro::Case::Result#| 7.588k i/100ms
|
|
2014
|
-
# Micro::Case::Result#then 6.681k i/100ms
|
|
2015
|
-
|
|
2016
|
-
# Calculating -------------------------------------
|
|
2017
|
-
# Interactor::Organizer 24.280k (±24.5%) i/s - 112.710k in 5.013334s
|
|
2018
|
-
# Micro::Cases.flow([]) 74.999k (± 9.8%) i/s - 375.750k in 5.055777s
|
|
2019
|
-
# Micro::Case flow in a class 46.681k (± 9.3%) i/s - 236.436k in 5.105105s
|
|
2020
|
-
# Micro::Case including the class 41.921k (± 8.9%) i/s - 209.814k in 5.043622s
|
|
2021
|
-
# Micro::Case::Result#| 78.280k (±12.6%) i/s - 386.988k in 5.022146s
|
|
2022
|
-
# Micro::Case::Result#then 68.898k (± 8.8%) i/s - 347.412k in 5.080116s
|
|
2023
|
-
|
|
2024
|
-
# Comparison:
|
|
2025
|
-
# Micro::Case::Result#|: 78280.4 i/s
|
|
2026
|
-
# Micro::Cases.flow([]): 74999.4 i/s - same-ish: difference falls within error
|
|
2027
|
-
# Micro::Case::Result#then: 68898.4 i/s - same-ish: difference falls within error
|
|
2028
|
-
# Micro::Case flow in a class: 46681.0 i/s - 1.68x (± 0.00) slower
|
|
2029
|
-
# Micro::Case including the class: 41920.8 i/s - 1.87x (± 0.00) slower
|
|
2030
|
-
# Interactor::Organizer: 24280.0 i/s - 3.22x (± 0.00) slower
|
|
2031
|
-
```
|
|
2032
|
-
</details>
|
|
1575
|
+
class CreateOrder < Micro::Case
|
|
1576
|
+
attribute :id, accept: Integer
|
|
2033
1577
|
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
### Execuntando os benchmarks
|
|
2039
|
-
|
|
2040
|
-
#### Performance (Benchmarks IPS)
|
|
1578
|
+
attribute :customer do
|
|
1579
|
+
attribute :name, accept: String
|
|
1580
|
+
attribute :email, accept: String
|
|
1581
|
+
end
|
|
2041
1582
|
|
|
2042
|
-
|
|
1583
|
+
def call!
|
|
1584
|
+
Success result: { order: Order.create!(id:, customer_id: customer.id) }
|
|
1585
|
+
end
|
|
1586
|
+
end
|
|
2043
1587
|
|
|
2044
|
-
|
|
1588
|
+
CreateOrder
|
|
1589
|
+
.call(id: 42, customer: { name: 'Ada', email: 'ada@example.com' })
|
|
1590
|
+
.success? # => true
|
|
2045
1591
|
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
1592
|
+
CreateOrder
|
|
1593
|
+
.call(id: 42, customer: { name: 42, email: 'ada@example.com' })
|
|
1594
|
+
.type # => :invalid_attributes
|
|
2049
1595
|
```
|
|
2050
1596
|
|
|
2051
|
-
|
|
1597
|
+
O hash aninhado é acessível como `customer.name`, `customer.email`.
|
|
2052
1598
|
|
|
2053
|
-
|
|
2054
|
-
ruby benchmarks/perfomance/flow/failure_results.rb
|
|
2055
|
-
ruby benchmarks/perfomance/flow/success_results.rb
|
|
2056
|
-
```
|
|
1599
|
+
### Aceitando outra classe de atributos
|
|
2057
1600
|
|
|
2058
|
-
|
|
1601
|
+
`accept:` pode apontar para outra classe — hashes que chegam são automaticamente convertidos em instâncias dela:
|
|
2059
1602
|
|
|
2060
|
-
|
|
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
|
|
2061
1609
|
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
./benchmarks/memory/use_case/success/without_transitions/analyze.sh
|
|
2065
|
-
```
|
|
1610
|
+
attribute :name, accept: String
|
|
1611
|
+
attribute :address, accept: Address
|
|
2066
1612
|
|
|
2067
|
-
|
|
1613
|
+
def call!
|
|
1614
|
+
Success result: { profile: Profile.create!(name:, address: address.to_h) }
|
|
1615
|
+
end
|
|
1616
|
+
end
|
|
2068
1617
|
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
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!`
|
|
2072
1623
|
```
|
|
2073
1624
|
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
### Comparações
|
|
2077
|
-
|
|
2078
|
-
Confira as implementações do mesmo caso de uso com diferentes gems/abstrações.
|
|
2079
|
-
|
|
2080
|
-
* [interactor](https://github.com/serradura/u-case/blob/main/comparisons/interactor.rb)
|
|
2081
|
-
* [u-case](https://github.com/serradura/u-case/blob/main/comparisons/u-case.rb)
|
|
2082
|
-
|
|
2083
|
-
[⬆️ Voltar para o índice](#índice-)
|
|
2084
|
-
|
|
2085
|
-
## Exemplos
|
|
2086
|
-
|
|
2087
|
-
### 1️⃣ Criação de usuários
|
|
2088
|
-
|
|
2089
|
-
> 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`.
|
|
2090
|
-
>
|
|
2091
|
-
> Link: https://github.com/serradura/u-case/blob/main/examples/users_creation
|
|
2092
|
-
|
|
2093
|
-
### 2️⃣ Rails App (API)
|
|
2094
|
-
|
|
2095
|
-
> 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.
|
|
2096
|
-
>
|
|
2097
|
-
> Link: https://github.com/serradura/from-fat-controllers-to-use-cases
|
|
2098
|
-
|
|
2099
|
-
### 3️⃣ CLI calculator
|
|
2100
|
-
|
|
2101
|
-
> 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.
|
|
2102
|
-
>
|
|
2103
|
-
> Link: https://github.com/serradura/u-case/tree/main/examples/calculator
|
|
2104
|
-
|
|
2105
|
-
### 4️⃣ Interceptando exceções dentro dos casos de uso
|
|
2106
|
-
|
|
2107
|
-
> 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).
|
|
2108
1626
|
|
|
2109
|
-
[⬆️ Voltar
|
|
1627
|
+
[⬆️ Voltar ao topo](#índice-)
|
|
2110
1628
|
|
|
2111
1629
|
## Desenvolvimento
|
|
2112
1630
|
|
|
2113
|
-
|
|
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.
|
|
2114
1632
|
|
|
2115
|
-
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)).
|
|
2116
1634
|
|
|
2117
1635
|
## Contribuindo
|
|
2118
1636
|
|
|
2119
|
-
|
|
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).
|
|
2120
1638
|
|
|
2121
1639
|
## Licença
|
|
2122
1640
|
|
|
2123
|
-
|
|
1641
|
+
Disponível como open source sob os termos da [MIT License](https://opensource.org/licenses/MIT).
|
|
2124
1642
|
|
|
2125
1643
|
## Código de conduta
|
|
2126
1644
|
|
|
2127
|
-
|
|
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).
|