u-case 5.5.0 → 5.7.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.5.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 -->
@@ -44,10 +44,12 @@ unreleased| https://github.com/serradura/u-case/blob/main/README.md
44
44
  - [Como utilizar os hooks dos resultados?](#como-utilizar-os-hooks-dos-resultados)
45
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
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)
47
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)
48
49
  - [Como usar o método `Micro::Case::Result#then`?](#como-usar-o-método-microcaseresultthen)
49
50
  - [O que acontece quando um `Micro::Case::Result#then` recebe um bloco?](#o-que-acontece-quando-um-microcaseresultthen-recebe-um-bloco)
50
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)
51
53
  - [`Micro::Cases::Flow` - Como compor casos de uso?](#microcasesflow---como-compor-casos-de-uso)
52
54
  - [É possível compor um fluxo com outros fluxos?](#é-possível-compor-um-fluxo-com-outros-fluxos)
53
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)
@@ -55,6 +57,7 @@ unreleased| https://github.com/serradura/u-case/blob/main/README.md
55
57
  - [`Micro::Case::Result#transitions` schema](#microcaseresulttransitions-schema)
56
58
  - [É possível desabilitar o `Micro::Case::Result#transitions`?](#é-possível-desabilitar-o-microcaseresulttransitions)
57
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)
58
61
  - [`Micro::Case::Strict` - O que é um caso de uso estrito?](#microcasestrict---o-que-é-um-caso-de-uso-estrito)
59
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)
60
63
  - [`Micro::Cases::Safe::Flow`](#microcasessafeflow)
@@ -89,7 +92,7 @@ unreleased| https://github.com/serradura/u-case/blob/main/README.md
89
92
  | u-case | branch | ruby | activemodel | u-attributes |
90
93
  | ---------------- | ------ | -------- | -------------- | -------------- |
91
94
  | unreleased | main | >= 2.7 | >= 6.0 | >= 2.8, < 4.0 |
92
- | 5.5.0 | v5.x | >= 2.7 | >= 6.0 | >= 2.8, < 4.0 |
95
+ | 5.6.0 | v5.x | >= 2.7 | >= 6.0 | >= 2.8, < 4.0 |
93
96
  | 5.1.0 | v5.x | >= 2.7 | >= 6.0 | >= 2.7, < 4.0 |
94
97
  | 4.5.1 | v4.x | >= 2.2.0 | >= 3.2, <= 8.1 | >= 2.7, < 3.0 |
95
98
 
@@ -499,6 +502,54 @@ Double
499
502
 
500
503
  [⬆️ Voltar para o índice](#índice-)
501
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`).
508
+
509
+ ```ruby
510
+ result = Divide.call(a: 10, b: 2)
511
+
512
+ case result
513
+ in { success: _, data: { number: Numeric => number } }
514
+ puts "deu #{number}"
515
+ in { failure: :invalid_attributes, data: { invalid_attributes: errors } }
516
+ warn "entrada inválida: #{errors.keys.join(", ")}"
517
+ in { failure: :exception, data: { exception: } }
518
+ warn "boom: #{exception.message}"
519
+ end
520
+ ```
521
+
522
+ Os hash patterns expõem essas chaves:
523
+
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.
535
+
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:
537
+
538
+ ```ruby
539
+ case result
540
+ in [:success, :ok, { number: Integer => n }]
541
+ n
542
+ in [:failure, :invalid_attributes, { invalid_attributes: errors }]
543
+ # ...
544
+ in [:failure, :exception, { exception: }]
545
+ # ...
546
+ end
547
+ ```
548
+
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.
550
+
551
+ [⬆️ Voltar para o índice](#índice-)
552
+
502
553
  #### O que acontece se um hook de resultado for declarado múltiplas vezes?
503
554
 
504
555
  Resposta: Se o tipo do resultado for identificado o hook será sempre executado.
@@ -631,6 +682,243 @@ Todo::FindAllForUser
631
682
 
632
683
  [⬆️ Voltar para o índice](#índice-)
633
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`.
713
+
714
+ ##### Um exemplo mínimo
715
+
716
+ ```ruby
717
+ class SumHalf < Micro::Case
718
+ attribute :sum
719
+
720
+ def call!
721
+ Success :third_sum, result: { sum: sum + 0.5 }
722
+ end
723
+ end
724
+
725
+ class DoSomeSum < Micro::Case
726
+ attributes :a, :b
727
+
728
+ def call!
729
+ validate_numbers
730
+ .then(:sum_a_and_b)
731
+ .then(:add, number: 3)
732
+ .then(SumHalf)
733
+ end
734
+
735
+ private
736
+
737
+ def validate_numbers
738
+ Kind.of?(Numeric, a, b) ? Success(:valid) : Failure()
739
+ end
740
+
741
+ def sum_a_and_b
742
+ Success :first_sum, result: { sum: a + b }
743
+ end
744
+
745
+ def add(sum:, number:, **)
746
+ Success :second_sum, result: { sum: sum + number }
747
+ end
748
+ end
749
+
750
+ result = DoSomeSum.call(a: 1, b: 2)
751
+
752
+ result.success? # true
753
+ result.data # { sum: 6.5 }
754
+ result.transitions # 4 entradas — veja abaixo
755
+ ```
756
+
757
+ `result.transitions` para a chamada acima:
758
+
759
+ ```ruby
760
+ [
761
+ { use_case: { class: DoSomeSum, attributes: { a: 1, b: 2 } },
762
+ success: { type: :valid, result: { valid: true } },
763
+ accessible_attributes: [:a, :b] },
764
+
765
+ { use_case: { class: DoSomeSum, attributes: { a: 1, b: 2 } },
766
+ success: { type: :first_sum, result: { sum: 3 } },
767
+ accessible_attributes: [:a, :b, :valid] },
768
+
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] },
772
+
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
+ ]
777
+ ```
778
+
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:
789
+
790
+ ```ruby
791
+ def call!
792
+ validate_numbers | :sum_a_and_b | :add | SumHalf
793
+ end
794
+ ```
795
+
796
+ Ambas as formas produzem `result.data` e `result.transitions`
797
+ idênticos.
798
+
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.
817
+
818
+ ##### Formas lambda / `Method`
819
+
820
+ Lambdas (e objetos `Method` ligados) recebem os dados acumulados
821
+ **posicionalmente** como um único `Hash`:
822
+
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)
829
+ end
830
+ ```
831
+
832
+ ##### Uma falha interrompe a cadeia
833
+
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:
838
+
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
+ ```
846
+
847
+ ##### Usando um caso com steps internos dentro de um flow externo
848
+
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:
852
+
853
+ ```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
867
+
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:
874
+
875
+ ```ruby
876
+ class CreateUserWithProfileInline < Micro::Case
877
+ attributes :name, :info
878
+
879
+ def call!
880
+ create_user
881
+ .then(:create_profile)
882
+ end
883
+
884
+ private
885
+
886
+ def create_user
887
+ user = User.create(name: name)
888
+ Success result: { user: user }
889
+ end
890
+
891
+ def create_profile(user:, **)
892
+ profile = UserProfile.create(user_id: user.id, info: info)
893
+ return Failure(:invalid_profile) if profile.errors.any?
894
+
895
+ Success result: { user: user, profile: profile }
896
+ end
897
+ 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
+ ```
903
+
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.
912
+
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).
919
+
920
+ [⬆️ Voltar para o índice](#índice-)
921
+
634
922
  ### `Micro::Cases::Flow` - Como compor casos de uso?
635
923
 
636
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:
@@ -979,6 +1267,297 @@ result[:number] # "8"
979
1267
 
980
1268
  [⬆️ Voltar para o índice](#índice-)
981
1269
 
1270
+ #### Como executar um caso de uso ou flow dentro de uma transação de banco de dados?
1271
+
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).
1276
+
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`:
1284
+
1285
+ ```ruby
1286
+ class CreateUserWithAProfile < Micro::Case
1287
+ def call!
1288
+ transaction {
1289
+ call(CreateUser).then(CreateUserProfile)
1290
+ }
1291
+ end
1292
+ end
1293
+ ```
1294
+
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`, …):
1300
+
1301
+ ```ruby
1302
+ class CreateAuditEntry < Micro::Case
1303
+ def call!
1304
+ transaction(with: AnalyticsRecord) {
1305
+ call(WriteAuditLog).then(BumpCounter)
1306
+ }
1307
+ end
1308
+ end
1309
+ ```
1310
+
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.
1322
+
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`.
1327
+
1328
+ ##### `transaction with: …` — declarando o padrão para um caso
1329
+
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:
1334
+
1335
+ ```ruby
1336
+ class ApplicationUseCase < Micro::Case
1337
+ transaction with: ApplicationRecord
1338
+ end
1339
+
1340
+ class CreateUserWithAProfile < ApplicationUseCase
1341
+ flow(transaction: true, steps: [CreateUser, CreateUserProfile])
1342
+ # transaction: true resolve para ApplicationRecord porque é o que
1343
+ # a classe hospedeira declarou via `transaction with:`.
1344
+ end
1345
+
1346
+ class BillingCase < ApplicationUseCase
1347
+ transaction with: BillingRecord
1348
+ # sobrescreve a declaração herdada para este ramo da hierarquia
1349
+ end
1350
+ ```
1351
+
1352
+ ##### `Micro::Cases.flow(transaction: …, steps: [...])` — transações no nível do flow
1353
+
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:
1358
+
1359
+ ```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).
1362
+ Micro::Cases.flow(transaction: true, steps: [CreateUser, CreateUserProfile])
1363
+
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.
1366
+ Micro::Cases.flow(transaction: { with: AnalyticsRecord }, steps: [
1367
+ WriteAuditLog,
1368
+ BumpCounter
1369
+ ])
1370
+
1371
+ # safe_flow faz rollback em falhas E em exceções inesperadas
1372
+ Micro::Cases.safe_flow(transaction: { with: ApplicationRecord }, steps: [
1373
+ CreateUser,
1374
+ CreateUserProfile
1375
+ ])
1376
+
1377
+ # Forma a nível de classe
1378
+ class CreateUserWithAProfile < Micro::Case
1379
+ flow(transaction: true, steps: [CreateUser, CreateUserProfile])
1380
+ end
1381
+ ```
1382
+
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:
1386
+
1387
+ ```ruby
1388
+ class CreateUserAndProfile < Micro::Case
1389
+ flow(transaction: true, steps: [CreateUser, CreateUserProfile])
1390
+ end
1391
+
1392
+ SignUpFlow = Micro::Cases.flow([
1393
+ NormalizeParams,
1394
+ ValidatePassword,
1395
+ CreateUserAndProfile,
1396
+ EnqueueIndexingJob
1397
+ ])
1398
+ ```
1399
+
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`.
1405
+
1406
+ ##### `config.default_transaction_class { … }` — padrão global
1407
+
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:
1411
+
1412
+ ```ruby
1413
+ # config/initializers/u_case.rb
1414
+ Micro::Case.config do |config|
1415
+ config.default_transaction_class { ApplicationRecord }
1416
+ end
1417
+ ```
1418
+
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:
1424
+
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`).
1431
+
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.
1436
+
1437
+ ##### Flows com steps internos sob transações
1438
+
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`.
1446
+
1447
+ Existem duas formas naturais de dar rollback transacional a um flow
1448
+ interno. Ambas reutilizam os helpers já documentados acima:
1449
+
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:
1456
+
1457
+ ```ruby
1458
+ class CreateUserWithProfileInline < Micro::Case
1459
+ attributes :name, :info
1460
+
1461
+ def call!
1462
+ create_user
1463
+ .then(:create_profile)
1464
+ end
1465
+
1466
+ private
1467
+
1468
+ def create_user
1469
+ user = User.create(name: name)
1470
+ Success result: { user: user }
1471
+ end
1472
+
1473
+ def create_profile(user:, **)
1474
+ profile = UserProfile.create(user_id: user.id, info: info)
1475
+ return Failure(:invalid_profile) if profile.errors.any?
1476
+
1477
+ Success result: { user: user, profile: profile }
1478
+ end
1479
+ end
1480
+
1481
+ SignUp = Micro::Cases.flow(transaction: true, steps: [
1482
+ NormalizeParams,
1483
+ CreateUserWithProfileInline, # ← falha interna agora reverte
1484
+ EnqueueIndexingJob
1485
+ ])
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
+ ```
1496
+
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.
1502
+
1503
+ **2. Usar o helper inline `Micro::Case#transaction`** para escopar o
1504
+ rollback a um único `call!` sem envolver um flow externo:
1505
+
1506
+ ```ruby
1507
+ class CreateUserWithProfileInline < Micro::Case
1508
+ def call!
1509
+ transaction {
1510
+ create_user
1511
+ .then(:create_profile)
1512
+ }
1513
+ end
1514
+ end
1515
+ ```
1516
+
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.
1529
+
1530
+ ##### Observações de comportamento
1531
+
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
+
982
1561
  ### `Micro::Case::Strict` - O que é um caso de uso estrito?
983
1562
 
984
1563
  Resposta: é um tipo de caso de uso que exigirá todas as palavras-chave (atributos) em sua inicialização.