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