u-case 5.6.0 → 5.7.1

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.
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  <p align="center">
2
- <h1 align="center" id="-case"><img src="./assets/ucase_logo_v1.png" alt="μ-case" height="150"></h1>
3
- <p align="center"><i>Represent use cases in a simple and powerful way while writing modular, expressive and sequentially logical code.</i></p>
2
+ <h1 align="center" id="-case"><img src="./assets/ucase_logo_v2.png" alt="μ-case" height="250"></h1>
3
+ <p align="center"><i>Represent use cases in a simple and powerful way: write modular, expressive, sequentially logical code.</i></p>
4
4
  <p align="center">
5
5
  <a href="https://badge.fury.io/rb/u-case"><img src="https://badge.fury.io/rb/u-case.svg" alt="Gem Version" height="18"></a>
6
6
  <a href="https://github.com/serradura/u-case/actions/workflows/ci.yml"><img alt="Build Status" src="https://github.com/serradura/u-case/actions/workflows/ci.yml/badge.svg"></a>
@@ -11,78 +11,195 @@
11
11
  <img src="https://img.shields.io/badge/Ruby%20%3E%3D%202.7%2C%20%3C%3D%20Head-ruby.svg?colorA=444&colorB=333" alt="Ruby">
12
12
  <img src="https://img.shields.io/badge/Rails%20%3E%3D%206.0%2C%20%3C%3D%20Edge-rails.svg?colorA=444&colorB=333" alt="Rails">
13
13
  </p>
14
+ <p align="center">🇧🇷&nbsp;🇵🇹 <a href="https://github.com/serradura/u-case/blob/main/README.pt-BR.md">Leia este README em português</a></p>
14
15
  </p>
15
16
 
16
- The main project goals are:
17
- 1. Easy to use and easy to learn (input **>>** process **>>** output).
18
- 2. Promote immutability (transforming data instead of modifying it) and data integrity.
19
- 3. No callbacks (ex: before, after, around) to avoid code indirections that could compromise the state and understanding of application flows.
20
- 4. Solve complex business logic, by allowing the composition of use cases (flow creation).
21
- 5. Be fast and optimized (Check out the [benchmarks](#benchmarks) section).
17
+ > [!IMPORTANT]
18
+ > **No breaking API changes — ever.** From here on, `u-case`'s public API and runtime contracts won't break. The gem's role is to remain a stable, backward-compatible foundation for the projects that already depend on it. Any "next major" rethink of the abstractions belongs in [`solid-process`](https://github.com/solid-process/solid-process) (a redesign that applies what we've learned since `u-case` was created), **not** in a future `u-case` 6.x.
19
+ >
20
+ > Major version bumps signal only that a Ruby or Rails version was dropped from the supported matrix per SemVer, a dependency-floor change. Your code keeps working.
21
+ >
22
+ > See the full statement on [issue #131](https://github.com/serradura/u-case/issues/131#issuecomment-4531231882).
22
23
 
23
- > **Note:** Check out the repo https://github.com/serradura/from-fat-controllers-to-use-cases to see a Rails application that uses this gem to handle its business logic.
24
+ ## Quick start <!-- omit in toc -->
24
25
 
25
- ## Documentation <!-- omit in toc -->
26
+ That's the whole shape: `attributes`, a `call!` method, `Success(...)` or `Failure(...)`. Everything else in this README is a way to make that shape easier to **compose**, **validate**, **observe**, and **transact**.
27
+
28
+ ```ruby
29
+ require 'u-case'
30
+
31
+ class Slugify < Micro::Case
32
+ attribute :title, accept: String
33
+
34
+ def call!
35
+ slug = title.downcase.strip.gsub(/[^a-z0-9]+/, '-').gsub(/^-|-$/, '')
36
+
37
+ slug.empty? ? Failure(:blank_title) : Success(result: { slug: })
38
+ end
39
+ end
40
+
41
+ Slugify.call(title: 'Hello, World!')
42
+ # => #<Micro::Case::Result success? type=:ok data={ slug: "hello-world" }>
43
+
44
+ Slugify
45
+ .call(title: 42)
46
+ .on_success { puts it[:slug] }
47
+ .on_failure(:invalid_attributes) { warn it[:errors] }
48
+ # warn: { "title" => "expected to be a kind of String" }
49
+
50
+ # ---------------------------------------------
51
+ # Branching on the result? Pattern-match on it:
52
+ # ---------------------------------------------
53
+ case Slugify.call(title: 'Hello, World!')
54
+ in { success: _, result: { slug: } }
55
+ redirect_to "/posts/#{slug}"
56
+ in { failure: :invalid_attributes, result: { errors: } }
57
+ render status: 422, json: { errors: }
58
+ in { failure: :blank_title }
59
+ render status: 422, json: { error: 'title required' }
60
+ end
61
+ ```
62
+
63
+ Need a structured input? Declare attributes with a block — child attributes inherit the host's feature mix (see [Going further with `u-attributes`](#going-further-with-u-attributes)):
64
+
65
+ ```ruby
66
+ class CreateOrder < Micro::Case
67
+ UUID = -> { it.is?(String) && it.match?(/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/) }
68
+
69
+ attribute :uuid, accept: UUID
70
+
71
+ attribute :customer do
72
+ attribute :name, accept: String
73
+ attribute :email, accept: String
74
+ end
75
+
76
+ def call!
77
+ transaction do
78
+ customer = Customer.create_or_find_by!(email: customer.email) { it.name = customer.name }
79
+
80
+ order = Order.create!(uuid:, customer_id: customer.id)
81
+
82
+ Success result: { customer:, order: }
83
+ end
84
+ end
85
+ end
86
+ ```
87
+
88
+ Need atomic, multi-step work? Wrap a whole flow in a transaction with one kwarg, or scope an `ActiveRecord::Base.transaction` to a single `call!`:
89
+
90
+ ```ruby
91
+ # A transactional flow — every step inside the same transaction:
92
+ SignUp = Micro::Cases.flow(transaction: true, steps: [
93
+ NormalizeParams,
94
+ CreateUser,
95
+ CreateProfile
96
+ ])
97
+
98
+ # An inline transaction { ... } inside call!:
99
+ class CreateUserWithProfile < Micro::Case
100
+ attribute :name, accept: String
101
+ attribute :email, accept: String
102
+ attribute :password, accept: String
103
+ attribute :password_confirmation, accept: String
104
+
105
+ def call!
106
+ transaction {
107
+ create_user
108
+ .then(CreateProfile)
109
+ }
110
+ end
111
+
112
+ def create_user
113
+ user = User.create(name:, email:, password:, password_confirmation:)
114
+
115
+ user.persisted? ? Success(result: { user: }) : Failure(result: { user: })
116
+ end
117
+ end
118
+ ```
26
119
 
27
- Version | Documentation
28
- --------- | -------------
29
- unreleased| https://github.com/serradura/u-case/blob/main/README.md
30
- 5.6.0 | https://github.com/serradura/u-case/blob/v5.x/README.md
31
- 4.5.1 | https://github.com/serradura/u-case/blob/v4.x/README.md
120
+ See [Composing use cases](#composing-use-cases) and [Going further with `u-attributes`](#going-further-with-u-attributes) for the full story.
32
121
 
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).
122
+ ## What you get <!-- omit in toc -->
123
+
124
+ - **Easy** — input → process → output. A use case is a class with `attributes`, a `call!` method, and returns a `Result`.
125
+ - **Immutable & callback-free** — no lifecycle callbacks. Data flows forward; nothing mutates in place.
126
+ - **Composable three ways** — chain use cases via [`flows`](#flows) or [`Result#then`](#internal-steps--resultthen-chains).
127
+ - **Typed results** — every call returns a [`Result`](#working-with-results).
128
+ - **Pattern matching** — Ruby `case`/`in` works out of the box. (See [Pattern matching](#pattern-matching)).
129
+ - **Result contracts** — declare which types and values a use case can return. (See [Result contracts](#result-contracts)).
130
+ - **Inspectable execution** — every flow records each step's input, output. (See [`transitions`](#inspecting-execution-with-resulttransitions)).
131
+ - ⚡ **Transactions on demand** — wrap a use case, a flow, in an [`ActiveRecord` transaction](#transactions).
132
+ - **Exception-safe by opt-in** — [`Micro::Case::Safe`](#safe-mode--capturing-exceptions) turns unhandled exceptions into `:exception` failures.
133
+ - **Fast** — Check out the [benchmarks](#performance), with no global state.
134
+
135
+ > See a real Rails app using this gem: [from-fat-controllers-to-use-cases](https://github.com/serradura/from-fat-controllers-to-use-cases).
136
+
137
+ ## Documentation <!-- omit in toc -->
138
+
139
+ | Version | Documentation |
140
+ | ---------- | ------------------------------------------------------- |
141
+ | unreleased | https://github.com/serradura/u-case/blob/main/README.md |
142
+ | 5.7.1 | https://github.com/serradura/u-case/blob/v5.x/README.md |
143
+ | 4.5.2 | https://github.com/serradura/u-case/blob/v4.x/README.md |
34
144
 
35
145
  ## Table of Contents <!-- omit in toc -->
146
+
36
147
  - [Compatibility](#compatibility)
37
148
  - [Dependencies](#dependencies)
38
149
  - [Installation](#installation)
39
150
  - [Usage](#usage)
40
- - [`Micro::Case` - How to define a use case?](#microcase---how-to-define-a-use-case)
41
- - [`Micro::Case::Result` - What is a use case result?](#microcaseresult---what-is-a-use-case-result)
42
- - [What are the default result types?](#what-are-the-default-result-types)
43
- - [How to define custom result types?](#how-to-define-custom-result-types)
44
- - [Is it possible to define a custom type without a result data?](#is-it-possible-to-define-a-custom-type-without-a-result-data)
45
- - [How to declare a results contract?](#how-to-declare-a-results-contract)
46
- - [How to use the result hooks?](#how-to-use-the-result-hooks)
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
- - [Using decomposition to access the result data and type](#using-decomposition-to-access-the-result-data-and-type)
49
- - [What happens if a result hook was declared multiple times?](#what-happens-if-a-result-hook-was-declared-multiple-times)
50
- - [How to use the `Micro::Case::Result#then` method?](#how-to-use-the-microcaseresultthen-method)
51
- - [What does happens when a `Micro::Case::Result#then` receives a block?](#what-does-happens-when-a-microcaseresultthen-receives-a-block)
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)
54
- - [`Micro::Cases::Flow` - How to compose use cases?](#microcasesflow---how-to-compose-use-cases)
55
- - [Is it possible to compose a flow with other flows?](#is-it-possible-to-compose-a-flow-with-other-flows)
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
- - [How to understand what is happening during a flow execution?](#how-to-understand-what-is-happening-during-a-flow-execution)
58
- - [`Micro::Case::Result#transitions` schema](#microcaseresulttransitions-schema)
59
- - [Is it possible disable the `Micro::Case::Result#transitions`?](#is-it-possible-disable-the-microcaseresulttransitions)
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)
62
- - [`Micro::Case::Strict` - What is a strict use case?](#microcasestrict---what-is-a-strict-use-case)
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)
64
- - [`Micro::Cases::Safe::Flow`](#microcasessafeflow)
65
- - [`Micro::Case::Result#on_exception`](#microcaseresulton_exception)
66
- - [Opting out of the safe mechanism](#opting-out-of-the-safe-mechanism)
67
- - [Validating attributes with `accept:` / `reject:`](#validating-attributes-with-accept--reject)
68
- - [`u-case/with_activemodel_validation` - How to validate the use case attributes?](#u-casewith_activemodel_validation---how-to-validate-the-use-case-attributes)
69
- - [If I enabled the auto validation, is it possible to disable it only in specific use cases?](#if-i-enabled-the-auto-validation-is-it-possible-to-disable-it-only-in-specific-use-cases)
70
- - [`Kind::Validator`](#kindvalidator)
71
- - [`Micro::Case.config`](#microcaseconfig)
72
- - [Benchmarks](#benchmarks)
73
- - [`Micro::Case`](#microcase)
74
- - [Success results](#success-results)
75
- - [Failure results](#failure-results)
76
- - [`Micro::Cases::Flow`](#microcasesflow)
151
+ - [Defining a use case](#defining-a-use-case)
152
+ - [The basics](#the-basics)
153
+ - [Strict mode required attributes](#strict-mode--required-attributes)
154
+ - [Safe mode capturing exceptions](#safe-mode--capturing-exceptions)
155
+ - [Safe flows](#safe-flows)
156
+ - [`Result#on_exception`](#resulton_exception)
157
+ - [Opting out of Safe](#opting-out-of-safe)
158
+ - [Working with results](#working-with-results)
159
+ - [The Result API](#the-result-api)
160
+ - [Default and custom result types](#default-and-custom-result-types)
161
+ - [Result contracts](#result-contracts)
162
+ - [Result hooks](#result-hooks)
163
+ - [Pattern matching](#pattern-matching)
164
+ - [Decomposition](#decomposition)
165
+ - [Dynamic continuations with `Result#then`](#dynamic-continuations-with-resultthen)
166
+ - [Validating attributes](#validating-attributes)
167
+ - [`accept:` and `reject:` (default)](#accept-and-reject-default)
168
+ - [ActiveModel integration (opt-in)](#activemodel-integration-opt-in)
169
+ - [Disabling auto-validation for a specific use case](#disabling-auto-validation-for-a-specific-use-case)
170
+ - [`Kind::Validator`](#kindvalidator)
171
+ - [Composing use cases](#composing-use-cases)
172
+ - [Flows](#flows)
173
+ - [Composing flows together](#composing-flows-together)
174
+ - [Data accumulation through a flow](#data-accumulation-through-a-flow)
175
+ - [Inspecting execution with `result.transitions`](#inspecting-execution-with-resulttransitions)
176
+ - [Composing a flow that includes itself](#composing-a-flow-that-includes-itself)
177
+ - [Internal steps `Result#then` chains](#internal-steps--resultthen-chains)
178
+ - [Accepted link shapes](#accepted-link-shapes)
179
+ - [A minimal example](#a-minimal-example)
180
+ - [`|` pipe alias](#-pipe-alias)
181
+ - [Lambda / `Method` forms](#lambda--method-forms)
182
+ - [`Failure` short-circuits the chain](#failure-short-circuits-the-chain)
183
+ - [Using an internal-step case inside an outer flow](#using-an-internal-step-case-inside-an-outer-flow)
184
+ - [Persistence without a transaction](#persistence-without-a-transaction)
185
+ - [Transactions](#transactions)
186
+ - [Inline `transaction { ... }` inside `call!`](#inline-transaction----inside-call)
187
+ - [`transaction with: …` — declaring the default for a case](#transaction-with---declaring-the-default-for-a-case)
188
+ - [Flow-level transactions](#flow-level-transactions)
189
+ - [Global default — `config.default_transaction_class { … }`](#global-default--configdefault_transaction_class---)
190
+ - [Internal-step flows under transactions](#internal-step-flows-under-transactions)
191
+ - [Behavior notes](#behavior-notes)
192
+ - [Configuration](#configuration)
193
+ - [Performance](#performance)
77
194
  - [Running the benchmarks](#running-the-benchmarks)
78
- - [Performance (Benchmarks IPS)](#performance-benchmarks-ips)
79
- - [Memory profiling](#memory-profiling)
195
+ - [Disabling runtime checks](#disabling-runtime-checks)
80
196
  - [Comparisons](#comparisons)
81
197
  - [Examples](#examples)
82
- - [1️⃣ Users creation](#1️⃣-users-creation)
83
- - [2️⃣ Rails App (API)](#2️⃣-rails-app-api)
84
- - [3️⃣ CLI calculator](#3️⃣-cli-calculator)
85
- - [4️⃣ Rescuing exceptions inside of the use cases](#4️⃣-rescuing-exceptions-inside-of-the-use-cases)
198
+ - [An end-to-end sign-up flow](#an-end-to-end-sign-up-flow)
199
+ - [More examples](#more-examples)
200
+ - [Going further with `u-attributes`](#going-further-with-u-attributes)
201
+ - [Nested attributes (block form)](#nested-attributes-block-form)
202
+ - [Accepting another attribute class](#accepting-another-attribute-class)
86
203
  - [Development](#development)
87
204
  - [Contributing](#contributing)
88
205
  - [License](#license)
@@ -90,17 +207,16 @@ unreleased| https://github.com/serradura/u-case/blob/main/README.md
90
207
 
91
208
  ## Compatibility
92
209
 
93
- | u-case | branch | ruby | activemodel | u-attributes |
94
- | ---------------- | ------ | -------- | -------------- | -------------- |
95
- | unreleased | main | >= 2.7 | >= 6.0 | >= 2.8, < 4.0 |
96
- | 5.6.0 | v5.x | >= 2.7 | >= 6.0 | >= 2.8, < 4.0 |
97
- | 5.1.0 | v5.x | >= 2.7 | >= 6.0 | >= 2.7, < 4.0 |
98
- | 4.5.1 | v4.x | >= 2.2.0 | >= 3.2, <= 8.1 | >= 2.7, < 3.0 |
210
+ | u-case | branch | ruby | activemodel | u-attributes |
211
+ | ---------- | ------ | -------- | -------------- | ------------- |
212
+ | unreleased | main | >= 2.7 | >= 6.0 | >= 2.8, < 4.0 |
213
+ | 5.7.1 | v5.x | >= 2.7 | >= 6.0 | >= 2.8, < 4.0 |
214
+ | 4.5.2 | v4.x | >= 2.2.0 | >= 3.2, <= 8.1 | >= 2.7, < 3.0 |
99
215
 
100
216
  This library is tested (CI matrix) against:
101
217
 
102
218
  | Ruby / Rails | 6.0 | 6.1 | 7.0 | 7.1 | 7.2 | 8.0 | 8.1 | Edge |
103
- |--------------|-----|-----|-----|-----|-----|-----|-----|------|
219
+ | ------------ | --- | --- | --- | --- | --- | --- | --- | ---- |
104
220
  | 2.7 | ✅ | ✅ | ✅ | ✅ | | | | |
105
221
  | 3.0 | ✅ | ✅ | ✅ | ✅ | | | | |
106
222
  | 3.1 | | | ✅ | ✅ | ✅ | | | |
@@ -110,19 +226,12 @@ This library is tested (CI matrix) against:
110
226
  | 4.x | | | | | | | ✅ | ✅ |
111
227
  | Head | | | | | | | ✅ | ✅ |
112
228
 
113
- > Note: The activemodel is an optional dependency, this module [can be enabled](#u-casewith_activemodel_validation---how-to-validate-use-case-attributes) to validate the use cases' attributes.
229
+ > ActiveModel is an optional dependency enable [`u-case/with_activemodel_validation`](#activemodel-integration-opt-in) only if you want it.
114
230
 
115
231
  ## Dependencies
116
232
 
117
- 1. [`kind`](https://github.com/serradura/kind) gem.
118
-
119
- A simple type system (at runtime) for Ruby.
120
-
121
- It is used to validate some internal u-case's methods input. This gem also exposes an [`ActiveModel validator`](https://github.com/serradura/kind#kindvalidator-activemodelvalidations) when requiring the [`u-case/with_activemodel_validation`](#u-casewith_activemodel_validation---how-to-validate-use-case-attributes) module, or when the [`Micro::Case.config`](#microcaseconfig) was used to enable it.
122
- 2. [`u-attributes`](https://github.com/serradura/u-attributes) gem.
123
-
124
- This gem allows defining read-only attributes, that is, your objects will have only getters to access their attributes data.
125
- It is used to define the use case attributes.
233
+ 1. **[`kind`](https://github.com/serradura/kind)** — a runtime type system for Ruby, used to validate some internal `u-case` inputs. Also exposes the [`Kind::Validator`](https://github.com/serradura/kind#kindvalidator-activemodelvalidations) that ships with [`u-case/with_activemodel_validation`](#activemodel-integration-opt-in). The examples below use `Kind.of?(SomeClass, *values)` as shorthand for runtime type checks — equivalent to `values.all? { |v| v.is_a?(SomeClass) }`.
234
+ 2. **[`u-attributes`](https://github.com/serradura/u-attributes)** — read-only attribute declarations (getters only). Used for the use case's `attributes`.
126
235
 
127
236
  ## Installation
128
237
 
@@ -132,1098 +241,973 @@ Add this line to your application's Gemfile:
132
241
  gem 'u-case', '~> 5.0'
133
242
  ```
134
243
 
135
- And then execute:
136
-
137
- $ bundle
138
-
139
- Or install it yourself as:
140
-
141
- $ gem install u-case
244
+ Then run `bundle`, or install it yourself with `gem install u-case`.
142
245
 
143
246
  ## Usage
144
247
 
145
- ### `Micro::Case` - How to define a use case?
248
+ ### Defining a use case
249
+
250
+ #### The basics
146
251
 
147
252
  ```ruby
148
- class Multiply < Micro::Case
149
- # 1. Define its input as attributes
150
- attributes :a, :b
253
+ class ValidateEmail < Micro::Case
254
+ # 1. Declare the input as attributes
255
+ attribute :address
151
256
 
152
- # 2. Define the method `call!` with its business logic
257
+ # 2. Implement call! with the business logic
153
258
  def call!
154
-
155
- # 3. Wrap the use case output using the `Success(result: *)` or `Failure(result: *)` methods
156
- if a.is_a?(Numeric) && b.is_a?(Numeric)
157
- Success result: { number: a * b }
259
+ # 3. Wrap the output with Success(...) or Failure(...)
260
+ if address.is_a?(String) && address.match?(/\A[^@\s]+@[^@\s]+\.[^@\s]+\z/)
261
+ Success result: { address: address.downcase }
158
262
  else
159
- Failure result: { message: '`a` and `b` attributes must be numeric' }
263
+ Failure result: { message: '`address` must be a valid email' }
160
264
  end
161
265
  end
162
266
  end
163
267
 
164
- #========================#
165
- # Performing an use case #
166
- #========================#
268
+ result = ValidateEmail.call(address: 'Ada@Example.com')
269
+ result.success? # => true
270
+ result.data # => { address: "ada@example.com" }
167
271
 
168
- # Success result
272
+ bad_result = ValidateEmail.call(address: 'not-an-email')
273
+ bad_result.failure? # => true
274
+ bad_result.data # => { message: "`address` must be a valid email" }
275
+ ```
169
276
 
170
- result = Multiply.call(a: 2, b: 2)
277
+ The object returned by `.call` is a [`Micro::Case::Result`](#working-with-results) the subject of the next section.
171
278
 
172
- result.success? # true
173
- result.data # { number: 4 }
279
+ #### Strict mode — required attributes
174
280
 
175
- # Failure result
281
+ `Micro::Case::Strict` requires every declared attribute to be passed on `.call`. Missing keywords raise `ArgumentError`:
176
282
 
177
- bad_result = Multiply.call(a: 2, b: '2')
283
+ ```ruby
284
+ class FormatGreeting < Micro::Case::Strict
285
+ attributes :name, :time_of_day
178
286
 
179
- bad_result.failure? # true
180
- bad_result.data # { message: "`a` and `b` attributes must be numeric" }
287
+ def call!
288
+ Success result: { message: "Good #{time_of_day}, #{name}!" }
289
+ end
290
+ end
181
291
 
182
- # Note:
183
- # ----
184
- # The result of a Micro::Case.call is an instance of Micro::Case::Result
292
+ FormatGreeting.call(name: 'Ada')
293
+ # => ArgumentError (missing keyword: :time_of_day)
185
294
  ```
186
295
 
187
- [⬆️ Back to Top](#table-of-contents-)
296
+ Use it when you want missing input to fail loudly instead of letting `time_of_day` arrive as `nil` and produce a silently wrong message.
188
297
 
189
- ### `Micro::Case::Result` - What is a use case result?
190
-
191
- A `Micro::Case::Result` stores the use cases output data. These are their main methods:
192
- - `#success?` returns true if is a successful result.
193
- - `#failure?` returns true if is an unsuccessful result.
194
- - `#use_case` returns the use case responsible for it. This feature is handy to handle a flow failure (this topic will be covered ahead).
195
- - `#type` a Symbol which gives meaning for the result, this is useful to declare different types of failures or success.
196
- - `#data` the result data itself.
197
- - `#[]` and `#values_at` are shortcuts to access the `#data` values.
198
- - `#fetch` and `#fetch_values` are another way of accessing values of the result data, but raises a `KeyError` if the one of the keys are not present in the result.
199
- - `#keys` returns an array of keys present in the result data.
200
- - `#key?` returns `true` if the key is present in `#data`.
201
- - `#value?` returns `true` if the given value is present in `#data`.
202
- - `#slice` returns a new hash that includes only the given keys. If the given keys don't exist, an empty hash is returned.
203
- - `#on_success` or `#on_failure` are hook methods that help you to define the application flow.
204
- - `#then` this method will allow applying a new use case if the current result was a success. The idea of this feature is to allow the creation of dynamic flows.
205
- - `#transitions` returns an array with all of transformations wich a result [has during a flow](#how-to-understand-what-is-happening-during-a-flow-execution).
206
-
207
- > **Note:** for backward compatibility, you could use the `#value` method as an alias of `#data` method.
298
+ #### Safe mode capturing exceptions
208
299
 
209
- [⬆️ Back to Top](#table-of-contents-)
300
+ `Micro::Case::Safe` is another base class. It auto-intercepts any exception raised inside `call!` and turns it into a `Failure` with `type: :exception`. The exception itself is available under `result[:exception]`:
210
301
 
211
- #### What are the default result types?
302
+ ```ruby
303
+ require 'json'
304
+ require 'logger'
212
305
 
213
- Every result has a type, and these are their default values:
214
- - `:ok` when success
215
- - `:error` or `:exception` when failures
306
+ AppLogger = Logger.new(STDOUT)
216
307
 
217
- ```ruby
218
- class Divide < Micro::Case
219
- attributes :a, :b
308
+ class ParseJsonPayload < Micro::Case::Safe
309
+ attribute :payload
220
310
 
221
311
  def call!
222
- if invalid_attributes.empty?
223
- Success result: { number: a / b }
224
- else
225
- Failure result: { invalid_attributes: invalid_attributes }
226
- end
227
- rescue => exception
228
- Failure result: exception
229
- end
312
+ return Failure(:blank_payload) if payload.to_s.empty?
230
313
 
231
- private def invalid_attributes
232
- attributes.select { |_key, value| !value.is_a?(Numeric) }
314
+ Success result: { data: JSON.parse(payload) }
233
315
  end
234
316
  end
235
317
 
236
- # Success result
318
+ result = ParseJsonPayload.call(payload: 'not-valid-json')
319
+ result.type # => :exception
320
+ result.data # => { exception: #<JSON::ParserError ...> }
321
+ result[:exception].is_a?(JSON::ParserError) # => true
237
322
 
238
- result = Divide.call(a: 2, b: 2)
239
-
240
- result.type # :ok
241
- result.data # { number: 1 }
242
- result.success? # true
243
- result.use_case # #<Divide:0x0000 @__attributes={"a"=>2, "b"=>2}, @a=2, @b=2, @__result=...>
323
+ result.on_failure(:exception) do
324
+ AppLogger.error(it[:exception].message)
325
+ end
326
+ ```
244
327
 
245
- # Failure result (type == :error)
328
+ To branch on the exception class, use `case`/`when` (or [pattern matching](#pattern-matching)) inside the hook:
246
329
 
247
- bad_result = Divide.call(a: 2, b: '2')
330
+ ```ruby
331
+ result.on_failure(:exception) do |data, use_case|
332
+ case (e = data[:exception])
333
+ when JSON::ParserError then AppLogger.error("malformed JSON: #{e.message}")
334
+ else AppLogger.debug("#{use_case.class.name} raised #{e.class}")
335
+ end
336
+ end
337
+ ```
248
338
 
249
- bad_result.type # :error
250
- bad_result.data # { invalid_attributes: { "b"=>"2" } }
251
- bad_result.failure? # true
252
- bad_result.use_case # #<Divide:0x0000 @__attributes={"a"=>2, "b"=>"2"}, @a=2, @b="2", @__result=...>
339
+ You can still `rescue` an exception explicitly inside a Safe use case — see [these test examples](https://github.com/serradura/u-case/blob/main/test/micro/case/safe_test.rb).
253
340
 
254
- # Failure result (type == :exception)
341
+ ##### Safe flows
255
342
 
256
- err_result = Divide.call(a: 2, b: 0)
343
+ A safe flow intercepts exceptions in any of its steps:
257
344
 
258
- err_result.type # :exception
259
- err_result.data # { exception: <ZeroDivisionError: divided by 0> }
260
- err_result.failure? # true
261
- err_result.use_case # #<Divide:0x0000 @__attributes={"a"=>2, "b"=>0}, @a=2, @b=0, @__result=#<Micro::Case::Result:0x0000 @use_case=#<Divide:0x0000 ...>, @type=:exception, @value=#<ZeroDivisionError: divided by 0>, @success=false>
345
+ ```ruby
346
+ module Users
347
+ Create = Micro::Cases.safe_flow([
348
+ ProcessParams,
349
+ ValidateParams,
350
+ Persist,
351
+ SendToCRM
352
+ ])
262
353
 
263
- # Note:
264
- # ----
265
- # Any Exception instance which is wrapped by
266
- # the Failure(result: *) method will receive `:exception` instead of the `:error` type.
354
+ # Or as a class:
355
+ class Create < Micro::Case::Safe
356
+ flow ProcessParams,
357
+ ValidateParams,
358
+ Persist,
359
+ SendToCRM
360
+ end
361
+ end
267
362
  ```
268
363
 
269
- [⬆️ Back to Top](#table-of-contents-)
270
-
271
- #### How to define custom result types?
364
+ ##### `Result#on_exception`
272
365
 
273
- Answer: Use a symbol as the argument of `Success()`, `Failure()` methods and declare the `result:` keyword to set the result data.
366
+ Exceptions are easier to follow when they're handled like any other failure. `Result#on_exception` is a hook that fires when `type` is `:exception` it reads the same as `on_failure(:exception)`, but makes the intent explicit:
274
367
 
275
368
  ```ruby
276
- class Multiply < Micro::Case
277
- attributes :a, :b
369
+ class ParseJsonPayload < Micro::Case::Safe
370
+ attribute :payload
278
371
 
279
372
  def call!
280
- if a.is_a?(Numeric) && b.is_a?(Numeric)
281
- Success result: { number: a * b }
282
- else
283
- Failure :invalid_data, result: {
284
- attributes: attributes.reject { |_, input| input.is_a?(Numeric) }
285
- }
286
- end
373
+ Success result: { data: JSON.parse(payload) }
287
374
  end
288
375
  end
289
376
 
290
- # Success result
291
-
292
- result = Multiply.call(a: 3, b: 2)
377
+ ParseJsonPayload
378
+ .call(payload: 'not-valid-json')
379
+ .on_success { puts it[:data].inspect }
380
+ .on_exception(Encoding::CompatibilityError) { puts 'Encoding mismatch.' }
381
+ .on_exception(JSON::ParserError) { puts 'Malformed JSON.' }
382
+ .on_exception { |_e, _use_case| puts 'Something went wrong.' }
383
+ # Malformed JSON.
384
+ # Something went wrong.
385
+ ```
293
386
 
294
- result.type # :ok
295
- result.data # { number: 6 }
296
- result.success? # true
387
+ > Both the typed `on_exception(JSON::ParserError)` and the catch-all `on_exception` fire — like all u-case hooks, every match runs in declaration order (see [Result hooks](#result-hooks)).
297
388
 
298
- # Failure result
389
+ ##### Opting out of Safe
299
390
 
300
- bad_result = Multiply.call(a: 3, b: '2')
391
+ The Safe mechanism is opinionated: any unhandled exception becomes a `:exception` failure. That convenience can fragment a codebase — some exceptions handled by `rescue` inside `call!`, others by `on_exception` later. If you want a single explicit convention (plain `rescue` only), disable Safe entirely:
301
392
 
302
- bad_result.type # :invalid_data
303
- bad_result.data # { attributes: {"b"=>"2"} }
304
- bad_result.failure? # true
393
+ ```ruby
394
+ Micro::Case.config do |config|
395
+ config.disable_safe_features = true
396
+ end
305
397
  ```
306
398
 
399
+ When set to `true`, the following raise `Micro::Case::Error::SafeFeaturesDisabled`:
400
+
401
+ - subclassing `Micro::Case::Safe`
402
+ - calling `Micro::Cases.safe_flow(...)`
403
+ - calling `Micro::Case::Result#on_exception`
404
+
307
405
  [⬆️ Back to Top](#table-of-contents-)
308
406
 
309
- #### Is it possible to define a custom type without a result data?
407
+ ### Working with results
408
+
409
+ A `Micro::Case::Result` carries the use case's output. The methods you'll reach for most:
410
+
411
+ #### The Result API
412
+
413
+ - `#success?` / `#failure?` — boolean discriminants.
414
+ - `#type` — `Symbol` describing the result (`:ok`, `:error`, `:exception`, or any custom type).
415
+ - `#data` — the result data hash. `#value` is a backwards-compatible alias.
416
+ - `#[]`, `#values_at`, `#fetch`, `#fetch_values`, `#keys`, `#key?`, `#value?`, `#slice` — `Hash`-like access into `#data`.
417
+ - `#use_case` — the use case instance that produced the result (handy for failure diagnostics inside a flow).
418
+ - `#on_success` / `#on_failure` / `#on_exception` — hooks for branching on the result.
419
+ - `#then` — apply another use case (or lambda / method / symbol) to a successful result; the basis for [internal steps](#internal-steps--resultthen-chains) and [dynamic continuations](#dynamic-continuations-with-resultthen).
420
+ - `#transitions` — array of every step that produced this result; see [inspecting execution](#inspecting-execution-with-resulttransitions).
421
+
422
+ Result objects also support [pattern matching](#pattern-matching) and [array decomposition](#decomposition).
423
+
424
+ #### Default and custom result types
425
+
426
+ Every result carries a type. The defaults:
427
+
428
+ - `:ok` — for `Success(...)`.
429
+ - `:error` — for `Failure(...)` whose payload is a `Hash`.
430
+ - `:exception` — for `Failure(result: some_exception)` (an `Exception` instance).
431
+
432
+ ```ruby
433
+ class FetchUser < Micro::Case
434
+ attribute :id
435
+
436
+ def call!
437
+ return Failure(result: { errors: { id: 'must be an Integer' } }) unless id.is_a?(Integer)
438
+
439
+ Success result: { user: User.find(id) }
440
+ rescue => exception
441
+ Failure result: exception
442
+ end
443
+ end
444
+
445
+ FetchUser.call(id: 1).type # => :ok
446
+ FetchUser.call(id: 'x').type # => :error
447
+ FetchUser.call(id: 999_999).type # => :exception (ActiveRecord::RecordNotFound)
448
+ ```
310
449
 
311
- Answer: Yes, it is possible. But this will have special behavior because the result data will be a hash with the given type as the key and `true` as its value.
450
+ Pass a symbol as the first argument of `Success(...)` / `Failure(...)` to give the result a custom type:
312
451
 
313
452
  ```ruby
314
- class Multiply < Micro::Case
315
- attributes :a, :b
453
+ class MergeTags < Micro::Case
454
+ attributes :primary, :secondary
316
455
 
317
456
  def call!
318
- if a.is_a?(Numeric) && b.is_a?(Numeric)
319
- Success result: { number: a * b }
457
+ if primary.is_a?(Array) && secondary.is_a?(Array)
458
+ Success result: { tags: (primary + secondary).uniq }
320
459
  else
321
- Failure(:invalid_data)
460
+ Failure :invalid_input, result: {
461
+ attributes: attributes.reject { |_, v| v.is_a?(Array) }
462
+ }
322
463
  end
323
464
  end
324
465
  end
325
466
 
326
- result = Multiply.call(a: 2, b: '2')
467
+ MergeTags.call(primary: %w[ruby], secondary: 'rails').type # => :invalid_input
468
+ ```
327
469
 
328
- result.failure? # true
329
- result.data # { :invalid_data => true }
330
- result.type # :invalid_data
331
- result.use_case.attributes # {"a"=>2, "b"=>"2"}
470
+ Passing only the symbol (no `result:`) is allowed — the data becomes `{ <symbol> => true }`. This shape is useful as a quick discriminant inside a flow:
332
471
 
333
- # Note:
334
- # ----
335
- # This feature is handy to handle failures in a flow
336
- # (this topic will be covered ahead).
337
- ```
472
+ ```ruby
473
+ def call!
474
+ return Failure(:invalid_input) unless primary.is_a?(Array) && secondary.is_a?(Array)
338
475
 
339
- [⬆️ Back to Top](#table-of-contents-)
476
+ Success result: { tags: (primary + secondary).uniq }
477
+ end
478
+
479
+ # result.data => { invalid_input: true }
480
+ ```
340
481
 
341
- #### How to declare a results contract?
482
+ #### Result contracts
342
483
 
343
- Answer: Use the `results do |on| ... end` macro to declare which result types your use case can return, and which keys each one requires. When a contract is declared, `Success(...)` / `Failure(...)` calls that use an undeclared type raise `Micro::Case::Error::UnexpectedResultType`, and calls that omit a declared required key raise `Micro::Case::Error::MissingResultKeys`.
484
+ Use the `results do |on| ... end` macro to declare which result types your use case can produce and which keys each one requires. Calls that use an undeclared type raise `Micro::Case::Error::UnexpectedResultType`; calls that omit a declared required key raise `Micro::Case::Error::MissingResultKeys`.
344
485
 
345
486
  ```ruby
346
- class Divide < Micro::Case
347
- attributes :a, :b
487
+ class PublishPost < Micro::Case
488
+ attribute :post
348
489
 
349
490
  results do |on|
350
- on.failure(:attributes_must_be_numbers)
351
- on.failure(:division_by_zero)
491
+ on.failure(:already_published)
492
+ on.failure(:missing_content)
352
493
 
353
- on.success(result: [:division])
494
+ on.success(result: [:post])
354
495
  end
355
496
 
356
497
  def call!
357
- return Failure(:attributes_must_be_numbers) unless Kind.of?(Numeric, a, b)
358
- return Failure(:division_by_zero) if b == 0
498
+ return Failure(:already_published) if post.published?
499
+ return Failure(:missing_content) if post.body.to_s.strip.empty?
359
500
 
360
- Success result: { division: a / b }
501
+ post.update!(status: :published, published_at: Time.current)
502
+ Success result: { post: }
361
503
  end
362
504
  end
363
505
 
364
- Divide.call(a: 10, b: 2).data # => { division: 5 }
365
- Divide.call(a: 10, b: 0).type # => :division_by_zero
366
- Divide.call(a: 'x', b: 2).type # => :attributes_must_be_numbers
506
+ PublishPost.call(post: ready_post).data # => { post: #<Post ...> }
507
+ PublishPost.call(post: empty_post).type # => :missing_content
508
+ PublishPost.call(post: already_live_post).type # => :already_published
367
509
  ```
368
510
 
369
- A type passed to `on.success` / `on.failure` without a `result:` argument declares the type with no required keys (any payload — including the implicit `{ type => true }` from `Failure(:my_type)` — is accepted). When `result: [:key1, :key2]` is given, those keys must be present in the result hash; extra keys are allowed.
511
+ A type passed without `result:` declares it with no required keys (any payload — including the implicit `{ type => true }` from `Failure(:my_type)` — is accepted). With `result: [:key1, :key2]`, those keys must be present in the result hash; extra keys are fine.
370
512
 
371
513
  ```ruby
372
- class Wrong < Micro::Case
514
+ class CreateComment < Micro::Case
373
515
  results do |on|
374
- on.success(result: [:value])
375
- on.failure(:known)
516
+ on.success(result: [:comment])
517
+ on.failure(:spam)
376
518
  end
377
519
 
378
520
  def call!
379
- Success(:other, result: { value: 1 }) # raises Micro::Case::Error::UnexpectedResultType
380
- # Success(result: { wrong: 1 }) # raises Micro::Case::Error::MissingResultKeys
381
- # Failure(:other) # raises Micro::Case::Error::UnexpectedResultType
521
+ Success(:moderated, result: { comment: ... }) # raises Micro::Case::Error::UnexpectedResultType
522
+ # Success(result: { body: '...' }) # raises Micro::Case::Error::MissingResultKeys
523
+ # Failure(:rate_limited) # raises Micro::Case::Error::UnexpectedResultType
382
524
  end
383
525
  end
384
526
  ```
385
527
 
386
528
  Notes:
529
+
387
530
  - Use cases without a `results` block keep their previous unrestricted behavior — the contract is opt-in.
388
531
  - Subclasses inherit the parent's contract.
389
- - Rescued exceptions in `Micro::Case::Safe` (which produce `Failure(result: exception)` automatically) bypass the contract.
390
-
391
- [⬆️ Back to Top](#table-of-contents-)
532
+ - The auto-failure produced by [`accept:` / `reject:`](#accept-and-reject-default) attribute validation bypasses the contract — combining `results` with attribute validation does **not** require declaring `:invalid_attributes`.
533
+ - Rescued exceptions in [`Micro::Case::Safe`](#safe-mode--capturing-exceptions) (which produce `Failure(result: exception)`) bypass the contract too.
534
+ - Contracts are independent of [hooks](#result-hooks) and [pattern matching](#pattern-matching): the contract fires at `Success(...)` / `Failure(...)` call time, inside `call!`. Once a `Result` exists, callers consume it normally — there is no caller-side enforcement.
392
535
 
393
- #### How to use the result hooks?
536
+ #### Result hooks
394
537
 
395
- As [mentioned earlier](#microcaseresult---what-is-a-use-case-result), the `Micro::Case::Result` has two methods to improve the application flow control. They are: `#on_success`, `on_failure`.
396
-
397
- The examples below show how to use them:
538
+ `on_success` and `on_failure` branch on the result type. Pass a symbol to match a specific type, or no argument to match anything:
398
539
 
399
540
  ```ruby
400
- class Double < Micro::Case
401
- attribute :number
541
+ class ChangePassword < Micro::Case
542
+ attributes :user, :new_password
402
543
 
403
544
  def call!
404
- return Failure :invalid, result: { msg: 'number must be a numeric value' } unless number.is_a?(Numeric)
405
- return Failure :lte_zero, result: { msg: 'number must be greater than 0' } if number <= 0
545
+ return Failure(:weak, result: { msg: 'password too short' }) unless new_password.is_a?(String) && new_password.length >= 8
546
+ return Failure(:reused, result: { msg: 'password recently used' }) if user.recently_used?(new_password)
406
547
 
407
- Success result: { number: number * 2 }
548
+ user.update_password!(new_password)
549
+ Success result: { user: }
408
550
  end
409
551
  end
410
552
 
411
- #================================#
412
- # Printing the output if success #
413
- #================================#
414
-
415
- Double
416
- .call(number: 3)
417
- .on_success { |result| p result[:number] }
418
- .on_failure(:invalid) { |result| raise TypeError, result[:msg] }
419
- .on_failure(:lte_zero) { |result| raise ArgumentError, result[:msg] }
420
-
421
- # The output will be:
422
- # 6
423
-
424
- #=============================#
425
- # Raising an error if failure #
426
- #=============================#
427
-
428
- Double
429
- .call(number: -1)
430
- .on_success { |result| p result[:number] }
431
- .on_failure { |_result, use_case| puts "#{use_case.class.name} was the use case responsible for the failure" }
432
- .on_failure(:invalid) { |result| raise TypeError, result[:msg] }
433
- .on_failure(:lte_zero) { |result| raise ArgumentError, result[:msg] }
434
-
435
- # The outputs will be:
436
- #
437
- # 1. It will print the message: Double was the use case responsible for the failure
438
- # 2. It will raise the exception: ArgumentError (the number must be greater than 0)
439
-
440
- # Note:
441
- # ----
442
- # The use case responsible for the result will always be accessible as the second hook argument
553
+ ChangePassword
554
+ .call(user: ada, new_password: 'long-enough-1')
555
+ .on_success { audit "password updated for #{it[:user].id}" }
556
+ .on_failure(:weak) { raise ArgumentError, it[:msg] }
557
+ .on_failure(:reused) { raise ArgumentError, it[:msg] }
558
+
559
+ ChangePassword
560
+ .call(user: ada, new_password: 'short')
561
+ .on_failure { |_r, use_case| audit "#{use_case.class.name} failed" } # 1. ChangePassword failed
562
+ .on_failure(:weak) { raise ArgumentError, it[:msg] } # 2. ArgumentError
443
563
  ```
444
564
 
445
- #### Why the hook usage without a defined type exposes the result itself?
565
+ > The use case responsible for the result is always available as the hook's second block argument.
446
566
 
447
- Answer: To allow you to define how to handle the program flow using some conditional statement like an `if` or `case when`.
567
+ Without an explicit type, the block receives the whole result, so you can branch with a `case` statement:
448
568
 
449
569
  ```ruby
450
- class Double < Micro::Case
451
- attribute :number
452
-
453
- def call!
454
- return Failure(:invalid) unless number.is_a?(Numeric)
455
- return Failure :lte_zero, result: attributes(:number) if number <= 0
456
-
457
- Success result: { number: number * 2 }
458
- end
459
- end
460
-
461
- Double
462
- .call(number: -1)
570
+ ChangePassword
571
+ .call(user: ada, new_password: 'short')
463
572
  .on_failure do |result, use_case|
464
573
  case result.type
465
- when :invalid then raise TypeError, "number must be a numeric value"
466
- when :lte_zero then raise ArgumentError, "number `#{result[:number]}` must be greater than 0"
574
+ when :weak then raise ArgumentError, 'password too short'
575
+ when :reused then raise ArgumentError, 'password recently used'
467
576
  else raise NotImplementedError
468
577
  end
469
578
  end
470
-
471
- # The output will be an exception:
472
- #
473
- # ArgumentError (number `-1` must be greater than 0)
474
579
  ```
475
580
 
476
- > **Note:** The same that was did in the previous examples could be done with `#on_success` hook!
477
-
478
- ##### Using decomposition to access the result data and type
479
-
480
- The syntax to decompose an Array can be used in assignments and in method/block arguments.
481
- If you doesn't know it, check out the [Ruby doc](https://ruby-doc.org/core-2.2.0/doc/syntax/assignment_rdoc.html#label-Array+Decomposition).
581
+ If the same hook is declared multiple times, every match fires:
482
582
 
483
583
  ```ruby
484
- # The object exposed in the hook without a type is a Micro::Case::Result and it can be decomposed. e.g:
584
+ calls = 0
585
+ result = ChangePassword.call(user: ada, new_password: 'long-enough-1')
485
586
 
486
- Double
487
- .call(number: -2)
488
- .on_failure do |(data, type), use_case|
489
- case type
490
- when :invalid then raise TypeError, 'number must be a numeric value'
491
- when :lte_zero then raise ArgumentError, "number `#{data[:number]}` must be greater than 0"
492
- else raise NotImplementedError
493
- end
494
- end
587
+ result
588
+ .on_success { calls += 1 }
589
+ .on_success { calls += 1 }
590
+ .on_success(:ok) { calls += 1 }
591
+ .on_success(:ok) { calls += 1 }
495
592
 
496
- # The output will be the exception:
497
- #
498
- # ArgumentError (the number `-2` must be greater than 0)
593
+ calls # => 4
499
594
  ```
500
595
 
501
- > **Note:** The same that was did in the previous examples could be done with `#on_success` hook!
596
+ #### Pattern matching
502
597
 
503
- [⬆️ Back to Top](#table-of-contents-)
598
+ `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 `case`/`in` works out of the box (Ruby ≥ 2.7):
504
599
 
505
- #### What happens if a result hook was declared multiple times?
600
+ ```ruby
601
+ case result
602
+ in { success: _, data: { number: Numeric => number } }
603
+ puts "got #{number}"
604
+ in { failure: :invalid_attributes, data: { invalid_attributes: errors } }
605
+ warn "bad input: #{errors.keys.join(", ")}"
606
+ in { failure: :exception, data: { exception: } }
607
+ warn "boom: #{exception.message}"
608
+ end
609
+ ```
506
610
 
507
- Answer: The hook always will be triggered if it matches the result type.
611
+ The hash patterns expose these keys:
508
612
 
509
- ```ruby
510
- class Double < Micro::Case
511
- attributes :number
613
+ | Key | Present on | Value |
614
+ | -------------- | ------------ | ---------------------------------------------------------------------------- |
615
+ | `success:` | success only | the result `type` (e.g. `:ok`) |
616
+ | `failure:` | failure only | the result `type` (e.g. `:invalid_attributes`) |
617
+ | `type:` | always | the result `type` |
618
+ | `data:` | always | the result `data` hash |
619
+ | `result:` | always | alias of `data:` (matches the `Success(result: …)` keyword at the call site) |
620
+ | `use_case:` | always | the use case instance that produced the result |
621
+ | `transitions:` | always | the result `transitions` array |
512
622
 
513
- def call!
514
- if number.is_a?(Numeric)
515
- Success :computed, result: { number: number * 2 }
516
- else
517
- Failure :invalid, result: { msg: 'number must be a numeric value' }
518
- end
519
- end
520
- end
623
+ `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:
521
624
 
522
- result = Double.call(number: 3)
523
- result.data # { number: 6 }
524
- result[:number] * 4 # 24
625
+ ```ruby
626
+ case result
627
+ in [:success, :ok, { number: Integer => n }]
628
+ n
629
+ in [:failure, :invalid_attributes, { invalid_attributes: errors }]
630
+ # ...
631
+ in [:failure, :exception, { exception: }]
632
+ # ...
633
+ end
634
+ ```
525
635
 
526
- accum = 0
636
+ > `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 intentionally return different shapes.
527
637
 
528
- result
529
- .on_success { |result| accum += result[:number] }
530
- .on_success { |result| accum += result[:number] }
531
- .on_success(:computed) { |result| accum += result[:number] }
532
- .on_success(:computed) { |result| accum += result[:number] }
638
+ #### Decomposition
533
639
 
534
- accum # 24
640
+ Inside a hook without a type, the result can also be array-decomposed into `[data, type]`:
535
641
 
536
- result[:number] * 4 == accum # true
642
+ ```ruby
643
+ ChangePassword
644
+ .call(user: ada, new_password: 'short')
645
+ .on_failure do |(data, type), use_case|
646
+ case type
647
+ when :weak then raise ArgumentError, data[:msg]
648
+ when :reused then raise ArgumentError, data[:msg]
649
+ else raise NotImplementedError
650
+ end
651
+ end
537
652
  ```
538
653
 
539
- #### How to use the `Micro::Case::Result#then` method?
654
+ #### Dynamic continuations with `Result#then`
540
655
 
541
- This method allows you to create dynamic flows, so, with it, you can add new use cases or flows to continue the result transformation. e.g:
656
+ `Result#then` applies another use case (or callable) to a successful result `Failure` short-circuits. Use it to build dynamic continuations from a result that already exists:
542
657
 
543
658
  ```ruby
544
- class ForbidNegativeNumber < Micro::Case
545
- attribute :number
659
+ class FindActiveUser < Micro::Case
660
+ attribute :email
546
661
 
547
662
  def call!
548
- return Success result: attributes if number >= 0
663
+ user = User.active.find_by(email:)
549
664
 
550
- Failure result: attributes
665
+ return Success result: { user: } if user
666
+
667
+ Failure result: { email: }
551
668
  end
552
669
  end
553
670
 
554
- class Add3 < Micro::Case
555
- attribute :number
671
+ class GenerateInviteToken < Micro::Case
672
+ attribute :user
556
673
 
557
674
  def call!
558
- Success result: { number: number + 3 }
675
+ Success result: { user:, token: SecureRandom.hex(16) }
559
676
  end
560
677
  end
561
678
 
562
- result1 =
563
- ForbidNegativeNumber
564
- .call(number: -1)
565
- .then(Add3)
566
-
567
- result1.data # {'number' => -1}
568
- result1.failure? # true
569
-
570
- # ---
571
-
572
- result2 =
573
- ForbidNegativeNumber
574
- .call(number: 1)
575
- .then(Add3)
576
-
577
- result2.data # {'number' => 4}
578
- result2.success? # true
679
+ FindActiveUser.call(email: 'unknown@example.com').then(GenerateInviteToken).failure? # => true
680
+ FindActiveUser.call(email: 'ada@example.com').then(GenerateInviteToken).data
681
+ # => { user: #<User ...>, token: "9f2b…" }
579
682
  ```
580
683
 
581
- > **Note:** this method changes the [`Micro::Case::Result#transitions`](#how-to-understand-what-is-happening-during-a-flow-execution).
582
-
583
- [⬆️ Back to Top](#table-of-contents-)
584
-
585
- ##### What does happens when a `Micro::Case::Result#then` receives a block?
586
-
587
- It will yields self (a `Micro::Case::Result` instance) to the block, and will return the output of the block instead of itself. e.g:
684
+ Passing a block yields `self` (a `Micro::Case::Result`) and returns the block's value — handy for unwrapping into a non-result type:
588
685
 
589
686
  ```ruby
590
- class Add < Micro::Case
591
- attributes :a, :b
687
+ class FindUser < Micro::Case
688
+ attribute :email
592
689
 
593
690
  def call!
594
- if Kind.of?(Numeric, a, b)
595
- Success result: { sum: a + b }
596
- else
597
- Failure(:attributes_arent_numbers)
598
- end
691
+ user = User.find_by(email:)
692
+
693
+ user ? Success(result: { user: }) : Failure(:not_found)
599
694
  end
600
695
  end
601
696
 
602
- # --
603
-
604
- success_result =
605
- Add
606
- .call(a: 2, b: 2)
607
- .then { |result| result.success? ? result[:sum] : 0 }
608
-
609
- puts success_result # 4
610
-
611
- # --
612
-
613
- failure_result =
614
- Add
615
- .call(a: 2, b: '2')
616
- .then { |result| result.success? ? result[:sum] : 0 }
617
-
618
- puts failure_result # 0
697
+ FindUser.call(email: 'ada@example.com').then { it.success? ? it[:user].id : nil } # => 42
698
+ FindUser.call(email: 'unknown@example.com').then { it.success? ? it[:user].id : nil } # => nil
619
699
  ```
620
700
 
621
- [⬆️ Back to Top](#table-of-contents-)
622
-
623
- ##### How to make attributes data injection using this feature?
624
-
625
- Pass a Hash as the second argument of the `Micro::Case::Result#then` method.
701
+ Pass an extra `Hash` to inject attributes into the next use case:
626
702
 
627
703
  ```ruby
628
704
  Todo::FindAllForUser
629
705
  .call(user: current_user, params: params)
630
706
  .then(Paginate)
631
707
  .then(Serialize::PaginatedRelationAsJson, serializer: Todo::Serializer)
632
- .on_success { |result| render_json(200, data: result[:todos]) }
708
+ .on_success { render_json(200, data: it[:todos]) }
633
709
  ```
634
710
 
711
+ > `Result#then` also accepts a `Symbol`, a `Method` object, or a `Lambda` — see [Internal steps](#internal-steps--resultthen-chains).
712
+
635
713
  [⬆️ Back to Top](#table-of-contents-)
636
714
 
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`.
715
+ ### Validating attributes
663
716
 
664
- ##### A minimal example
717
+ #### `accept:` and `reject:` (default)
718
+
719
+ Since 5.2.0, every use case includes [`u-attributes`' `accept` extension](https://github.com/serradura/u-attributes). Declare a type expectation (or any predicate) on the attribute, and the use case fails automatically with `type: :invalid_attributes` when an attribute is rejected — no need to validate inside `call!`:
665
720
 
666
721
  ```ruby
667
- class SumHalf < Micro::Case
668
- attribute :sum
722
+ class CreateUser < Micro::Case
723
+ attribute :name, accept: String
724
+ attribute :email, accept: ->(v) { v.is_a?(String) && v.include?('@') }
725
+ attribute :age, accept: Integer, allow_nil: true
669
726
 
670
727
  def call!
671
- Success :third_sum, result: { sum: sum + 0.5 }
728
+ Success result: { user: User.create!(attributes) }
672
729
  end
673
730
  end
674
731
 
675
- class DoSomeSum < Micro::Case
676
- attributes :a, :b
732
+ CreateUser.call(name: 'Bob', email: 'bob@example.com')
733
+ # => #<Success type=:ok ...>
677
734
 
678
- def call!
679
- validate_numbers
680
- .then(:sum_a_and_b)
681
- .then(:add, number: 3)
682
- .then(SumHalf)
683
- end
735
+ CreateUser.call(name: 42, email: 'not-an-email')
736
+ # => #<Failure type=:invalid_attributes data={
737
+ # errors: {
738
+ # "name" => "expected to be a kind of String",
739
+ # "email" => "is invalid"
740
+ # }
741
+ # }>
742
+ ```
684
743
 
685
- private
744
+ The failure type follows the same setting used by the ActiveModel integration — see `set_activemodel_validation_errors_failure` in [Configuration](#configuration).
686
745
 
687
- def validate_numbers
688
- Kind.of?(Numeric, a, b) ? Success(:valid) : Failure()
689
- end
746
+ #### ActiveModel integration (opt-in)
690
747
 
691
- def sum_a_and_b
692
- Success :first_sum, result: { sum: a + b }
693
- end
748
+ You can layer Rails-style `validates` on top of `accept:` / `reject:` for richer rules (`presence`, `numericality`, `format`, custom validators…). Requires [`activemodel >= 6.0`](https://rubygems.org/gems/activemodel) in your application.
749
+
750
+ The simplest form — `validates` is available on every use case, you fail manually:
751
+
752
+ ```ruby
753
+ class CreatePost < Micro::Case
754
+ attributes :title, :body
755
+
756
+ validates :title, :body, presence: true
757
+ validates :title, length: { maximum: 120 }
758
+
759
+ def call!
760
+ return Failure :invalid_attributes, result: { errors: self.errors } if invalid?
694
761
 
695
- def add(sum:, number:, **)
696
- Success :second_sum, result: { sum: sum + number }
762
+ Success result: { post: Post.create!(title:, body:) }
697
763
  end
698
764
  end
765
+ ```
699
766
 
700
- result = DoSomeSum.call(a: 1, b: 2)
767
+ To make use cases **auto-fail** on `invalid?`, require the auto-validation entry point:
701
768
 
702
- result.success? # true
703
- result.data # { sum: 6.5 }
704
- result.transitions # 4 entries — see below
769
+ ```ruby
770
+ # Gemfile
771
+ gem 'u-case', require: 'u-case/with_activemodel_validation'
705
772
  ```
706
773
 
707
- `result.transitions` for the call above:
774
+ …or enable it via [Configuration](#configuration). The example then collapses:
708
775
 
709
776
  ```ruby
710
- [
711
- { use_case: { class: DoSomeSum, attributes: { a: 1, b: 2 } },
712
- success: { type: :valid, result: { valid: true } },
713
- accessible_attributes: [:a, :b] },
777
+ require 'u-case/with_activemodel_validation'
714
778
 
715
- { use_case: { class: DoSomeSum, attributes: { a: 1, b: 2 } },
716
- success: { type: :first_sum, result: { sum: 3 } },
717
- accessible_attributes: [:a, :b, :valid] },
779
+ class CreatePost < Micro::Case
780
+ attributes :title, :body
718
781
 
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] },
782
+ validates :title, :body, presence: true
783
+ validates :title, length: { maximum: 120 }
722
784
 
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
- ]
785
+ def call!
786
+ Success result: { post: Post.create!(title:, body:) }
787
+ end
788
+ end
727
789
  ```
728
790
 
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.
791
+ When both `accept:` and ActiveModel validation are present, the execution order is:
734
792
 
735
- ##### The `|` (pipe) alias
793
+ 1. `u-attributes` resolves each attribute's default.
794
+ 2. `u-attributes` runs the `accept:` / `reject:` checks.
795
+ 3. `u-case` runs the ActiveModel validations **only if** every attribute was accepted.
796
+
797
+ > Auto-validation is also inherited by `Micro::Case::Strict` and `Micro::Case::Safe`.
736
798
 
737
- `|` is sugar for `.then(...)`. The previous example becomes:
799
+ ##### Disabling auto-validation for a specific use case
800
+
801
+ Use the `disable_auto_validation` macro:
738
802
 
739
803
  ```ruby
740
- def call!
741
- validate_numbers | :sum_a_and_b | :add | SumHalf
804
+ require 'u-case/with_activemodel_validation'
805
+
806
+ class CountPosts < Micro::Case
807
+ disable_auto_validation
808
+
809
+ attribute :user
810
+ validates :user, presence: true
811
+
812
+ def call!
813
+ Success result: { count: user.posts.count }
814
+ end
742
815
  end
816
+
817
+ CountPosts.call(user: nil)
818
+ # => NoMethodError (undefined method `posts' for nil:NilClass)
743
819
  ```
744
820
 
745
- Both forms produce identical `result.data` and `result.transitions`.
821
+ ##### `Kind::Validator`
746
822
 
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:
823
+ The [`kind` gem](https://github.com/serradura/kind) ships a [`Kind::Validator`](https://github.com/serradura/kind#kindvalidator-activemodelvalidations) for ActiveModel that validates types via its runtime type system. Requiring `'u-case/with_activemodel_validation'` also loads `Kind::Validator`:
797
824
 
798
825
  ```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:
826
+ class Todo::List::AddItem < Micro::Case
827
+ attributes :user, :params
818
828
 
819
- ```ruby
820
- class CreateUserWithProfileInline < Micro::Case
821
- attributes :name, :info
829
+ validates :user, kind: User
830
+ validates :params, kind: ActionController::Parameters
822
831
 
823
832
  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?
833
+ todo_params = params.require(:todo).permit(:title, :due_at)
834
+ todo = user.todos.create(todo_params)
838
835
 
839
- Success result: { user: user, profile: profile }
836
+ Success result: { todo: todo }
837
+ rescue ActionController::ParameterMissing => e
838
+ Failure :parameter_missing, result: { message: e.message }
840
839
  end
841
840
  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
841
  ```
847
842
 
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
843
  [⬆️ Back to Top](#table-of-contents-)
865
844
 
866
- ### `Micro::Cases::Flow` - How to compose use cases?
845
+ ### Composing use cases
846
+
847
+ A composition chains use cases so that each step's `Success` data feeds the next step's input. There are two ways to compose: [Flows](#flows) — covering both `Micro::Cases.flow(...)` and the class-level `flow ...` macro — and [Internal steps](#internal-steps--resultthen-chains) (the `Result#then` / `|` chain inside a single `call!`). Either form can be wrapped in a [Transaction](#transactions).
867
848
 
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.
849
+ #### Flows
850
+
851
+ A `Micro::Cases::Flow` is a stand-alone composition. Build one with `Micro::Cases.flow([...])` or the class-level `flow ...` macro:
869
852
 
870
853
  ```ruby
871
854
  module Steps
872
- class ConvertTextToNumbers < Micro::Case
873
- attribute :numbers
855
+ class ParseTags < Micro::Case
856
+ attribute :tags
874
857
 
875
858
  def call!
876
- if numbers.all? { |value| String(value) =~ /\d+/ }
877
- Success result: { numbers: numbers.map(&:to_i) }
859
+ if tags.is_a?(String)
860
+ Success result: { tags: tags.split(',').map(&:strip) }
878
861
  else
879
- Failure result: { message: 'numbers must contain only numeric types' }
862
+ Failure result: { message: 'tags must be a comma-separated String' }
880
863
  end
881
864
  end
882
865
  end
883
866
 
884
- class Add2 < Micro::Case::Strict
885
- attribute :numbers
886
-
887
- def call!
888
- Success result: { numbers: numbers.map { |number| number + 2 } }
889
- end
867
+ class Downcase < Micro::Case::Strict
868
+ attribute :tags
869
+ def call!; Success result: { tags: tags.map(&:downcase) }; end
890
870
  end
891
871
 
892
- class Double < Micro::Case::Strict
893
- attribute :numbers
894
-
895
- def call!
896
- Success result: { numbers: numbers.map { |number| number * 2 } }
897
- end
872
+ class StripHashPrefix < Micro::Case::Strict
873
+ attribute :tags
874
+ def call!; Success result: { tags: tags.map { it.sub(/\A#/, '') } }; end
898
875
  end
899
876
 
900
- class Square < Micro::Case::Strict
901
- attribute :numbers
902
-
903
- def call!
904
- Success result: { numbers: numbers.map { |number| number * number } }
905
- end
877
+ class RemoveDuplicates < Micro::Case::Strict
878
+ attribute :tags
879
+ def call!; Success result: { tags: tags.uniq }; end
906
880
  end
907
881
  end
908
882
 
909
- #-------------------------------------------#
910
- # Creating a flow using Micro::Cases.flow() #
911
- #-------------------------------------------#
912
-
913
- Add2ToAllNumbers = Micro::Cases.flow([
914
- Steps::ConvertTextToNumbers,
915
- Steps::Add2
883
+ # Using the module-level constructor:
884
+ DowncaseTags = Micro::Cases.flow([
885
+ Steps::ParseTags,
886
+ Steps::Downcase
916
887
  ])
917
888
 
918
- result = Add2ToAllNumbers.call(numbers: %w[1 1 2 2 3 4])
889
+ DowncaseTags.call(tags: 'Ruby, Rails, RUBY').data
890
+ # => { tags: ["ruby", "rails", "ruby"] }
919
891
 
920
- result.success? # true
921
- result.data # {:numbers => [3, 3, 4, 4, 5, 6]}
922
-
923
- #-------------------------------#
924
- # Creating a flow using classes #
925
- #-------------------------------#
926
-
927
- class DoubleAllNumbers < Micro::Case
928
- flow Steps::ConvertTextToNumbers,
929
- Steps::Double
892
+ # Using a class:
893
+ class NormalizeTags < Micro::Case
894
+ flow Steps::ParseTags,
895
+ Steps::Downcase,
896
+ Steps::StripHashPrefix,
897
+ Steps::RemoveDuplicates
930
898
  end
931
899
 
932
- DoubleAllNumbers.
933
- call(numbers: %w[1 1 b 2 3 4]).
934
- on_failure { |result| puts result[:message] } # "numbers must contain only numeric types"
900
+ NormalizeTags
901
+ .call(tags: 42)
902
+ .on_failure { puts it[:message] }
903
+ # => "tags must be a comma-separated String"
935
904
  ```
936
905
 
937
- When happening a failure, the use case responsible will be accessible in the result.
906
+ When a flow fails, `Result#use_case` points to the step responsible:
938
907
 
939
908
  ```ruby
940
- result = DoubleAllNumbers.call(numbers: %w[1 1 b 2 3 4])
909
+ result = NormalizeTags.call(tags: 42)
910
+ result.failure? # => true
911
+ result.use_case.is_a?(Steps::ParseTags) # => true
912
+ ```
941
913
 
942
- result.failure? # true
943
- result.use_case.is_a?(Steps::ConvertTextToNumbers) # true
914
+ ##### Composing flows together
944
915
 
945
- result.on_failure do |_message, use_case|
946
- puts "#{use_case.class.name} was the use case responsible for the failure" # Steps::ConvertTextToNumbers was the use case responsible for the failure
947
- end
916
+ Flows can be steps inside other flows. Mix any of the three composition styles:
917
+
918
+ ```ruby
919
+ DowncaseTags = Micro::Cases.flow([Steps::ParseTags, Steps::Downcase])
920
+ DedupedTags = Micro::Cases.flow([Steps::ParseTags, Steps::RemoveDuplicates])
921
+ DowncaseAndDedupedTags = Micro::Cases.flow([DowncaseTags, Steps::RemoveDuplicates])
922
+ StrippedAndDeduped = Micro::Cases.flow([Steps::ParseTags, Steps::StripHashPrefix, Steps::RemoveDuplicates])
923
+
924
+ DowncaseAndDedupedTags
925
+ .call(tags: 'Ruby, Rails, RUBY')
926
+ .on_success { p it[:tags] } # => ["ruby", "rails"]
948
927
  ```
949
928
 
950
- [⬆️ Back to Top](#table-of-contents-)
929
+ > See [`test/micro/cases/flow/blend_test.rb`](https://github.com/serradura/u-case/blob/main/test/micro/cases/flow/blend_test.rb) for every blending combination.
951
930
 
952
- #### Is it possible to compose a flow with other flows?
931
+ ##### Data accumulation through a flow
953
932
 
954
- Answer: Yes, it is possible.
933
+ Each step's `Success` output is merged into a running attributes hash that becomes the next step's input. Steps don't have to thread inputs manually — they declare what they need:
955
934
 
956
935
  ```ruby
957
- module Steps
958
- class ConvertTextToNumbers < Micro::Case
959
- attribute :numbers
936
+ module Users
937
+ class FindByEmail < Micro::Case
938
+ attribute :email
960
939
 
961
940
  def call!
962
- if numbers.all? { |value| String(value) =~ /\d+/ }
963
- Success result: { numbers: numbers.map(&:to_i) }
964
- else
965
- Failure result: { message: 'numbers must contain only numeric types' }
966
- end
967
- end
968
- end
941
+ user = User.find_by(email:)
969
942
 
970
- class Add2 < Micro::Case::Strict
971
- attribute :numbers
943
+ return Success result: { user: } if user
972
944
 
973
- def call!
974
- Success result: { numbers: numbers.map { |number| number + 2 } }
945
+ Failure(:user_not_found)
975
946
  end
976
947
  end
977
948
 
978
- class Double < Micro::Case::Strict
979
- attribute :numbers
949
+ class ValidatePassword < Micro::Case::Strict
950
+ attributes :user, :password
980
951
 
981
952
  def call!
982
- Success result: { numbers: numbers.map { |number| number * 2 } }
983
- end
984
- end
985
-
986
- class Square < Micro::Case::Strict
987
- attribute :numbers
953
+ return Failure(:user_must_be_persisted) if user.new_record?
954
+ return Failure(:wrong_password) if user.wrong_password?(password)
988
955
 
989
- def call!
990
- Success result: { numbers: numbers.map { |number| number * number } }
956
+ Success result: attributes(:user)
991
957
  end
992
958
  end
959
+
960
+ Authenticate = Micro::Cases.flow([FindByEmail, ValidatePassword])
993
961
  end
994
962
 
995
- DoubleAllNumbers =
996
- Micro::Cases.flow([Steps::ConvertTextToNumbers, Steps::Double])
963
+ Users::Authenticate
964
+ .call(email: 'somebody@test.com', password: 'password')
965
+ .on_success { sign_in(it[:user]) }
966
+ .on_failure(:wrong_password) { render status: 401 }
967
+ .on_failure(:user_not_found) { render status: 404 }
968
+ ```
997
969
 
998
- SquareAllNumbers =
999
- Micro::Cases.flow([Steps::ConvertTextToNumbers, Steps::Square])
970
+ `ValidatePassword` declares `:user` as one of its attributes but isn't passed it explicitly — it inherits it from `FindByEmail`'s success result. That's the accumulation contract: output → input.
1000
971
 
1001
- DoubleAllNumbersAndAdd2 =
1002
- Micro::Cases.flow([DoubleAllNumbers, Steps::Add2])
972
+ ##### Inspecting execution with `result.transitions`
1003
973
 
1004
- SquareAllNumbersAndAdd2 =
1005
- Micro::Cases.flow([SquareAllNumbers, Steps::Add2])
974
+ Every use case (and every internal step) contributes one entry to `result.transitions`. Use it to debug, trace, or test a flow's execution:
1006
975
 
1007
- SquareAllNumbersAndDouble =
1008
- Micro::Cases.flow([SquareAllNumbersAndAdd2, DoubleAllNumbers])
976
+ ```ruby
977
+ user_authenticated = Users::Authenticate.call(email: 'rodrigo@test.com', password: '...')
1009
978
 
1010
- DoubleAllNumbersAndSquareAndAdd2 =
1011
- Micro::Cases.flow([DoubleAllNumbers, SquareAllNumbersAndAdd2])
979
+ user_authenticated.transitions
980
+ # => [
981
+ # {
982
+ # use_case: {
983
+ # class: Users::FindByEmail,
984
+ # attributes: { email: 'rodrigo@test.com' }
985
+ # },
986
+ # success: { type: :ok, result: { user: #<User ...> } },
987
+ # accessible_attributes: [ :email, :password ]
988
+ # },
989
+ # {
990
+ # use_case: {
991
+ # class: Users::ValidatePassword,
992
+ # attributes: { user: #<User ...>, password: '...' }
993
+ # },
994
+ # success: { type: :ok, result: { user: #<User ...> } },
995
+ # accessible_attributes: [ :email, :password, :user ]
996
+ # }
997
+ # ]
998
+ ```
1012
999
 
1013
- SquareAllNumbersAndDouble
1014
- .call(numbers: %w[1 1 2 2 3 4])
1015
- .on_success { |result| p result[:numbers] } # [6, 6, 12, 12, 22, 36]
1000
+ Schema:
1016
1001
 
1017
- DoubleAllNumbersAndSquareAndAdd2
1018
- .call(numbers: %w[1 1 2 2 3 4])
1019
- .on_success { |result| p result[:numbers] } # [6, 6, 18, 18, 38, 66]
1002
+ ```ruby
1003
+ [
1004
+ {
1005
+ use_case: {
1006
+ class: <Micro::Case>, # the use case executed
1007
+ attributes: <Hash> # input
1008
+ },
1009
+ [success:, failure:] => { # output (one of the two)
1010
+ type: <Symbol>, # :ok / :error / :exception / custom
1011
+ result: <Hash> # data
1012
+ },
1013
+ accessible_attributes: <Array> # attributes accessible at this step
1014
+ # (grows with each success)
1015
+ }
1016
+ ]
1020
1017
  ```
1021
1018
 
1022
- > **Note:** You can blend any [approach](#microcasesflow---how-to-compose-use-cases) to create use case flows - [examples](https://github.com/serradura/u-case/blob/714c6b658fc6aa02617e6833ddee09eddc760f2a/test/micro/cases/flow/blend_test.rb#L5-L35).
1019
+ `accessible_attributes` grows as each step's `Success` is merged into the running data. [`Result#then`](#dynamic-continuations-with-resultthen) also contributes a transition.
1023
1020
 
1024
- [⬆️ Back to Top](#table-of-contents-)
1021
+ To disable transitions globally (saves a hash per step), see [Configuration](#configuration).
1025
1022
 
1026
- #### Is it possible a flow accumulates its input and merges each success result to use as the argument of the next use cases?
1023
+ ##### Composing a flow that includes itself
1027
1024
 
1028
- Answer: Yes, it is possible! Look at the example below to understand how the data accumulation works inside of a flow execution.
1025
+ A class can use itself as a step inside its own `flow` declaration via `self.call!`:
1029
1026
 
1030
1027
  ```ruby
1031
- module Users
1032
- class FindByEmail < Micro::Case
1033
- attribute :email
1028
+ class ParseTagsString < Micro::Case
1029
+ attribute :input
1030
+ def call!; Success result: { tags: input.split(',').map(&:strip) }; end
1031
+ end
1034
1032
 
1035
- def call!
1036
- user = User.find_by(email: email)
1033
+ class JoinTagsArray < Micro::Case
1034
+ attribute :tags
1035
+ def call!; Success result: { input: tags.join(', ') }; end
1036
+ end
1037
1037
 
1038
- return Success result: { user: user } if user
1038
+ class CleanTags < Micro::Case
1039
+ flow ParseTagsString,
1040
+ self.call!,
1041
+ JoinTagsArray
1039
1042
 
1040
- Failure(:user_not_found)
1041
- end
1043
+ attribute :tags
1044
+
1045
+ def call!
1046
+ Success result: { tags: tags.map(&:downcase).uniq }
1042
1047
  end
1043
1048
  end
1044
1049
 
1045
- module Users
1046
- class ValidatePassword < Micro::Case::Strict
1047
- attributes :user, :password
1050
+ CleanTags.call(input: 'Ruby, RUBY, Rails').data[:input] # => "ruby, rails"
1051
+ ```
1048
1052
 
1049
- def call!
1050
- return Failure(:user_must_be_persisted) if user.new_record?
1051
- return Failure(:wrong_password) if user.wrong_password?(password)
1053
+ Works with `Micro::Case::Safe` too — see [`test/micro/case/safe/with_inner_flow_test.rb`](https://github.com/serradura/u-case/blob/main/test/micro/case/safe/with_inner_flow_test.rb).
1052
1054
 
1053
- return Success result: attributes(:user)
1054
- end
1055
- end
1056
- end
1055
+ #### Internal steps — `Result#then` chains
1057
1056
 
1058
- module Users
1059
- Authenticate = Micro::Cases.flow([
1060
- FindByEmail,
1061
- ValidatePassword
1062
- ])
1063
- end
1057
+ `Result#then` (and its `|` pipe alias) is u-case's **third way of composing a flow** — alongside `Micro::Cases.flow(...)` and the class-level `flow ...` macro. Instead of wiring sibling use cases together, you keep the chain _inside_ a single use case's `call!`. Each link is a method, lambda, or another use case class; each link returns a `Micro::Case::Result`; each link's `Success` data becomes the next link's keyword arguments; each link contributes a row to `result.transitions`.
1064
1058
 
1065
- Users::Authenticate
1066
- .call(email: 'somebody@test.com', password: 'password')
1067
- .on_success { |result| sign_in(result[:user]) }
1068
- .on_failure(:wrong_password) { render status: 401 }
1069
- .on_failure(:user_not_found) { render status: 404 }
1070
- ```
1059
+ ##### Accepted link shapes
1060
+
1061
+ | Argument shape | Example |
1062
+ | ------------------------ | ------------------------------------------------ |
1063
+ | `Symbol` (method name) | `result.then(:strip_title)` |
1064
+ | Bound `Method` object | `result.then(method(:strip_title))` |
1065
+ | `Lambda` / `Proc` | `result.then(-> data { strip_title(**data) })` |
1066
+ | Use case class | `result.then(CapitalizeTitle)` |
1067
+ | `Symbol` + Hash defaults | `result.then(:add, number: 3)` |
1068
+ | Block | `result.then { \|r\| r.success? ? r[:sum] : 0 }` |
1071
1069
 
1072
- First, let's see the attributes used by each use case:
1070
+ The connecting method **must** return a `Micro::Case::Result`. Anything else raises `Micro::Case::Error::UnexpectedResult` (e.g. a method returning a plain `Hash` is rejected with `MyCase#method(:foo) must return an instance of Micro::Case::Result`).
1071
+
1072
+ ##### A minimal example
1073
1073
 
1074
1074
  ```ruby
1075
- class Users::FindByEmail < Micro::Case
1076
- attribute :email
1075
+ class CapitalizeTitle < Micro::Case
1076
+ attribute :title
1077
+
1078
+ def call!
1079
+ Success :capitalized, result: { title: title.split.map(&:capitalize).join(' ') }
1080
+ end
1077
1081
  end
1078
1082
 
1079
- class Users::ValidatePassword < Micro::Case
1080
- attributes :user, :password
1083
+ class CreateBlogPost < Micro::Case
1084
+ attributes :raw_title, :body
1085
+
1086
+ def call!
1087
+ validate_input
1088
+ .then(:strip_title)
1089
+ .then(:slugify, separator: '-')
1090
+ .then(CapitalizeTitle)
1091
+ end
1092
+
1093
+ private
1094
+
1095
+ def validate_input
1096
+ Kind.of?(String, raw_title, body) ? Success(:valid) : Failure()
1097
+ end
1098
+
1099
+ def strip_title
1100
+ Success :stripped, result: { title: raw_title.strip }
1101
+ end
1102
+
1103
+ def slugify(title:, separator:, **)
1104
+ slug = title.downcase.gsub(/[^a-z0-9]+/, separator)
1105
+ Success :slugified, result: { title:, slug: }
1106
+ end
1081
1107
  end
1108
+
1109
+ CreateBlogPost.call(raw_title: ' hello world ', body: 'lorem ipsum').data
1110
+ # => { title: "Hello World" }
1082
1111
  ```
1083
1112
 
1084
- As you can see the `Users::ValidatePassword` expects a user as its input. So, how does it receives the user?
1085
- Answer: It receives the user from the `Users::FindByEmail` success result!
1113
+ Symbol-, method-, and lambda-based links all run **as the host use case**, so they report `class: CreateBlogPost` in `result.transitions`. Only the `CapitalizeTitle` link (another use case class) contributes a transition with a different `use_case.class`. `accessible_attributes` grows as each link's `Success` output merges into the running data — by the time `CapitalizeTitle` runs, `slug` is also reachable upstream.
1086
1114
 
1087
- And this is the power of use cases composition because the output of one step will compose the input of the next use case in the flow!
1115
+ ##### `|` pipe alias
1088
1116
 
1089
- > input **>>** process **>>** output
1117
+ `|` is sugar for `.then(...)`. The previous example reads:
1090
1118
 
1091
- > **Note:** Check out these test examples [Micro::Cases::Flow](https://github.com/serradura/u-case/blob/c96a3650469da40dc9f83ff678204055b7015d01/test/micro/cases/flow/result_transitions_test.rb) and [Micro::Cases::Safe::Flow](https://github.com/serradura/u-case/blob/c96a3650469da40dc9f83ff678204055b7015d01/test/micro/cases/safe/flow/result_transitions_test.rb) to see different use cases having access to the data in a flow.
1119
+ ```ruby
1120
+ def call!
1121
+ validate_input | :strip_title | :slugify | CapitalizeTitle
1122
+ end
1123
+ ```
1092
1124
 
1093
- [⬆️ Back to Top](#table-of-contents-)
1125
+ Both forms produce identical `result.data` and `result.transitions`.
1094
1126
 
1095
- #### How to understand what is happening during a flow execution?
1127
+ > **Elixir-style chains with `it` (Ruby 3.4):** Ruby 3.4 exposes `it` as the implicit first parameter of a block/lambda body, so a chain can read almost exactly like Elixir's `|>`. Each lambda receives the accumulated data hash as `it` and must still terminate in a `Success(...)` / `Failure(...)`:
1128
+ >
1129
+ > ```ruby
1130
+ > def call!
1131
+ > validate_something \
1132
+ > | -> { do_something_with(**it) } \
1133
+ > | -> { and_another_thing_with(**it) }
1134
+ > end
1135
+ > ```
1136
+ >
1137
+ > On Ruby 2.7 – 3.3 (where `it` is just an undefined identifier), use the explicit form `->(data) { do_something_with(**data) }`.
1096
1138
 
1097
- Use `Micro::Case::Result#transitions`!
1139
+ ##### Lambda / `Method` forms
1098
1140
 
1099
- Let's use the [previous section example](#is-it-possible-a-flow-accumulates-its-input-and-merges-each-success-result-to-use-as-the-argument-of-the-next-use-cases) to ilustrate how to use this feature.
1141
+ Lambdas (and bound `Method` objects) receive the accumulated data **positionally** as a single Hash:
1100
1142
 
1101
1143
  ```ruby
1102
- user_authenticated =
1103
- Users::Authenticate.call(email: 'rodrigo@test.com', password: user_password)
1104
-
1105
- user_authenticated.transitions
1106
- [
1107
- {
1108
- :use_case => {
1109
- :class => Users::FindByEmail,
1110
- :attributes => { :email => "rodrigo@test.com" }
1111
- },
1112
- :success => {
1113
- :type => :ok,
1114
- :result => {
1115
- :user => #<User:0x00007fb57b1c5f88 @email="rodrigo@test.com" ...>
1116
- }
1117
- },
1118
- :accessible_attributes => [ :email, :password ]
1119
- },
1120
- {
1121
- :use_case => {
1122
- :class => Users::ValidatePassword,
1123
- :attributes => {
1124
- :user => #<User:0x00007fb57b1c5f88 @email="rodrigo@test.com" ...>
1125
- :password => "123456"
1126
- }
1127
- },
1128
- :success => {
1129
- :type => :ok,
1130
- :result => {
1131
- :user => #<User:0x00007fb57b1c5f88 @email="rodrigo@test.com" ...>
1132
- }
1133
- },
1134
- :accessible_attributes => [ :email, :password, :user ]
1135
- }
1136
- ]
1144
+ def call!
1145
+ validate_input
1146
+ .then(method(:strip_title))
1147
+ .then(->(data) { slugify(**data, separator: '-') })
1148
+ .then(CapitalizeTitle)
1149
+ end
1137
1150
  ```
1138
1151
 
1139
- The example above shows the output generated by the `Micro::Case::Result#transitions`.
1140
- With it is possible to analyze the use cases' execution order and what were the given `inputs` (`[:attributes]`) and `outputs` (`[:success][:result]`) in the entire execution.
1152
+ ##### `Failure` short-circuits the chain
1141
1153
 
1142
- And look up the `accessible_attributes` property, it shows whats attributes are accessible in that flow step. For example, in the last step, you can see that the `accessible_attributes` increased because of the [data flow accumulation](#is-it-possible-a-flow-accumulates-its-input-and-merges-each-success-result-to-use-as-the-argument-of-the-next-use-cases).
1154
+ Returning `Failure(...)` from any link halts the rest of the chain immediately exactly like a step in a top-level flow returning a failure. The remaining `.then(...)` / `|` links are not invoked; the final `result` is the failure.
1143
1155
 
1144
- > **Note:** The [`Micro::Case::Result#then`](#how-to-use-the-microcaseresultthen-method) increments the `Micro::Case::Result#transitions`.
1156
+ ##### Using an internal-step case inside an outer flow
1157
+
1158
+ A use case that composes internally is just a use case, so it drops into any flow:
1145
1159
 
1146
- ##### `Micro::Case::Result#transitions` schema
1147
1160
  ```ruby
1148
- [
1149
- {
1150
- use_case: {
1151
- class: <Micro::Case>,# Use case which was executed
1152
- attributes: <Hash> # (Input) The use case's attributes
1153
- },
1154
- [success:, failure:] => { # (Output)
1155
- type: <Symbol>, # Result type. Defaults:
1156
- # Success = :ok, Failure = :error/:exception
1157
- result: <Hash> # The data returned by the use case result
1158
- },
1159
- accessible_attributes: <Array>, # Properties that can be accessed by the use case's attributes,
1160
- # it starts with Hash used to invoke it and that will be incremented
1161
- # with the result values of each use case in the flow.
1162
- }
1163
- ]
1161
+ PublishWorkflow = Micro::Cases.flow([
1162
+ AuthorizePublisher,
1163
+ CreateBlogPost, # ← uses .then(:method) internally
1164
+ EnqueueIndexingJob
1165
+ ])
1164
1166
  ```
1165
1167
 
1166
- ##### Is it possible disable the `Micro::Case::Result#transitions`?
1168
+ The host's internal transitions are interleaved with the outer flow's leaf transitions in execution order. If `CreateBlogPost` produces 4 internal transitions and the outer flow has 2 other leaf steps, the final `result.transitions` has 6 entries.
1167
1169
 
1168
- Answer: Yes, it is! You can use the `Micro::Case.config` to do this. [Link to](#microcaseconfig) this section.
1170
+ ##### Persistence without a transaction
1169
1171
 
1170
- #### Is it possible to declare a flow that includes the use case itself as a step?
1171
-
1172
- Answer: Yes, it is! You can use `self` or the `self.call!` macro. e.g:
1172
+ By default when neither the host class nor the outer flow uses `transaction: true` internal steps behave like any other code in `call!`: side-effects from earlier links **persist** even if a later link returns `Failure`. The chain stops, but anything already written stays written:
1173
1173
 
1174
1174
  ```ruby
1175
- class ConvertTextToNumber < Micro::Case
1176
- attribute :text
1175
+ class CreateUserWithProfileInline < Micro::Case
1176
+ attributes :name, :info
1177
1177
 
1178
1178
  def call!
1179
- Success result: { number: text.to_i }
1179
+ create_user.then(:create_profile)
1180
1180
  end
1181
- end
1182
1181
 
1183
- class ConvertNumberToText < Micro::Case
1184
- attribute :number
1182
+ private
1185
1183
 
1186
- def call!
1187
- Success result: { text: number.to_s }
1184
+ def create_user
1185
+ user = User.create(name:)
1186
+ Success result: { user: }
1188
1187
  end
1189
- end
1190
-
1191
- class Double < Micro::Case
1192
- flow ConvertTextToNumber,
1193
- self.call!,
1194
- ConvertNumberToText
1195
1188
 
1196
- attribute :number
1189
+ def create_profile(user:, **)
1190
+ profile = UserProfile.create(user_id: user.id, info:)
1191
+ return Failure(:invalid_profile) if profile.errors.any?
1197
1192
 
1198
- def call!
1199
- Success result: { number: number * 2 }
1193
+ Success result: { user:, profile: }
1200
1194
  end
1201
1195
  end
1202
1196
 
1203
- result = Double.call(text: '4')
1204
-
1205
- result.success? # true
1206
- result[:number] # "8"
1197
+ CreateUserWithProfileInline.call(name: 'Rodrigo', info: '')
1198
+ # create_user already INSERTed the user row; create_profile failed.
1199
+ # user is persisted; profile is not. No automatic rollback.
1207
1200
  ```
1208
1201
 
1209
- > **Note:** This feature can be used with the Micro::Case::Safe. Checkout this test to see an example: https://github.com/serradura/u-case/blob/714c6b658fc6aa02617e6833ddee09eddc760f2a/test/micro/case/safe/with_inner_flow_test.rb
1202
+ To roll the partial writes back, wrap the chain in a [transaction](#transactions).
1210
1203
 
1211
- [⬆️ Back to Top](#table-of-contents-)
1204
+ #### Transactions
1212
1205
 
1213
- #### How to run a use case or flow inside a database transaction?
1206
+ u-case ships two complementary helpers for wrapping work in an `ActiveRecord::Base.transaction`. Both are opt-in — `active_record` is **not** required by the gem, so you load ActiveRecord yourself (Rails apps already do).
1214
1207
 
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).
1208
+ ##### Inline `transaction { ... }` inside `call!`
1219
1209
 
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`:
1210
+ `Micro::Case#transaction` (and `Micro::Case::Safe#transaction`) is a private instance helper that wraps a block in a database transaction and issues `ActiveRecord::Rollback` whenever the block's result is a `Failure`. The original result is returned either way, so you can keep chaining with `Result#then`:
1227
1211
 
1228
1212
  ```ruby
1229
1213
  class CreateUserWithAProfile < Micro::Case
@@ -1235,11 +1219,7 @@ class CreateUserWithAProfile < Micro::Case
1235
1219
  end
1236
1220
  ```
1237
1221
 
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`, …):
1222
+ If the block returns a failure (or raises), every row written inside the block is rolled back. The helper accepts `with:` to pick the ActiveRecord class on which `.transaction` is opened — useful for multi-database Rails apps (`ApplicationRecord`, `AnalyticsRecord`, `BillingRecord`, …):
1243
1223
 
1244
1224
  ```ruby
1245
1225
  class CreateAuditEntry < Micro::Case
@@ -1251,28 +1231,15 @@ class CreateAuditEntry < Micro::Case
1251
1231
  end
1252
1232
  ```
1253
1233
 
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.
1234
+ When `with:` is omitted, the helper falls back to the class macro (`transaction with: …`) and then to the global default callback.
1264
1235
 
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`.
1236
+ > Any class passed via `with:` (inline helper, class macro, or flow kwarg) **must be a subclass of `ActiveRecord::Base`**. Non-AR classes are rejected with `ArgumentError`.
1237
+ >
1238
+ > **Backward compatibility:** the pre-5.6.0 positional form `transaction(:activerecord) { ... }` still works as an alias for `transaction { ... }`; any other positional value raises `ArgumentError`.
1269
1239
 
1270
1240
  ##### `transaction with: …` — declaring the default for a case
1271
1241
 
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:
1242
+ A class macro lets a case declare which ActiveRecord class should own its transactions, so neither the inline helper nor any wrapping flow needs to spell it out. The declaration is inherited:
1276
1243
 
1277
1244
  ```ruby
1278
1245
  class ApplicationUseCase < Micro::Case
@@ -1281,8 +1248,7 @@ end
1281
1248
 
1282
1249
  class CreateUserWithAProfile < ApplicationUseCase
1283
1250
  flow(transaction: true, steps: [CreateUser, CreateUserProfile])
1284
- # transaction: true resolves to ApplicationRecord because that's
1285
- # what the host class declared via `transaction with:`.
1251
+ # transaction: true resolves to ApplicationRecord (inherited).
1286
1252
  end
1287
1253
 
1288
1254
  class BillingCase < ApplicationUseCase
@@ -1291,26 +1257,21 @@ class BillingCase < ApplicationUseCase
1291
1257
  end
1292
1258
  ```
1293
1259
 
1294
- ##### `Micro::Cases.flow(transaction: …, steps: [...])` — flow-level transactions
1260
+ ##### Flow-level transactions
1295
1261
 
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:
1262
+ Pass `transaction:` together with `steps:` to wrap an entire flow in a single transaction. If any step returns a failure (or raises, in a `safe_flow`), every database write performed during the flow is rolled back. Three forms:
1300
1263
 
1301
1264
  ```ruby
1302
- # Use the class-level macro (if the host case declared one) or the
1303
- # global default (`ActiveRecord::Base` unless configured otherwise).
1265
+ # Use the class-level macro (if the host case declared one) or the global default.
1304
1266
  Micro::Cases.flow(transaction: true, steps: [CreateUser, CreateUserProfile])
1305
1267
 
1306
- # Pick an explicit ActiveRecord class for this flow only — same `with:`
1307
- # vocabulary as the inline helper and the class macro.
1268
+ # Pick an explicit ActiveRecord class for this flow only — same `with:` vocabulary.
1308
1269
  Micro::Cases.flow(transaction: { with: AnalyticsRecord }, steps: [
1309
1270
  WriteAuditLog,
1310
1271
  BumpCounter
1311
1272
  ])
1312
1273
 
1313
- # safe_flow rolls back on failures AND on unexpected exceptions
1274
+ # safe_flow rolls back on failures AND on unexpected exceptions.
1314
1275
  Micro::Cases.safe_flow(transaction: { with: ApplicationRecord }, steps: [
1315
1276
  CreateUser,
1316
1277
  CreateUserProfile
@@ -1322,9 +1283,7 @@ class CreateUserWithAProfile < Micro::Case
1322
1283
  end
1323
1284
  ```
1324
1285
 
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:
1286
+ To nest a transactional flow inside another flow, wrap it in a use case class — `Micro::Cases.flow([...])` flattens `Flow` instances passed as steps, but does **not** flatten classes:
1328
1287
 
1329
1288
  ```ruby
1330
1289
  class CreateUserAndProfile < Micro::Case
@@ -1339,17 +1298,11 @@ SignUpFlow = Micro::Cases.flow([
1339
1298
  ])
1340
1299
  ```
1341
1300
 
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`.
1301
+ If `transaction: true` is used while `ActiveRecord::Base` is not loaded, the flow raises `Micro::Cases::Error::TransactionAdapterMissing` on the first call so the misconfiguration surfaces immediately. Passing `transaction: { with: SomeClass }` skips this check — `SomeClass` is trusted to respond to `.transaction`.
1347
1302
 
1348
- ##### `config.default_transaction_class { … }` — global default
1303
+ ##### Global default — `config.default_transaction_class { … }`
1349
1304
 
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:
1305
+ For Rails apps that use a single abstract record (`ApplicationRecord`), configure it once in an initializer instead of declaring it on every case or flow:
1353
1306
 
1354
1307
  ```ruby
1355
1308
  # config/initializers/u_case.rb
@@ -1358,60 +1311,44 @@ Micro::Case.config do |config|
1358
1311
  end
1359
1312
  ```
1360
1313
 
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:
1314
+ The callback (block or lambda) is invoked **every time** a transaction opens — no memoization — so the return value can depend on runtime state (per-tenant routing, etc.). The default is `-> { ::ActiveRecord::Base }`.
1315
+
1316
+ Resolution order, when a transaction opens:
1365
1317
 
1366
- 1. **Call-site override.** `transaction: { with: X }` on a flow kwarg, or
1367
- `transaction(with: X) { ... }` on the inline helper.
1318
+ 1. **Call-site override** `transaction: { with: X }` on a flow kwarg, or `transaction(with: X) { ... }` on the inline helper.
1368
1319
  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`).
1320
+ 3. **`Micro::Case.config.default_transaction_class.call`** — the global callback (defaults to `ActiveRecord::Base`).
1371
1321
 
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.
1322
+ A non-callable assignment to `default_transaction_class=` raises `ArgumentError` at config time so typos like `config.default_transaction_class = 'ApplicationRecord'` fail loudly instead of crashing the first transaction.
1375
1323
 
1376
1324
  ##### Internal-step flows under transactions
1377
1325
 
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`.
1326
+ [Internal steps](#internal-steps--resultthen-chains) — the `Result#then(:symbol)` / `|` form built inline inside a single `call!` — are an _internal_ flow. By default they have **no transactional rollback**: side-effects from earlier `.then(:method)` links persist even when a later link returns `Failure`.
1384
1327
 
1385
- There are two natural ways to give an internal flow transactional
1386
- rollback. Both reuse the helpers already covered above:
1328
+ Two natural ways to give them rollback:
1387
1329
 
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:
1330
+ **1. Wrap the host case in a `transaction: true` flow.** Recommended once the host case sits inside a larger pipeline. The transaction spans the whole flow call, so a `Failure` _anywhere_ — including from any internal `.then(:method)` link — rolls back every database write:
1393
1331
 
1394
1332
  ```ruby
1395
1333
  class CreateUserWithProfileInline < Micro::Case
1396
1334
  attributes :name, :info
1397
1335
 
1398
1336
  def call!
1399
- create_user
1400
- .then(:create_profile)
1337
+ create_user.then(:create_profile)
1401
1338
  end
1402
1339
 
1403
1340
  private
1404
1341
 
1405
1342
  def create_user
1406
- user = User.create(name: name)
1407
- Success result: { user: user }
1343
+ user = User.create(name:)
1344
+ Success result: { user: }
1408
1345
  end
1409
1346
 
1410
1347
  def create_profile(user:, **)
1411
- profile = UserProfile.create(user_id: user.id, info: info)
1348
+ profile = UserProfile.create(user_id: user.id, info:)
1412
1349
  return Failure(:invalid_profile) if profile.errors.any?
1413
1350
 
1414
- Success result: { user: user, profile: profile }
1351
+ Success result: { user:, profile: }
1415
1352
  end
1416
1353
  end
1417
1354
 
@@ -1420,687 +1357,302 @@ SignUp = Micro::Cases.flow(transaction: true, steps: [
1420
1357
  CreateUserWithProfileInline, # ← internal failure now rolls back
1421
1358
  EnqueueIndexingJob
1422
1359
  ])
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
1360
  ```
1433
1361
 
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.
1362
+ If `create_profile` returns `Failure(:invalid_profile)`, the `User` row inserted earlier is rolled back as part of the same `ActiveRecord::Base.transaction`. The result still surfaces the failure type and the partial transitions, but no row is left behind.
1439
1363
 
1440
- **2. Use the inline `Micro::Case#transaction` helper** to scope the
1441
- rollback to a single `call!` without involving an outer flow:
1364
+ **2. Use the inline `transaction { ... }` helper** to scope the rollback to a single `call!` without involving an outer flow:
1442
1365
 
1443
1366
  ```ruby
1444
1367
  class CreateUserWithProfileInline < Micro::Case
1445
1368
  def call!
1446
1369
  transaction {
1447
- create_user
1448
- .then(:create_profile)
1370
+ create_user.then(:create_profile)
1449
1371
  }
1450
1372
  end
1451
1373
  end
1452
1374
  ```
1453
1375
 
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.
1376
+ The two approaches compose. If `CreateUserWithProfileInline` (using inline `transaction { ... }`) sits inside an outer `transaction: true` flow, ActiveRecord joins the inner transaction into the outer one by default — an outer failure rolls back the inner's writes too.
1465
1377
 
1466
1378
  ##### Behavior notes
1467
1379
 
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
-
1494
- ### `Micro::Case::Strict` - What is a strict use case?
1495
-
1496
- Answer: it is a kind of use case that will require all the keywords (attributes) on its initialization.
1497
-
1498
- ```ruby
1499
- class Double < Micro::Case::Strict
1500
- attribute :numbers
1501
-
1502
- def call!
1503
- Success result: { numbers: numbers.map { |number| number * 2 } }
1504
- end
1505
- end
1506
-
1507
- Double.call({})
1508
-
1509
- # The output will be:
1510
- # ArgumentError (missing keyword: :numbers)
1511
- ```
1380
+ - **Result is unaffected.** `transaction: true` only affects database side-effects. `result.data`, `result.type`, `result.transitions`, and `result.accessible_attributes` are identical to those of an equivalent non-transactional flow.
1381
+ - **`Flow` instances get flattened.** `Micro::Cases.flow([inner_flow, Other])` flattens `inner_flow` into its leaf steps — a transactional `Flow` instance passed this way **loses its transaction**. Wrap reusable transactional flows in a use case class to preserve their transaction when nested.
1382
+ - **Nested transactions join the outer one.** ActiveRecord joins them by default (no `requires_new: true`). A failure anywhere in the chain rolls back **everything** written inside the outermost transaction.
1383
+ - **A non-transactional outer commits the inner.** If the outer flow is not transactional and the inner transactional flow succeeds, the inner's writes commit at the end of the inner step. A failure in a later (non-transactional) step **does not** undo those writes.
1384
+ - **Plain `Micro::Cases.flow(transaction: true, ...)` re-raises exceptions.** The transaction still rolls back, but the caller has to rescue. Use `Micro::Cases.safe_flow(transaction: true, ...)` (or the class-level form with `Micro::Case::Safe`) to capture the exception as a `:exception` failure result.
1512
1385
 
1513
1386
  [⬆️ Back to Top](#table-of-contents-)
1514
1387
 
1515
- ### `Micro::Case::Safe` - Is there some feature to auto handle exceptions inside of a use case or flow?
1388
+ ## Configuration
1516
1389
 
1517
- Yes, there is one! Like `Micro::Case::Strict` the `Micro::Case::Safe` is another kind of use case. It has the ability to auto intercept any exception as a failure result. e.g:
1390
+ `Micro::Case.config` exposes the gem's toggles. Set them once typically in a Rails initializer:
1518
1391
 
1519
1392
  ```ruby
1520
- require 'logger'
1521
-
1522
- AppLogger = Logger.new(STDOUT)
1393
+ Micro::Case.config do |config|
1394
+ # Auto-fail use cases on ActiveModel validation errors.
1395
+ config.enable_activemodel_validation = false
1523
1396
 
1524
- class Divide < Micro::Case::Safe
1525
- attributes :a, :b
1397
+ # Type symbol used by the auto-failure produced when ActiveModel validation
1398
+ # rejects an attribute (shared with the accept:/reject: rejection failure).
1399
+ # Default is :invalid_attributes.
1400
+ config.set_activemodel_validation_errors_failure = :invalid_attributes
1526
1401
 
1527
- def call!
1528
- if a.is_a?(Integer) && b.is_a?(Integer)
1529
- Success result: { number: a / b}
1530
- else
1531
- Failure(:not_an_integer)
1532
- end
1533
- end
1534
- end
1535
-
1536
- result = Divide.call(a: 2, b: 0)
1537
- result.type == :exception # true
1538
- result.data # { exception: #<ZeroDivisionError...> }
1539
- result[:exception].is_a?(ZeroDivisionError) # true
1402
+ # Record Micro::Case::Result#transitions on every flow step.
1403
+ # Set to false to save the per-step hash allocation in hot paths.
1404
+ config.enable_transitions = true
1540
1405
 
1541
- result.on_failure(:exception) do |result|
1542
- AppLogger.error(result[:exception].message) # E, [2019-08-21T00:05:44.195506 #9532] ERROR -- : divided by 0
1543
- end
1544
- ```
1406
+ # Forbid the Safe APIs to enforce a single exception-handling convention
1407
+ # (plain `rescue` inside use cases). When true, the following raise
1408
+ # Micro::Case::Error::SafeFeaturesDisabled:
1409
+ # - subclassing Micro::Case::Safe
1410
+ # - calling Micro::Cases.safe_flow(...)
1411
+ # - calling Micro::Case::Result#on_exception
1412
+ config.disable_safe_features = false
1545
1413
 
1546
- If you need to handle a specific error, I recommend the usage of a case statement. e,g:
1414
+ # Skip the gem's internal argument/contract checks for a small perf win in
1415
+ # production once your test suite has exercised the code paths. Misuse will
1416
+ # then surface as downstream errors instead of the gem's curated ones.
1417
+ config.disable_runtime_checks = false
1547
1418
 
1548
- ```ruby
1549
- result.on_failure(:exception) do |data, use_case|
1550
- case exception = data[:exception]
1551
- when ZeroDivisionError then AppLogger.error(exception.message)
1552
- else AppLogger.debug("#{use_case.class.name} was the use case responsible for the exception")
1553
- end
1419
+ # The ActiveRecord class used by `transaction: true`. Pass a block (or lambda).
1420
+ # The default is `-> { ::ActiveRecord::Base }`. Override to use a per-app
1421
+ # abstract record like ApplicationRecord.
1422
+ config.default_transaction_class { ApplicationRecord }
1554
1423
  end
1555
1424
  ```
1556
1425
 
1557
- > **Note:** It is possible to rescue an exception even when is a safe use case. Examples: https://github.com/serradura/u-case/blob/714c6b658fc6aa02617e6833ddee09eddc760f2a/test/micro/case/safe_test.rb#L90-L118
1426
+ All internal checks live in `Micro::Case::Check::Enabled` (the default). Toggling `disable_runtime_checks = true` swaps `Micro::Case.check` to `Micro::Case::Check::Disabled`, whose methods are no-ops the validations themselves stop running on each call.
1558
1427
 
1559
1428
  [⬆️ Back to Top](#table-of-contents-)
1560
1429
 
1561
- #### `Micro::Cases::Safe::Flow`
1430
+ ## Performance
1562
1431
 
1563
- As the safe use cases, safe flows can intercept an exception in any of its steps. These are the ways to define one:
1432
+ In benchmarks against comparable abstractions, `Micro::Case` is the fastest after `Dry::Monads`:
1564
1433
 
1565
- ```ruby
1566
- module Users
1567
- Create = Micro::Cases.safe_flow([
1568
- ProcessParams,
1569
- ValidateParams,
1570
- Persist,
1571
- SendToCRM
1572
- ])
1573
- end
1574
- ```
1575
-
1576
- Defining within classes:
1577
-
1578
- ```ruby
1579
- module Users
1580
- class Create < Micro::Case::Safe
1581
- flow ProcessParams,
1582
- ValidateParams,
1583
- Persist,
1584
- SendToCRM
1585
- end
1586
- end
1587
- ```
1588
-
1589
- [⬆️ Back to Top](#table-of-contents-)
1434
+ | Gem / Abstraction | Success (i/s) | Failure (i/s) |
1435
+ | ---------------------- | ------------: | ------------: |
1436
+ | Dry::Monads | 315,635.1 | 135,386.9 |
1437
+ | **Micro::Case** | 75,837.7 | 73,489.3 |
1438
+ | Interactor | 59,745.5 | 27,037.0 |
1439
+ | Trailblazer::Operation | 28,423.9 | 29,016.4 |
1440
+ | Dry::Transaction | 10,130.9 | 8,988.6 |
1590
1441
 
1591
- #### `Micro::Case::Result#on_exception`
1442
+ For flows, the `|` pipe alias is the fastest composition style:
1592
1443
 
1593
- In functional programming errors/exceptions are handled as regular data, the idea is to transform the output even when it happens an unexpected behavior. For many, [exceptions are very similar to the GOTO statement](https://softwareengineering.stackexchange.com/questions/189222/are-exceptions-as-control-flow-considered-a-serious-antipattern-if-so-why), jumping the application flow to paths which could be difficult to figure out how things work in a system.
1444
+ | Composition style | Success | Failure |
1445
+ | -------------------------------- | -----------: | -----------: |
1446
+ | `Result#\|` (pipe) | 80,936.2 | 78,280.4 |
1447
+ | `Micro::Cases.flow(...)` | same-ish | same-ish |
1448
+ | `Result#then` | same-ish | same-ish |
1449
+ | Class with inner `flow` | 1.72× slower | 1.68× slower |
1450
+ | Class including itself as a step | 1.93× slower | 1.87× slower |
1451
+ | `Interactor::Organizer` | 3.33× slower | 3.22× slower |
1594
1452
 
1595
- To address this the `Micro::Case::Result` has a special hook `#on_exception` to helping you to handle the control flow in the case of exceptions.
1453
+ > `Dry::Monads`, `Dry::Transaction`, and `Trailblazer::Operation` don't ship a flow-equivalent feature and are excluded from the flow table.
1596
1454
 
1597
- > **Note**: this feature will work better if you use it with a `Micro::Case::Safe` flow or use case.
1598
-
1599
- **How does it work?**
1600
-
1601
- ```ruby
1602
- class Divide < Micro::Case::Safe
1603
- attributes :a, :b
1455
+ ### Running the benchmarks
1604
1456
 
1605
- def call!
1606
- Success result: { division: a / b }
1607
- end
1608
- end
1457
+ ```sh
1458
+ # Use cases
1459
+ ruby benchmarks/perfomance/use_case/success_results.rb
1460
+ ruby benchmarks/perfomance/use_case/failure_results.rb
1609
1461
 
1610
- Divide
1611
- .call(a: 2, b: 0)
1612
- .on_success { |result| puts result[:division] }
1613
- .on_exception(TypeError) { puts 'Please, use only numeric attributes.' }
1614
- .on_exception(ZeroDivisionError) { |_error| puts "Can't divide a number by 0." }
1615
- .on_exception { |_error, _use_case| puts 'Oh no, something went wrong!' }
1616
-
1617
- # Output:
1618
- # -------
1619
- # Can't divide a number by 0
1620
- # Oh no, something went wrong!
1621
-
1622
- Divide
1623
- .call(a: 2, b: '2')
1624
- .on_success { |result| puts result[:division] }
1625
- .on_exception(TypeError) { puts 'Please, use only numeric attributes.' }
1626
- .on_exception(ZeroDivisionError) { |_error| puts "Can't divide a number by 0." }
1627
- .on_exception { |_error, _use_case| puts 'Oh no, something went wrong!' }
1628
-
1629
- # Output:
1630
- # -------
1631
- # Please, use only numeric attributes.
1632
- # Oh no, something went wrong!
1462
+ # Flows
1463
+ ruby benchmarks/perfomance/flow/success_results.rb
1464
+ ruby benchmarks/perfomance/flow/failure_results.rb
1633
1465
  ```
1634
1466
 
1635
- As you can see, this hook has the same behavior of `result.on_failure(:exception)`, but, the idea here is to have a better communication in the code, making an explicit reference when some failure happened because of an exception.
1636
-
1637
- [⬆️ Back to Top](#table-of-contents-)
1467
+ Memory profiling:
1638
1468
 
1639
- #### Opting out of the safe mechanism
1640
-
1641
- The "safe" mechanism is opinionated: it converts any unhandled exception inside a use case (or any step of a flow) into a failure result with `type: :exception`. That is powerful, but it can also produce a **fragmented codebase**, where some exceptions are handled with `rescue` inside `call!` and others are handled later via `on_exception` / `on_failure(:exception)` — making the control flow hard to reason about.
1642
-
1643
- If you prefer a single, explicit convention for exception handling — namely, plain `rescue` statements inside your use cases — you can disable the safe APIs entirely:
1644
-
1645
- ```ruby
1646
- Micro::Case.config do |config|
1647
- config.disable_safe_features = true
1648
- end
1469
+ ```sh
1470
+ ./benchmarks/memory/use_case/success/with_transitions/analyze.sh
1471
+ ./benchmarks/memory/use_case/success/without_transitions/analyze.sh
1472
+ ./benchmarks/memory/flow/success/with_transitions/analyze.sh
1473
+ ./benchmarks/memory/flow/success/without_transitions/analyze.sh
1649
1474
  ```
1650
1475
 
1651
- Once enabled, the following will raise `Micro::Case::Error::SafeFeaturesDisabled`, ensuring no one in the codebase can reintroduce the safe path by accident:
1476
+ ### Disabling runtime checks
1652
1477
 
1653
- - Subclassing `Micro::Case::Safe`
1654
- - Calling `Micro::Cases.safe_flow(...)`
1655
- - Calling `Micro::Case::Result#on_exception`
1656
-
1657
- See [`Micro::Case.config`](#microcaseconfig) for the full list of available toggles.
1658
-
1659
- [⬆️ Back to Top](#table-of-contents-)
1660
-
1661
- ### Validating attributes with `accept:` / `reject:`
1662
-
1663
- Since `u-case 5.2.0`, every use case includes the [`accept` extension](https://github.com/serradura/u-attributes#accept-extension) from [`u-attributes`](https://github.com/serradura/u-attributes) (requires `u-attributes >= 2.8`). You can declare type expectations (or any other check) directly on the attribute, and the use case will fail automatically with the `:invalid_attributes` type when any attribute is rejected — no need to validate inside `call!`.
1478
+ Set `disable_runtime_checks = true` for an extra few percent in production once your test suite has exercised the code paths:
1664
1479
 
1665
1480
  ```ruby
1666
- class CreateUser < Micro::Case
1667
- attribute :name, accept: String
1668
- attribute :email, accept: ->(value) { value.is_a?(String) && value.include?('@') }
1669
- attribute :age, accept: Integer, allow_nil: true
1670
-
1671
- def call!
1672
- Success result: { user: User.create!(attributes) }
1673
- end
1674
- end
1675
-
1676
- CreateUser.call(name: 'Bob', email: 'bob@example.com')
1677
- # => #<Success type=:ok ...>
1678
-
1679
- CreateUser.call(name: 42, email: 'not-an-email')
1680
- # => #<Failure type=:invalid_attributes data={
1681
- # errors: {
1682
- # "name" => "expected to be a kind of String",
1683
- # "email" => "is invalid"
1684
- # }
1685
- # }>
1481
+ Micro::Case.config { it.disable_runtime_checks = true }
1686
1482
  ```
1687
1483
 
1688
- The failure type follows the same setting used by the ActiveModel validation integration see [`Micro::Case.config`](#microcaseconfig) and `set_activemodel_validation_errors_failure`.
1484
+ Measured wins (see [`benchmarks/perfomance/runtime_checks/compare.rb`](https://github.com/serradura/u-case/blob/main/benchmarks/perfomance/runtime_checks/compare.rb)) are JIT-dependent: within noise on stock Ruby, ~3–5% on Ruby 3.2 +YJIT, ~4–7% on Ruby 4.0 +PRISM.
1689
1485
 
1690
- When combined with [`u-case/with_activemodel_validation`](#u-casewith_activemodel_validation---how-to-validate-the-use-case-attributes), the execution order is:
1486
+ ### Comparisons
1691
1487
 
1692
- 1. `u-attributes` resolves the default value of each attribute.
1693
- 2. `u-attributes` runs the `accept:` / `reject:` checks.
1694
- 3. `u-case` runs the `ActiveModel` validations **only if** every attribute was accepted.
1488
+ Side-by-side implementations of the same use case in other libraries:
1695
1489
 
1696
- [⬆️ Back to Top](#table-of-contents-)
1490
+ - [Interactor](https://github.com/serradura/u-case/blob/main/comparisons/interactor.rb)
1491
+ - [u-case](https://github.com/serradura/u-case/blob/main/comparisons/u-case.rb)
1697
1492
 
1698
- ### `u-case/with_activemodel_validation` - How to validate the use case attributes?
1493
+ [⬆️ Back to Top](#table-of-contents-)
1699
1494
 
1700
- **Requirement:**
1495
+ ## Examples
1701
1496
 
1702
- To do this your application must have the [activemodel >= 3.2, < 6.1.0](https://rubygems.org/gems/activemodel) as a dependency.
1497
+ ### An end-to-end sign-up flow
1703
1498
 
1704
- By default, if your application has ActiveModel as a dependency, any kind of use case can make use of it to validate its attributes.
1499
+ Three use cases composed into a transactional flow, using `accept:` validation, result contracts, and hooks:
1705
1500
 
1706
1501
  ```ruby
1707
- class Multiply < Micro::Case
1708
- attributes :a, :b
1502
+ class NormalizeParams < Micro::Case
1503
+ attribute :params, accept: Hash
1709
1504
 
1710
- validates :a, :b, presence: true, numericality: true
1711
-
1712
- def call!
1713
- return Failure :invalid_attributes, result: { errors: self.errors } if invalid?
1714
-
1715
- Success result: { number: a * b }
1505
+ results do |on|
1506
+ on.success(result: [:name, :email])
1507
+ on.failure(:invalid_params)
1716
1508
  end
1717
- end
1718
- ```
1719
-
1720
- But if do you want an automatic way to fail your use cases on validation errors, you could do:
1721
-
1722
- 1. **require 'u-case/with_activemodel_validation'** in the Gemfile
1723
-
1724
- ```ruby
1725
- gem 'u-case', require: 'u-case/with_activemodel_validation'
1726
- ```
1727
1509
 
1728
- 2. Use the `Micro::Case.config` to enable it. [Link to](#microcaseconfig) this section.
1729
-
1730
- Using this approach, you can rewrite the previous example with less code. e.g:
1731
-
1732
- ```ruby
1733
- require 'u-case/with_activemodel_validation'
1734
-
1735
- class Multiply < Micro::Case
1736
- attributes :a, :b
1510
+ def call!
1511
+ name = params[:name].to_s.strip
1512
+ email = params[:email].to_s.strip.downcase
1737
1513
 
1738
- validates :a, :b, presence: true, numericality: true
1514
+ return Failure(:invalid_params) if name.empty? || email.empty?
1739
1515
 
1740
- def call!
1741
- Success result: { number: a * b }
1516
+ Success result: { name:, email: }
1742
1517
  end
1743
1518
  end
1744
- ```
1745
-
1746
- > **Note:** After requiring the validation mode, the `Micro::Case::Strict` and `Micro::Case::Safe` classes will inherit this new behavior.
1747
1519
 
1748
- #### If I enabled the auto validation, is it possible to disable it only in specific use cases?
1749
-
1750
- Answer: Yes, it is possible. To do this, you will need to use the `disable_auto_validation` macro. e.g:
1520
+ class CreateUser < Micro::Case
1521
+ attributes :name, :email
1751
1522
 
1752
- ```ruby
1753
- require 'u-case/with_activemodel_validation'
1523
+ results do |on|
1524
+ on.success(result: [:user])
1525
+ on.failure(:invalid_user)
1526
+ end
1754
1527
 
1755
- class Multiply < Micro::Case
1756
- disable_auto_validation
1528
+ def call!
1529
+ user = User.create(name:, email:)
1757
1530
 
1758
- attribute :a
1759
- attribute :b
1760
- validates :a, :b, presence: true, numericality: true
1531
+ return Failure(:invalid_user, result: { errors: user.errors }) if user.errors.any?
1761
1532
 
1762
- def call!
1763
- Success result: { number: a * b }
1533
+ Success result: { user: }
1764
1534
  end
1765
1535
  end
1766
1536
 
1767
- Multiply.call(a: 2, b: 'a')
1768
-
1769
- # The output will be:
1770
- # TypeError (String can't be coerced into Integer)
1771
- ```
1772
-
1773
- [⬆️ Back to Top](#table-of-contents-)
1774
-
1775
- #### `Kind::Validator`
1776
-
1777
- The [kind gem](https://github.com/serradura/kind) has a module to enable the validation of data type through [`ActiveModel validations`](https://guides.rubyonrails.org/active_model_basics.html#validations). So, when you require the `'u-case/with_activemodel_validation'`, this module will also require the [`Kind::Validator`](https://github.com/serradura/kind#kindvalidator-activemodelvalidations).
1778
-
1779
- The example below shows how to validate the attributes types.
1780
-
1781
- ```ruby
1782
- class Todo::List::AddItem < Micro::Case
1783
- attributes :user, :params
1537
+ class CreateProfile < Micro::Case
1538
+ attributes :user
1784
1539
 
1785
- validates :user, kind: User
1786
- validates :params, kind: ActionController::Parameters
1540
+ results do |on|
1541
+ on.success(result: [:profile])
1542
+ on.failure(:invalid_profile)
1543
+ end
1787
1544
 
1788
1545
  def call!
1789
- todo_params = params.require(:todo).permit(:title, :due_at)
1546
+ profile = Profile.create(user_id: user.id)
1790
1547
 
1791
- todo = user.todos.create(todo_params)
1548
+ return Failure(:invalid_profile, result: { errors: profile.errors }) if profile.errors.any?
1792
1549
 
1793
- Success result: { todo: todo }
1794
- rescue ActionController::ParameterMissing => e
1795
- Failure :parameter_missing, result: { message: e.message }
1550
+ Success result: { profile: }
1796
1551
  end
1797
1552
  end
1798
- ```
1799
-
1800
- [⬆️ Back to Top](#table-of-contents-)
1801
-
1802
- ## `Micro::Case.config`
1803
1553
 
1804
- The idea of this resource is to allow the configuration of some `u-case` features/modules.
1805
- I recommend you use it only once in your codebase. e.g. In a Rails initializer.
1554
+ SignUp = Micro::Cases.flow(transaction: true, steps: [
1555
+ NormalizeParams,
1556
+ CreateUser,
1557
+ CreateProfile
1558
+ ])
1806
1559
 
1807
- You can see below, which are the available configurations with their default values:
1560
+ SignUp
1561
+ .call(params: { name: 'Ada', email: 'ADA@EXAMPLE.com' })
1562
+ .on_success { render json: { user_id: it[:user].id } }
1563
+ .on_failure(:invalid_params) { render status: 422 }
1564
+ .on_failure(:invalid_user) { render status: 422, json: { errors: it[:errors] } }
1565
+ .on_failure(:invalid_profile) { render status: 422, json: { errors: it[:errors] } }
1566
+ ```
1808
1567
 
1809
- ```ruby
1810
- Micro::Case.config do |config|
1811
- # Use ActiveModel to auto-validate your use cases' attributes.
1812
- config.enable_activemodel_validation = false
1568
+ If `CreateProfile` fails, the `User` row inserted by `CreateUser` is rolled back — that's `transaction: true` doing its job. The result surfaces `:invalid_profile`, the hook fires, and the database is left clean.
1813
1569
 
1814
- # Use to enable/disable the `Micro::Case::Results#transitions`.
1815
- config.enable_transitions = true
1570
+ ### More examples
1816
1571
 
1817
- # Use to forbid the "safe" features and ensure a single way to handle exceptions
1818
- # (via standard `rescue` statements). When set to `true`, the following will raise
1819
- # `Micro::Case::Error::SafeFeaturesDisabled`:
1820
- # - Subclassing `Micro::Case::Safe`
1821
- # - Calling `Micro::Cases.safe_flow(...)`
1822
- # - Calling `Micro::Case::Result#on_exception`
1823
- config.disable_safe_features = false
1824
-
1825
- # Use to skip the gem's internal argument/contract checks (e.g., "is this a
1826
- # Micro::Case?", "is the result type a Symbol?", "is the use case a kind of
1827
- # Micro::Case?"). Set to `true` in production for a small performance boost
1828
- # once your code paths are exercised by your test suite. The trade-off is that
1829
- # incorrect usage will surface as confusing downstream errors instead of the
1830
- # gem's curated ones (e.g. `Micro::Case::Error::InvalidUseCase`).
1831
- config.disable_runtime_checks = false
1832
- end
1833
- ```
1834
-
1835
- All checks are consolidated in `Micro::Case::Check::Enabled` (the default).
1836
- Toggling `disable_runtime_checks = true` swaps `Micro::Case.check` to
1837
- `Micro::Case::Check::Disabled` — a module with the same signature whose
1838
- methods are no-ops — so the validations themselves are not run on each call.
1572
+ - **[Users creation flow](https://github.com/serradura/u-case/blob/main/examples/users_creation)** sanitize, validate, persist; demonstrates every composition style.
1573
+ - **[Rails app (API)](https://github.com/serradura/from-fat-controllers-to-use-cases)** different architectures across commits; the last one uses `Micro::Case` for the business logic.
1574
+ - **[CLI calculator](https://github.com/serradura/u-case/tree/main/examples/calculator)** — Rake tasks demonstrating user-input handling and failure-type-driven control flow.
1575
+ - **[Rescuing exceptions](https://github.com/serradura/u-case/blob/main/examples/rescuing_exceptions.rb)** — patterns for exception handling inside use cases.
1839
1576
 
1840
1577
  [⬆️ Back to Top](#table-of-contents-)
1841
1578
 
1842
- ## Benchmarks
1579
+ ## Going further with `u-attributes`
1843
1580
 
1844
- ### `Micro::Case`
1581
+ `Micro::Case`'s `attribute` / `attributes` macros come from [`u-attributes`](https://github.com/serradura/u-attributes), and every feature that gem supports is available on every use case. Two patterns worth knowing — **both require [`u-attributes >= 3.1`](https://github.com/serradura/u-attributes)**:
1845
1582
 
1846
- #### Success results
1583
+ ### Nested attributes (block form)
1847
1584
 
1848
- | Gem / Abstraction | Iterations per second | Comparison |
1849
- | ----------------- | --------------------: | ----------------: |
1850
- | Dry::Monads | 315635.1 | _**The Fastest**_ |
1851
- | **Micro::Case** | 75837.7 | 4.16x slower |
1852
- | Interactor | 59745.5 | 5.28x slower |
1853
- | Trailblazer::Operation | 28423.9 | 11.10x slower |
1854
- | Dry::Transaction | 10130.9 | 31.16x slower |
1855
-
1856
- <details>
1857
- <summary>Show the full <a href="https://github.com/evanphx/benchmark-ips">benchmark/ips</a> results.</summary>
1585
+ Declare an attribute that itself has attributes — useful when your input is a structured object instead of a flat hash. `accept:` on the inner attributes still participates in the parent's `:invalid_attributes` failure:
1858
1586
 
1859
1587
  ```ruby
1860
- # Warming up --------------------------------------
1861
- # Interactor 5.711k i/100ms
1862
- # Trailblazer::Operation
1863
- # 2.283k i/100ms
1864
- # Dry::Monads 31.130k i/100ms
1865
- # Dry::Transaction 994.000 i/100ms
1866
- # Micro::Case 7.911k i/100ms
1867
- # Micro::Case::Safe 7.911k i/100ms
1868
- # Micro::Case::Strict 6.248k i/100ms
1869
-
1870
- # Calculating -------------------------------------
1871
- # Interactor 59.746k (±29.9%) i/s - 274.128k in 5.049901s
1872
- # Trailblazer::Operation
1873
- # 28.424k (±15.8%) i/s - 141.546k in 5.087882s
1874
- # Dry::Monads 315.635k (± 6.1%) i/s - 1.588M in 5.048914s
1875
- # Dry::Transaction 10.131k (± 6.4%) i/s - 50.694k in 5.025150s
1876
- # Micro::Case 75.838k (± 9.7%) i/s - 379.728k in 5.052573s
1877
- # Micro::Case::Safe 75.461k (±10.1%) i/s - 379.728k in 5.079238s
1878
- # Micro::Case::Strict 64.235k (± 9.0%) i/s - 324.896k in 5.097028s
1879
-
1880
- # Comparison:
1881
- # Dry::Monads: 315635.1 i/s
1882
- # Micro::Case: 75837.7 i/s - 4.16x (± 0.00) slower
1883
- # Micro::Case::Safe: 75461.3 i/s - 4.18x (± 0.00) slower
1884
- # Micro::Case::Strict: 64234.9 i/s - 4.91x (± 0.00) slower
1885
- # Interactor: 59745.5 i/s - 5.28x (± 0.00) slower
1886
- # Trailblazer::Operation: 28423.9 i/s - 11.10x (± 0.00) slower
1887
- # Dry::Transaction: 10130.9 i/s - 31.16x (± 0.00) slower
1888
- ```
1889
- </details>
1890
-
1891
- https://github.com/serradura/u-case/blob/main/benchmarks/perfomance/use_case/success_results.rb
1588
+ class CreateOrder < Micro::Case
1589
+ attribute :id, accept: Integer
1892
1590
 
1893
- #### Failure results
1591
+ attribute :customer do
1592
+ attribute :name, accept: String
1593
+ attribute :email, accept: String
1594
+ end
1894
1595
 
1895
- | Gem / Abstraction | Iterations per second | Comparison |
1896
- | ----------------- | --------------------: | ----------------: |
1897
- | Dry::Monads | 135386.9 | _**The Fastest**_ |
1898
- | **Micro::Case** | 73489.3 | 1.85x slower |
1899
- | Trailblazer::Operation | 29016.4 | 4.67x slower |
1900
- | Interactor | 27037.0 | 5.01x slower |
1901
- | Dry::Transaction | 8988.6 | 15.06x slower |
1596
+ def call!
1597
+ Success result: { order: Order.create!(id:, customer_id: customer.id) }
1598
+ end
1599
+ end
1902
1600
 
1903
- <details>
1904
- <summary>Show the full <a href="https://github.com/evanphx/benchmark-ips">benchmark/ips</a> results.</summary>
1601
+ CreateOrder
1602
+ .call(id: 42, customer: { name: 'Ada', email: 'ada@example.com' })
1603
+ .success? # => true
1905
1604
 
1906
- ```ruby
1907
- # Warming up --------------------------------------
1908
- # Interactor 2.626k i/100ms
1909
- # Trailblazer::Operation 2.343k i/100ms
1910
- # Dry::Monads 13.386k i/100ms
1911
- # Dry::Transaction 868.000 i/100ms
1912
- # Micro::Case 7.603k i/100ms
1913
- # Micro::Case::Safe 7.598k i/100ms
1914
- # Micro::Case::Strict 6.178k i/100ms
1915
-
1916
- # Calculating -------------------------------------
1917
- # Interactor 27.037k (±24.9%) i/s - 128.674k in 5.102133s
1918
- # Trailblazer::Operation 29.016k (±12.4%) i/s - 145.266k in 5.074991s
1919
- # Dry::Monads 135.387k (±15.1%) i/s - 669.300k in 5.055356s
1920
- # Dry::Transaction 8.989k (± 9.2%) i/s - 45.136k in 5.084820s
1921
- # Micro::Case 73.247k (± 9.9%) i/s - 364.944k in 5.030449s
1922
- # Micro::Case::Safe 73.489k (± 9.6%) i/s - 364.704k in 5.007282s
1923
- # Micro::Case::Strict 61.980k (± 8.0%) i/s - 308.900k in 5.014821s
1924
-
1925
- # Comparison:
1926
- # Dry::Monads: 135386.9 i/s
1927
- # Micro::Case::Safe: 73489.3 i/s - 1.84x (± 0.00) slower
1928
- # Micro::Case: 73246.6 i/s - 1.85x (± 0.00) slower
1929
- # Micro::Case::Strict: 61979.7 i/s - 2.18x (± 0.00) slower
1930
- # Trailblazer::Operation: 29016.4 i/s - 4.67x (± 0.00) slower
1931
- # Interactor: 27037.0 i/s - 5.01x (± 0.00) slower
1932
- # Dry::Transaction: 8988.6 i/s - 15.06x (± 0.00) slower
1605
+ CreateOrder
1606
+ .call(id: 42, customer: { name: 42, email: 'ada@example.com' })
1607
+ .type # => :invalid_attributes
1933
1608
  ```
1934
- </details>
1935
-
1936
- https://github.com/serradura/u-case/blob/main/benchmarks/perfomance/use_case/failure_results.rb
1937
-
1938
- ---
1939
1609
 
1940
- ### `Micro::Cases::Flow`
1610
+ The nested hash is accessible as `customer.name`, `customer.email`.
1941
1611
 
1942
- | Gems / Abstraction | [Success results](https://github.com/serradura/u-case/blob/main/benchmarks/perfomance/flow/success_results.rb) | [Failure results](https://github.com/serradura/u-case/blob/main/benchmarks/perfomance/flow/failure_results.rb) |
1943
- | ------------------------------------------- | ----------------: | ----------------: |
1944
- | Micro::Case::Result `pipe` method | 80936.2 i/s | 78280.4 i/s |
1945
- | Micro::Case::Result `then` method | 0x slower | 0x slower |
1946
- | Micro::Cases.flow | 0x slower | 0x slower |
1947
- | Micro::Case class with an inner flow | 1.72x slower | 1.68x slower |
1948
- | Micro::Case class including itself as a step| 1.93x slower | 1.87x slower |
1949
- | Interactor::Organizer | 3.33x slower | 3.22x slower |
1612
+ ### Accepting another attribute class
1950
1613
 
1951
- \* The `Dry::Monads`, `Dry::Transaction`, `Trailblazer::Operation` gems are out of this analysis because all of them doesn't have this kind of feature.
1952
-
1953
- <details>
1954
- <summary><strong>Success results</strong> - Show the full benchmark/ips results.</summary>
1955
-
1956
- ```ruby
1957
- # Warming up --------------------------------------
1958
- # Interactor::Organizer 1.809k i/100ms
1959
- # Micro::Cases.flow([]) 7.808k i/100ms
1960
- # Micro::Case flow in a class 4.816k i/100ms
1961
- # Micro::Case including the class 4.094k i/100ms
1962
- # Micro::Case::Result#| 7.656k i/100ms
1963
- # Micro::Case::Result#then 7.138k i/100ms
1964
-
1965
- # Calculating -------------------------------------
1966
- # Interactor::Organizer 24.290k (±24.0%) i/s - 113.967k in 5.032825s
1967
- # Micro::Cases.flow([]) 74.790k (±11.1%) i/s - 374.784k in 5.071740s
1968
- # Micro::Case flow in a class 47.043k (± 8.0%) i/s - 235.984k in 5.047477s
1969
- # Micro::Case including the class 42.030k (± 8.5%) i/s - 208.794k in 5.002138s
1970
- # Micro::Case::Result#| 80.936k (±15.9%) i/s - 398.112k in 5.052531s
1971
- # Micro::Case::Result#then 71.459k (± 8.8%) i/s - 356.900k in 5.030526s
1972
-
1973
- # Comparison:
1974
- # Micro::Case::Result#|: 80936.2 i/s
1975
- # Micro::Cases.flow([]): 74790.1 i/s - same-ish: difference falls within error
1976
- # Micro::Case::Result#then: 71459.5 i/s - same-ish: difference falls within error
1977
- # Micro::Case flow in a class: 47042.6 i/s - 1.72x (± 0.00) slower
1978
- # Micro::Case including the class: 42030.2 i/s - 1.93x (± 0.00) slower
1979
- # Interactor::Organizer: 24290.3 i/s - 3.33x (± 0.00) slower
1980
- ```
1981
- </details>
1982
-
1983
- <details>
1984
- <summary><strong>Failure results</strong> - Show the full benchmark/ips results.</summary>
1614
+ `accept:` can target another class incoming hashes auto-coerce into instances of it:
1985
1615
 
1986
1616
  ```ruby
1987
- # Warming up --------------------------------------
1988
- # Interactor::Organizer 1.734k i/100ms
1989
- # Micro::Cases.flow([]) 7.515k i/100ms
1990
- # Micro::Case flow in a class 4.636k i/100ms
1991
- # Micro::Case including the class 4.114k i/100ms
1992
- # Micro::Case::Result#| 7.588k i/100ms
1993
- # Micro::Case::Result#then 6.681k i/100ms
1994
-
1995
- # Calculating -------------------------------------
1996
- # Interactor::Organizer 24.280k (±24.5%) i/s - 112.710k in 5.013334s
1997
- # Micro::Cases.flow([]) 74.999k (± 9.8%) i/s - 375.750k in 5.055777s
1998
- # Micro::Case flow in a class 46.681k (± 9.3%) i/s - 236.436k in 5.105105s
1999
- # Micro::Case including the class 41.921k (± 8.9%) i/s - 209.814k in 5.043622s
2000
- # Micro::Case::Result#| 78.280k (±12.6%) i/s - 386.988k in 5.022146s
2001
- # Micro::Case::Result#then 68.898k (± 8.8%) i/s - 347.412k in 5.080116s
2002
-
2003
- # Comparison:
2004
- # Micro::Case::Result#|: 78280.4 i/s
2005
- # Micro::Cases.flow([]): 74999.4 i/s - same-ish: difference falls within error
2006
- # Micro::Case::Result#then: 68898.4 i/s - same-ish: difference falls within error
2007
- # Micro::Case flow in a class: 46681.0 i/s - 1.68x (± 0.00) slower
2008
- # Micro::Case including the class: 41920.8 i/s - 1.87x (± 0.00) slower
2009
- # Interactor::Organizer: 24280.0 i/s - 3.22x (± 0.00) slower
2010
- ```
2011
- </details>
2012
-
2013
- https://github.com/serradura/u-case/blob/main/benchmarks/perfomance/flow/
2014
-
2015
- [⬆️ Back to Top](#table-of-contents-)
2016
-
2017
- ### Running the benchmarks
2018
-
2019
- #### Performance (Benchmarks IPS)
2020
-
2021
- Clone this repo and access its folder, then run the commands below:
2022
-
2023
- **Use cases**
2024
-
2025
- ```sh
2026
- ruby benchmarks/perfomance/use_case/failure_results.rb
2027
- ruby benchmarks/perfomance/use_case/success_results.rb
2028
- ```
2029
-
2030
- **Flows**
2031
-
2032
- ```sh
2033
- ruby benchmarks/perfomance/flow/failure_results.rb
2034
- ruby benchmarks/perfomance/flow/success_results.rb
2035
- ```
2036
-
2037
- #### Memory profiling
2038
-
2039
- **Use cases**
1617
+ class CreateProfile < Micro::Case
1618
+ Address = Micro::Attributes.new do
1619
+ attribute :city, accept: String
1620
+ attribute :postal, accept: String
1621
+ end
2040
1622
 
2041
- ```sh
2042
- ./benchmarks/memory/use_case/success/with_transitions/analyze.sh
2043
- ./benchmarks/memory/use_case/success/without_transitions/analyze.sh
2044
- ```
1623
+ attribute :name, accept: String
1624
+ attribute :address, accept: Address
2045
1625
 
2046
- **Flows**
1626
+ def call!
1627
+ Success result: { profile: Profile.create!(name:, address: address.to_h) }
1628
+ end
1629
+ end
2047
1630
 
2048
- ```sh
2049
- ./benchmarks/memory/flow/success/with_transitions/analyze.sh
2050
- ./benchmarks/memory/flow/success/without_transitions/analyze.sh
1631
+ CreateProfile.call(
1632
+ name: 'Rodrigo',
1633
+ address: { city: 'Rio', postal: '20000-000' }
1634
+ )
1635
+ # => Success — `address` is an Address instance inside `call!`
2051
1636
  ```
2052
1637
 
2053
- [⬆️ Back to Top](#table-of-contents-)
2054
-
2055
- ### Comparisons
2056
-
2057
- Check it out implementations of the same use case with different gems/abstractions.
2058
-
2059
- * [interactor](https://github.com/serradura/u-case/blob/main/comparisons/interactor.rb)
2060
- * [u-case](https://github.com/serradura/u-case/blob/main/comparisons/u-case.rb)
2061
-
2062
- [⬆️ Back to Top](#table-of-contents-)
2063
-
2064
- ## Examples
2065
-
2066
- ### 1️⃣ Users creation
2067
-
2068
- > An example of a flow that defines steps to sanitize, validate, and persist its input data. It has all possible approaches to represent use cases using the `u-case` gem.
2069
- >
2070
- > Link: https://github.com/serradura/u-case/blob/main/examples/users_creation
2071
-
2072
- ### 2️⃣ Rails App (API)
2073
-
2074
- > This project shows different kinds of architecture (one per commit), and in the last one, how to use the `Micro::Case` gem to handle the application business logic.
2075
- >
2076
- > Link: https://github.com/serradura/from-fat-controllers-to-use-cases
2077
-
2078
- ### 3️⃣ CLI calculator
2079
-
2080
- > Rake tasks to demonstrate how to handle user data, and how to use different failure types to control the program flow.
2081
- >
2082
- > Link: https://github.com/serradura/u-case/tree/main/examples/calculator
2083
-
2084
- ### 4️⃣ Rescuing exceptions inside of the use cases
2085
-
2086
- > Link: https://github.com/serradura/u-case/blob/main/examples/rescuing_exceptions.rb
1638
+ For defaults, `allow_nil:`, custom validators, and the rest of the feature set, see the [`u-attributes`](https://github.com/serradura/u-attributes) README.
2087
1639
 
2088
1640
  [⬆️ Back to Top](#table-of-contents-)
2089
1641
 
2090
1642
  ## Development
2091
1643
 
2092
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `./test.sh` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
1644
+ After checking out the repo, run `bin/setup` to install dependencies and refresh appraisals. Then `bundle exec rake test` runs the default suite, `bundle exec appraisal <name> rake test` runs one Rails appraisal (see `Appraisals`), and `bundle exec rake matrix` runs the full local matrix for the active Ruby. `bin/console` opens an interactive prompt.
2093
1645
 
2094
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
1646
+ To install onto your local machine, run `bundle exec rake install`. To release a new version, bump `lib/micro/case/version.rb`, then `bundle exec rake release` (creates the git tag, pushes commits and tags, pushes the `.gem` to [rubygems.org](https://rubygems.org)).
2095
1647
 
2096
1648
  ## Contributing
2097
1649
 
2098
- Bug reports and pull requests are welcome on GitHub at https://github.com/serradura/u-case. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
1650
+ Bug reports and pull requests are welcome on GitHub at https://github.com/serradura/u-case. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](https://contributor-covenant.org) code of conduct.
2099
1651
 
2100
1652
  ## License
2101
1653
 
2102
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
1654
+ Available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
2103
1655
 
2104
1656
  ## Code of Conduct
2105
1657
 
2106
- Everyone interacting in the Micro::Case projects codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/serradura/u-case/blob/main/CODE_OF_CONDUCT.md).
1658
+ Everyone interacting in the Micro::Case project's codebases, issue trackers, chat rooms, and mailing lists is expected to follow the [code of conduct](https://github.com/serradura/u-case/blob/main/CODE_OF_CONDUCT.md).