u-case 5.5.0 → 5.7.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: bcab602e98bfb71b2bb2439dd162b3401098bfa41b8234ad39a1a7dd6dad5e6a
4
+ data.tar.gz: 4d975264e714cb751d595bc945915ac1273d486c0dd5ee6ce156076cbe858ba2
5
5
  SHA512:
6
- metadata.gz: 154ce36f3efefa81e8e0dcc191cfd25396a7ad0b431f6d2a8190974036ed21f70faa617cc6834569f7fe02855b712abb33dca1da44285913c578ff73cc7cdfcc
7
- data.tar.gz: 20f5e74caf9b58ae935bb39dc9780fd7bcfe21e68f4adbec162cff797284939067b2e254e790a2eb17ac8de3ce7a30460af5908170ba136730fa94b784f84a87
6
+ metadata.gz: 3f5107a4329d04aab99e0458fa5784705c78cc648fdaee53be172b306501f824ccf3af5f8c70629b2ad39289745cb5f79ebb39fa95958cc1820d20913f65111a
7
+ data.tar.gz: 29be61f84a1e4cbdb42f13bbcf72a8f4e3eaf7618300c0f169461adfc21b12a2ad9215e651bc6a9f366fc3658987d19ff3feadfec61609ff3e3d4860e6b7585d
data/CHANGELOG.md CHANGED
@@ -7,6 +7,28 @@ 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.7.0] - 2026-05-25
11
+ ### Added
12
+ - Pattern matching support on `Micro::Case::Result` via `#deconstruct` and `#deconstruct_keys` (closes #146). Purely additive — no existing API is changed or removed. `#deconstruct` returns `[status, type, data]` where `status` is `:success` or `:failure`, so array patterns like `in [:failure, :invalid_attributes, { invalid_attributes: errors }]` can use the status as a discriminant — mirroring how libraries with separate `Success`/`Failure` classes are pattern-matched, even though `Micro::Case::Result` is a single class. `#deconstruct_keys` exposes `:type`, `:data`, `:result` (alias of `:data` that matches the `Success(result: …)` creation-site vocabulary), `:use_case` and `:transitions` on every result; `:success` is present only on success results and `:failure` only on failure results, and both carry the result `type` symbol as their value so `in { failure: :invalid_attributes }` works. `#deconstruct_keys` honors Ruby's `keys` argument and only computes the requested entries (relevant for `:transitions`, which allocates a duped array).
13
+ - READMEs (EN + pt-BR) document the new pattern under the `Micro::Case::Result` section, including the key table, the `data:` / `result:` alias note, and the intentional shape difference between `#deconstruct` (`[status, type, data]`, used by pattern matching) and `#to_ary` (`[data, type]`, unchanged, used by multi-assignment).
14
+
15
+ ## [5.6.0] - 2026-05-24
16
+ ### Added
17
+ - `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).
18
+ - `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.
19
+ - 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.
20
+ - 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.
21
+
22
+ ### Changed
23
+ - `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`).
24
+ - 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.
25
+ - `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.
26
+ - `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.
27
+ - 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.
28
+ - 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.
29
+ - 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.
30
+ - 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.
31
+
10
32
  ## [5.5.0] - 2026-05-24
11
33
  ### Added
12
34
  - `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 +500,8 @@ First release under the `u-case` name (renamed from `u-service`).
478
500
  - `Micro::Service::Result` with `Success`/`Failure` factories and helper methods for returning typed results from services.
479
501
  - Runtime dependency on `u-attributes` for service input declaration.
480
502
 
503
+ [5.7.0]: https://github.com/serradura/u-case/compare/v5.6.0...v5.7.0
504
+ [5.6.0]: https://github.com/serradura/u-case/compare/v5.5.0...v5.6.0
481
505
  [5.5.0]: https://github.com/serradura/u-case/compare/v5.4.0...v5.5.0
482
506
  [5.4.0]: https://github.com/serradura/u-case/compare/v5.3.1...v5.4.0
483
507
  [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).
@@ -46,10 +46,12 @@ unreleased| https://github.com/serradura/u-case/blob/main/README.md
46
46
  - [How to use the result hooks?](#how-to-use-the-result-hooks)
47
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)
48
48
  - [Using decomposition to access the result data and type](#using-decomposition-to-access-the-result-data-and-type)
49
+ - [Using pattern matching to destructure a result](#using-pattern-matching-to-destructure-a-result)
49
50
  - [What happens if a result hook was declared multiple times?](#what-happens-if-a-result-hook-was-declared-multiple-times)
50
51
  - [How to use the `Micro::Case::Result#then` method?](#how-to-use-the-microcaseresultthen-method)
51
52
  - [What does happens when a `Micro::Case::Result#then` receives a block?](#what-does-happens-when-a-microcaseresultthen-receives-a-block)
52
53
  - [How to make attributes data injection using this feature?](#how-to-make-attributes-data-injection-using-this-feature)
54
+ - [Internal steps — building a flow inline inside `call!`](#internal-steps--building-a-flow-inline-inside-call)
53
55
  - [`Micro::Cases::Flow` - How to compose use cases?](#microcasesflow---how-to-compose-use-cases)
54
56
  - [Is it possible to compose a flow with other flows?](#is-it-possible-to-compose-a-flow-with-other-flows)
55
57
  - [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 +59,7 @@ unreleased| https://github.com/serradura/u-case/blob/main/README.md
57
59
  - [`Micro::Case::Result#transitions` schema](#microcaseresulttransitions-schema)
58
60
  - [Is it possible disable the `Micro::Case::Result#transitions`?](#is-it-possible-disable-the-microcaseresulttransitions)
59
61
  - [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)
62
+ - [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
63
  - [`Micro::Case::Strict` - What is a strict use case?](#microcasestrict---what-is-a-strict-use-case)
61
64
  - [`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
65
  - [`Micro::Cases::Safe::Flow`](#microcasessafeflow)
@@ -91,7 +94,7 @@ unreleased| https://github.com/serradura/u-case/blob/main/README.md
91
94
  | u-case | branch | ruby | activemodel | u-attributes |
92
95
  | ---------------- | ------ | -------- | -------------- | -------------- |
93
96
  | unreleased | main | >= 2.7 | >= 6.0 | >= 2.8, < 4.0 |
94
- | 5.5.0 | v5.x | >= 2.7 | >= 6.0 | >= 2.8, < 4.0 |
97
+ | 5.6.0 | v5.x | >= 2.7 | >= 6.0 | >= 2.8, < 4.0 |
95
98
  | 5.1.0 | v5.x | >= 2.7 | >= 6.0 | >= 2.7, < 4.0 |
96
99
  | 4.5.1 | v4.x | >= 2.2.0 | >= 3.2, <= 8.1 | >= 2.7, < 3.0 |
97
100
 
@@ -500,6 +503,54 @@ Double
500
503
 
501
504
  [⬆️ Back to Top](#table-of-contents-)
502
505
 
506
+ ##### Using pattern matching to destructure a result
507
+
508
+ `Micro::Case::Result` implements [`deconstruct`](https://docs.ruby-lang.org/en/3.4/syntax/pattern_matching_rdoc.html) and [`deconstruct_keys`](https://docs.ruby-lang.org/en/3.4/syntax/pattern_matching_rdoc.html), so Ruby's `case`/`in` pattern matching works out of the box (requires Ruby `>= 2.7`).
509
+
510
+ ```ruby
511
+ result = Divide.call(a: 10, b: 2)
512
+
513
+ case result
514
+ in { success: _, data: { number: Numeric => number } }
515
+ puts "got #{number}"
516
+ in { failure: :invalid_attributes, data: { invalid_attributes: errors } }
517
+ warn "bad input: #{errors.keys.join(", ")}"
518
+ in { failure: :exception, data: { exception: } }
519
+ warn "boom: #{exception.message}"
520
+ end
521
+ ```
522
+
523
+ The hash patterns expose these keys:
524
+
525
+ | Key | Present on | Value |
526
+ | --------------- | ----------------- | ------------------------------------------------------- |
527
+ | `success:` | success only | the result `type` (e.g. `:ok`) |
528
+ | `failure:` | failure only | the result `type` (e.g. `:invalid_attributes`) |
529
+ | `type:` | always | the result `type` |
530
+ | `data:` | always | the result `data` hash |
531
+ | `result:` | always | alias of `data:` (matches the `Success(result: …)` keyword used at the creation site) |
532
+ | `use_case:` | always | the use case instance that produced the result |
533
+ | `transitions:` | always | the result `transitions` array |
534
+
535
+ > **Note:** On the **reader** side, `Result#data` is also accessible as `Result#value` (existing alias). On the **pattern-matching** side, the `data:` key is also accessible as `result:` — both refer to the same payload.
536
+
537
+ `Result#deconstruct` returns a three-element array `[status, type, data]` where `status` is `:success` or `:failure`, so array patterns can use the status as a discriminant — mirroring how libraries with separate `Success`/`Failure` classes are pattern-matched, even though `Micro::Case::Result` is a single class:
538
+
539
+ ```ruby
540
+ case result
541
+ in [:success, :ok, { number: Integer => n }]
542
+ n
543
+ in [:failure, :invalid_attributes, { invalid_attributes: errors }]
544
+ # ...
545
+ in [:failure, :exception, { exception: }]
546
+ # ...
547
+ end
548
+ ```
549
+
550
+ > **Note:** `Result#to_ary` is unchanged and still returns `[data, type]` (used by multi-assignment, e.g. `data, type = result`). Ruby's pattern matching uses `#deconstruct`, so the two hooks intentionally return different shapes.
551
+
552
+ [⬆️ Back to Top](#table-of-contents-)
553
+
503
554
  #### What happens if a result hook was declared multiple times?
504
555
 
505
556
  Answer: The hook always will be triggered if it matches the result type.
@@ -632,6 +683,235 @@ Todo::FindAllForUser
632
683
 
633
684
  [⬆️ Back to Top](#table-of-contents-)
634
685
 
686
+ #### Internal steps — building a flow inline inside `call!`
687
+
688
+ `Result#then` (and its `|` pipe alias) is u-case's **third way of
689
+ composing a flow**, side by side with `Micro::Cases.flow(...)` and the
690
+ class-level `flow ...` macro. Instead of wiring sibling use cases
691
+ together, you keep the chain *inside* a single use case's `call!`:
692
+ each link is a method, lambda or another use case class; each link
693
+ returns a `Micro::Case::Result`; each link's `Success` data becomes
694
+ the next link's keyword arguments; and each link contributes a row to
695
+ `result.transitions` — just like a step in a top-level flow.
696
+
697
+ ##### What `Result#then` (and `|`) accept
698
+
699
+ | Argument shape | Example |
700
+ | --- | --- |
701
+ | `Symbol` (method name) | `result.then(:sum_a_and_b)` |
702
+ | Bound `Method` object | `result.then(method(:sum_a_and_b))` |
703
+ | `Lambda` / `Proc` | `result.then(-> data { sum_a_and_b(**data) })` |
704
+ | Use case class | `result.then(SumHalf)` |
705
+ | `Symbol` + Hash defaults | `result.then(:add, number: 3)` |
706
+ | Block | `result.then { \|r\| r.success? ? r[:sum] : 0 }` |
707
+
708
+ The connecting method **must** return a `Micro::Case::Result`. Anything
709
+ else raises `Micro::Case::Error::UnexpectedResult` — for example a
710
+ method that returns a plain `Hash` will be rejected with a message like
711
+ `MyCase#method(:foo) must return an instance of Micro::Case::Result`.
712
+
713
+ ##### A minimal example
714
+
715
+ ```ruby
716
+ class SumHalf < Micro::Case
717
+ attribute :sum
718
+
719
+ def call!
720
+ Success :third_sum, result: { sum: sum + 0.5 }
721
+ end
722
+ end
723
+
724
+ class DoSomeSum < Micro::Case
725
+ attributes :a, :b
726
+
727
+ def call!
728
+ validate_numbers
729
+ .then(:sum_a_and_b)
730
+ .then(:add, number: 3)
731
+ .then(SumHalf)
732
+ end
733
+
734
+ private
735
+
736
+ def validate_numbers
737
+ Kind.of?(Numeric, a, b) ? Success(:valid) : Failure()
738
+ end
739
+
740
+ def sum_a_and_b
741
+ Success :first_sum, result: { sum: a + b }
742
+ end
743
+
744
+ def add(sum:, number:, **)
745
+ Success :second_sum, result: { sum: sum + number }
746
+ end
747
+ end
748
+
749
+ result = DoSomeSum.call(a: 1, b: 2)
750
+
751
+ result.success? # true
752
+ result.data # { sum: 6.5 }
753
+ result.transitions # 4 entries — see below
754
+ ```
755
+
756
+ `result.transitions` for the call above:
757
+
758
+ ```ruby
759
+ [
760
+ { use_case: { class: DoSomeSum, attributes: { a: 1, b: 2 } },
761
+ success: { type: :valid, result: { valid: true } },
762
+ accessible_attributes: [:a, :b] },
763
+
764
+ { use_case: { class: DoSomeSum, attributes: { a: 1, b: 2 } },
765
+ success: { type: :first_sum, result: { sum: 3 } },
766
+ accessible_attributes: [:a, :b, :valid] },
767
+
768
+ { use_case: { class: DoSomeSum, attributes: { a: 1, b: 2 } },
769
+ success: { type: :second_sum, result: { sum: 6 } },
770
+ accessible_attributes: [:a, :b, :valid, :number, :sum] },
771
+
772
+ { use_case: { class: SumHalf, attributes: { sum: 6 } },
773
+ success: { type: :third_sum, result: { sum: 6.5 } },
774
+ accessible_attributes: [:a, :b, :valid, :number, :sum] }
775
+ ]
776
+ ```
777
+
778
+ Symbol-, method- and lambda-based links all run **as the host use
779
+ case**, so the first three transitions report `class: DoSomeSum`. Only
780
+ the `SumHalf` link, which is another use case class, contributes a
781
+ transition with a different `use_case.class`. The `accessible_attributes`
782
+ grows as each link's `Success` output is merged into the running data.
783
+
784
+ ##### The `|` (pipe) alias
785
+
786
+ `|` is sugar for `.then(...)`. The previous example becomes:
787
+
788
+ ```ruby
789
+ def call!
790
+ validate_numbers | :sum_a_and_b | :add | SumHalf
791
+ end
792
+ ```
793
+
794
+ Both forms produce identical `result.data` and `result.transitions`.
795
+
796
+ > **Elixir-style chains with `it` (Ruby ≥ 3.4):** because Ruby 3.4
797
+ > exposes `it` as the implicit first parameter of a block/lambda body,
798
+ > you can write a chain that reads almost exactly like Elixir's
799
+ > `|>` pipe. Each lambda receives the accumulated data hash as `it`
800
+ > and must still terminate in a `Success(...)` / `Failure(...)` call:
801
+ >
802
+ > ```ruby
803
+ > def call!
804
+ > validate_something \
805
+ > | -> { do_something_with(**it) } \
806
+ > | -> { and_another_thing_with(**it) }
807
+ > end
808
+ > ```
809
+ >
810
+ > On Ruby 2.7 – 3.3 (where `it` is just an undefined identifier),
811
+ > use the portable explicit form `->(data) { do_something_with(**data) }`
812
+ > shown in the next section.
813
+
814
+ ##### Lambda / `Method` forms
815
+
816
+ Lambdas (and bound `Method` objects) receive the accumulated data
817
+ **positionally** as a single Hash:
818
+
819
+ ```ruby
820
+ def call!
821
+ validate_numbers
822
+ .then(method(:sum_a_and_b))
823
+ .then(->(data) { add(**data, number: 3) })
824
+ .then(SumHalf)
825
+ end
826
+ ```
827
+
828
+ ##### Failure short-circuits the chain
829
+
830
+ Returning `Failure(...)` from any link halts the rest of the chain
831
+ immediately — exactly like a step in a top-level flow returning a
832
+ failure. The remaining `.then(...)` / `|` links are not invoked, and
833
+ the final `result` is the failure:
834
+
835
+ ```ruby
836
+ DoSomeSum.call(a: 1, b: '2')
837
+
838
+ # validate_numbers returns Failure() → :sum_a_and_b, :add and SumHalf
839
+ # never run. result.failure? == true, result.transitions has 1 entry.
840
+ ```
841
+
842
+ ##### Using an internal-step case inside an outer flow
843
+
844
+ A use case that composes internally with `.then(...)` is just a use
845
+ case, so you can drop it into any flow constructor:
846
+
847
+ ```ruby
848
+ SignUp = Micro::Cases.flow([
849
+ NormalizeParams,
850
+ DoSomeSum, # ← uses .then(:method) internally
851
+ EnqueueIndexingJob
852
+ ])
853
+ ```
854
+
855
+ The host class's internal transitions are interleaved with the outer
856
+ flow's leaf transitions in execution order. If `DoSomeSum` produces 4
857
+ internal transitions and the outer flow has 2 other leaf steps, the
858
+ final `result.transitions` has 6 entries.
859
+
860
+ ##### Internal steps **without** transactions
861
+
862
+ By default — i.e. when neither the host class nor the outer flow uses
863
+ `transaction: true` — internal steps behave like any other code in
864
+ `call!`: side-effects made by earlier links **persist** even if a
865
+ later link returns `Failure`. The chain is interrupted, but anything
866
+ already written to the database stays written:
867
+
868
+ ```ruby
869
+ class CreateUserWithProfileInline < Micro::Case
870
+ attributes :name, :info
871
+
872
+ def call!
873
+ create_user
874
+ .then(:create_profile)
875
+ end
876
+
877
+ private
878
+
879
+ def create_user
880
+ user = User.create(name: name)
881
+ Success result: { user: user }
882
+ end
883
+
884
+ def create_profile(user:, **)
885
+ profile = UserProfile.create(user_id: user.id, info: info)
886
+ return Failure(:invalid_profile) if profile.errors.any?
887
+
888
+ Success result: { user: user, profile: profile }
889
+ end
890
+ end
891
+
892
+ CreateUserWithProfileInline.call(name: 'Rodrigo', info: '')
893
+ # create_user already INSERTed the user row; create_profile failed.
894
+ # user is persisted; profile is not. No automatic rollback.
895
+ ```
896
+
897
+ If you need the partial side-effects to be undone, wrap the chain in
898
+ a transaction. Because internal steps are just another way of
899
+ expressing a flow (an *internal* flow), the transactional story is
900
+ exactly the one already documented in
901
+ [How to run a use case or flow inside a database transaction?](#how-to-run-a-use-case-or-flow-inside-a-database-transaction)
902
+ below — the "Internal-step flows under transactions" subsection
903
+ there walks through both the inline `transaction { ... }` form and
904
+ the `transaction: true` flow form for an internal-step host case.
905
+
906
+ > **Note:** See `test/micro/case/internal_steps/with_symbols_test.rb`,
907
+ > `with_methods_test.rb` and `with_lambdas_test.rb` for full examples
908
+ > of each form, and
909
+ > `test/micro/cases/flow/internal_steps_in_flows_test.rb` for the
910
+ > interaction with flows and transactions (accumulation, transitions,
911
+ > rollback at every nesting level).
912
+
913
+ [⬆️ Back to Top](#table-of-contents-)
914
+
635
915
  ### `Micro::Cases::Flow` - How to compose use cases?
636
916
 
637
917
  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 +1259,287 @@ result[:number] # "8"
979
1259
 
980
1260
  [⬆️ Back to Top](#table-of-contents-)
981
1261
 
1262
+ #### How to run a use case or flow inside a database transaction?
1263
+
1264
+ `u-case` ships with two complementary helpers for wrapping work in an
1265
+ `ActiveRecord::Base.transaction`. Both opt-in — `active_record` is **not**
1266
+ required by the gem, so you need to load ActiveRecord yourself (Rails
1267
+ applications already do).
1268
+
1269
+ ##### `Micro::Case#transaction` — inline transactions inside `call!`
1270
+
1271
+ `Micro::Case#transaction` (and `Micro::Case::Safe#transaction`) is a private
1272
+ instance helper that wraps a block in a database transaction and issues an
1273
+ `ActiveRecord::Rollback` whenever the block's result is a `Failure`. The
1274
+ original result is returned either way, so you can keep chaining with
1275
+ `Result#then`:
1276
+
1277
+ ```ruby
1278
+ class CreateUserWithAProfile < Micro::Case
1279
+ def call!
1280
+ transaction {
1281
+ call(CreateUser).then(CreateUserProfile)
1282
+ }
1283
+ end
1284
+ end
1285
+ ```
1286
+
1287
+ If the block returns a failure (or raises), every row written inside the
1288
+ block is rolled back. The helper accepts an optional `with:` kwarg to pick
1289
+ the ActiveRecord class on which `.transaction` is opened — useful for
1290
+ multi-database Rails apps (`ApplicationRecord`, `AnalyticsRecord`,
1291
+ `BillingRecord`, …):
1292
+
1293
+ ```ruby
1294
+ class CreateAuditEntry < Micro::Case
1295
+ def call!
1296
+ transaction(with: AnalyticsRecord) {
1297
+ call(WriteAuditLog).then(BumpCounter)
1298
+ }
1299
+ end
1300
+ end
1301
+ ```
1302
+
1303
+ When `with:` is omitted, the helper falls back to the class macro
1304
+ (`transaction with: …`) and then to the global default callback (see below),
1305
+ which ships as `-> { ::ActiveRecord::Base }`.
1306
+
1307
+ > **Note:** any class passed via `with:` (here, on the class macro, or on a
1308
+ > flow's `transaction:` kwarg) **must be a subclass of `ActiveRecord::Base`**.
1309
+ > Non-AR classes are rejected with `ArgumentError`. The class-macro
1310
+ > validation runs at class-eval time when ActiveRecord is already loaded
1311
+ > (typical Rails app); otherwise it's deferred to runtime, so initializer
1312
+ > load order doesn't break declarations.
1313
+
1314
+ > **Backward compatibility:** the pre-5.6.0 positional form
1315
+ > `transaction(:activerecord) { ... }` still works as an alias for
1316
+ > `transaction { ... }`. Any other positional value raises `ArgumentError` —
1317
+ > the legacy helper accepted only `:activerecord`.
1318
+
1319
+ ##### `transaction with: …` — declaring the default for a case
1320
+
1321
+ A class macro lets a case declare which ActiveRecord class should own its
1322
+ transactions, so neither the inline helper nor any flow that wraps the
1323
+ case needs to spell it out at every call site. The declaration is inherited
1324
+ by subclasses:
1325
+
1326
+ ```ruby
1327
+ class ApplicationUseCase < Micro::Case
1328
+ transaction with: ApplicationRecord
1329
+ end
1330
+
1331
+ class CreateUserWithAProfile < ApplicationUseCase
1332
+ flow(transaction: true, steps: [CreateUser, CreateUserProfile])
1333
+ # transaction: true resolves to ApplicationRecord because that's
1334
+ # what the host class declared via `transaction with:`.
1335
+ end
1336
+
1337
+ class BillingCase < ApplicationUseCase
1338
+ transaction with: BillingRecord
1339
+ # overrides the inherited declaration for this branch of the tree
1340
+ end
1341
+ ```
1342
+
1343
+ ##### `Micro::Cases.flow(transaction: …, steps: [...])` — flow-level transactions
1344
+
1345
+ Pass `transaction:` together with `steps:` to wrap an entire flow in a
1346
+ single transaction. If any step returns a failure (or raises, in a
1347
+ `safe_flow`), every database write performed during the flow is rolled back.
1348
+ The kwarg accepts three forms:
1349
+
1350
+ ```ruby
1351
+ # Use the class-level macro (if the host case declared one) or the
1352
+ # global default (`ActiveRecord::Base` unless configured otherwise).
1353
+ Micro::Cases.flow(transaction: true, steps: [CreateUser, CreateUserProfile])
1354
+
1355
+ # Pick an explicit ActiveRecord class for this flow only — same `with:`
1356
+ # vocabulary as the inline helper and the class macro.
1357
+ Micro::Cases.flow(transaction: { with: AnalyticsRecord }, steps: [
1358
+ WriteAuditLog,
1359
+ BumpCounter
1360
+ ])
1361
+
1362
+ # safe_flow rolls back on failures AND on unexpected exceptions
1363
+ Micro::Cases.safe_flow(transaction: { with: ApplicationRecord }, steps: [
1364
+ CreateUser,
1365
+ CreateUserProfile
1366
+ ])
1367
+
1368
+ # Class-level form
1369
+ class CreateUserWithAProfile < Micro::Case
1370
+ flow(transaction: true, steps: [CreateUser, CreateUserProfile])
1371
+ end
1372
+ ```
1373
+
1374
+ To nest a transactional flow inside another flow, wrap it in a use case
1375
+ class — `Micro::Cases.flow([...])` flattens `Flow` instances passed as
1376
+ steps, but does **not** flatten classes:
1377
+
1378
+ ```ruby
1379
+ class CreateUserAndProfile < Micro::Case
1380
+ flow(transaction: true, steps: [CreateUser, CreateUserProfile])
1381
+ end
1382
+
1383
+ SignUpFlow = Micro::Cases.flow([
1384
+ NormalizeParams,
1385
+ ValidatePassword,
1386
+ CreateUserAndProfile,
1387
+ EnqueueIndexingJob
1388
+ ])
1389
+ ```
1390
+
1391
+ If `transaction: true` is used while `ActiveRecord::Base` is not loaded the
1392
+ flow raises `Micro::Cases::Error::TransactionAdapterMissing` on the first
1393
+ call so the misconfiguration surfaces immediately. Passing `transaction: {
1394
+ with: SomeClass }` skips this check — `SomeClass` is trusted to respond
1395
+ to `.transaction`.
1396
+
1397
+ ##### `config.default_transaction_class { … }` — global default
1398
+
1399
+ For Rails apps that use a single abstract record (`ApplicationRecord`),
1400
+ configure it once in an initializer instead of declaring it on every case
1401
+ or flow:
1402
+
1403
+ ```ruby
1404
+ # config/initializers/u_case.rb
1405
+ Micro::Case.config do |config|
1406
+ config.default_transaction_class { ApplicationRecord }
1407
+ end
1408
+ ```
1409
+
1410
+ The callback (block or lambda) is invoked **every time** a transaction
1411
+ opens — no memoization — so it's safe to make the return value depend on
1412
+ runtime state (per-tenant routing, etc.). The default is
1413
+ `-> { ::ActiveRecord::Base }`. Resolution order, when a transaction opens:
1414
+
1415
+ 1. **Call-site override.** `transaction: { with: X }` on a flow kwarg, or
1416
+ `transaction(with: X) { ... }` on the inline helper.
1417
+ 2. **Host case's `transaction with: X` macro** (walks ancestors).
1418
+ 3. **`Micro::Case.config.default_transaction_class.call`** — the global
1419
+ callback (defaults to `ActiveRecord::Base`).
1420
+
1421
+ A non-callable assignment to `default_transaction_class=` raises
1422
+ `ArgumentError` at config time so typos like `config.default_transaction_class
1423
+ = 'ApplicationRecord'` fail loudly instead of crashing the first transaction.
1424
+
1425
+ ##### Internal-step flows under transactions
1426
+
1427
+ [Internal steps](#internal-steps--building-a-flow-inline-inside-call)
1428
+ (the `Result#then(:symbol)` / `|` form built inline inside a single
1429
+ `call!`) are u-case's third way of composing a flow — an *internal*
1430
+ flow. By default an internal flow has **no transactional rollback**:
1431
+ side-effects from earlier `.then(:method)` links persist even when a
1432
+ later link returns `Failure`.
1433
+
1434
+ There are two natural ways to give an internal flow transactional
1435
+ rollback. Both reuse the helpers already covered above:
1436
+
1437
+ **1. Wrap the host case in a `transaction: true` flow.** This is the
1438
+ recommended way once the host case is composed with the rest of the
1439
+ pipeline. The transaction spans the whole flow call, so a `Failure`
1440
+ *anywhere* — including from any internal `.then(:method)` link — rolls
1441
+ back every database write performed during that call:
1442
+
1443
+ ```ruby
1444
+ class CreateUserWithProfileInline < Micro::Case
1445
+ attributes :name, :info
1446
+
1447
+ def call!
1448
+ create_user
1449
+ .then(:create_profile)
1450
+ end
1451
+
1452
+ private
1453
+
1454
+ def create_user
1455
+ user = User.create(name: name)
1456
+ Success result: { user: user }
1457
+ end
1458
+
1459
+ def create_profile(user:, **)
1460
+ profile = UserProfile.create(user_id: user.id, info: info)
1461
+ return Failure(:invalid_profile) if profile.errors.any?
1462
+
1463
+ Success result: { user: user, profile: profile }
1464
+ end
1465
+ end
1466
+
1467
+ SignUp = Micro::Cases.flow(transaction: true, steps: [
1468
+ NormalizeParams,
1469
+ CreateUserWithProfileInline, # ← internal failure now rolls back
1470
+ EnqueueIndexingJob
1471
+ ])
1472
+
1473
+ # Or class-level:
1474
+ class SignUp < Micro::Case
1475
+ flow(transaction: true, steps: [
1476
+ NormalizeParams,
1477
+ CreateUserWithProfileInline,
1478
+ EnqueueIndexingJob
1479
+ ])
1480
+ end
1481
+ ```
1482
+
1483
+ If `create_profile` (the internal `.then(:create_profile)` link)
1484
+ returns `Failure(:invalid_profile)`, the `User` row inserted earlier
1485
+ by `create_user` is rolled back as part of the same
1486
+ `ActiveRecord::Base.transaction`. The result still surfaces the
1487
+ failure type and the partial transitions, but no row is left behind.
1488
+
1489
+ **2. Use the inline `Micro::Case#transaction` helper** to scope the
1490
+ rollback to a single `call!` without involving an outer flow:
1491
+
1492
+ ```ruby
1493
+ class CreateUserWithProfileInline < Micro::Case
1494
+ def call!
1495
+ transaction {
1496
+ create_user
1497
+ .then(:create_profile)
1498
+ }
1499
+ end
1500
+ end
1501
+ ```
1502
+
1503
+ This is appropriate when the host case is invoked on its own (not
1504
+ inside a flow) and you still want the internal flow to be atomic. The
1505
+ `transaction` block returns the chain's `Result` as-is, so you can
1506
+ keep composing with `Result#then` after it.
1507
+
1508
+ The two approaches **compose**. If you put `CreateUserWithProfileInline`
1509
+ (already using inline `transaction { ... }`) inside an outer
1510
+ `transaction: true` flow, ActiveRecord joins the inner transaction
1511
+ into the outer one by default — an outer failure rolls back the
1512
+ inner's writes too. See the **Behavior notes** below for the full
1513
+ nesting / flatten rules.
1514
+
1515
+ ##### Behavior notes
1516
+
1517
+ - **Result is unaffected.** `transaction: true` only affects database
1518
+ side-effects. `result.data`, `result.type`, `result.transitions` and
1519
+ `result.accessible_attributes` are identical to those of an equivalent
1520
+ non-transactional flow.
1521
+ - **`Flow` instances get flattened.** `Micro::Cases.flow([inner_flow,
1522
+ Other])` flattens `inner_flow` into its leaf steps, which means a
1523
+ transactional `Flow` instance passed this way **loses its
1524
+ transaction**. Wrap reusable transactional flows in a use case class
1525
+ (the snippet above) to preserve their transaction when nested.
1526
+ - **Nested transactions join the outer one.** When a transactional flow
1527
+ is nested inside another transactional flow, ActiveRecord joins them
1528
+ by default (no `requires_new: true`). A failure anywhere in the chain
1529
+ rolls back **everything** written inside the outermost transaction —
1530
+ including writes performed by the inner flow.
1531
+ - **A non-transactional outer commits the inner.** If the outer flow is
1532
+ not transactional and the inner transactional flow succeeds, the
1533
+ inner's writes are committed at the end of the inner step. A failure
1534
+ in a later (non-transactional) step **does not** undo those writes.
1535
+ - **Plain `Micro::Cases.flow(transaction: true, ...)` re-raises
1536
+ exceptions.** The transaction still rolls back, but the caller has to
1537
+ rescue. Use `Micro::Cases.safe_flow(transaction: true, ...)` (or the
1538
+ class-level form with `Micro::Case::Safe`) to capture the exception
1539
+ as a `:exception` failure result.
1540
+
1541
+ [⬆️ Back to Top](#table-of-contents-)
1542
+
982
1543
  ### `Micro::Case::Strict` - What is a strict use case?
983
1544
 
984
1545
  Answer: it is a kind of use case that will require all the keywords (attributes) on its initialization.