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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4e57208798d170fbccde7b54f5beb6220cecbfd70c3983fedcb701070a73c7bc
4
- data.tar.gz: e4a0c472d1fd6edacf6ac3495d902ca851a88f5420360b708bdf2f204818cc1a
3
+ metadata.gz: 5539d3b5cac24a50728c4dd8285e2f36c4a155f344a5f95763b8b02afaa64505
4
+ data.tar.gz: 794905f57057a6f198200bbd2b5c3c958440b3fa3154366a636025553a084553
5
5
  SHA512:
6
- metadata.gz: 154ce36f3efefa81e8e0dcc191cfd25396a7ad0b431f6d2a8190974036ed21f70faa617cc6834569f7fe02855b712abb33dca1da44285913c578ff73cc7cdfcc
7
- data.tar.gz: 20f5e74caf9b58ae935bb39dc9780fd7bcfe21e68f4adbec162cff797284939067b2e254e790a2eb17ac8de3ce7a30460af5908170ba136730fa94b784f84a87
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.5.0 | https://github.com/serradura/u-case/blob/v5.x/README.md
30
+ 5.6.0 | https://github.com/serradura/u-case/blob/v5.x/README.md
31
31
  4.5.1 | https://github.com/serradura/u-case/blob/v4.x/README.md
32
32
 
33
33
  > **Note:** Você entende português? 🇧🇷&nbsp;🇵🇹 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.5.0 | v5.x | >= 2.7 | >= 6.0 | >= 2.8, < 4.0 |
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.