u-case 5.4.0 → 5.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.pt-BR.md CHANGED
@@ -27,7 +27,7 @@ Principais objetivos deste projeto:
27
27
  Versão | Documentação
28
28
  --------- | -------------
29
29
  unreleased| https://github.com/serradura/u-case/blob/main/README.md
30
- 5.4.0 | https://github.com/serradura/u-case/blob/v5.x/README.md
30
+ 5.6.0 | https://github.com/serradura/u-case/blob/v5.x/README.md
31
31
  4.5.1 | https://github.com/serradura/u-case/blob/v4.x/README.md
32
32
 
33
33
  ## Índice <!-- omit in toc -->
@@ -40,6 +40,7 @@ unreleased| https://github.com/serradura/u-case/blob/main/README.md
40
40
  - [O que são os tipos de resultados?](#o-que-são-os-tipos-de-resultados)
41
41
  - [Como definir tipos customizados de resultados?](#como-definir-tipos-customizados-de-resultados)
42
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)
43
44
  - [Como utilizar os hooks dos resultados?](#como-utilizar-os-hooks-dos-resultados)
44
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)
45
46
  - [Usando decomposição para acessar os dados e tipo do resultado](#usando-decomposição-para-acessar-os-dados-e-tipo-do-resultado)
@@ -47,6 +48,7 @@ unreleased| https://github.com/serradura/u-case/blob/main/README.md
47
48
  - [Como usar o método `Micro::Case::Result#then`?](#como-usar-o-método-microcaseresultthen)
48
49
  - [O que acontece quando um `Micro::Case::Result#then` recebe um bloco?](#o-que-acontece-quando-um-microcaseresultthen-recebe-um-bloco)
49
50
  - [Como fazer injeção de dependência usando este recurso?](#como-fazer-injeção-de-dependência-usando-este-recurso)
51
+ - [Steps internos — construindo um flow inline dentro do `call!`](#steps-internos--construindo-um-flow-inline-dentro-do-call)
50
52
  - [`Micro::Cases::Flow` - Como compor casos de uso?](#microcasesflow---como-compor-casos-de-uso)
51
53
  - [É possível compor um fluxo com outros fluxos?](#é-possível-compor-um-fluxo-com-outros-fluxos)
52
54
  - [É possível que um fluxo acumule sua entrada e mescle cada resultado de sucesso para usar como argumento dos próximos casos de uso?](#é-possível-que-um-fluxo-acumule-sua-entrada-e-mescle-cada-resultado-de-sucesso-para-usar-como-argumento-dos-próximos-casos-de-uso)
@@ -54,6 +56,7 @@ unreleased| https://github.com/serradura/u-case/blob/main/README.md
54
56
  - [`Micro::Case::Result#transitions` schema](#microcaseresulttransitions-schema)
55
57
  - [É possível desabilitar o `Micro::Case::Result#transitions`?](#é-possível-desabilitar-o-microcaseresulttransitions)
56
58
  - [É possível declarar um fluxo que inclui o próprio caso de uso?](#é-possível-declarar-um-fluxo-que-inclui-o-próprio-caso-de-uso)
59
+ - [Como executar um caso de uso ou flow dentro de uma transação de banco de dados?](#como-executar-um-caso-de-uso-ou-flow-dentro-de-uma-transação-de-banco-de-dados)
57
60
  - [`Micro::Case::Strict` - O que é um caso de uso estrito?](#microcasestrict---o-que-é-um-caso-de-uso-estrito)
58
61
  - [`Micro::Case::Safe` - Existe algum recurso para lidar automaticamente com exceções dentro de um caso de uso ou fluxo?](#microcasesafe---existe-algum-recurso-para-lidar-automaticamente-com-exceções-dentro-de-um-caso-de-uso-ou-fluxo)
59
62
  - [`Micro::Cases::Safe::Flow`](#microcasessafeflow)
@@ -88,7 +91,7 @@ unreleased| https://github.com/serradura/u-case/blob/main/README.md
88
91
  | u-case | branch | ruby | activemodel | u-attributes |
89
92
  | ---------------- | ------ | -------- | -------------- | -------------- |
90
93
  | unreleased | main | >= 2.7 | >= 6.0 | >= 2.8, < 4.0 |
91
- | 5.4.0 | v5.x | >= 2.7 | >= 6.0 | >= 2.8, < 4.0 |
94
+ | 5.6.0 | v5.x | >= 2.7 | >= 6.0 | >= 2.8, < 4.0 |
92
95
  | 5.1.0 | v5.x | >= 2.7 | >= 6.0 | >= 2.7, < 4.0 |
93
96
  | 4.5.1 | v4.x | >= 2.2.0 | >= 3.2, <= 8.1 | >= 2.7, < 3.0 |
94
97
 
@@ -333,6 +336,58 @@ result.use_case.attributes # {"a"=>2, "b"=>"2"}
333
336
 
334
337
  [⬆️ Voltar para o índice](#índice-)
335
338
 
339
+ #### Como declarar um contrato de resultados?
340
+
341
+ Resposta: Utilize a macro `results do |on| ... end` para declarar quais tipos de resultado o caso de uso pode retornar e quais chaves cada um exige. Quando há um contrato declarado, chamadas a `Success(...)` / `Failure(...)` que usem um tipo não declarado levantam `Micro::Case::Error::UnexpectedResultType`, e chamadas que omitam uma chave obrigatória declarada levantam `Micro::Case::Error::MissingResultKeys`.
342
+
343
+ ```ruby
344
+ class Divide < Micro::Case
345
+ attributes :a, :b
346
+
347
+ results do |on|
348
+ on.failure(:attributes_must_be_numbers)
349
+ on.failure(:division_by_zero)
350
+
351
+ on.success(result: [:division])
352
+ end
353
+
354
+ def call!
355
+ return Failure(:attributes_must_be_numbers) unless Kind.of?(Numeric, a, b)
356
+ return Failure(:division_by_zero) if b == 0
357
+
358
+ Success result: { division: a / b }
359
+ end
360
+ end
361
+
362
+ Divide.call(a: 10, b: 2).data # => { division: 5 }
363
+ Divide.call(a: 10, b: 0).type # => :division_by_zero
364
+ Divide.call(a: 'x', b: 2).type # => :attributes_must_be_numbers
365
+ ```
366
+
367
+ Um tipo declarado em `on.success` / `on.failure` sem o argumento `result:` é aceito sem chaves obrigatórias (qualquer payload — inclusive o implícito `{ tipo => true }` de `Failure(:meu_tipo)` — é aceito). Quando `result: [:chave_1, :chave_2]` é informado, essas chaves precisam estar presentes no hash de resultado; chaves extras são permitidas.
368
+
369
+ ```ruby
370
+ class Wrong < Micro::Case
371
+ results do |on|
372
+ on.success(result: [:value])
373
+ on.failure(:known)
374
+ end
375
+
376
+ def call!
377
+ Success(:other, result: { value: 1 }) # levanta Micro::Case::Error::UnexpectedResultType
378
+ # Success(result: { wrong: 1 }) # levanta Micro::Case::Error::MissingResultKeys
379
+ # Failure(:other) # levanta Micro::Case::Error::UnexpectedResultType
380
+ end
381
+ end
382
+ ```
383
+
384
+ Notas:
385
+ - Casos de uso sem o bloco `results` mantêm o comportamento anterior sem restrições — o contrato é opt-in.
386
+ - Subclasses herdam o contrato declarado na classe pai.
387
+ - Exceções capturadas em `Micro::Case::Safe` (que geram `Failure(result: exception)` automaticamente) são exemptas do contrato.
388
+
389
+ [⬆️ Voltar para o índice](#índice-)
390
+
336
391
  #### Como utilizar os hooks dos resultados?
337
392
 
338
393
  Como [mencionando anteriormente](#microcaseresult---o-que-é-o-resultado-de-um-caso-de-uso), o `Micro::Case::Result` tem dois métodos para melhorar o controle do fluxo da aplicação. São eles:
@@ -578,6 +633,243 @@ Todo::FindAllForUser
578
633
 
579
634
  [⬆️ Voltar para o índice](#índice-)
580
635
 
636
+ #### Steps internos — construindo um flow inline dentro do `call!`
637
+
638
+ `Result#then` (e seu alias `|`) é a **terceira forma de compor um
639
+ flow** no u-case, lado a lado com `Micro::Cases.flow(...)` e a macro
640
+ de nível de classe `flow ...`. Em vez de ligar casos de uso entre si,
641
+ você mantém o encadeamento *dentro* do `call!` de um único caso de
642
+ uso: cada elo é um método, lambda ou outra classe de caso de uso;
643
+ cada elo retorna um `Micro::Case::Result`; os dados do `Success` de
644
+ cada elo viram os argumentos nomeados do próximo; e cada elo
645
+ contribui com uma linha em `result.transitions` — exatamente como um
646
+ step em um flow de nível superior.
647
+
648
+ ##### O que `Result#then` (e `|`) aceitam
649
+
650
+ | Formato | Exemplo |
651
+ | --- | --- |
652
+ | `Symbol` (nome de método) | `result.then(:sum_a_and_b)` |
653
+ | Objeto `Method` ligado | `result.then(method(:sum_a_and_b))` |
654
+ | `Lambda` / `Proc` | `result.then(-> data { sum_a_and_b(**data) })` |
655
+ | Classe de caso de uso | `result.then(SumHalf)` |
656
+ | `Symbol` + Hash de defaults | `result.then(:add, number: 3)` |
657
+ | Bloco | `result.then { \|r\| r.success? ? r[:sum] : 0 }` |
658
+
659
+ O método conectado **precisa** retornar um `Micro::Case::Result`.
660
+ Qualquer outro retorno levanta `Micro::Case::Error::UnexpectedResult`
661
+ — por exemplo um método que devolve um `Hash` será rejeitado com uma
662
+ mensagem do tipo `MeuCase#method(:foo) must return an instance of
663
+ Micro::Case::Result`.
664
+
665
+ ##### Um exemplo mínimo
666
+
667
+ ```ruby
668
+ class SumHalf < Micro::Case
669
+ attribute :sum
670
+
671
+ def call!
672
+ Success :third_sum, result: { sum: sum + 0.5 }
673
+ end
674
+ end
675
+
676
+ class DoSomeSum < Micro::Case
677
+ attributes :a, :b
678
+
679
+ def call!
680
+ validate_numbers
681
+ .then(:sum_a_and_b)
682
+ .then(:add, number: 3)
683
+ .then(SumHalf)
684
+ end
685
+
686
+ private
687
+
688
+ def validate_numbers
689
+ Kind.of?(Numeric, a, b) ? Success(:valid) : Failure()
690
+ end
691
+
692
+ def sum_a_and_b
693
+ Success :first_sum, result: { sum: a + b }
694
+ end
695
+
696
+ def add(sum:, number:, **)
697
+ Success :second_sum, result: { sum: sum + number }
698
+ end
699
+ end
700
+
701
+ result = DoSomeSum.call(a: 1, b: 2)
702
+
703
+ result.success? # true
704
+ result.data # { sum: 6.5 }
705
+ result.transitions # 4 entradas — veja abaixo
706
+ ```
707
+
708
+ `result.transitions` para a chamada acima:
709
+
710
+ ```ruby
711
+ [
712
+ { use_case: { class: DoSomeSum, attributes: { a: 1, b: 2 } },
713
+ success: { type: :valid, result: { valid: true } },
714
+ accessible_attributes: [:a, :b] },
715
+
716
+ { use_case: { class: DoSomeSum, attributes: { a: 1, b: 2 } },
717
+ success: { type: :first_sum, result: { sum: 3 } },
718
+ accessible_attributes: [:a, :b, :valid] },
719
+
720
+ { use_case: { class: DoSomeSum, attributes: { a: 1, b: 2 } },
721
+ success: { type: :second_sum, result: { sum: 6 } },
722
+ accessible_attributes: [:a, :b, :valid, :number, :sum] },
723
+
724
+ { use_case: { class: SumHalf, attributes: { sum: 6 } },
725
+ success: { type: :third_sum, result: { sum: 6.5 } },
726
+ accessible_attributes: [:a, :b, :valid, :number, :sum] }
727
+ ]
728
+ ```
729
+
730
+ Elos baseados em `Symbol`, `Method` e `lambda` rodam **como o caso de
731
+ uso hospedeiro**, portanto as três primeiras transições reportam
732
+ `class: DoSomeSum`. Apenas o elo `SumHalf`, que é outra classe de
733
+ caso de uso, contribui com uma transição com `use_case.class`
734
+ diferente. O `accessible_attributes` cresce conforme o `Success` de
735
+ cada elo é mesclado nos dados acumulados.
736
+
737
+ ##### O alias `|` (pipe)
738
+
739
+ `|` é açúcar para `.then(...)`. O exemplo anterior fica:
740
+
741
+ ```ruby
742
+ def call!
743
+ validate_numbers | :sum_a_and_b | :add | SumHalf
744
+ end
745
+ ```
746
+
747
+ Ambas as formas produzem `result.data` e `result.transitions`
748
+ idênticos.
749
+
750
+ > **Encadeamento estilo Elixir com `it` (Ruby ≥ 3.4):** como o Ruby
751
+ > 3.4 expõe `it` como o primeiro parâmetro implícito do corpo de um
752
+ > bloco/lambda, é possível escrever uma cadeia que se lê quase
753
+ > exatamente como o operador `|>` do Elixir. Cada lambda recebe o
754
+ > hash de dados acumulados como `it` e ainda precisa terminar em
755
+ > uma chamada `Success(...)` / `Failure(...)`:
756
+ >
757
+ > ```ruby
758
+ > def call!
759
+ > validate_something \
760
+ > | -> { do_something_with(**it) } \
761
+ > | -> { and_another_thing_with(**it) }
762
+ > end
763
+ > ```
764
+ >
765
+ > No Ruby 2.7 – 3.3 (onde `it` é apenas um identificador
766
+ > indefinido), use a forma explícita portátil
767
+ > `->(data) { do_something_with(**data) }` mostrada na próxima seção.
768
+
769
+ ##### Formas lambda / `Method`
770
+
771
+ Lambdas (e objetos `Method` ligados) recebem os dados acumulados
772
+ **posicionalmente** como um único `Hash`:
773
+
774
+ ```ruby
775
+ def call!
776
+ validate_numbers
777
+ .then(method(:sum_a_and_b))
778
+ .then(->(data) { add(**data, number: 3) })
779
+ .then(SumHalf)
780
+ end
781
+ ```
782
+
783
+ ##### Uma falha interrompe a cadeia
784
+
785
+ Retornar `Failure(...)` em qualquer elo interrompe o restante da
786
+ cadeia imediatamente — exatamente como um step de um flow de nível
787
+ superior retornando uma falha. Os demais elos `.then(...)` / `|` não
788
+ são invocados, e o `result` final é a falha:
789
+
790
+ ```ruby
791
+ DoSomeSum.call(a: 1, b: '2')
792
+
793
+ # validate_numbers retorna Failure() → :sum_a_and_b, :add e SumHalf
794
+ # nunca rodam. result.failure? == true, result.transitions tem 1
795
+ # entrada.
796
+ ```
797
+
798
+ ##### Usando um caso com steps internos dentro de um flow externo
799
+
800
+ Um caso de uso que compõe internamente com `.then(...)` continua
801
+ sendo apenas um caso de uso, portanto pode ser colocado em qualquer
802
+ construtor de flow:
803
+
804
+ ```ruby
805
+ SignUp = Micro::Cases.flow([
806
+ NormalizeParams,
807
+ DoSomeSum, # ← usa .then(:method) internamente
808
+ EnqueueIndexingJob
809
+ ])
810
+ ```
811
+
812
+ As transições internas da classe hospedeira são intercaladas com as
813
+ transições dos steps externos na ordem de execução. Se `DoSomeSum`
814
+ produz 4 transições internas e o flow externo tem 2 outros steps,
815
+ `result.transitions` final tem 6 entradas.
816
+
817
+ ##### Steps internos **sem** transações
818
+
819
+ Por padrão — isto é, quando nem a classe hospedeira nem o flow
820
+ externo usam `transaction: true` — os steps internos se comportam
821
+ como qualquer outro código em `call!`: efeitos colaterais feitos por
822
+ elos anteriores **persistem** mesmo se um elo posterior retornar
823
+ `Failure`. A cadeia é interrompida, mas tudo que já foi escrito no
824
+ banco permanece escrito:
825
+
826
+ ```ruby
827
+ class CreateUserWithProfileInline < Micro::Case
828
+ attributes :name, :info
829
+
830
+ def call!
831
+ create_user
832
+ .then(:create_profile)
833
+ end
834
+
835
+ private
836
+
837
+ def create_user
838
+ user = User.create(name: name)
839
+ Success result: { user: user }
840
+ end
841
+
842
+ def create_profile(user:, **)
843
+ profile = UserProfile.create(user_id: user.id, info: info)
844
+ return Failure(:invalid_profile) if profile.errors.any?
845
+
846
+ Success result: { user: user, profile: profile }
847
+ end
848
+ end
849
+
850
+ CreateUserWithProfileInline.call(name: 'Rodrigo', info: '')
851
+ # create_user já INSERIU a linha do user; create_profile falhou.
852
+ # user está persistido; profile não. Não há rollback automático.
853
+ ```
854
+
855
+ Se você precisar que os efeitos colaterais parciais sejam desfeitos,
856
+ envolva a cadeia em uma transação. Como steps internos são apenas
857
+ outra forma de expressar um flow (um flow *interno*), a história
858
+ transacional é exatamente a que já está documentada em
859
+ [Como executar um caso de uso ou flow dentro de uma transação de banco de dados?](#como-executar-um-caso-de-uso-ou-flow-dentro-de-uma-transação-de-banco-de-dados)
860
+ abaixo — a subseção "Flows com steps internos sob transações" lá
861
+ percorre tanto a forma inline `transaction { ... }` quanto a forma
862
+ com `transaction: true` para um caso hospedeiro de steps internos.
863
+
864
+ > **Nota:** Veja `test/micro/case/internal_steps/with_symbols_test.rb`,
865
+ > `with_methods_test.rb` e `with_lambdas_test.rb` para exemplos
866
+ > completos de cada forma, e
867
+ > `test/micro/cases/flow/internal_steps_in_flows_test.rb` para a
868
+ > interação com flows e transações (acumulação, transições e
869
+ > rollback em todos os níveis de aninhamento).
870
+
871
+ [⬆️ Voltar para o índice](#índice-)
872
+
581
873
  ### `Micro::Cases::Flow` - Como compor casos de uso?
582
874
 
583
875
  Chamamos de **fluxo** uma composição de casos de uso. A ideia principal desse recurso é usar/reutilizar casos de uso como etapas de um novo caso de uso. Exemplo:
@@ -926,6 +1218,297 @@ result[:number] # "8"
926
1218
 
927
1219
  [⬆️ Voltar para o índice](#índice-)
928
1220
 
1221
+ #### Como executar um caso de uso ou flow dentro de uma transação de banco de dados?
1222
+
1223
+ O `u-case` traz dois helpers complementares para envolver o trabalho em
1224
+ um `ActiveRecord::Base.transaction`. Ambos são opt-in — a gem **não**
1225
+ requer `active_record` automaticamente, então você precisa carregar o
1226
+ ActiveRecord por conta própria (aplicações Rails já o fazem).
1227
+
1228
+ ##### `Micro::Case#transaction` — transações inline dentro do `call!`
1229
+
1230
+ `Micro::Case#transaction` (e `Micro::Case::Safe#transaction`) é um helper
1231
+ privado de instância que envolve um bloco em uma transação de banco e
1232
+ dispara um `ActiveRecord::Rollback` sempre que o resultado do bloco for
1233
+ um `Failure`. O resultado original é devolvido nos dois casos, permitindo
1234
+ continuar encadeando com `Result#then`:
1235
+
1236
+ ```ruby
1237
+ class CreateUserWithAProfile < Micro::Case
1238
+ def call!
1239
+ transaction {
1240
+ call(CreateUser).then(CreateUserProfile)
1241
+ }
1242
+ end
1243
+ end
1244
+ ```
1245
+
1246
+ Se o bloco retornar uma falha (ou levantar uma exceção), todas as linhas
1247
+ gravadas dentro do bloco serão revertidas. O helper aceita um kwarg
1248
+ opcional `with:` para escolher a classe ActiveRecord sobre a qual
1249
+ `.transaction` é aberta — útil em aplicações Rails com múltiplos bancos
1250
+ (`ApplicationRecord`, `AnalyticsRecord`, `BillingRecord`, …):
1251
+
1252
+ ```ruby
1253
+ class CreateAuditEntry < Micro::Case
1254
+ def call!
1255
+ transaction(with: AnalyticsRecord) {
1256
+ call(WriteAuditLog).then(BumpCounter)
1257
+ }
1258
+ end
1259
+ end
1260
+ ```
1261
+
1262
+ Quando `with:` é omitido, o helper cai no macro de classe
1263
+ (`transaction with: …`) e depois no callback global padrão (veja abaixo),
1264
+ que vem com `-> { ::ActiveRecord::Base }`.
1265
+
1266
+ > **Nota:** qualquer classe passada via `with:` (aqui, no macro de classe ou
1267
+ > no kwarg `transaction:` de um flow) **precisa ser uma subclasse de
1268
+ > `ActiveRecord::Base`**. Classes não-AR são rejeitadas com `ArgumentError`.
1269
+ > A validação do macro de classe roda em tempo de class-eval quando o
1270
+ > ActiveRecord já está carregado (caso típico de apps Rails); caso
1271
+ > contrário, é adiada para runtime, então a ordem de carregamento de
1272
+ > initializers não quebra declarações.
1273
+
1274
+ > **Compatibilidade retroativa:** a forma posicional pré-5.6.0
1275
+ > `transaction(:activerecord) { ... }` continua funcionando como alias de
1276
+ > `transaction { ... }`. Qualquer outro valor posicional levanta
1277
+ > `ArgumentError` — o helper antigo aceitava apenas `:activerecord`.
1278
+
1279
+ ##### `transaction with: …` — declarando o padrão para um caso
1280
+
1281
+ Um macro no nível de classe permite que um caso declare qual classe
1282
+ ActiveRecord deve ser dona de suas transações, para que nem o helper
1283
+ inline nem qualquer flow que envolva o caso precise especificá-la em cada
1284
+ ponto de chamada. A declaração é herdada por subclasses:
1285
+
1286
+ ```ruby
1287
+ class ApplicationUseCase < Micro::Case
1288
+ transaction with: ApplicationRecord
1289
+ end
1290
+
1291
+ class CreateUserWithAProfile < ApplicationUseCase
1292
+ flow(transaction: true, steps: [CreateUser, CreateUserProfile])
1293
+ # transaction: true resolve para ApplicationRecord porque é o que
1294
+ # a classe hospedeira declarou via `transaction with:`.
1295
+ end
1296
+
1297
+ class BillingCase < ApplicationUseCase
1298
+ transaction with: BillingRecord
1299
+ # sobrescreve a declaração herdada para este ramo da hierarquia
1300
+ end
1301
+ ```
1302
+
1303
+ ##### `Micro::Cases.flow(transaction: …, steps: [...])` — transações no nível do flow
1304
+
1305
+ Passe `transaction:` junto com `steps:` para envolver um flow inteiro em
1306
+ uma única transação. Se qualquer step retornar uma falha (ou levantar uma
1307
+ exceção, no caso de `safe_flow`), todas as escritas realizadas no banco
1308
+ durante o flow serão revertidas. O kwarg aceita três formas:
1309
+
1310
+ ```ruby
1311
+ # Usa o macro de nível de classe (se a classe hospedeira declarou um) ou
1312
+ # o padrão global (`ActiveRecord::Base` salvo configuração).
1313
+ Micro::Cases.flow(transaction: true, steps: [CreateUser, CreateUserProfile])
1314
+
1315
+ # Escolhe uma classe ActiveRecord explícita só para este flow — mesmo
1316
+ # vocabulário `with:` usado pelo helper inline e pelo macro de classe.
1317
+ Micro::Cases.flow(transaction: { with: AnalyticsRecord }, steps: [
1318
+ WriteAuditLog,
1319
+ BumpCounter
1320
+ ])
1321
+
1322
+ # safe_flow faz rollback em falhas E em exceções inesperadas
1323
+ Micro::Cases.safe_flow(transaction: { with: ApplicationRecord }, steps: [
1324
+ CreateUser,
1325
+ CreateUserProfile
1326
+ ])
1327
+
1328
+ # Forma a nível de classe
1329
+ class CreateUserWithAProfile < Micro::Case
1330
+ flow(transaction: true, steps: [CreateUser, CreateUserProfile])
1331
+ end
1332
+ ```
1333
+
1334
+ Para aninhar um flow transacional dentro de outro flow, envolva-o em uma
1335
+ classe de caso de uso — `Micro::Cases.flow([...])` achata instâncias de
1336
+ `Flow` passadas como steps, mas **não** achata classes:
1337
+
1338
+ ```ruby
1339
+ class CreateUserAndProfile < Micro::Case
1340
+ flow(transaction: true, steps: [CreateUser, CreateUserProfile])
1341
+ end
1342
+
1343
+ SignUpFlow = Micro::Cases.flow([
1344
+ NormalizeParams,
1345
+ ValidatePassword,
1346
+ CreateUserAndProfile,
1347
+ EnqueueIndexingJob
1348
+ ])
1349
+ ```
1350
+
1351
+ Se `transaction: true` for usado sem que `ActiveRecord::Base` esteja
1352
+ carregado, o flow levantará `Micro::Cases::Error::TransactionAdapterMissing`
1353
+ na primeira chamada, sinalizando a configuração incorreta imediatamente.
1354
+ Passar `transaction: { with: SomeClass }` pula essa verificação —
1355
+ `SomeClass` é considerada confiável e basta responder a `.transaction`.
1356
+
1357
+ ##### `config.default_transaction_class { … }` — padrão global
1358
+
1359
+ Para aplicações Rails que usam um único abstract record
1360
+ (`ApplicationRecord`), configure-o uma vez em um initializer em vez de
1361
+ declará-lo em cada caso ou flow:
1362
+
1363
+ ```ruby
1364
+ # config/initializers/u_case.rb
1365
+ Micro::Case.config do |config|
1366
+ config.default_transaction_class { ApplicationRecord }
1367
+ end
1368
+ ```
1369
+
1370
+ O callback (block ou lambda) é invocado **a cada abertura** de transação
1371
+ — sem memoização — então é seguro fazer o valor de retorno depender de
1372
+ estado em tempo de execução (roteamento por tenant, etc.). O padrão é
1373
+ `-> { ::ActiveRecord::Base }`. Ordem de resolução quando uma transação
1374
+ abre:
1375
+
1376
+ 1. **Override no ponto de chamada.** `transaction: { with: X }` no
1377
+ kwarg do flow, ou `transaction(with: X) { ... }` no helper inline.
1378
+ 2. **Macro `transaction with: X` da classe hospedeira** (sobe pela
1379
+ hierarquia).
1380
+ 3. **`Micro::Case.config.default_transaction_class.call`** — o callback
1381
+ global (padrão `ActiveRecord::Base`).
1382
+
1383
+ Uma atribuição não-callable a `default_transaction_class=` levanta
1384
+ `ArgumentError` no momento da configuração para que erros como
1385
+ `config.default_transaction_class = 'ApplicationRecord'` falhem
1386
+ imediatamente em vez de quebrar a primeira transação.
1387
+
1388
+ ##### Flows com steps internos sob transações
1389
+
1390
+ Os [steps internos](#steps-internos--construindo-um-flow-inline-dentro-do-call)
1391
+ (a forma `Result#then(:symbol)` / `|` construída inline dentro de um
1392
+ único `call!`) são a terceira forma do u-case de compor um flow —
1393
+ um flow *interno*. Por padrão, um flow interno **não tem rollback
1394
+ transacional**: efeitos colaterais de elos `.then(:método)`
1395
+ anteriores persistem mesmo quando um elo posterior retorna
1396
+ `Failure`.
1397
+
1398
+ Existem duas formas naturais de dar rollback transacional a um flow
1399
+ interno. Ambas reutilizam os helpers já documentados acima:
1400
+
1401
+ **1. Envolver o caso hospedeiro em um flow com `transaction: true`.**
1402
+ Esta é a forma recomendada assim que o caso hospedeiro é composto
1403
+ com o resto do pipeline. A transação cobre a chamada inteira do flow,
1404
+ então um `Failure` *em qualquer ponto* — incluindo de qualquer elo
1405
+ `.then(:método)` interno — reverte todas as escritas de banco feitas
1406
+ durante a chamada:
1407
+
1408
+ ```ruby
1409
+ class CreateUserWithProfileInline < Micro::Case
1410
+ attributes :name, :info
1411
+
1412
+ def call!
1413
+ create_user
1414
+ .then(:create_profile)
1415
+ end
1416
+
1417
+ private
1418
+
1419
+ def create_user
1420
+ user = User.create(name: name)
1421
+ Success result: { user: user }
1422
+ end
1423
+
1424
+ def create_profile(user:, **)
1425
+ profile = UserProfile.create(user_id: user.id, info: info)
1426
+ return Failure(:invalid_profile) if profile.errors.any?
1427
+
1428
+ Success result: { user: user, profile: profile }
1429
+ end
1430
+ end
1431
+
1432
+ SignUp = Micro::Cases.flow(transaction: true, steps: [
1433
+ NormalizeParams,
1434
+ CreateUserWithProfileInline, # ← falha interna agora reverte
1435
+ EnqueueIndexingJob
1436
+ ])
1437
+
1438
+ # Ou no nível de classe:
1439
+ class SignUp < Micro::Case
1440
+ flow(transaction: true, steps: [
1441
+ NormalizeParams,
1442
+ CreateUserWithProfileInline,
1443
+ EnqueueIndexingJob
1444
+ ])
1445
+ end
1446
+ ```
1447
+
1448
+ Se `create_profile` (o elo `.then(:create_profile)` interno) retornar
1449
+ `Failure(:invalid_profile)`, a linha de `User` inserida antes por
1450
+ `create_user` é revertida como parte da mesma
1451
+ `ActiveRecord::Base.transaction`. O resultado ainda expõe o tipo da
1452
+ falha e as transições parciais, mas nenhuma linha permanece no banco.
1453
+
1454
+ **2. Usar o helper inline `Micro::Case#transaction`** para escopar o
1455
+ rollback a um único `call!` sem envolver um flow externo:
1456
+
1457
+ ```ruby
1458
+ class CreateUserWithProfileInline < Micro::Case
1459
+ def call!
1460
+ transaction {
1461
+ create_user
1462
+ .then(:create_profile)
1463
+ }
1464
+ end
1465
+ end
1466
+ ```
1467
+
1468
+ Útil quando o caso hospedeiro é invocado isoladamente (não dentro de
1469
+ um flow) e você ainda quer que o flow interno seja atômico. O bloco
1470
+ `transaction` retorna o `Result` da cadeia como está, então você pode
1471
+ continuar compondo com `Result#then` depois dele.
1472
+
1473
+ As duas abordagens **se compõem**. Se você colocar
1474
+ `CreateUserWithProfileInline` (que já usa `transaction { ... }`
1475
+ inline) dentro de um flow externo com `transaction: true`, o
1476
+ ActiveRecord junta a transação interna à externa por padrão — uma
1477
+ falha externa reverte também as escritas internas. Veja as
1478
+ **Observações de comportamento** abaixo para as regras completas de
1479
+ aninhamento / achatamento.
1480
+
1481
+ ##### Observações de comportamento
1482
+
1483
+ - **O resultado não é afetado.** `transaction: true` afeta apenas os
1484
+ efeitos colaterais no banco. `result.data`, `result.type`,
1485
+ `result.transitions` e `result.accessible_attributes` são idênticos
1486
+ aos de um flow equivalente sem transação.
1487
+ - **Instâncias de `Flow` são achatadas.** `Micro::Cases.flow([flow_interno,
1488
+ Outro])` achata `flow_interno` em seus steps internos, o que faz com
1489
+ que uma instância de `Flow` transacional passada dessa forma **perca
1490
+ sua transação**. Envolva flows transacionais reutilizáveis em uma
1491
+ classe de caso de uso (como no snippet acima) para preservar a
1492
+ transação ao aninhar.
1493
+ - **Transações aninhadas se unem à transação externa.** Quando um flow
1494
+ transacional é aninhado dentro de outro flow transacional, o
1495
+ ActiveRecord as une por padrão (sem `requires_new: true`). Uma falha
1496
+ em qualquer ponto da cadeia reverte **tudo** que foi escrito dentro
1497
+ da transação mais externa — incluindo escritas feitas pelo flow
1498
+ interno.
1499
+ - **Um externo não-transacional comita o interno.** Se o flow externo
1500
+ não for transacional e o flow transacional interno tiver sucesso, as
1501
+ escritas do interno são comitadas ao final daquele step. Uma falha
1502
+ em um step posterior (não-transacional) **não** desfaz essas
1503
+ escritas.
1504
+ - **`Micro::Cases.flow(transaction: true, ...)` simples re-lança
1505
+ exceções.** A transação ainda é revertida, mas o chamador precisa
1506
+ fazer rescue. Use `Micro::Cases.safe_flow(transaction: true, ...)`
1507
+ (ou a forma de classe com `Micro::Case::Safe`) para capturar a
1508
+ exceção como uma falha do tipo `:exception`.
1509
+
1510
+ [⬆️ Voltar para o índice](#índice-)
1511
+
929
1512
  ### `Micro::Case::Strict` - O que é um caso de uso estrito?
930
1513
 
931
1514
  Resposta: é um tipo de caso de uso que exigirá todas as palavras-chave (atributos) em sua inicialização.