u-case 5.7.0 → 5.7.1

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