u-case 5.5.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.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 -->
@@ -48,6 +48,7 @@ unreleased| https://github.com/serradura/u-case/blob/main/README.md
48
48
  - [Como usar o método `Micro::Case::Result#then`?](#como-usar-o-método-microcaseresultthen)
49
49
  - [O que acontece quando um `Micro::Case::Result#then` recebe um bloco?](#o-que-acontece-quando-um-microcaseresultthen-recebe-um-bloco)
50
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)
51
52
  - [`Micro::Cases::Flow` - Como compor casos de uso?](#microcasesflow---como-compor-casos-de-uso)
52
53
  - [É possível compor um fluxo com outros fluxos?](#é-possível-compor-um-fluxo-com-outros-fluxos)
53
54
  - [É possível que um fluxo acumule sua entrada e mescle cada resultado de sucesso para usar como argumento dos próximos casos de uso?](#é-possível-que-um-fluxo-acumule-sua-entrada-e-mescle-cada-resultado-de-sucesso-para-usar-como-argumento-dos-próximos-casos-de-uso)
@@ -55,6 +56,7 @@ unreleased| https://github.com/serradura/u-case/blob/main/README.md
55
56
  - [`Micro::Case::Result#transitions` schema](#microcaseresulttransitions-schema)
56
57
  - [É possível desabilitar o `Micro::Case::Result#transitions`?](#é-possível-desabilitar-o-microcaseresulttransitions)
57
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)
58
60
  - [`Micro::Case::Strict` - O que é um caso de uso estrito?](#microcasestrict---o-que-é-um-caso-de-uso-estrito)
59
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)
60
62
  - [`Micro::Cases::Safe::Flow`](#microcasessafeflow)
@@ -89,7 +91,7 @@ unreleased| https://github.com/serradura/u-case/blob/main/README.md
89
91
  | u-case | branch | ruby | activemodel | u-attributes |
90
92
  | ---------------- | ------ | -------- | -------------- | -------------- |
91
93
  | unreleased | main | >= 2.7 | >= 6.0 | >= 2.8, < 4.0 |
92
- | 5.5.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 |
93
95
  | 5.1.0 | v5.x | >= 2.7 | >= 6.0 | >= 2.7, < 4.0 |
94
96
  | 4.5.1 | v4.x | >= 2.2.0 | >= 3.2, <= 8.1 | >= 2.7, < 3.0 |
95
97
 
@@ -631,6 +633,243 @@ Todo::FindAllForUser
631
633
 
632
634
  [⬆️ Voltar para o índice](#índice-)
633
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
+
634
873
  ### `Micro::Cases::Flow` - Como compor casos de uso?
635
874
 
636
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:
@@ -979,6 +1218,297 @@ result[:number] # "8"
979
1218
 
980
1219
  [⬆️ Voltar para o índice](#índice-)
981
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
+
982
1512
  ### `Micro::Case::Strict` - O que é um caso de uso estrito?
983
1513
 
984
1514
  Resposta: é um tipo de caso de uso que exigirá todas as palavras-chave (atributos) em sua inicialização.
@@ -60,6 +60,67 @@ module Micro
60
60
  Kind::Hash[arg]
61
61
  end
62
62
 
63
+ def flow_steps_kwarg!(args, steps, label)
64
+ return unless args && steps
65
+
66
+ raise ArgumentError,
67
+ "#{label} accepts a positional collection OR `steps:`, not both"
68
+ end
69
+
70
+ def transaction_kwarg!(value)
71
+ return nil if value.nil? || value == false
72
+ return true if value == true
73
+
74
+ if value.is_a?(Class)
75
+ transaction_owner!(value)
76
+ return value
77
+ end
78
+
79
+ if value.is_a?(Hash)
80
+ extra = value.keys - [:with]
81
+
82
+ raise ArgumentError,
83
+ "transaction: unsupported key(s) #{extra.inspect} (only `:with` is accepted)" unless extra.empty?
84
+
85
+ with = value[:with]
86
+ transaction_owner!(with)
87
+
88
+ return with
89
+ end
90
+
91
+ raise ArgumentError,
92
+ "transaction: #{value.inspect} is not supported (accepts `true`, `false`, `nil`, or `{ with: SomeARClass }`)"
93
+ end
94
+
95
+ def activerecord_loaded!
96
+ return if defined?(::ActiveRecord::Base)
97
+
98
+ raise ::Micro::Cases::Error::TransactionAdapterMissing
99
+ end
100
+
101
+ # Validates a transaction owner class. We accept Class instances
102
+ # only; the AR-subclass check is enforced if (and only if)
103
+ # ActiveRecord is already loaded — otherwise we defer to runtime
104
+ # so that load-order quirks (Rails initializers running before
105
+ # the AR autoload) don't break class-eval-time declarations.
106
+ def transaction_owner!(klass)
107
+ raise ArgumentError,
108
+ "transaction owner #{klass.inspect} must be a subclass of ActiveRecord::Base" unless klass.is_a?(Class)
109
+
110
+ return unless defined?(::ActiveRecord::Base)
111
+ return if klass <= ::ActiveRecord::Base
112
+
113
+ raise ArgumentError,
114
+ "transaction owner #{klass.inspect} must be a subclass of ActiveRecord::Base"
115
+ end
116
+
117
+ def transaction_class_callback!(callable)
118
+ return if callable.respond_to?(:call)
119
+
120
+ raise ArgumentError,
121
+ "Micro::Case.config.default_transaction_class= expects a callable (a block, lambda or proc), got #{callable.inspect}"
122
+ end
123
+
63
124
  def results_contract!(use_case_class, kind, type, value)
64
125
  contract = use_case_class.__results_contract__
65
126
  return unless contract
@@ -108,6 +169,16 @@ module Micro
108
169
  def flow_use_cases!(_use_cases); end
109
170
  def map_args!(_args); end
110
171
  def hash!(arg); arg; end
172
+ def flow_steps_kwarg!(_args, _steps, _label); end
173
+ def transaction_kwarg!(value)
174
+ return true if value == true
175
+ return value if value.is_a?(Class)
176
+ return value[:with] if value.is_a?(Hash) && value[:with].is_a?(Class)
177
+ nil
178
+ end
179
+ def activerecord_loaded!; end
180
+ def transaction_owner!(_klass); end
181
+ def transaction_class_callback!(_callable); end
111
182
  def results_contract!(_use_case_class, _kind, _type, _value); end
112
183
  end
113
184
  end
@@ -52,6 +52,22 @@ module Micro
52
52
 
53
53
  @activemodel_validation_errors_failure = :invalid_attributes
54
54
  end
55
+
56
+ DEFAULT_TRANSACTION_CLASS_CALLBACK = -> { ::ActiveRecord::Base }.freeze
57
+
58
+ def default_transaction_class=(callable)
59
+ ::Micro::Case.check.transaction_class_callback!(callable)
60
+
61
+ @default_transaction_class = callable
62
+ end
63
+
64
+ def default_transaction_class(&block)
65
+ return self.default_transaction_class = block if block
66
+
67
+ return @default_transaction_class if defined?(@default_transaction_class)
68
+
69
+ DEFAULT_TRANSACTION_CLASS_CALLBACK
70
+ end
55
71
  end
56
72
  end
57
73
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Micro
4
4
  class Case
5
- VERSION = '5.5.0'.freeze
5
+ VERSION = '5.6.0'.freeze
6
6
  end
7
7
  end