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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5539d3b5cac24a50728c4dd8285e2f36c4a155f344a5f95763b8b02afaa64505
|
|
4
|
+
data.tar.gz: 794905f57057a6f198200bbd2b5c3c958440b3fa3154366a636025553a084553
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fc924d76d4e1dbc1dc1a2567688a881a17b726b74210ad57277af2fdd7456e02358501d2f34171d0a027227f5e99a2853aad82f64bca2007e18fefc518e3e622
|
|
7
|
+
data.tar.gz: e20f832a0b48456a653e48710c2444c8649dce2a68477c4299b856b23939074d01457215e6c3290c1e417cd38724318289f27055505d1b2865b90ca4c15fee0d
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
> **Note:** This gem was originally published as `u-service` (versions 0.1.0 – 1.0.0) and renamed to `u-case` starting with `u-case 1.0.0` on 2019-09-15.
|
|
9
9
|
|
|
10
|
+
## [5.6.0] - 2026-05-24
|
|
11
|
+
### Added
|
|
12
|
+
- `Micro::Cases.flow(transaction: true, steps: [...])` and `Micro::Cases.safe_flow(transaction: true, steps: [...])` to wrap an entire flow in an `ActiveRecord::Base.transaction`. Any step that returns a failure (or, in `safe_flow`, raises) triggers an `ActiveRecord::Rollback`. The same kwargs are accepted by the class-level macro: `class MyCase < Micro::Case; flow(transaction: true, steps: [...]); end` (closes #44).
|
|
13
|
+
- `Micro::Cases::Error::TransactionAdapterMissing`, raised on the first call when `transaction: true` is used without `ActiveRecord::Base` loaded. The gem still does **not** require `active_record` automatically — applications must load it themselves.
|
|
14
|
+
- Three new methods on `Micro::Case::Check` — `flow_steps_kwarg!`, `transaction_kwarg!` and `activerecord_loaded!` — so the transaction-flow validation participates in `config.disable_runtime_checks = true`. All inline `raise ArgumentError` / `raise Error::TransactionAdapterMissing` sites in `lib/micro/cases.rb`, `lib/micro/case.rb` and `lib/micro/cases/flow.rb` now route through `Micro::Case.check`, matching the convention introduced in 5.4.0.
|
|
15
|
+
- Multi-database support for transactions. Use cases can declare which ActiveRecord class should own their transactions with the new `transaction with: SomeRecord` class macro (inherited like `flow` / `attributes`); the inline `Micro::Case#transaction(with:)` helper and the flow-level `transaction: { with: SomeRecord }` kwarg share the same `with:` vocabulary. `transaction: true` remains the "use the default" shortcut. A new `Micro::Case.config.default_transaction_class { ApplicationRecord }` callback (block or lambda) lets Rails apps configure the abstract record once in an initializer; the default is `-> { ::ActiveRecord::Base }`. Two new checks (`transaction_owner!`, `transaction_class_callback!`) route the new validations through `Micro::Case::Check`. Resolution order at transaction-open time: call-site `with:` override > host case's `transaction with:` macro > global callback.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- `Micro::Case#transaction` instance helper signature changed from `transaction(adapter = :activerecord)` to `transaction(adapter = nil, with: nil)`. The pre-5.6.0 form `transaction(:activerecord) { ... }` keeps working as an alias for `transaction { ... }`; any other positional value raises `ArgumentError` (the legacy helper only accepted `:activerecord`).
|
|
19
|
+
- Transaction owners (`with:` on the inline helper, on the class macro, and on the flow `transaction:` kwarg) must be subclasses of `ActiveRecord::Base`. Non-AR classes are rejected with `ArgumentError` — the gem's rollback signaling hardcodes `ActiveRecord::Rollback`, so non-AR transaction objects (Sequel, custom adapters) are explicitly out of scope. The class-macro validation runs at class-eval time when AR is already loaded; otherwise it defers to runtime so initializer load order doesn't break declarations.
|
|
20
|
+
- `Micro::Cases.flow([], steps: [...])` and `safe_flow([], steps: [...])` now treat an empty positional array as "no positional collection" instead of raising the "both provided" error.
|
|
21
|
+
- `Check::Disabled#transaction_kwarg!` now returns `nil` (no transaction) for unrecognized inputs instead of silently coercing them to `true`. A typo under `disable_runtime_checks = true` therefore stays non-transactional rather than upgrading to a real transaction against the default class.
|
|
22
|
+
- READMEs (EN + pt-BR) now document `Micro::Case#transaction` (the inline `transaction { ... }` helper available inside `call!`) and the new flow-level `transaction:` kwarg, including behavior notes for nested transactions (AR-joined semantics), the `Flow`-instance flattening footgun, and the difference between plain and safe transactional flows on exceptions.
|
|
23
|
+
- READMEs (EN + pt-BR) now describe **internal steps** — `Result#then(:symbol)` / `Result#then(method(:name))` / `Result#then(-> { })` / `|` — as u-case's third way of composing a flow, alongside `Micro::Cases.flow` and the class-level `flow` macro. The new section spells out the data-flow contract (each link's `Success` result becomes the next link's kwargs), the transition recording behavior, and the fact that internal steps are fully composable inside outer flows and transactional flows.
|
|
24
|
+
- Transaction composition matrix test suite (`test/micro/cases/flow/transaction_composition_matrix_test.rb`) crossing all 8 wrappers (4 non-tx × 4 tx) at level 1 and level 2 of nesting, plus deep-rollback cases, behavioral parity assertions (tx vs non-tx `result.data` / `transitions` / `accessible_attributes` are equal), and `Result#then` accumulation across a tx boundary.
|
|
25
|
+
- Internal-steps-in-flows test suite (`test/micro/cases/flow/internal_steps_in_flows_test.rb`) that drops symbol-, method- and lambda-based internal-step use cases into every flow wrapper (non-tx and tx) and asserts behavioral parity with the leaf-pair equivalent: accumulated `result.data`, total transition count (3 internal + 3 outer = 6 for a 5-step chain), interleaved transition order, and rollback of internal-step database writes when a `Failure` is returned from inside the internal chain under a transactional outer flow.
|
|
26
|
+
|
|
10
27
|
## [5.5.0] - 2026-05-24
|
|
11
28
|
### Added
|
|
12
29
|
- `Micro::Case.results { |on| ... }` macro to declare a results contract — the allowed `Success`/`Failure` types and the result keys each one requires. `Success(...)` / `Failure(...)` calls that use an undeclared type now raise `Micro::Case::Error::UnexpectedResultType`; calls missing a declared required key raise `Micro::Case::Error::MissingResultKeys`. Use cases without a `results` block keep their previous unrestricted behavior. The check routes through `Micro::Case::Check#results_contract!`, so it is also bypassed when `config.disable_runtime_checks = true` (closes #22). Carve-outs so contracts don't break neighbouring features:
|
|
@@ -478,6 +495,7 @@ First release under the `u-case` name (renamed from `u-service`).
|
|
|
478
495
|
- `Micro::Service::Result` with `Success`/`Failure` factories and helper methods for returning typed results from services.
|
|
479
496
|
- Runtime dependency on `u-attributes` for service input declaration.
|
|
480
497
|
|
|
498
|
+
[5.6.0]: https://github.com/serradura/u-case/compare/v5.5.0...v5.6.0
|
|
481
499
|
[5.5.0]: https://github.com/serradura/u-case/compare/v5.4.0...v5.5.0
|
|
482
500
|
[5.4.0]: https://github.com/serradura/u-case/compare/v5.3.1...v5.4.0
|
|
483
501
|
[5.3.1]: https://github.com/serradura/u-case/compare/v5.3.0...v5.3.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).
|
|
@@ -50,6 +50,7 @@ unreleased| https://github.com/serradura/u-case/blob/main/README.md
|
|
|
50
50
|
- [How to use the `Micro::Case::Result#then` method?](#how-to-use-the-microcaseresultthen-method)
|
|
51
51
|
- [What does happens when a `Micro::Case::Result#then` receives a block?](#what-does-happens-when-a-microcaseresultthen-receives-a-block)
|
|
52
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)
|
|
53
54
|
- [`Micro::Cases::Flow` - How to compose use cases?](#microcasesflow---how-to-compose-use-cases)
|
|
54
55
|
- [Is it possible to compose a flow with other flows?](#is-it-possible-to-compose-a-flow-with-other-flows)
|
|
55
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)
|
|
@@ -57,6 +58,7 @@ unreleased| https://github.com/serradura/u-case/blob/main/README.md
|
|
|
57
58
|
- [`Micro::Case::Result#transitions` schema](#microcaseresulttransitions-schema)
|
|
58
59
|
- [Is it possible disable the `Micro::Case::Result#transitions`?](#is-it-possible-disable-the-microcaseresulttransitions)
|
|
59
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)
|
|
60
62
|
- [`Micro::Case::Strict` - What is a strict use case?](#microcasestrict---what-is-a-strict-use-case)
|
|
61
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)
|
|
62
64
|
- [`Micro::Cases::Safe::Flow`](#microcasessafeflow)
|
|
@@ -91,7 +93,7 @@ unreleased| https://github.com/serradura/u-case/blob/main/README.md
|
|
|
91
93
|
| u-case | branch | ruby | activemodel | u-attributes |
|
|
92
94
|
| ---------------- | ------ | -------- | -------------- | -------------- |
|
|
93
95
|
| unreleased | main | >= 2.7 | >= 6.0 | >= 2.8, < 4.0 |
|
|
94
|
-
| 5.
|
|
96
|
+
| 5.6.0 | v5.x | >= 2.7 | >= 6.0 | >= 2.8, < 4.0 |
|
|
95
97
|
| 5.1.0 | v5.x | >= 2.7 | >= 6.0 | >= 2.7, < 4.0 |
|
|
96
98
|
| 4.5.1 | v4.x | >= 2.2.0 | >= 3.2, <= 8.1 | >= 2.7, < 3.0 |
|
|
97
99
|
|
|
@@ -632,6 +634,235 @@ Todo::FindAllForUser
|
|
|
632
634
|
|
|
633
635
|
[⬆️ Back to Top](#table-of-contents-)
|
|
634
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
|
+
|
|
635
866
|
### `Micro::Cases::Flow` - How to compose use cases?
|
|
636
867
|
|
|
637
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.
|
|
@@ -979,6 +1210,287 @@ result[:number] # "8"
|
|
|
979
1210
|
|
|
980
1211
|
[⬆️ Back to Top](#table-of-contents-)
|
|
981
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
|
+
|
|
982
1494
|
### `Micro::Case::Strict` - What is a strict use case?
|
|
983
1495
|
|
|
984
1496
|
Answer: it is a kind of use case that will require all the keywords (attributes) on its initialization.
|