u-case 5.4.0 → 5.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +29 -0
- data/CLAUDE.md +11 -2
- data/README.md +567 -2
- data/README.pt-BR.md +585 -2
- data/lib/micro/case/check.rb +104 -0
- data/lib/micro/case/config.rb +16 -0
- data/lib/micro/case/error.rb +24 -1
- data/lib/micro/case/result/contract.rb +60 -0
- data/lib/micro/case/version.rb +1 -1
- data/lib/micro/case.rb +72 -11
- data/lib/micro/cases/error.rb +9 -0
- data/lib/micro/cases/flow.rb +46 -8
- data/lib/micro/cases.rb +12 -4
- metadata +2 -1
data/README.md
CHANGED
|
@@ -27,7 +27,7 @@ The main project goals are:
|
|
|
27
27
|
Version | Documentation
|
|
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
|
> **Note:** Você entende português? 🇧🇷 🇵🇹 Verifique o [README traduzido em pt-BR](https://github.com/serradura/u-case/blob/main/README.pt-BR.md).
|
|
@@ -42,6 +42,7 @@ unreleased| https://github.com/serradura/u-case/blob/main/README.md
|
|
|
42
42
|
- [What are the default result types?](#what-are-the-default-result-types)
|
|
43
43
|
- [How to define custom result types?](#how-to-define-custom-result-types)
|
|
44
44
|
- [Is it possible to define a custom type without a result data?](#is-it-possible-to-define-a-custom-type-without-a-result-data)
|
|
45
|
+
- [How to declare a results contract?](#how-to-declare-a-results-contract)
|
|
45
46
|
- [How to use the result hooks?](#how-to-use-the-result-hooks)
|
|
46
47
|
- [Why the hook usage without a defined type exposes the result itself?](#why-the-hook-usage-without-a-defined-type-exposes-the-result-itself)
|
|
47
48
|
- [Using decomposition to access the result data and type](#using-decomposition-to-access-the-result-data-and-type)
|
|
@@ -49,6 +50,7 @@ unreleased| https://github.com/serradura/u-case/blob/main/README.md
|
|
|
49
50
|
- [How to use the `Micro::Case::Result#then` method?](#how-to-use-the-microcaseresultthen-method)
|
|
50
51
|
- [What does happens when a `Micro::Case::Result#then` receives a block?](#what-does-happens-when-a-microcaseresultthen-receives-a-block)
|
|
51
52
|
- [How to make attributes data injection using this feature?](#how-to-make-attributes-data-injection-using-this-feature)
|
|
53
|
+
- [Internal steps — building a flow inline inside `call!`](#internal-steps--building-a-flow-inline-inside-call)
|
|
52
54
|
- [`Micro::Cases::Flow` - How to compose use cases?](#microcasesflow---how-to-compose-use-cases)
|
|
53
55
|
- [Is it possible to compose a flow with other flows?](#is-it-possible-to-compose-a-flow-with-other-flows)
|
|
54
56
|
- [Is it possible a flow accumulates its input and merges each success result to use as the argument of the next use cases?](#is-it-possible-a-flow-accumulates-its-input-and-merges-each-success-result-to-use-as-the-argument-of-the-next-use-cases)
|
|
@@ -56,6 +58,7 @@ unreleased| https://github.com/serradura/u-case/blob/main/README.md
|
|
|
56
58
|
- [`Micro::Case::Result#transitions` schema](#microcaseresulttransitions-schema)
|
|
57
59
|
- [Is it possible disable the `Micro::Case::Result#transitions`?](#is-it-possible-disable-the-microcaseresulttransitions)
|
|
58
60
|
- [Is it possible to declare a flow that includes the use case itself as a step?](#is-it-possible-to-declare-a-flow-that-includes-the-use-case-itself-as-a-step)
|
|
61
|
+
- [How to run a use case or flow inside a database transaction?](#how-to-run-a-use-case-or-flow-inside-a-database-transaction)
|
|
59
62
|
- [`Micro::Case::Strict` - What is a strict use case?](#microcasestrict---what-is-a-strict-use-case)
|
|
60
63
|
- [`Micro::Case::Safe` - Is there some feature to auto handle exceptions inside of a use case or flow?](#microcasesafe---is-there-some-feature-to-auto-handle-exceptions-inside-of-a-use-case-or-flow)
|
|
61
64
|
- [`Micro::Cases::Safe::Flow`](#microcasessafeflow)
|
|
@@ -90,7 +93,7 @@ unreleased| https://github.com/serradura/u-case/blob/main/README.md
|
|
|
90
93
|
| u-case | branch | ruby | activemodel | u-attributes |
|
|
91
94
|
| ---------------- | ------ | -------- | -------------- | -------------- |
|
|
92
95
|
| unreleased | main | >= 2.7 | >= 6.0 | >= 2.8, < 4.0 |
|
|
93
|
-
| 5.
|
|
96
|
+
| 5.6.0 | v5.x | >= 2.7 | >= 6.0 | >= 2.8, < 4.0 |
|
|
94
97
|
| 5.1.0 | v5.x | >= 2.7 | >= 6.0 | >= 2.7, < 4.0 |
|
|
95
98
|
| 4.5.1 | v4.x | >= 2.2.0 | >= 3.2, <= 8.1 | >= 2.7, < 3.0 |
|
|
96
99
|
|
|
@@ -335,6 +338,58 @@ result.use_case.attributes # {"a"=>2, "b"=>"2"}
|
|
|
335
338
|
|
|
336
339
|
[⬆️ Back to Top](#table-of-contents-)
|
|
337
340
|
|
|
341
|
+
#### How to declare a results contract?
|
|
342
|
+
|
|
343
|
+
Answer: Use the `results do |on| ... end` macro to declare which result types your use case can return, and which keys each one requires. When a contract is declared, `Success(...)` / `Failure(...)` calls that use an undeclared type raise `Micro::Case::Error::UnexpectedResultType`, and calls that omit a declared required key raise `Micro::Case::Error::MissingResultKeys`.
|
|
344
|
+
|
|
345
|
+
```ruby
|
|
346
|
+
class Divide < Micro::Case
|
|
347
|
+
attributes :a, :b
|
|
348
|
+
|
|
349
|
+
results do |on|
|
|
350
|
+
on.failure(:attributes_must_be_numbers)
|
|
351
|
+
on.failure(:division_by_zero)
|
|
352
|
+
|
|
353
|
+
on.success(result: [:division])
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def call!
|
|
357
|
+
return Failure(:attributes_must_be_numbers) unless Kind.of?(Numeric, a, b)
|
|
358
|
+
return Failure(:division_by_zero) if b == 0
|
|
359
|
+
|
|
360
|
+
Success result: { division: a / b }
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
Divide.call(a: 10, b: 2).data # => { division: 5 }
|
|
365
|
+
Divide.call(a: 10, b: 0).type # => :division_by_zero
|
|
366
|
+
Divide.call(a: 'x', b: 2).type # => :attributes_must_be_numbers
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
A type passed to `on.success` / `on.failure` without a `result:` argument declares the type with no required keys (any payload — including the implicit `{ type => true }` from `Failure(:my_type)` — is accepted). When `result: [:key1, :key2]` is given, those keys must be present in the result hash; extra keys are allowed.
|
|
370
|
+
|
|
371
|
+
```ruby
|
|
372
|
+
class Wrong < Micro::Case
|
|
373
|
+
results do |on|
|
|
374
|
+
on.success(result: [:value])
|
|
375
|
+
on.failure(:known)
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def call!
|
|
379
|
+
Success(:other, result: { value: 1 }) # raises Micro::Case::Error::UnexpectedResultType
|
|
380
|
+
# Success(result: { wrong: 1 }) # raises Micro::Case::Error::MissingResultKeys
|
|
381
|
+
# Failure(:other) # raises Micro::Case::Error::UnexpectedResultType
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Notes:
|
|
387
|
+
- Use cases without a `results` block keep their previous unrestricted behavior — the contract is opt-in.
|
|
388
|
+
- Subclasses inherit the parent's contract.
|
|
389
|
+
- Rescued exceptions in `Micro::Case::Safe` (which produce `Failure(result: exception)` automatically) bypass the contract.
|
|
390
|
+
|
|
391
|
+
[⬆️ Back to Top](#table-of-contents-)
|
|
392
|
+
|
|
338
393
|
#### How to use the result hooks?
|
|
339
394
|
|
|
340
395
|
As [mentioned earlier](#microcaseresult---what-is-a-use-case-result), the `Micro::Case::Result` has two methods to improve the application flow control. They are: `#on_success`, `on_failure`.
|
|
@@ -579,6 +634,235 @@ Todo::FindAllForUser
|
|
|
579
634
|
|
|
580
635
|
[⬆️ Back to Top](#table-of-contents-)
|
|
581
636
|
|
|
637
|
+
#### Internal steps — building a flow inline inside `call!`
|
|
638
|
+
|
|
639
|
+
`Result#then` (and its `|` pipe alias) is u-case's **third way of
|
|
640
|
+
composing a flow**, side by side with `Micro::Cases.flow(...)` and the
|
|
641
|
+
class-level `flow ...` macro. Instead of wiring sibling use cases
|
|
642
|
+
together, you keep the chain *inside* a single use case's `call!`:
|
|
643
|
+
each link is a method, lambda or another use case class; each link
|
|
644
|
+
returns a `Micro::Case::Result`; each link's `Success` data becomes
|
|
645
|
+
the next link's keyword arguments; and each link contributes a row to
|
|
646
|
+
`result.transitions` — just like a step in a top-level flow.
|
|
647
|
+
|
|
648
|
+
##### What `Result#then` (and `|`) accept
|
|
649
|
+
|
|
650
|
+
| Argument shape | Example |
|
|
651
|
+
| --- | --- |
|
|
652
|
+
| `Symbol` (method name) | `result.then(:sum_a_and_b)` |
|
|
653
|
+
| Bound `Method` object | `result.then(method(:sum_a_and_b))` |
|
|
654
|
+
| `Lambda` / `Proc` | `result.then(-> data { sum_a_and_b(**data) })` |
|
|
655
|
+
| Use case class | `result.then(SumHalf)` |
|
|
656
|
+
| `Symbol` + Hash defaults | `result.then(:add, number: 3)` |
|
|
657
|
+
| Block | `result.then { \|r\| r.success? ? r[:sum] : 0 }` |
|
|
658
|
+
|
|
659
|
+
The connecting method **must** return a `Micro::Case::Result`. Anything
|
|
660
|
+
else raises `Micro::Case::Error::UnexpectedResult` — for example a
|
|
661
|
+
method that returns a plain `Hash` will be rejected with a message like
|
|
662
|
+
`MyCase#method(:foo) must return an instance of Micro::Case::Result`.
|
|
663
|
+
|
|
664
|
+
##### A minimal example
|
|
665
|
+
|
|
666
|
+
```ruby
|
|
667
|
+
class SumHalf < Micro::Case
|
|
668
|
+
attribute :sum
|
|
669
|
+
|
|
670
|
+
def call!
|
|
671
|
+
Success :third_sum, result: { sum: sum + 0.5 }
|
|
672
|
+
end
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
class DoSomeSum < Micro::Case
|
|
676
|
+
attributes :a, :b
|
|
677
|
+
|
|
678
|
+
def call!
|
|
679
|
+
validate_numbers
|
|
680
|
+
.then(:sum_a_and_b)
|
|
681
|
+
.then(:add, number: 3)
|
|
682
|
+
.then(SumHalf)
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
private
|
|
686
|
+
|
|
687
|
+
def validate_numbers
|
|
688
|
+
Kind.of?(Numeric, a, b) ? Success(:valid) : Failure()
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
def sum_a_and_b
|
|
692
|
+
Success :first_sum, result: { sum: a + b }
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
def add(sum:, number:, **)
|
|
696
|
+
Success :second_sum, result: { sum: sum + number }
|
|
697
|
+
end
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
result = DoSomeSum.call(a: 1, b: 2)
|
|
701
|
+
|
|
702
|
+
result.success? # true
|
|
703
|
+
result.data # { sum: 6.5 }
|
|
704
|
+
result.transitions # 4 entries — see below
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
`result.transitions` for the call above:
|
|
708
|
+
|
|
709
|
+
```ruby
|
|
710
|
+
[
|
|
711
|
+
{ use_case: { class: DoSomeSum, attributes: { a: 1, b: 2 } },
|
|
712
|
+
success: { type: :valid, result: { valid: true } },
|
|
713
|
+
accessible_attributes: [:a, :b] },
|
|
714
|
+
|
|
715
|
+
{ use_case: { class: DoSomeSum, attributes: { a: 1, b: 2 } },
|
|
716
|
+
success: { type: :first_sum, result: { sum: 3 } },
|
|
717
|
+
accessible_attributes: [:a, :b, :valid] },
|
|
718
|
+
|
|
719
|
+
{ use_case: { class: DoSomeSum, attributes: { a: 1, b: 2 } },
|
|
720
|
+
success: { type: :second_sum, result: { sum: 6 } },
|
|
721
|
+
accessible_attributes: [:a, :b, :valid, :number, :sum] },
|
|
722
|
+
|
|
723
|
+
{ use_case: { class: SumHalf, attributes: { sum: 6 } },
|
|
724
|
+
success: { type: :third_sum, result: { sum: 6.5 } },
|
|
725
|
+
accessible_attributes: [:a, :b, :valid, :number, :sum] }
|
|
726
|
+
]
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
Symbol-, method- and lambda-based links all run **as the host use
|
|
730
|
+
case**, so the first three transitions report `class: DoSomeSum`. Only
|
|
731
|
+
the `SumHalf` link, which is another use case class, contributes a
|
|
732
|
+
transition with a different `use_case.class`. The `accessible_attributes`
|
|
733
|
+
grows as each link's `Success` output is merged into the running data.
|
|
734
|
+
|
|
735
|
+
##### The `|` (pipe) alias
|
|
736
|
+
|
|
737
|
+
`|` is sugar for `.then(...)`. The previous example becomes:
|
|
738
|
+
|
|
739
|
+
```ruby
|
|
740
|
+
def call!
|
|
741
|
+
validate_numbers | :sum_a_and_b | :add | SumHalf
|
|
742
|
+
end
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
Both forms produce identical `result.data` and `result.transitions`.
|
|
746
|
+
|
|
747
|
+
> **Elixir-style chains with `it` (Ruby ≥ 3.4):** because Ruby 3.4
|
|
748
|
+
> exposes `it` as the implicit first parameter of a block/lambda body,
|
|
749
|
+
> you can write a chain that reads almost exactly like Elixir's
|
|
750
|
+
> `|>` pipe. Each lambda receives the accumulated data hash as `it`
|
|
751
|
+
> and must still terminate in a `Success(...)` / `Failure(...)` call:
|
|
752
|
+
>
|
|
753
|
+
> ```ruby
|
|
754
|
+
> def call!
|
|
755
|
+
> validate_something \
|
|
756
|
+
> | -> { do_something_with(**it) } \
|
|
757
|
+
> | -> { and_another_thing_with(**it) }
|
|
758
|
+
> end
|
|
759
|
+
> ```
|
|
760
|
+
>
|
|
761
|
+
> On Ruby 2.7 – 3.3 (where `it` is just an undefined identifier),
|
|
762
|
+
> use the portable explicit form `->(data) { do_something_with(**data) }`
|
|
763
|
+
> shown in the next section.
|
|
764
|
+
|
|
765
|
+
##### Lambda / `Method` forms
|
|
766
|
+
|
|
767
|
+
Lambdas (and bound `Method` objects) receive the accumulated data
|
|
768
|
+
**positionally** as a single Hash:
|
|
769
|
+
|
|
770
|
+
```ruby
|
|
771
|
+
def call!
|
|
772
|
+
validate_numbers
|
|
773
|
+
.then(method(:sum_a_and_b))
|
|
774
|
+
.then(->(data) { add(**data, number: 3) })
|
|
775
|
+
.then(SumHalf)
|
|
776
|
+
end
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
##### Failure short-circuits the chain
|
|
780
|
+
|
|
781
|
+
Returning `Failure(...)` from any link halts the rest of the chain
|
|
782
|
+
immediately — exactly like a step in a top-level flow returning a
|
|
783
|
+
failure. The remaining `.then(...)` / `|` links are not invoked, and
|
|
784
|
+
the final `result` is the failure:
|
|
785
|
+
|
|
786
|
+
```ruby
|
|
787
|
+
DoSomeSum.call(a: 1, b: '2')
|
|
788
|
+
|
|
789
|
+
# validate_numbers returns Failure() → :sum_a_and_b, :add and SumHalf
|
|
790
|
+
# never run. result.failure? == true, result.transitions has 1 entry.
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
##### Using an internal-step case inside an outer flow
|
|
794
|
+
|
|
795
|
+
A use case that composes internally with `.then(...)` is just a use
|
|
796
|
+
case, so you can drop it into any flow constructor:
|
|
797
|
+
|
|
798
|
+
```ruby
|
|
799
|
+
SignUp = Micro::Cases.flow([
|
|
800
|
+
NormalizeParams,
|
|
801
|
+
DoSomeSum, # ← uses .then(:method) internally
|
|
802
|
+
EnqueueIndexingJob
|
|
803
|
+
])
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
The host class's internal transitions are interleaved with the outer
|
|
807
|
+
flow's leaf transitions in execution order. If `DoSomeSum` produces 4
|
|
808
|
+
internal transitions and the outer flow has 2 other leaf steps, the
|
|
809
|
+
final `result.transitions` has 6 entries.
|
|
810
|
+
|
|
811
|
+
##### Internal steps **without** transactions
|
|
812
|
+
|
|
813
|
+
By default — i.e. when neither the host class nor the outer flow uses
|
|
814
|
+
`transaction: true` — internal steps behave like any other code in
|
|
815
|
+
`call!`: side-effects made by earlier links **persist** even if a
|
|
816
|
+
later link returns `Failure`. The chain is interrupted, but anything
|
|
817
|
+
already written to the database stays written:
|
|
818
|
+
|
|
819
|
+
```ruby
|
|
820
|
+
class CreateUserWithProfileInline < Micro::Case
|
|
821
|
+
attributes :name, :info
|
|
822
|
+
|
|
823
|
+
def call!
|
|
824
|
+
create_user
|
|
825
|
+
.then(:create_profile)
|
|
826
|
+
end
|
|
827
|
+
|
|
828
|
+
private
|
|
829
|
+
|
|
830
|
+
def create_user
|
|
831
|
+
user = User.create(name: name)
|
|
832
|
+
Success result: { user: user }
|
|
833
|
+
end
|
|
834
|
+
|
|
835
|
+
def create_profile(user:, **)
|
|
836
|
+
profile = UserProfile.create(user_id: user.id, info: info)
|
|
837
|
+
return Failure(:invalid_profile) if profile.errors.any?
|
|
838
|
+
|
|
839
|
+
Success result: { user: user, profile: profile }
|
|
840
|
+
end
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
CreateUserWithProfileInline.call(name: 'Rodrigo', info: '')
|
|
844
|
+
# create_user already INSERTed the user row; create_profile failed.
|
|
845
|
+
# user is persisted; profile is not. No automatic rollback.
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
If you need the partial side-effects to be undone, wrap the chain in
|
|
849
|
+
a transaction. Because internal steps are just another way of
|
|
850
|
+
expressing a flow (an *internal* flow), the transactional story is
|
|
851
|
+
exactly the one already documented in
|
|
852
|
+
[How to run a use case or flow inside a database transaction?](#how-to-run-a-use-case-or-flow-inside-a-database-transaction)
|
|
853
|
+
below — the "Internal-step flows under transactions" subsection
|
|
854
|
+
there walks through both the inline `transaction { ... }` form and
|
|
855
|
+
the `transaction: true` flow form for an internal-step host case.
|
|
856
|
+
|
|
857
|
+
> **Note:** See `test/micro/case/internal_steps/with_symbols_test.rb`,
|
|
858
|
+
> `with_methods_test.rb` and `with_lambdas_test.rb` for full examples
|
|
859
|
+
> of each form, and
|
|
860
|
+
> `test/micro/cases/flow/internal_steps_in_flows_test.rb` for the
|
|
861
|
+
> interaction with flows and transactions (accumulation, transitions,
|
|
862
|
+
> rollback at every nesting level).
|
|
863
|
+
|
|
864
|
+
[⬆️ Back to Top](#table-of-contents-)
|
|
865
|
+
|
|
582
866
|
### `Micro::Cases::Flow` - How to compose use cases?
|
|
583
867
|
|
|
584
868
|
We call as **flow** a composition of use cases. The main idea of this feature is to use/reuse use cases as steps of a new use case. e.g.
|
|
@@ -926,6 +1210,287 @@ result[:number] # "8"
|
|
|
926
1210
|
|
|
927
1211
|
[⬆️ Back to Top](#table-of-contents-)
|
|
928
1212
|
|
|
1213
|
+
#### How to run a use case or flow inside a database transaction?
|
|
1214
|
+
|
|
1215
|
+
`u-case` ships with two complementary helpers for wrapping work in an
|
|
1216
|
+
`ActiveRecord::Base.transaction`. Both opt-in — `active_record` is **not**
|
|
1217
|
+
required by the gem, so you need to load ActiveRecord yourself (Rails
|
|
1218
|
+
applications already do).
|
|
1219
|
+
|
|
1220
|
+
##### `Micro::Case#transaction` — inline transactions inside `call!`
|
|
1221
|
+
|
|
1222
|
+
`Micro::Case#transaction` (and `Micro::Case::Safe#transaction`) is a private
|
|
1223
|
+
instance helper that wraps a block in a database transaction and issues an
|
|
1224
|
+
`ActiveRecord::Rollback` whenever the block's result is a `Failure`. The
|
|
1225
|
+
original result is returned either way, so you can keep chaining with
|
|
1226
|
+
`Result#then`:
|
|
1227
|
+
|
|
1228
|
+
```ruby
|
|
1229
|
+
class CreateUserWithAProfile < Micro::Case
|
|
1230
|
+
def call!
|
|
1231
|
+
transaction {
|
|
1232
|
+
call(CreateUser).then(CreateUserProfile)
|
|
1233
|
+
}
|
|
1234
|
+
end
|
|
1235
|
+
end
|
|
1236
|
+
```
|
|
1237
|
+
|
|
1238
|
+
If the block returns a failure (or raises), every row written inside the
|
|
1239
|
+
block is rolled back. The helper accepts an optional `with:` kwarg to pick
|
|
1240
|
+
the ActiveRecord class on which `.transaction` is opened — useful for
|
|
1241
|
+
multi-database Rails apps (`ApplicationRecord`, `AnalyticsRecord`,
|
|
1242
|
+
`BillingRecord`, …):
|
|
1243
|
+
|
|
1244
|
+
```ruby
|
|
1245
|
+
class CreateAuditEntry < Micro::Case
|
|
1246
|
+
def call!
|
|
1247
|
+
transaction(with: AnalyticsRecord) {
|
|
1248
|
+
call(WriteAuditLog).then(BumpCounter)
|
|
1249
|
+
}
|
|
1250
|
+
end
|
|
1251
|
+
end
|
|
1252
|
+
```
|
|
1253
|
+
|
|
1254
|
+
When `with:` is omitted, the helper falls back to the class macro
|
|
1255
|
+
(`transaction with: …`) and then to the global default callback (see below),
|
|
1256
|
+
which ships as `-> { ::ActiveRecord::Base }`.
|
|
1257
|
+
|
|
1258
|
+
> **Note:** any class passed via `with:` (here, on the class macro, or on a
|
|
1259
|
+
> flow's `transaction:` kwarg) **must be a subclass of `ActiveRecord::Base`**.
|
|
1260
|
+
> Non-AR classes are rejected with `ArgumentError`. The class-macro
|
|
1261
|
+
> validation runs at class-eval time when ActiveRecord is already loaded
|
|
1262
|
+
> (typical Rails app); otherwise it's deferred to runtime, so initializer
|
|
1263
|
+
> load order doesn't break declarations.
|
|
1264
|
+
|
|
1265
|
+
> **Backward compatibility:** the pre-5.6.0 positional form
|
|
1266
|
+
> `transaction(:activerecord) { ... }` still works as an alias for
|
|
1267
|
+
> `transaction { ... }`. Any other positional value raises `ArgumentError` —
|
|
1268
|
+
> the legacy helper accepted only `:activerecord`.
|
|
1269
|
+
|
|
1270
|
+
##### `transaction with: …` — declaring the default for a case
|
|
1271
|
+
|
|
1272
|
+
A class macro lets a case declare which ActiveRecord class should own its
|
|
1273
|
+
transactions, so neither the inline helper nor any flow that wraps the
|
|
1274
|
+
case needs to spell it out at every call site. The declaration is inherited
|
|
1275
|
+
by subclasses:
|
|
1276
|
+
|
|
1277
|
+
```ruby
|
|
1278
|
+
class ApplicationUseCase < Micro::Case
|
|
1279
|
+
transaction with: ApplicationRecord
|
|
1280
|
+
end
|
|
1281
|
+
|
|
1282
|
+
class CreateUserWithAProfile < ApplicationUseCase
|
|
1283
|
+
flow(transaction: true, steps: [CreateUser, CreateUserProfile])
|
|
1284
|
+
# transaction: true resolves to ApplicationRecord because that's
|
|
1285
|
+
# what the host class declared via `transaction with:`.
|
|
1286
|
+
end
|
|
1287
|
+
|
|
1288
|
+
class BillingCase < ApplicationUseCase
|
|
1289
|
+
transaction with: BillingRecord
|
|
1290
|
+
# overrides the inherited declaration for this branch of the tree
|
|
1291
|
+
end
|
|
1292
|
+
```
|
|
1293
|
+
|
|
1294
|
+
##### `Micro::Cases.flow(transaction: …, steps: [...])` — flow-level transactions
|
|
1295
|
+
|
|
1296
|
+
Pass `transaction:` together with `steps:` to wrap an entire flow in a
|
|
1297
|
+
single transaction. If any step returns a failure (or raises, in a
|
|
1298
|
+
`safe_flow`), every database write performed during the flow is rolled back.
|
|
1299
|
+
The kwarg accepts three forms:
|
|
1300
|
+
|
|
1301
|
+
```ruby
|
|
1302
|
+
# Use the class-level macro (if the host case declared one) or the
|
|
1303
|
+
# global default (`ActiveRecord::Base` unless configured otherwise).
|
|
1304
|
+
Micro::Cases.flow(transaction: true, steps: [CreateUser, CreateUserProfile])
|
|
1305
|
+
|
|
1306
|
+
# Pick an explicit ActiveRecord class for this flow only — same `with:`
|
|
1307
|
+
# vocabulary as the inline helper and the class macro.
|
|
1308
|
+
Micro::Cases.flow(transaction: { with: AnalyticsRecord }, steps: [
|
|
1309
|
+
WriteAuditLog,
|
|
1310
|
+
BumpCounter
|
|
1311
|
+
])
|
|
1312
|
+
|
|
1313
|
+
# safe_flow rolls back on failures AND on unexpected exceptions
|
|
1314
|
+
Micro::Cases.safe_flow(transaction: { with: ApplicationRecord }, steps: [
|
|
1315
|
+
CreateUser,
|
|
1316
|
+
CreateUserProfile
|
|
1317
|
+
])
|
|
1318
|
+
|
|
1319
|
+
# Class-level form
|
|
1320
|
+
class CreateUserWithAProfile < Micro::Case
|
|
1321
|
+
flow(transaction: true, steps: [CreateUser, CreateUserProfile])
|
|
1322
|
+
end
|
|
1323
|
+
```
|
|
1324
|
+
|
|
1325
|
+
To nest a transactional flow inside another flow, wrap it in a use case
|
|
1326
|
+
class — `Micro::Cases.flow([...])` flattens `Flow` instances passed as
|
|
1327
|
+
steps, but does **not** flatten classes:
|
|
1328
|
+
|
|
1329
|
+
```ruby
|
|
1330
|
+
class CreateUserAndProfile < Micro::Case
|
|
1331
|
+
flow(transaction: true, steps: [CreateUser, CreateUserProfile])
|
|
1332
|
+
end
|
|
1333
|
+
|
|
1334
|
+
SignUpFlow = Micro::Cases.flow([
|
|
1335
|
+
NormalizeParams,
|
|
1336
|
+
ValidatePassword,
|
|
1337
|
+
CreateUserAndProfile,
|
|
1338
|
+
EnqueueIndexingJob
|
|
1339
|
+
])
|
|
1340
|
+
```
|
|
1341
|
+
|
|
1342
|
+
If `transaction: true` is used while `ActiveRecord::Base` is not loaded the
|
|
1343
|
+
flow raises `Micro::Cases::Error::TransactionAdapterMissing` on the first
|
|
1344
|
+
call so the misconfiguration surfaces immediately. Passing `transaction: {
|
|
1345
|
+
with: SomeClass }` skips this check — `SomeClass` is trusted to respond
|
|
1346
|
+
to `.transaction`.
|
|
1347
|
+
|
|
1348
|
+
##### `config.default_transaction_class { … }` — global default
|
|
1349
|
+
|
|
1350
|
+
For Rails apps that use a single abstract record (`ApplicationRecord`),
|
|
1351
|
+
configure it once in an initializer instead of declaring it on every case
|
|
1352
|
+
or flow:
|
|
1353
|
+
|
|
1354
|
+
```ruby
|
|
1355
|
+
# config/initializers/u_case.rb
|
|
1356
|
+
Micro::Case.config do |config|
|
|
1357
|
+
config.default_transaction_class { ApplicationRecord }
|
|
1358
|
+
end
|
|
1359
|
+
```
|
|
1360
|
+
|
|
1361
|
+
The callback (block or lambda) is invoked **every time** a transaction
|
|
1362
|
+
opens — no memoization — so it's safe to make the return value depend on
|
|
1363
|
+
runtime state (per-tenant routing, etc.). The default is
|
|
1364
|
+
`-> { ::ActiveRecord::Base }`. Resolution order, when a transaction opens:
|
|
1365
|
+
|
|
1366
|
+
1. **Call-site override.** `transaction: { with: X }` on a flow kwarg, or
|
|
1367
|
+
`transaction(with: X) { ... }` on the inline helper.
|
|
1368
|
+
2. **Host case's `transaction with: X` macro** (walks ancestors).
|
|
1369
|
+
3. **`Micro::Case.config.default_transaction_class.call`** — the global
|
|
1370
|
+
callback (defaults to `ActiveRecord::Base`).
|
|
1371
|
+
|
|
1372
|
+
A non-callable assignment to `default_transaction_class=` raises
|
|
1373
|
+
`ArgumentError` at config time so typos like `config.default_transaction_class
|
|
1374
|
+
= 'ApplicationRecord'` fail loudly instead of crashing the first transaction.
|
|
1375
|
+
|
|
1376
|
+
##### Internal-step flows under transactions
|
|
1377
|
+
|
|
1378
|
+
[Internal steps](#internal-steps--building-a-flow-inline-inside-call)
|
|
1379
|
+
(the `Result#then(:symbol)` / `|` form built inline inside a single
|
|
1380
|
+
`call!`) are u-case's third way of composing a flow — an *internal*
|
|
1381
|
+
flow. By default an internal flow has **no transactional rollback**:
|
|
1382
|
+
side-effects from earlier `.then(:method)` links persist even when a
|
|
1383
|
+
later link returns `Failure`.
|
|
1384
|
+
|
|
1385
|
+
There are two natural ways to give an internal flow transactional
|
|
1386
|
+
rollback. Both reuse the helpers already covered above:
|
|
1387
|
+
|
|
1388
|
+
**1. Wrap the host case in a `transaction: true` flow.** This is the
|
|
1389
|
+
recommended way once the host case is composed with the rest of the
|
|
1390
|
+
pipeline. The transaction spans the whole flow call, so a `Failure`
|
|
1391
|
+
*anywhere* — including from any internal `.then(:method)` link — rolls
|
|
1392
|
+
back every database write performed during that call:
|
|
1393
|
+
|
|
1394
|
+
```ruby
|
|
1395
|
+
class CreateUserWithProfileInline < Micro::Case
|
|
1396
|
+
attributes :name, :info
|
|
1397
|
+
|
|
1398
|
+
def call!
|
|
1399
|
+
create_user
|
|
1400
|
+
.then(:create_profile)
|
|
1401
|
+
end
|
|
1402
|
+
|
|
1403
|
+
private
|
|
1404
|
+
|
|
1405
|
+
def create_user
|
|
1406
|
+
user = User.create(name: name)
|
|
1407
|
+
Success result: { user: user }
|
|
1408
|
+
end
|
|
1409
|
+
|
|
1410
|
+
def create_profile(user:, **)
|
|
1411
|
+
profile = UserProfile.create(user_id: user.id, info: info)
|
|
1412
|
+
return Failure(:invalid_profile) if profile.errors.any?
|
|
1413
|
+
|
|
1414
|
+
Success result: { user: user, profile: profile }
|
|
1415
|
+
end
|
|
1416
|
+
end
|
|
1417
|
+
|
|
1418
|
+
SignUp = Micro::Cases.flow(transaction: true, steps: [
|
|
1419
|
+
NormalizeParams,
|
|
1420
|
+
CreateUserWithProfileInline, # ← internal failure now rolls back
|
|
1421
|
+
EnqueueIndexingJob
|
|
1422
|
+
])
|
|
1423
|
+
|
|
1424
|
+
# Or class-level:
|
|
1425
|
+
class SignUp < Micro::Case
|
|
1426
|
+
flow(transaction: true, steps: [
|
|
1427
|
+
NormalizeParams,
|
|
1428
|
+
CreateUserWithProfileInline,
|
|
1429
|
+
EnqueueIndexingJob
|
|
1430
|
+
])
|
|
1431
|
+
end
|
|
1432
|
+
```
|
|
1433
|
+
|
|
1434
|
+
If `create_profile` (the internal `.then(:create_profile)` link)
|
|
1435
|
+
returns `Failure(:invalid_profile)`, the `User` row inserted earlier
|
|
1436
|
+
by `create_user` is rolled back as part of the same
|
|
1437
|
+
`ActiveRecord::Base.transaction`. The result still surfaces the
|
|
1438
|
+
failure type and the partial transitions, but no row is left behind.
|
|
1439
|
+
|
|
1440
|
+
**2. Use the inline `Micro::Case#transaction` helper** to scope the
|
|
1441
|
+
rollback to a single `call!` without involving an outer flow:
|
|
1442
|
+
|
|
1443
|
+
```ruby
|
|
1444
|
+
class CreateUserWithProfileInline < Micro::Case
|
|
1445
|
+
def call!
|
|
1446
|
+
transaction {
|
|
1447
|
+
create_user
|
|
1448
|
+
.then(:create_profile)
|
|
1449
|
+
}
|
|
1450
|
+
end
|
|
1451
|
+
end
|
|
1452
|
+
```
|
|
1453
|
+
|
|
1454
|
+
This is appropriate when the host case is invoked on its own (not
|
|
1455
|
+
inside a flow) and you still want the internal flow to be atomic. The
|
|
1456
|
+
`transaction` block returns the chain's `Result` as-is, so you can
|
|
1457
|
+
keep composing with `Result#then` after it.
|
|
1458
|
+
|
|
1459
|
+
The two approaches **compose**. If you put `CreateUserWithProfileInline`
|
|
1460
|
+
(already using inline `transaction { ... }`) inside an outer
|
|
1461
|
+
`transaction: true` flow, ActiveRecord joins the inner transaction
|
|
1462
|
+
into the outer one by default — an outer failure rolls back the
|
|
1463
|
+
inner's writes too. See the **Behavior notes** below for the full
|
|
1464
|
+
nesting / flatten rules.
|
|
1465
|
+
|
|
1466
|
+
##### Behavior notes
|
|
1467
|
+
|
|
1468
|
+
- **Result is unaffected.** `transaction: true` only affects database
|
|
1469
|
+
side-effects. `result.data`, `result.type`, `result.transitions` and
|
|
1470
|
+
`result.accessible_attributes` are identical to those of an equivalent
|
|
1471
|
+
non-transactional flow.
|
|
1472
|
+
- **`Flow` instances get flattened.** `Micro::Cases.flow([inner_flow,
|
|
1473
|
+
Other])` flattens `inner_flow` into its leaf steps, which means a
|
|
1474
|
+
transactional `Flow` instance passed this way **loses its
|
|
1475
|
+
transaction**. Wrap reusable transactional flows in a use case class
|
|
1476
|
+
(the snippet above) to preserve their transaction when nested.
|
|
1477
|
+
- **Nested transactions join the outer one.** When a transactional flow
|
|
1478
|
+
is nested inside another transactional flow, ActiveRecord joins them
|
|
1479
|
+
by default (no `requires_new: true`). A failure anywhere in the chain
|
|
1480
|
+
rolls back **everything** written inside the outermost transaction —
|
|
1481
|
+
including writes performed by the inner flow.
|
|
1482
|
+
- **A non-transactional outer commits the inner.** If the outer flow is
|
|
1483
|
+
not transactional and the inner transactional flow succeeds, the
|
|
1484
|
+
inner's writes are committed at the end of the inner step. A failure
|
|
1485
|
+
in a later (non-transactional) step **does not** undo those writes.
|
|
1486
|
+
- **Plain `Micro::Cases.flow(transaction: true, ...)` re-raises
|
|
1487
|
+
exceptions.** The transaction still rolls back, but the caller has to
|
|
1488
|
+
rescue. Use `Micro::Cases.safe_flow(transaction: true, ...)` (or the
|
|
1489
|
+
class-level form with `Micro::Case::Safe`) to capture the exception
|
|
1490
|
+
as a `:exception` failure result.
|
|
1491
|
+
|
|
1492
|
+
[⬆️ Back to Top](#table-of-contents-)
|
|
1493
|
+
|
|
929
1494
|
### `Micro::Case::Strict` - What is a strict use case?
|
|
930
1495
|
|
|
931
1496
|
Answer: it is a kind of use case that will require all the keywords (attributes) on its initialization.
|