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.
data/README.pt-BR.md CHANGED
@@ -1,6 +1,6 @@
1
1
  <p align="center">
2
- <h1 align="center" id="-case"><img src="./assets/ucase_logo_v1.png" alt="μ-case" height="150"></h1>
3
- <p align="center"><i>Represente casos de uso de forma simples e poderosa ao escrever código modular, expressivo e sequencialmente lógico.</i></p>
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
- Principais objetivos deste projeto:
17
- 1. Fácil de usar e aprender ( entrada **>>** processamento **>>** saída ).
18
- 2. Promover imutabilidade (transformar dados ao invés de modificar) e integridade de dados.
19
- 3. Nada de callbacks (ex: before, after, around) para evitar indireções no código que possam comprometer o estado e entendimento dos fluxos da aplicação.
20
- 4. Resolver regras de negócio complexas, ao permitir uma composição de casos de uso (criação de fluxos).
21
- 5. Ser rápido e otimizado (verifique a [seção de benchmarks](#benchmarks)).
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
- > **Nota:** Verifique o repo https://github.com/serradura/from-fat-controllers-to-use-cases para ver uma aplicação Ruby on Rails que utiliza esta gem para resolver as regras de negócio.
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 | Documentação
28
- --------- | -------------
29
- unreleased| https://github.com/serradura/u-case/blob/main/README.md
30
- 5.6.0 | https://github.com/serradura/u-case/blob/v5.x/README.md
31
- 4.5.1 | https://github.com/serradura/u-case/blob/v4.x/README.md
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
- - [`Micro::Case` - Como definir um caso de uso?](#microcase---como-definir-um-caso-de-uso)
39
- - [`Micro::Case::Result` - O que é o resultado de um caso de uso?](#microcaseresult---o-que-é-o-resultado-de-um-caso-de-uso)
40
- - [O que são os tipos de resultados?](#o-que-são-os-tipos-de-resultados)
41
- - [Como definir tipos customizados de resultados?](#como-definir-tipos-customizados-de-resultados)
42
- - [É possível definir um tipo sem definir os dados do resultado?](#é-possível-definir-um-tipo-sem-definir-os-dados-do-resultado)
43
- - [Como declarar um contrato de resultados?](#como-declarar-um-contrato-de-resultados)
44
- - [Como utilizar os hooks dos resultados?](#como-utilizar-os-hooks-dos-resultados)
45
- - [Por que o hook sem um tipo definido expõe o próprio resultado?](#por-que-o-hook-sem-um-tipo-definido-expõe-o-próprio-resultado)
46
- - [Usando decomposição para acessar os dados e tipo do resultado](#usando-decomposição-para-acessar-os-dados-e-tipo-do-resultado)
47
- - [O que acontece se um hook de resultado for declarado múltiplas vezes?](#o-que-acontece-se-um-hook-de-resultado-for-declarado-múltiplas-vezes)
48
- - [Como usar o método `Micro::Case::Result#then`?](#como-usar-o-método-microcaseresultthen)
49
- - [O que acontece quando um `Micro::Case::Result#then` recebe um bloco?](#o-que-acontece-quando-um-microcaseresultthen-recebe-um-bloco)
50
- - [Como fazer injeção de dependência usando este recurso?](#como-fazer-injeção-de-dependência-usando-este-recurso)
51
- - [Steps internos — construindo um flow inline dentro do `call!`](#steps-internos--construindo-um-flow-inline-dentro-do-call)
52
- - [`Micro::Cases::Flow` - Como compor casos de uso?](#microcasesflow---como-compor-casos-de-uso)
53
- - [É possível compor um fluxo com outros fluxos?](#é-possível-compor-um-fluxo-com-outros-fluxos)
54
- - [É possível que um fluxo acumule sua entrada e mescle cada resultado de sucesso para usar como argumento dos próximos casos de uso?](#é-possível-que-um-fluxo-acumule-sua-entrada-e-mescle-cada-resultado-de-sucesso-para-usar-como-argumento-dos-próximos-casos-de-uso)
55
- - [Como entender o que aconteceu durante a execução de um flow?](#como-entender-o-que-aconteceu-durante-a-execução-de-um-flow)
56
- - [`Micro::Case::Result#transitions` schema](#microcaseresulttransitions-schema)
57
- - [É possível desabilitar o `Micro::Case::Result#transitions`?](#é-possível-desabilitar-o-microcaseresulttransitions)
58
- - [É possível declarar um fluxo que inclui o próprio caso de uso?](#é-possível-declarar-um-fluxo-que-inclui-o-próprio-caso-de-uso)
59
- - [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)
60
- - [`Micro::Case::Strict` - O que é um caso de uso estrito?](#microcasestrict---o-que-é-um-caso-de-uso-estrito)
61
- - [`Micro::Case::Safe` - Existe algum recurso para lidar automaticamente com exceções dentro de um caso de uso ou fluxo?](#microcasesafe---existe-algum-recurso-para-lidar-automaticamente-com-exceções-dentro-de-um-caso-de-uso-ou-fluxo)
62
- - [`Micro::Cases::Safe::Flow`](#microcasessafeflow)
63
- - [`Micro::Case::Result#on_exception`](#microcaseresulton_exception)
64
- - [Desabilitando o mecanismo "safe"](#desabilitando-o-mecanismo-safe)
65
- - [Validando atributos com `accept:` / `reject:`](#validando-atributos-com-accept--reject)
66
- - [`u-case/with_activemodel_validation` - Como validar os atributos do caso de uso?](#u-casewith_activemodel_validation---como-validar-os-atributos-do-caso-de-uso)
67
- - [Se eu habilitei a validação automática, é possível desabilitá-la apenas em casos de uso específicos?](#se-eu-habilitei-a-validação-automática-é-possível-desabilitá-la-apenas-em-casos-de-uso-específicos)
68
- - [`Kind::Validator`](#kindvalidator)
69
- - [`Micro::Case.config`](#microcaseconfig)
70
- - [Benchmarks](#benchmarks)
71
- - [`Micro::Case`](#microcase)
72
- - [Success results](#success-results)
73
- - [Failure results](#failure-results)
74
- - [`Micro::Cases::Flow`](#microcasesflow)
75
- - [Execuntando os benchmarks](#execuntando-os-benchmarks)
76
- - [Performance (Benchmarks IPS)](#performance-benchmarks-ips)
77
- - [Memory profiling](#memory-profiling)
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
- - [1️⃣ Criação de usuários](#1️⃣-criação-de-usuários)
81
- - [2️⃣ Rails App (API)](#2️⃣-rails-app-api)
82
- - [3️⃣ CLI calculator](#3️⃣-cli-calculator)
83
- - [4️⃣ Interceptando exceções dentro dos casos de uso](#4️⃣-interceptando-exceções-dentro-dos-casos-de-uso)
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 | branch | ruby | activemodel | u-attributes |
92
- | ---------------- | ------ | -------- | -------------- | -------------- |
93
- | unreleased | main | >= 2.7 | >= 6.0 | >= 2.8, < 4.0 |
94
- | 5.6.0 | v5.x | >= 2.7 | >= 6.0 | >= 2.8, < 4.0 |
95
- | 5.1.0 | v5.x | >= 2.7 | >= 6.0 | >= 2.7, < 4.0 |
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
- > Nota: O activemodel é uma dependência opcional, esse módulo que [pode ser habilitado](#u-casewith_activemodel_validation---como-validar-os-atributos-do-caso-de-uso) para validar os atributos dos casos de uso.
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. Gem [`kind`](https://github.com/serradura/kind).
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
- E então execute:
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
- ### `Micro::Case` - Como definir um caso de uso?
234
+ ### Definindo um caso de uso
235
+
236
+ #### O básico
144
237
 
145
238
  ```ruby
146
- class Multiply < Micro::Case
147
- # 1. Defina o input como atributos
148
- attributes :a, :b
239
+ class ValidateEmail < Micro::Case
240
+ # 1. Declare a entrada como atributos
241
+ attribute :address
149
242
 
150
- # 2. Defina o método `call!` com a regra de negócio
243
+ # 2. Implemente call! com a regra de negócio
151
244
  def call!
152
-
153
- # 3. Envolva o resultado do caso de uso com os métodos `Success(result: *)` ou `Failure(result: *)`
154
- if a.is_a?(Numeric) && b.is_a?(Numeric)
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: '`a` and `b` attributes must be numeric' }
249
+ Failure result: { message: '`address` must be a valid email' }
158
250
  end
159
251
  end
160
252
  end
161
253
 
162
- #===========================#
163
- # Executando um caso de uso #
164
- #===========================#
254
+ result = ValidateEmail.call(address: 'Ada@Example.com')
255
+ result.success? # => true
256
+ result.data # => { address: "ada@example.com" }
165
257
 
166
- # Resultado de sucesso
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
- result = Multiply.call(a: 2, b: 2)
263
+ O objeto retornado por `.call` é um [`Micro::Case::Result`](#trabalhando-com-resultados) assunto da próxima seção.
169
264
 
170
- result.success? # true
171
- result.data # { number: 4 }
265
+ #### Modo estrito — atributos obrigatórios
172
266
 
173
- # Resultado de falha
267
+ `Micro::Case::Strict` exige que todos os atributos declarados sejam passados em `.call`. Keywords faltantes lançam `ArgumentError`:
174
268
 
175
- bad_result = Multiply.call(a: 2, b: '2')
269
+ ```ruby
270
+ class FormatGreeting < Micro::Case::Strict
271
+ attributes :name, :time_of_day
176
272
 
177
- bad_result.failure? # true
178
- bad_result.data # { message: "`a` and `b` attributes must be numeric" }
273
+ def call!
274
+ Success result: { message: "Good #{time_of_day}, #{name}!" }
275
+ end
276
+ end
179
277
 
180
- # Nota:
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
- [⬆️ Voltar para o índice](#índice-)
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
- Um `Micro::Case::Result` armazena os dados de output de um caso de uso. Esses são seus métodos:
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
- > **Nota:** por conta de retrocompatibilidade, você pode usar o método `#value` como um alias para o método `#data`.
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
- [⬆️ Voltar para o índice](#índice-)
208
-
209
- #### O que são os tipos de resultados?
288
+ ```ruby
289
+ require 'json'
290
+ require 'logger'
210
291
 
211
- Todo resultado tem um tipo (`#type`), e estes são os valores padrões:
212
- - `:ok` em casos de sucesso;
213
- - `:error` ou `:exception` em casos de falhas.
292
+ AppLogger = Logger.new(STDOUT)
214
293
 
215
- ```ruby
216
- class Divide < Micro::Case
217
- attributes :a, :b
294
+ class ParseJsonPayload < Micro::Case::Safe
295
+ attribute :payload
218
296
 
219
297
  def call!
220
- if invalid_attributes.empty?
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
- private def invalid_attributes
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
- # Resultado de sucesso
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 = Divide.call(a: 2, b: 2)
237
-
238
- result.type # :ok
239
- result.data # { number: 1 }
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
- # Resultado de falha (type == :error)
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
- bad_result = Divide.call(a: 2, b: '2')
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
- bad_result.type # :error
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
- # Resultado de falha (type == :exception)
327
+ ##### Flows seguros
253
328
 
254
- err_result = Divide.call(a: 2, b: 0)
329
+ Um flow seguro intercepta exceções em qualquer um de seus steps:
255
330
 
256
- err_result.type # :exception
257
- err_result.data # { exception: <ZeroDivisionError: divided by 0> }
258
- err_result.failure? # true
259
- err_result.use_case # #<Divide:0x0000 @__attributes={"a"=>2, "b"=>0}, @a=2, @b=0, @__result=#<Micro::Case::Result:0x0000 @use_case=#<Divide:0x0000 ...>, @type=:exception, @value=#<ZeroDivisionError: divided by 0>, @success=false>
331
+ ```ruby
332
+ module Users
333
+ Create = Micro::Cases.safe_flow([
334
+ ProcessParams,
335
+ ValidateParams,
336
+ Persist,
337
+ SendToCRM
338
+ ])
260
339
 
261
- # Nota:
262
- # ----
263
- # Toda instância de Exception será envolvida pelo método
264
- # Failure(result: *) que receberá o tipo `:exception` ao invés de `:error`.
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
- [⬆️ Voltar para o índice](#índice-)
268
-
269
- #### Como definir tipos customizados de resultados?
350
+ ##### `Result#on_exception`
270
351
 
271
- Resposta: Use um `Symbol` com argumento dos métodos `Success()`, `Failure()` e declare o `result:` keyword para definir os dados do resultado.
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 Multiply < Micro::Case
275
- attributes :a, :b
355
+ class ParseJsonPayload < Micro::Case::Safe
356
+ attribute :payload
276
357
 
277
358
  def call!
278
- if a.is_a?(Numeric) && b.is_a?(Numeric)
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
- # Resultado de sucesso
289
-
290
- result = Multiply.call(a: 3, b: 2)
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
- result.type # :ok
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
- # Resultado de falha
375
+ ##### Desabilitando o Safe
297
376
 
298
- bad_result = Multiply.call(a: 3, b: '2')
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
- bad_result.type # :invalid_data
301
- bad_result.data # { attributes: {"b"=>"2"} }
302
- bad_result.failure? # true
379
+ ```ruby
380
+ Micro::Case.config do |config|
381
+ config.disable_safe_features = true
382
+ end
303
383
  ```
304
384
 
305
- [⬆️ Voltar para o índice](#índice-)
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
- #### É possível definir um tipo sem definir os dados do resultado?
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
- Resposta: Sim, é possível. Mas isso terá um comportamento especial por conta dos dados do resultado ser um hash com o tipo definido como chave e `true` como o valor.
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 Multiply < Micro::Case
313
- attributes :a, :b
439
+ class MergeTags < Micro::Case
440
+ attributes :primary, :secondary
314
441
 
315
442
  def call!
316
- if a.is_a?(Numeric) && b.is_a?(Numeric)
317
- Success result: { number: a * b }
443
+ if primary.is_a?(Array) && secondary.is_a?(Array)
444
+ Success result: { tags: (primary + secondary).uniq }
318
445
  else
319
- Failure(:invalid_data)
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
- result = Multiply.call(a: 2, b: '2')
453
+ MergeTags.call(primary: %w[ruby], secondary: 'rails').type # => :invalid_input
454
+ ```
325
455
 
326
- result.failure? # true
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
- # Nota:
332
- # ----
333
- # Essa funcionalidade será muito útil para lidar com resultados de falha de um Flow
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
- [⬆️ Voltar para o índice](#índice-)
462
+ Success result: { tags: (primary + secondary).uniq }
463
+ end
464
+
465
+ # result.data => { invalid_input: true }
466
+ ```
338
467
 
339
- #### Como declarar um contrato de resultados?
468
+ #### Contratos de resultado
340
469
 
341
- Resposta: Utilize a macro `results do |on| ... end` para declarar quais tipos de resultado o caso de uso pode retornar e quais chaves cada um exige. Quando há um contrato declarado, chamadas a `Success(...)` / `Failure(...)` que usem um tipo não declarado levantam `Micro::Case::Error::UnexpectedResultType`, e chamadas que omitam uma chave obrigatória declarada levantam `Micro::Case::Error::MissingResultKeys`.
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 Divide < Micro::Case
345
- attributes :a, :b
473
+ class PublishPost < Micro::Case
474
+ attribute :post
346
475
 
347
476
  results do |on|
348
- on.failure(:attributes_must_be_numbers)
349
- on.failure(:division_by_zero)
477
+ on.failure(:already_published)
478
+ on.failure(:missing_content)
350
479
 
351
- on.success(result: [:division])
480
+ on.success(result: [:post])
352
481
  end
353
482
 
354
483
  def call!
355
- return Failure(:attributes_must_be_numbers) unless Kind.of?(Numeric, a, b)
356
- return Failure(:division_by_zero) if b == 0
484
+ return Failure(:already_published) if post.published?
485
+ return Failure(:missing_content) if post.body.to_s.strip.empty?
357
486
 
358
- Success result: { division: a / b }
487
+ post.update!(status: :published, published_at: Time.current)
488
+ Success result: { post: }
359
489
  end
360
490
  end
361
491
 
362
- Divide.call(a: 10, b: 2).data # => { division: 5 }
363
- Divide.call(a: 10, b: 0).type # => :division_by_zero
364
- Divide.call(a: 'x', b: 2).type # => :attributes_must_be_numbers
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 declarado em `on.success` / `on.failure` sem o argumento `result:` é aceito sem chaves obrigatórias (qualquer payload — inclusive o implícito `{ tipo => true }` de `Failure(:meu_tipo)` — é aceito). Quando `result: [:chave_1, :chave_2]` é informado, essas chaves precisam estar presentes no hash de resultado; chaves extras são permitidas.
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 Wrong < Micro::Case
500
+ class CreateComment < Micro::Case
371
501
  results do |on|
372
- on.success(result: [:value])
373
- on.failure(:known)
502
+ on.success(result: [:comment])
503
+ on.failure(:spam)
374
504
  end
375
505
 
376
506
  def call!
377
- Success(:other, result: { value: 1 }) # levanta Micro::Case::Error::UnexpectedResultType
378
- # Success(result: { wrong: 1 }) # levanta Micro::Case::Error::MissingResultKeys
379
- # Failure(:other) # levanta Micro::Case::Error::UnexpectedResultType
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
- Notas:
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
- #### Como utilizar os hooks dos resultados?
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
- Como [mencionando anteriormente](#microcaseresult---o-que-é-o-resultado-de-um-caso-de-uso), o `Micro::Case::Result` tem dois métodos para melhorar o controle do fluxo da aplicação. São eles:
394
- `#on_success`, `on_failure`.
522
+ #### Hooks de resultado
395
523
 
396
- Os exemplos abaixo os demonstram em uso:
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 Double < Micro::Case
400
- attribute :number
527
+ class ChangePassword < Micro::Case
528
+ attributes :user, :new_password
401
529
 
402
530
  def call!
403
- return Failure :invalid, result: { msg: 'number must be a numeric value' } unless number.is_a?(Numeric)
404
- return Failure :lte_zero, result: { msg: 'number must be greater than 0' } if number <= 0
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
- Success result: { number: number * 2 }
534
+ user.update_password!(new_password)
535
+ Success result: { user: }
407
536
  end
408
537
  end
409
538
 
410
- #================================#
411
- # Imprimindo o output se sucesso #
412
- #================================#
413
-
414
- Double
415
- .call(number: 3)
416
- .on_success { |result| p result[:number] }
417
- .on_failure(:invalid) { |result| raise TypeError, result[:msg] }
418
- .on_failure(:lte_zero) { |result| raise ArgumentError, result[:msg] }
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
- #### Por que o hook sem um tipo definido expõe o próprio resultado?
551
+ > O caso de uso responsável pelo resultado está sempre disponível como o segundo argumento do bloco do hook.
445
552
 
446
- Resposta: Para permitir que você defina o controle de fluxo da aplicação usando alguma estrutura condicional como um `if` ou `case when`.
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
- class Double < Micro::Case
450
- attribute :number
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 :invalid then raise TypeError, "number must be a numeric value"
465
- when :lte_zero then raise ArgumentError, "number `#{result[:number]}` must be greater than 0"
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
- > **Nota:** O mesmo que foi feito no exemplo anterior poderá ser feito com o hook `#on_success`!
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
- # O objeto exposto em hook sem um tipo é um Micro::Case::Result e ele pode ser decomposto. Exemplo:
570
+ calls = 0
571
+ result = ChangePassword.call(user: ada, new_password: 'long-enough-1')
484
572
 
485
- Double
486
- .call(number: -2)
487
- .on_failure do |(data, type), use_case|
488
- case type
489
- when :invalid then raise TypeError, 'number must be a numeric value'
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
- # O output será a exception:
496
- #
497
- # ArgumentError (the number `-2` must be greater than 0)
579
+ calls # => 4
498
580
  ```
499
581
 
500
- > **Nota:** O que mesmo pode ser feito com o `#on_success` hook!
582
+ #### Pattern matching
501
583
 
502
- [⬆️ Voltar para o índice](#índice-)
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
- #### O que acontece se um hook de resultado for declarado múltiplas vezes?
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
- Resposta: Se o tipo do resultado for identificado o hook será sempre executado.
597
+ Os hash patterns expõem essas chaves:
507
598
 
508
- ```ruby
509
- class Double < Micro::Case
510
- attributes :number
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
- def call!
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
- result = Double.call(number: 3)
522
- result.data # { number: 6 }
523
- result[:number] * 4 # 24
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
- accum = 0
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
- result
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
- accum # 24
626
+ Dentro de um hook sem tipo, o resultado também pode ser decomposto em array `[data, type]`:
534
627
 
535
- result[:number] * 4 == accum # true
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
- #### Como usar o método `Micro::Case::Result#then`?
640
+ #### Continuações dinâmicas com `Result#then`
539
641
 
540
- Este método permite você criar fluxos dinâmicos. Com ele, você pode adicionar novos casos de uso ou fluxos para continuar a transformação de um resultado. Exemplo:
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 ForbidNegativeNumber < Micro::Case
544
- attribute :number
645
+ class FindActiveUser < Micro::Case
646
+ attribute :email
545
647
 
546
648
  def call!
547
- return Success result: attributes if number >= 0
649
+ user = User.active.find_by(email:)
548
650
 
549
- Failure result: attributes
651
+ return Success result: { user: } if user
652
+
653
+ Failure result: { email: }
550
654
  end
551
655
  end
552
656
 
553
- class Add3 < Micro::Case
554
- attribute :number
657
+ class GenerateInviteToken < Micro::Case
658
+ attribute :user
555
659
 
556
660
  def call!
557
- Success result: { number: number + 3 }
661
+ Success result: { user:, token: SecureRandom.hex(16) }
558
662
  end
559
663
  end
560
664
 
561
- result1 =
562
- ForbidNegativeNumber
563
- .call(number: -1)
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
- > **Nota:** este método altera o [`Micro::Case::Result#transitions`](#como-entender-o-que-aconteceu-durante-a-execução-de-um-flow).
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 Add < Micro::Case
590
- attributes :a, :b
673
+ class FindUser < Micro::Case
674
+ attribute :email
591
675
 
592
676
  def call!
593
- if Kind.of?(Numeric, a, b)
594
- Success result: { sum: a + b }
595
- else
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
- [⬆️ Voltar para o índice](#índice-)
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 { |result| render_json(200, data: result[:todos]) }
694
+ .on_success { render_json(200, data: it[:todos]) }
632
695
  ```
633
696
 
634
- [⬆️ Voltar para o índice](#índice-)
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
- ##### Um exemplo mínimo
699
+ [⬆️ Voltar ao topo](#índice-)
666
700
 
667
- ```ruby
668
- class SumHalf < Micro::Case
669
- attribute :sum
701
+ ### Validando atributos
670
702
 
671
- def call!
672
- Success :third_sum, result: { sum: sum + 0.5 }
673
- end
674
- end
703
+ #### `accept:` e `reject:` (padrão)
675
704
 
676
- class DoSomeSum < Micro::Case
677
- attributes :a, :b
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
- validate_numbers
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
- private
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
- def sum_a_and_b
693
- Success :first_sum, result: { sum: a + b }
694
- end
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
- def add(sum:, number:, **)
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
- result = DoSomeSum.call(a: 1, b: 2)
732
+ #### Integração com ActiveModel (opt-in)
702
733
 
703
- result.success? # true
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
- `result.transitions` para a chamada acima:
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
- { use_case: { class: DoSomeSum, attributes: { a: 1, b: 2 } },
713
- success: { type: :valid, result: { valid: true } },
714
- accessible_attributes: [:a, :b] },
739
+ class CreatePost < Micro::Case
740
+ attributes :title, :body
715
741
 
716
- { use_case: { class: DoSomeSum, attributes: { a: 1, b: 2 } },
717
- success: { type: :first_sum, result: { sum: 3 } },
718
- accessible_attributes: [:a, :b, :valid] },
742
+ validates :title, :body, presence: true
743
+ validates :title, length: { maximum: 120 }
719
744
 
720
- { use_case: { class: DoSomeSum, attributes: { a: 1, b: 2 } },
721
- success: { type: :second_sum, result: { sum: 6 } },
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
- { use_case: { class: SumHalf, attributes: { sum: 6 } },
725
- success: { type: :third_sum, result: { sum: 6.5 } },
726
- accessible_attributes: [:a, :b, :valid, :number, :sum] }
727
- ]
748
+ Success result: { post: Post.create!(title:, body:) }
749
+ end
750
+ end
728
751
  ```
729
752
 
730
- Elos baseados em `Symbol`, `Method` e `lambda` rodam **como o caso de
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
- def call!
743
- validate_numbers | :sum_a_and_b | :add | SumHalf
744
- end
756
+ # Gemfile
757
+ gem 'u-case', require: 'u-case/with_activemodel_validation'
745
758
  ```
746
759
 
747
- Ambas as formas produzem `result.data` e `result.transitions`
748
- idênticos.
760
+ …ou habilite via [Configuração](#configuração). O exemplo então colapsa:
749
761
 
750
- > **Encadeamento estilo Elixir com `it` (Ruby ≥ 3.4):** como o Ruby
751
- > 3.4 expõe `it` como o primeiro parâmetro implícito do corpo de um
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
- ##### Formas lambda / `Method`
765
+ class CreatePost < Micro::Case
766
+ attributes :title, :body
770
767
 
771
- Lambdas (e objetos `Method` ligados) recebem os dados acumulados
772
- **posicionalmente** como um único `Hash`:
768
+ validates :title, :body, presence: true
769
+ validates :title, length: { maximum: 120 }
773
770
 
774
- ```ruby
775
- def call!
776
- validate_numbers
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
- ##### Uma falha interrompe a cadeia
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
- Retornar `Failure(...)` em qualquer elo interrompe o restante da
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
- DoSomeSum.call(a: 1, b: '2')
790
+ require 'u-case/with_activemodel_validation'
792
791
 
793
- # validate_numbers retorna Failure() → :sum_a_and_b, :add e SumHalf
794
- # nunca rodam. result.failure? == true, result.transitions tem 1
795
- # entrada.
796
- ```
792
+ class CountPosts < Micro::Case
793
+ disable_auto_validation
797
794
 
798
- ##### Usando um caso com steps internos dentro de um flow externo
795
+ attribute :user
796
+ validates :user, presence: true
799
797
 
800
- Um caso de uso que compõe internamente com `.then(...)` continua
801
- sendo apenas um caso de uso, portanto pode ser colocado em qualquer
802
- construtor de flow:
798
+ def call!
799
+ Success result: { count: user.posts.count }
800
+ end
801
+ end
803
802
 
804
- ```ruby
805
- SignUp = Micro::Cases.flow([
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
- As transições internas da classe hospedeira são intercaladas com as
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
- Por padrão isto é, quando nem a classe hospedeira nem o flow
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 CreateUserWithProfileInline < Micro::Case
828
- attributes :name, :info
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
- def create_user
838
- user = User.create(name: name)
839
- Success result: { user: user }
840
- end
815
+ validates :user, kind: User
816
+ validates :params, kind: ActionController::Parameters
841
817
 
842
- def create_profile(user:, **)
843
- profile = UserProfile.create(user_id: user.id, info: info)
844
- return Failure(:invalid_profile) if profile.errors.any?
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: { user: user, profile: profile }
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
- Se você precisar que os efeitos colaterais parciais sejam desfeitos,
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
- > **Nota:** Veja `test/micro/case/internal_steps/with_symbols_test.rb`,
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
- [⬆️ Voltar para o índice](#índice-)
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
- ### `Micro::Cases::Flow` - Como compor casos de uso?
835
+ #### Flows
874
836
 
875
- Chamamos de **fluxo** uma composição de casos de uso. A ideia principal desse recurso é usar/reutilizar casos de uso como etapas de um novo caso de uso. Exemplo:
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 ConvertTextToNumbers < Micro::Case
880
- attribute :numbers
841
+ class ParseTags < Micro::Case
842
+ attribute :tags
881
843
 
882
844
  def call!
883
- if numbers.all? { |value| String(value) =~ /\d+/ }
884
- Success result: { numbers: numbers.map(&:to_i) }
845
+ if tags.is_a?(String)
846
+ Success result: { tags: tags.split(',').map(&:strip) }
885
847
  else
886
- Failure result: { message: 'numbers must contain only numeric types' }
848
+ Failure result: { message: 'tags must be a comma-separated String' }
887
849
  end
888
850
  end
889
851
  end
890
852
 
891
- class Add2 < Micro::Case::Strict
892
- attribute :numbers
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 Double < Micro::Case::Strict
900
- attribute :numbers
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 Square < Micro::Case::Strict
908
- attribute :numbers
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
- # Criando um flow com Micro::Cases.flow() #
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
- result = Add2ToAllNumbers.call(numbers: %w[1 1 2 2 3 4])
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
- # Criando um flow usando classes #
932
- #--------------------------------#
933
-
934
- class DoubleAllNumbers < Micro::Case
935
- flow Steps::ConvertTextToNumbers,
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
- DoubleAllNumbers.
940
- call(numbers: %w[1 1 b 2 3 4]).
941
- on_failure { |result| puts result[:message] } # "numbers must contain only numeric types"
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
- Ao ocorrer uma falha, o caso de uso responsável ficará acessível no resultado. Exemplo:
892
+ Quando um flow falha, `Result#use_case` aponta para o step responsável:
945
893
 
946
894
  ```ruby
947
- result = DoubleAllNumbers.call(numbers: %w[1 1 b 2 3 4])
895
+ result = NormalizeTags.call(tags: 42)
896
+ result.failure? # => true
897
+ result.use_case.is_a?(Steps::ParseTags) # => true
898
+ ```
948
899
 
949
- result.failure? # true
950
- result.use_case.is_a?(Steps::ConvertTextToNumbers) # true
900
+ ##### Compondo flows entre si
951
901
 
952
- result.on_failure do |_message, use_case|
953
- puts "#{use_case.class.name} was the use case responsible for the failure" # Steps::ConvertTextToNumbers was the use case responsible for the failure
954
- end
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
- [⬆️ Voltar para o índice](#índice-)
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
- #### É possível compor um fluxo com outros fluxos?
917
+ ##### Acumulação de dados através de um flow
960
918
 
961
- Resposta: Sim, é possível.
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 Steps
965
- class ConvertTextToNumbers < Micro::Case
966
- attribute :numbers
922
+ module Users
923
+ class FindByEmail < Micro::Case
924
+ attribute :email
967
925
 
968
926
  def call!
969
- if numbers.all? { |value| String(value) =~ /\d+/ }
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
- class Add2 < Micro::Case::Strict
978
- attribute :numbers
929
+ return Success result: { user: } if user
979
930
 
980
- def call!
981
- Success result: { numbers: numbers.map { |number| number + 2 } }
931
+ Failure(:user_not_found)
982
932
  end
983
933
  end
984
934
 
985
- class Double < Micro::Case::Strict
986
- attribute :numbers
935
+ class ValidatePassword < Micro::Case::Strict
936
+ attributes :user, :password
987
937
 
988
938
  def call!
989
- Success result: { numbers: numbers.map { |number| number * 2 } }
990
- end
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
- def call!
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
- DoubleAllNumbers =
1003
- Micro::Cases.flow([Steps::ConvertTextToNumbers, Steps::Double])
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
- SquareAllNumbers =
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
- DoubleAllNumbersAndAdd2 =
1009
- Micro::Cases.flow([DoubleAllNumbers, Steps::Add2])
958
+ ##### Inspecionando a execução com `result.transitions`
1010
959
 
1011
- SquareAllNumbersAndAdd2 =
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
- SquareAllNumbersAndDouble =
1015
- Micro::Cases.flow([SquareAllNumbersAndAdd2, DoubleAllNumbers])
962
+ ```ruby
963
+ user_authenticated = Users::Authenticate.call(email: 'rodrigo@test.com', password: '...')
1016
964
 
1017
- DoubleAllNumbersAndSquareAndAdd2 =
1018
- Micro::Cases.flow([DoubleAllNumbers, SquareAllNumbersAndAdd2])
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
- SquareAllNumbersAndDouble
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
- DoubleAllNumbersAndSquareAndAdd2
1025
- .call(numbers: %w[1 1 2 2 3 4])
1026
- .on_success { |result| p result[:numbers] } # [6, 6, 18, 18, 38, 66]
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
- > **Nota:** Você pode mesclar qualquer [approach](#é-possível-compor-um-fluxo-com-outros-fluxos) para criar flows - [exemplos](https://github.com/serradura/u-case/blob/714c6b658fc6aa02617e6833ddee09eddc760f2a/test/micro/cases/flow/blend_test.rb#L5-L35).
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
- [⬆️ Voltar para o índice](#índice-)
1007
+ Para desabilitar transitions globalmente (economiza um hash por step), veja [Configuração](#configuração).
1032
1008
 
1033
- #### É possível que um fluxo acumule sua entrada e mescle cada resultado de sucesso para usar como argumento dos próximos casos de uso?
1009
+ ##### Compondo um flow que inclui a si mesmo
1034
1010
 
1035
- Resposta: Sim, é possível! Veja o exemplo abaixo para entender como funciona o acúmulo de dados dentro da execução de um fluxo.
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
- module Users
1039
- class FindByEmail < Micro::Case
1040
- attribute :email
1014
+ class ParseTagsString < Micro::Case
1015
+ attribute :input
1016
+ def call!; Success result: { tags: input.split(',').map(&:strip) }; end
1017
+ end
1041
1018
 
1042
- def call!
1043
- user = User.find_by(email: email)
1019
+ class JoinTagsArray < Micro::Case
1020
+ attribute :tags
1021
+ def call!; Success result: { input: tags.join(', ') }; end
1022
+ end
1044
1023
 
1045
- return Success result: { user: user } if user
1024
+ class CleanTags < Micro::Case
1025
+ flow ParseTagsString,
1026
+ self.call!,
1027
+ JoinTagsArray
1046
1028
 
1047
- Failure(:user_not_found)
1048
- end
1029
+ attribute :tags
1030
+
1031
+ def call!
1032
+ Success result: { tags: tags.map(&:downcase).uniq }
1049
1033
  end
1050
1034
  end
1051
1035
 
1052
- module Users
1053
- class ValidatePassword < Micro::Case::Strict
1054
- attributes :user, :password
1036
+ CleanTags.call(input: 'Ruby, RUBY, Rails').data[:input] # => "ruby, rails"
1037
+ ```
1055
1038
 
1056
- def call!
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
- return Success result: attributes(:user)
1061
- end
1062
- end
1063
- end
1041
+ #### Steps internos — cadeias com `Result#then`
1064
1042
 
1065
- module Users
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
- Users::Authenticate
1073
- .call(email: 'somebody@test.com', password: 'password')
1074
- .on_success { |result| sign_in(result[:user]) }
1075
- .on_failure(:wrong_password) { render status: 401 }
1076
- .on_failure(:user_not_found) { render status: 404 }
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
- Primeiro, vamos ver os atributos usados por cada caso de uso:
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 Users::FindByEmail < Micro::Case
1083
- attribute :email
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 Users::ValidatePassword < Micro::Case
1087
- attributes :user, :password
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
- Como você pode ver, `Users::ValidatePassword` espera um usuário como sua entrada. Então, como ele recebe o usuário?
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`. 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 está acessível upstream.
1093
1100
 
1094
- E este é o poder da composição de casos de uso porque o output de uma etapa irá compor a entrada do próximo caso de uso no fluxo!
1101
+ ##### Alias `|` (pipe)
1095
1102
 
1096
- > input **>>** processamento **>>** output
1103
+ `|` é açúcar sintático para `.then(...)`. O exemplo anterior fica:
1097
1104
 
1098
- > **Nota:** Verifique esses exemplos de teste [Micro::Cases::Flow](https://github.com/serradura/u-case/blob/c96a3650469da40dc9f83ff678204055b7015d01/test/micro/cases/flow/result_transitions_test.rb) e [Micro::Cases::Safe::Flow](https://github.com/serradura/u-case/blob/c96a3650469da40dc9f83ff678204055b7015d01/test/micro/cases/safe/flow/result_transitions_test.rb) para ver diferentes casos de uso tendo acesso aos dados de um fluxo.
1105
+ ```ruby
1106
+ def call!
1107
+ validate_input | :strip_title | :slugify | CapitalizeTitle
1108
+ end
1109
+ ```
1099
1110
 
1100
- [⬆️ Voltar para o índice](#índice-)
1111
+ As duas formas produzem o mesmo `result.data` e o mesmo `result.transitions`.
1101
1112
 
1102
- #### Como entender o que aconteceu durante a execução de um flow?
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
- Use `Micro::Case::Result#transitions`!
1125
+ ##### Formas Lambda / `Method`
1105
1126
 
1106
- Vamos usar os [exemplos da seção anterior](#is-it-possible-a-flow-accumulates-its-input-and-merges-each-success-result-to-use-as-the-argument-of-the-next-use-cases) para ilustrar como utilizar essa feature.
1127
+ Lambdas (e objetos `Method` bound) recebem os dados acumulados **posicionalmente** como um único Hash:
1107
1128
 
1108
1129
  ```ruby
1109
- user_authenticated =
1110
- Users::Authenticate.call(email: 'rodrigo@test.com', password: user_password)
1111
-
1112
- user_authenticated.transitions
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
- O exemplo acima mostra a saída gerada pelas `Micro::Case::Result#transitions`.
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
- E observe a propriedade `accessible_attributes`, ela mostra quais atributos são acessíveis nessa etapa do fluxo. Por exemplo, na última etapa, você pode ver que os atributos `accessible_attributes` aumentaram devido ao [acúmulo de fluxo de dados](#é-possível-que-um-fluxo-acumule-sua-entrada-e-mescle-cada-resultado-de-sucesso-para-usar-como-argumento-dos-próximos-casos-de-uso).
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
- > **Nota:** O [`Micro::Case::Result#then`](#how-to-use-the-microcaseresultthen-method) incrementa o `Micro::Case::Result#transitions`.
1142
+ ##### Usando um caso com steps internos dentro de um flow externo
1152
1143
 
1153
- ##### `Micro::Case::Result#transitions` schema
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
- ##### É possível desabilitar o `Micro::Case::Result#transitions`?
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
- Resposta: Sim! Você pode usar o `Micro::Case.config` para fazer isso. [Link para](#microcaseconfig) essa seção.
1156
+ ##### Persistência sem transação
1177
1157
 
1178
- #### É possível declarar um fluxo que inclui o próprio caso de uso?
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 ConvertTextToNumber < Micro::Case
1184
- attribute :text
1161
+ class CreateUserWithProfileInline < Micro::Case
1162
+ attributes :name, :info
1185
1163
 
1186
1164
  def call!
1187
- Success result: { number: text.to_i }
1165
+ create_user.then(:create_profile)
1188
1166
  end
1189
- end
1190
1167
 
1191
- class ConvertNumberToText < Micro::Case
1192
- attribute :number
1168
+ private
1193
1169
 
1194
- def call!
1195
- Success result: { text: number.to_s }
1170
+ def create_user
1171
+ user = User.create(name:)
1172
+ Success result: { user: }
1196
1173
  end
1197
- end
1198
1174
 
1199
- class Double < Micro::Case
1200
- flow ConvertTextToNumber,
1201
- self.call!,
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
- def call!
1207
- Success result: { number: number * 2 }
1179
+ Success result: { user:, profile: }
1208
1180
  end
1209
1181
  end
1210
1182
 
1211
- result = Double.call(text: '4')
1212
-
1213
- result.success? # true
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
- > **Note:** Essa funcionalidade pode ser usada com Micro::Case::Safe. Verifique esse teste para ver um example: https://github.com/serradura/u-case/blob/714c6b658fc6aa02617e6833ddee09eddc760f2a/test/micro/case/safe/with_inner_flow_test.rb
1188
+ Para reverter os writes parciais, envolva a cadeia em uma [transação](#transações).
1218
1189
 
1219
- [⬆️ Voltar para o índice](#índice-)
1190
+ #### Transações
1220
1191
 
1221
- #### Como executar um caso de uso ou flow dentro de uma transação de banco de dados?
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
- O `u-case` traz dois helpers complementares para envolver o trabalho em
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
- ##### `Micro::Case#transaction` transações inline dentro do `call!`
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 retornar uma falha (ou levantar uma exceção), todas as linhas
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 no macro de classe
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
- > **Compatibilidade retroativa:** a forma posicional pré-5.6.0
1275
- > `transaction(:activerecord) { ... }` continua funcionando como alias de
1276
- > `transaction { ... }`. Qualquer outro valor posicional levanta
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
- Um macro no nível de classe permite que um caso declare qual classe
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 porque é o que
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 hierarquia
1242
+ # sobrescreve a declaração herdada para este ramo da árvore
1300
1243
  end
1301
1244
  ```
1302
1245
 
1303
- ##### `Micro::Cases.flow(transaction: …, steps: [...])` — transações no nível do flow
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 o macro de nível de classe (se a classe hospedeira declarou um) ou
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 faz rollback em falhas E em exceções inesperadas
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-o em uma
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 sem que `ActiveRecord::Base` esteja
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 { … }` — padrão global
1289
+ ##### Padrão global — `config.default_transaction_class { … }`
1358
1290
 
1359
- Para aplicações Rails que usam um único abstract record
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 (block ou lambda) é invocado **a cada abertura** de transação
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
- 1. **Override no ponto de chamada.** `transaction: { with: X }` no
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
- Uma atribuição não-callable a `default_transaction_class=` levanta
1384
- `ArgumentError` no momento da configuração para que erros como
1385
- `config.default_transaction_class = 'ApplicationRecord'` falhem
1386
- imediatamente em vez de quebrar a primeira transação.
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
- Os [steps internos](#steps-internos--construindo-um-flow-inline-dentro-do-call)
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
- Existem duas formas naturais de dar rollback transacional a um flow
1399
- interno. Ambas reutilizam os helpers já documentados acima:
1314
+ Duas formas naturais de dar rollback:
1400
1315
 
1401
- **1. Envolver o caso hospedeiro em um flow com `transaction: true`.**
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: name)
1421
- Success result: { user: 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: 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: user, profile: profile }
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` (o elo `.then(:create_profile)` interno) retornar
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. Usar o helper inline `Micro::Case#transaction`** para escopar o
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
- Útil quando o caso hospedeiro é invocado isoladamente (não dentro de
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 apenas os
1484
- efeitos colaterais no banco. `result.data`, `result.type`,
1485
- `result.transitions` e `result.accessible_attributes` são idênticos
1486
- aos de um flow equivalente sem transação.
1487
- - **Instâncias de `Flow` são achatadas.** `Micro::Cases.flow([flow_interno,
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` 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
- ```ruby
1517
- class Double < Micro::Case::Strict
1518
- attribute :numbers
1372
+ [⬆️ Voltar ao topo](#índice-)
1519
1373
 
1520
- def call!
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
- Se você precisar lidar com um erro específico, recomendo o uso de uma instrução case. Exemplo:
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
- result.on_failure(:exception) do |data, use_case|
1568
- case exception = data[:exception]
1569
- when ZeroDivisionError then AppLogger.error(exception.message)
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
- [⬆️ Voltar para o índice](#índice-)
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
- #### `Micro::Cases::Safe::Flow`
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
- Como casos de uso seguros, os fluxos seguros podem interceptar uma exceção em qualquer uma de suas etapas. Estas são as maneiras de definir um:
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
- ```ruby
1585
- module Users
1586
- Create = Micro::Cases.safe_flow([
1587
- ProcessParams,
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
- Definindo dentro das classes:
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
- ```ruby
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
- [⬆️ Voltar para o índice](#índice-)
1417
+ ## Performance
1609
1418
 
1610
- #### `Micro::Case::Result#on_exception`
1419
+ Em benchmarks contra abstrações comparáveis, `Micro::Case` é o mais rápido depois do `Dry::Monads`:
1611
1420
 
1612
- Na programação funcional os erros/exceções são tratados como dados comuns, a ideia é transformar a saída mesmo quando ocorre um comportamento inesperado. Para muitos, [as exceções são muito semelhantes à instrução GOTO](https://softwareengineering.stackexchange.com/questions/189222/are-exceptions-as-control-flow-considered-a-serious-antipattern-if-so-why), pulando o fluxo do programa para caminhos que podem ser difíceis de descobrir como as coisas funcionam em um sistema.
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 resolver isso, o `Micro::Case::Result` tem um hook especial `#on_exception` para ajudá-lo a lidar com o fluxo de controle no caso de exceções.
1429
+ Para flows, o alias `|` pipe é o estilo de composição mais rápido:
1615
1430
 
1616
- > **Note**: essa funcionalidade funcionará melhor se for usada com um flow ou caso de uso `Micro::Case::Safe`.
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
- **Como ele funciona?**
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
- ```ruby
1621
- class Divide < Micro::Case::Safe
1622
- attributes :a, :b
1442
+ ### Executando os benchmarks
1623
1443
 
1624
- def call!
1625
- Success result: { division: a / b }
1626
- end
1627
- end
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
- Divide
1630
- .call(a: 2, b: 0)
1631
- .on_success { |result| puts result[:division] }
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
- Como você pode ver, este hook tem o mesmo comportamento de `result.on_failure(:exception)`, mas, a ideia aqui é ter uma melhor comunicação no código, fazendo uma referência explícita quando alguma falha acontecer por causa de uma exceção.
1655
-
1656
- [⬆️ Voltar para o índice](#índice-)
1454
+ Memory profiling:
1657
1455
 
1658
- #### Desabilitando o mecanismo "safe"
1659
-
1660
- O mecanismo "safe" é opinativo: ele converte qualquer exceção não tratada dentro de um caso de uso (ou em qualquer passo de um fluxo) em um resultado de falha com `type: :exception`. Isso é poderoso, mas também pode gerar uma **base de código fragmentada**, onde algumas exceções são tratadas com `rescue` dentro do `call!` e outras só são tratadas mais tarde via `on_exception` / `on_failure(:exception)` — tornando o fluxo de controle difícil de acompanhar.
1661
-
1662
- Se você prefere uma única convenção explícita para o tratamento de exceções — `rescue` padrão dentro dos seus casos de uso — é possível desabilitar as APIs "safe" por completo:
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
- Com isso ativo, os usos abaixo levantarão `Micro::Case::Error::SafeFeaturesDisabled`, garantindo que ninguém na base de código reintroduza o caminho "safe" sem querer:
1463
+ ### Desabilitando os checks em runtime
1671
1464
 
1672
- - Herdar de `Micro::Case::Safe`
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
- class CreateUser < Micro::Case
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
- O tipo da falha segue a mesma configuração usada pela integração com `ActiveModel` veja [`Micro::Case.config`](#microcaseconfig) e `set_activemodel_validation_errors_failure`.
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
- Quando combinado com [`u-case/with_activemodel_validation`](#u-casewith_activemodel_validation---como-validar-os-atributos-do-caso-de-uso), a ordem de execução é:
1473
+ ### Comparações
1710
1474
 
1711
- 1. O `u-attributes` resolve o valor padrão de cada atributo.
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
- [⬆️ Voltar para o índice](#índice-)
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
- ### `u-case/with_activemodel_validation` - Como validar os atributos do caso de uso?
1480
+ [⬆️ Voltar ao topo](#índice-)
1718
1481
 
1719
- **Requisitos:**
1482
+ ## Exemplos
1720
1483
 
1721
- Para fazer isso a sua aplicação deverá ter o [activemodel >= 3.2, < 6.1.0](https://rubygems.org/gems/activemodel) como dependência.
1484
+ ### Um flow completo de cadastro
1722
1485
 
1723
- Por padrão, se a sua aplicação tiver o ActiveModel como uma dependência, qualquer tipo de caso de uso pode fazer uso dele para validar seus atributos.
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 Multiply < Micro::Case
1727
- attributes :a, :b
1728
-
1729
- validates :a, :b, presence: true, numericality: true
1489
+ class NormalizeParams < Micro::Case
1490
+ attribute :params, accept: Hash
1730
1491
 
1731
- def call!
1732
- return Failure :invalid_attributes, result: { errors: self.errors } if invalid?
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
- Success result: { number: a * b }
1761
- end
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
- class Multiply < Micro::Case
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
- def call!
1782
- Success result: { number: a * b }
1503
+ Success result: { name:, email: }
1783
1504
  end
1784
1505
  end
1785
1506
 
1786
- Multiply.call(a: 2, b: 'a')
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
- validates :user, kind: User
1805
- validates :params, kind: ActionController::Parameters
1510
+ results do |on|
1511
+ on.success(result: [:user])
1512
+ on.failure(:invalid_user)
1513
+ end
1806
1514
 
1807
1515
  def call!
1808
- todo_params = params.require(:todo).permit(:title, :due_at)
1516
+ user = User.create(name:, email:)
1809
1517
 
1810
- todo = user.todos.create(todo_params)
1518
+ return Failure(:invalid_user, result: { errors: user.errors }) if user.errors.any?
1811
1519
 
1812
- Success result: { todo: todo }
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
- Você pode ver abaixo todas as configurações disponíveis com seus valores padrão:
1524
+ class CreateProfile < Micro::Case
1525
+ attributes :user
1827
1526
 
1828
- ```ruby
1829
- Micro::Case.config do |config|
1830
- # Use ActiveModel para auto-validar os atributos dos seus casos de uso.
1831
- config.enable_activemodel_validation = false
1527
+ results do |on|
1528
+ on.success(result: [:profile])
1529
+ on.failure(:invalid_profile)
1530
+ end
1832
1531
 
1833
- # Use para habilitar/desabilitar o `Micro::Case::Results#transitions`.
1834
- config.enable_transitions = true
1532
+ def call!
1533
+ profile = Profile.create(user_id: user.id)
1835
1534
 
1836
- # Use para proibir as funcionalidades "safe" e garantir uma única forma de tratar
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
- # Use para pular as verificações internas de argumento/contrato da gem (por
1845
- # exemplo, "isto é um Micro::Case?", "o tipo do resultado é um Symbol?",
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
- ## Benchmarks
1864
-
1865
- ### `Micro::Case`
1866
-
1867
- #### Success results
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
- ```ruby
1928
- # Warming up --------------------------------------
1929
- # Interactor 2.626k i/100ms
1930
- # Trailblazer::Operation 2.343k i/100ms
1931
- # Dry::Monads 13.386k i/100ms
1932
- # Dry::Transaction 868.000 i/100ms
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
- https://github.com/serradura/u-case/blob/main/benchmarks/perfomance/use_case/failure_results.
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
- ### `Micro::Cases::Flow`
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
- | Gem / Abstração | [Resultados de sucesso](https://github.com/serradura/u-case/blob/main/benchmarks/perfomance/flow/success_results.rb) | [Resultados de falha](https://github.com/serradura/u-case/blob/main/benchmarks/perfomance/flow/failure_results.rb) |
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
- \* As gems `Dry::Monads`, `Dry::Transaction`, `Trailblazer::Operation` estão fora desta análise por não terem esse tipo de funcionalidade.
1566
+ ## Indo além com `u-attributes`
1973
1567
 
1974
- <details>
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
- ```ruby
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
- <details>
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
- # Warming up --------------------------------------
2009
- # Interactor::Organizer 1.734k i/100ms
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
- https://github.com/serradura/u-case/blob/main/benchmarks/perfomance/flow/
2035
-
2036
- [⬆️ Voltar para o índice](#índice-)
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
- Clone este repositório e acesse a sua pasta, então execute os comandos abaixo:
1583
+ def call!
1584
+ Success result: { order: Order.create!(id:, customer_id: customer.id) }
1585
+ end
1586
+ end
2043
1587
 
2044
- **Casos de uso**
1588
+ CreateOrder
1589
+ .call(id: 42, customer: { name: 'Ada', email: 'ada@example.com' })
1590
+ .success? # => true
2045
1591
 
2046
- ```sh
2047
- ruby benchmarks/perfomance/use_case/failure_results.rb
2048
- ruby benchmarks/perfomance/use_case/success_results.rb
1592
+ CreateOrder
1593
+ .call(id: 42, customer: { name: 42, email: 'ada@example.com' })
1594
+ .type # => :invalid_attributes
2049
1595
  ```
2050
1596
 
2051
- **Flows**
1597
+ O hash aninhado é acessível como `customer.name`, `customer.email`.
2052
1598
 
2053
- ```sh
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
- #### Memory profiling
1601
+ `accept:` pode apontar para outra classe — hashes que chegam são automaticamente convertidos em instâncias dela:
2059
1602
 
2060
- **Casos de uso**
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
- ```sh
2063
- ./benchmarks/memory/use_case/success/with_transitions/analyze.sh
2064
- ./benchmarks/memory/use_case/success/without_transitions/analyze.sh
2065
- ```
1610
+ attribute :name, accept: String
1611
+ attribute :address, accept: Address
2066
1612
 
2067
- **Flows**
1613
+ def call!
1614
+ Success result: { profile: Profile.create!(name:, address: address.to_h) }
1615
+ end
1616
+ end
2068
1617
 
2069
- ```sh
2070
- ./benchmarks/memory/flow/success/with_transitions/analyze.sh
2071
- ./benchmarks/memory/flow/success/without_transitions/analyze.sh
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
- [⬆️ Voltar para o índice](#índice-)
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 para o índice](#índice-)
1627
+ [⬆️ Voltar ao topo](#índice-)
2110
1628
 
2111
1629
  ## Desenvolvimento
2112
1630
 
2113
- Após fazer o checking out do repo, execute `bin/setup` para instalar dependências. Então, execute `./test.sh` para executar os testes. Você pode executar `bin/console` para ter um prompt interativo que permitirá você experimenta-lá.
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 esta gem em sua máquina local, execute `bundle exec rake install`. Para lançar uma nova versão, atualize o número da versão em `version.rb` e execute` bundle exec rake release`, que criará uma tag git para a versão, enviará git commits e tags e enviará o arquivo `.gem`para [rubygems.org](https://rubygems.org).
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 push do `.gem` para o [rubygems.org](https://rubygems.org)).
2116
1634
 
2117
1635
  ## Contribuindo
2118
1636
 
2119
- Reportar bugs e solicitar 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 espera-se que os colaboradores sigam o código de conduta do [Covenant do Contribuidor](http://contributor-covenant.org).
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
- A gem está disponível como código aberto nos termos da [licença MIT](https://opensource.org/licenses/MIT).
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
- Espera-se que todos que interagem com o codebase do projeto `Micro::Case`, issue trackers, chat rooms and mailing lists sigam o [código de conduta](https://github.com/serradura/u-case/blob/main/CODE_OF_CONDUCT.md).
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).