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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +514 -2
- data/README.pt-BR.md +532 -2
- data/lib/micro/case/check.rb +71 -0
- data/lib/micro/case/config.rb +16 -0
- data/lib/micro/case/version.rb +1 -1
- data/lib/micro/case.rb +49 -7
- data/lib/micro/cases/error.rb +9 -0
- data/lib/micro/cases/flow.rb +46 -8
- data/lib/micro/cases.rb +12 -4
- metadata +1 -1
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.
|
|
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.
|
|
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.
|
data/lib/micro/case/check.rb
CHANGED
|
@@ -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
|
data/lib/micro/case/config.rb
CHANGED
|
@@ -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
|