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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +992 -1489
- data/README.pt-BR.md +986 -1517
- data/lib/micro/case/version.rb +1 -1
- 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/
|
|
3
|
-
<p align="center"><i>Represent use cases in a simple and powerful way
|
|
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">🇧🇷 🇵🇹 <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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
24
|
+
## Quick start <!-- omit in toc -->
|
|
24
25
|
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
- [
|
|
41
|
-
|
|
42
|
-
- [
|
|
43
|
-
- [
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
- [
|
|
51
|
-
- [
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
- [
|
|
55
|
-
- [
|
|
56
|
-
- [
|
|
57
|
-
- [
|
|
58
|
-
|
|
59
|
-
- [`
|
|
60
|
-
|
|
61
|
-
- [
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
- [`
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
- [
|
|
73
|
-
- [
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
80
|
-
- [Memory profiling](#memory-profiling)
|
|
195
|
+
- [Disabling runtime checks](#disabling-runtime-checks)
|
|
81
196
|
- [Comparisons](#comparisons)
|
|
82
197
|
- [Examples](#examples)
|
|
83
|
-
- [
|
|
84
|
-
- [
|
|
85
|
-
|
|
86
|
-
- [
|
|
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
|
|
95
|
-
|
|
|
96
|
-
| unreleased
|
|
97
|
-
| 5.
|
|
98
|
-
| 5.
|
|
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
|
-
>
|
|
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)
|
|
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
|
-
|
|
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
|
-
###
|
|
248
|
+
### Defining a use case
|
|
249
|
+
|
|
250
|
+
#### The basics
|
|
147
251
|
|
|
148
252
|
```ruby
|
|
149
|
-
class
|
|
150
|
-
# 1.
|
|
151
|
-
|
|
253
|
+
class ValidateEmail < Micro::Case
|
|
254
|
+
# 1. Declare the input as attributes
|
|
255
|
+
attribute :address
|
|
152
256
|
|
|
153
|
-
# 2.
|
|
257
|
+
# 2. Implement call! with the business logic
|
|
154
258
|
def call!
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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: '`
|
|
263
|
+
Failure result: { message: '`address` must be a valid email' }
|
|
161
264
|
end
|
|
162
265
|
end
|
|
163
266
|
end
|
|
164
267
|
|
|
165
|
-
|
|
166
|
-
#
|
|
167
|
-
|
|
268
|
+
result = ValidateEmail.call(address: 'Ada@Example.com')
|
|
269
|
+
result.success? # => true
|
|
270
|
+
result.data # => { address: "ada@example.com" }
|
|
168
271
|
|
|
169
|
-
|
|
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
|
-
|
|
277
|
+
The object returned by `.call` is a [`Micro::Case::Result`](#working-with-results) — the subject of the next section.
|
|
172
278
|
|
|
173
|
-
|
|
174
|
-
result.data # { number: 4 }
|
|
279
|
+
#### Strict mode — required attributes
|
|
175
280
|
|
|
176
|
-
|
|
281
|
+
`Micro::Case::Strict` requires every declared attribute to be passed on `.call`. Missing keywords raise `ArgumentError`:
|
|
177
282
|
|
|
178
|
-
|
|
283
|
+
```ruby
|
|
284
|
+
class FormatGreeting < Micro::Case::Strict
|
|
285
|
+
attributes :name, :time_of_day
|
|
179
286
|
|
|
180
|
-
|
|
181
|
-
|
|
287
|
+
def call!
|
|
288
|
+
Success result: { message: "Good #{time_of_day}, #{name}!" }
|
|
289
|
+
end
|
|
290
|
+
end
|
|
182
291
|
|
|
183
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
302
|
+
```ruby
|
|
303
|
+
require 'json'
|
|
304
|
+
require 'logger'
|
|
213
305
|
|
|
214
|
-
|
|
215
|
-
- `:ok` when success
|
|
216
|
-
- `:error` or `:exception` when failures
|
|
306
|
+
AppLogger = Logger.new(STDOUT)
|
|
217
307
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
attributes :a, :b
|
|
308
|
+
class ParseJsonPayload < Micro::Case::Safe
|
|
309
|
+
attribute :payload
|
|
221
310
|
|
|
222
311
|
def call!
|
|
223
|
-
if
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
328
|
+
To branch on the exception class, use `case`/`when` (or [pattern matching](#pattern-matching)) inside the hook:
|
|
247
329
|
|
|
248
|
-
|
|
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
|
-
|
|
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
|
-
|
|
341
|
+
##### Safe flows
|
|
256
342
|
|
|
257
|
-
|
|
343
|
+
A safe flow intercepts exceptions in any of its steps:
|
|
258
344
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
345
|
+
```ruby
|
|
346
|
+
module Users
|
|
347
|
+
Create = Micro::Cases.safe_flow([
|
|
348
|
+
ProcessParams,
|
|
349
|
+
ValidateParams,
|
|
350
|
+
Persist,
|
|
351
|
+
SendToCRM
|
|
352
|
+
])
|
|
263
353
|
|
|
264
|
-
#
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
#### How to define custom result types?
|
|
364
|
+
##### `Result#on_exception`
|
|
273
365
|
|
|
274
|
-
|
|
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
|
|
278
|
-
|
|
369
|
+
class ParseJsonPayload < Micro::Case::Safe
|
|
370
|
+
attribute :payload
|
|
279
371
|
|
|
280
372
|
def call!
|
|
281
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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.
|
|
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
|
-
|
|
389
|
+
##### Opting out of Safe
|
|
300
390
|
|
|
301
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
316
|
-
attributes :
|
|
453
|
+
class MergeTags < Micro::Case
|
|
454
|
+
attributes :primary, :secondary
|
|
317
455
|
|
|
318
456
|
def call!
|
|
319
|
-
if
|
|
320
|
-
Success result: {
|
|
457
|
+
if primary.is_a?(Array) && secondary.is_a?(Array)
|
|
458
|
+
Success result: { tags: (primary + secondary).uniq }
|
|
321
459
|
else
|
|
322
|
-
Failure
|
|
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
|
-
|
|
467
|
+
MergeTags.call(primary: %w[ruby], secondary: 'rails').type # => :invalid_input
|
|
468
|
+
```
|
|
328
469
|
|
|
329
|
-
result
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
476
|
+
Success result: { tags: (primary + secondary).uniq }
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
# result.data => { invalid_input: true }
|
|
480
|
+
```
|
|
341
481
|
|
|
342
|
-
####
|
|
482
|
+
#### Result contracts
|
|
343
483
|
|
|
344
|
-
|
|
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
|
|
348
|
-
|
|
487
|
+
class PublishPost < Micro::Case
|
|
488
|
+
attribute :post
|
|
349
489
|
|
|
350
490
|
results do |on|
|
|
351
|
-
on.failure(:
|
|
352
|
-
on.failure(:
|
|
491
|
+
on.failure(:already_published)
|
|
492
|
+
on.failure(:missing_content)
|
|
353
493
|
|
|
354
|
-
on.success(result: [:
|
|
494
|
+
on.success(result: [:post])
|
|
355
495
|
end
|
|
356
496
|
|
|
357
497
|
def call!
|
|
358
|
-
return Failure(:
|
|
359
|
-
return Failure(:
|
|
498
|
+
return Failure(:already_published) if post.published?
|
|
499
|
+
return Failure(:missing_content) if post.body.to_s.strip.empty?
|
|
360
500
|
|
|
361
|
-
|
|
501
|
+
post.update!(status: :published, published_at: Time.current)
|
|
502
|
+
Success result: { post: }
|
|
362
503
|
end
|
|
363
504
|
end
|
|
364
505
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
|
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
|
|
514
|
+
class CreateComment < Micro::Case
|
|
374
515
|
results do |on|
|
|
375
|
-
on.success(result: [:
|
|
376
|
-
on.failure(:
|
|
516
|
+
on.success(result: [:comment])
|
|
517
|
+
on.failure(:spam)
|
|
377
518
|
end
|
|
378
519
|
|
|
379
520
|
def call!
|
|
380
|
-
Success(:
|
|
381
|
-
# Success(result: {
|
|
382
|
-
# Failure(:
|
|
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
|
-
-
|
|
391
|
-
|
|
392
|
-
[
|
|
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
|
-
####
|
|
536
|
+
#### Result hooks
|
|
395
537
|
|
|
396
|
-
|
|
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
|
|
402
|
-
|
|
541
|
+
class ChangePassword < Micro::Case
|
|
542
|
+
attributes :user, :new_password
|
|
403
543
|
|
|
404
544
|
def call!
|
|
405
|
-
return Failure
|
|
406
|
-
return Failure
|
|
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
|
-
|
|
548
|
+
user.update_password!(new_password)
|
|
549
|
+
Success result: { user: }
|
|
409
550
|
end
|
|
410
551
|
end
|
|
411
552
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
.
|
|
420
|
-
.on_failure
|
|
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
|
-
|
|
565
|
+
> The use case responsible for the result is always available as the hook's second block argument.
|
|
447
566
|
|
|
448
|
-
|
|
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
|
-
|
|
452
|
-
|
|
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 :
|
|
467
|
-
when :
|
|
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
|
-
|
|
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
|
-
|
|
584
|
+
calls = 0
|
|
585
|
+
result = ChangePassword.call(user: ada, new_password: 'long-enough-1')
|
|
486
586
|
|
|
487
|
-
|
|
488
|
-
.
|
|
489
|
-
.
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
#
|
|
498
|
-
#
|
|
499
|
-
# ArgumentError (the number `-2` must be greater than 0)
|
|
593
|
+
calls # => 4
|
|
500
594
|
```
|
|
501
595
|
|
|
502
|
-
|
|
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
|
|
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
|
|
526
|
-
|
|
|
527
|
-
| `success:`
|
|
528
|
-
| `failure:`
|
|
529
|
-
| `type:`
|
|
530
|
-
| `data:`
|
|
531
|
-
| `result:`
|
|
532
|
-
| `use_case:`
|
|
533
|
-
| `transitions:`
|
|
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
|
|
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
|
-
>
|
|
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
|
-
####
|
|
638
|
+
#### Decomposition
|
|
555
639
|
|
|
556
|
-
|
|
640
|
+
Inside a hook without a type, the result can also be array-decomposed into `[data, type]`:
|
|
557
641
|
|
|
558
642
|
```ruby
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
####
|
|
654
|
+
#### Dynamic continuations with `Result#then`
|
|
589
655
|
|
|
590
|
-
|
|
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
|
|
594
|
-
attribute :
|
|
659
|
+
class FindActiveUser < Micro::Case
|
|
660
|
+
attribute :email
|
|
595
661
|
|
|
596
662
|
def call!
|
|
597
|
-
|
|
663
|
+
user = User.active.find_by(email:)
|
|
598
664
|
|
|
599
|
-
|
|
665
|
+
return Success result: { user: } if user
|
|
666
|
+
|
|
667
|
+
Failure result: { email: }
|
|
600
668
|
end
|
|
601
669
|
end
|
|
602
670
|
|
|
603
|
-
class
|
|
604
|
-
attribute :
|
|
671
|
+
class GenerateInviteToken < Micro::Case
|
|
672
|
+
attribute :user
|
|
605
673
|
|
|
606
674
|
def call!
|
|
607
|
-
Success result: {
|
|
675
|
+
Success result: { user:, token: SecureRandom.hex(16) }
|
|
608
676
|
end
|
|
609
677
|
end
|
|
610
678
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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
|
-
|
|
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
|
|
640
|
-
|
|
687
|
+
class FindUser < Micro::Case
|
|
688
|
+
attribute :email
|
|
641
689
|
|
|
642
690
|
def call!
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
717
|
-
attribute :
|
|
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
|
|
728
|
+
Success result: { user: User.create!(attributes) }
|
|
721
729
|
end
|
|
722
730
|
end
|
|
723
731
|
|
|
724
|
-
|
|
725
|
-
|
|
732
|
+
CreateUser.call(name: 'Bob', email: 'bob@example.com')
|
|
733
|
+
# => #<Success type=:ok ...>
|
|
726
734
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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
|
-
|
|
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
|
-
|
|
737
|
-
Kind.of?(Numeric, a, b) ? Success(:valid) : Failure()
|
|
738
|
-
end
|
|
746
|
+
#### ActiveModel integration (opt-in)
|
|
739
747
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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
|
-
|
|
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
|
-
|
|
767
|
+
To make use cases **auto-fail** on `invalid?`, require the auto-validation entry point:
|
|
750
768
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
769
|
+
```ruby
|
|
770
|
+
# Gemfile
|
|
771
|
+
gem 'u-case', require: 'u-case/with_activemodel_validation'
|
|
754
772
|
```
|
|
755
773
|
|
|
756
|
-
|
|
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
|
-
|
|
765
|
-
|
|
766
|
-
accessible_attributes: [:a, :b, :valid] },
|
|
779
|
+
class CreatePost < Micro::Case
|
|
780
|
+
attributes :title, :body
|
|
767
781
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
accessible_attributes: [:a, :b, :valid, :number, :sum] },
|
|
782
|
+
validates :title, :body, presence: true
|
|
783
|
+
validates :title, length: { maximum: 120 }
|
|
771
784
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
785
|
+
def call!
|
|
786
|
+
Success result: { post: Post.create!(title:, body:) }
|
|
787
|
+
end
|
|
788
|
+
end
|
|
776
789
|
```
|
|
777
790
|
|
|
778
|
-
|
|
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
|
-
|
|
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
|
-
|
|
799
|
+
##### Disabling auto-validation for a specific use case
|
|
800
|
+
|
|
801
|
+
Use the `disable_auto_validation` macro:
|
|
787
802
|
|
|
788
803
|
```ruby
|
|
789
|
-
|
|
790
|
-
|
|
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
|
-
|
|
821
|
+
##### `Kind::Validator`
|
|
795
822
|
|
|
796
|
-
|
|
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
|
-
|
|
849
|
-
|
|
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
|
-
|
|
869
|
-
|
|
870
|
-
attributes :name, :info
|
|
829
|
+
validates :user, kind: User
|
|
830
|
+
validates :params, kind: ActionController::Parameters
|
|
871
831
|
|
|
872
832
|
def call!
|
|
873
|
-
|
|
874
|
-
|
|
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: {
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
|
922
|
-
attribute :
|
|
855
|
+
class ParseTags < Micro::Case
|
|
856
|
+
attribute :tags
|
|
923
857
|
|
|
924
858
|
def call!
|
|
925
|
-
if
|
|
926
|
-
Success result: {
|
|
859
|
+
if tags.is_a?(String)
|
|
860
|
+
Success result: { tags: tags.split(',').map(&:strip) }
|
|
927
861
|
else
|
|
928
|
-
Failure result: { message: '
|
|
862
|
+
Failure result: { message: 'tags must be a comma-separated String' }
|
|
929
863
|
end
|
|
930
864
|
end
|
|
931
865
|
end
|
|
932
866
|
|
|
933
|
-
class
|
|
934
|
-
attribute :
|
|
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
|
|
942
|
-
attribute :
|
|
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
|
|
950
|
-
attribute :
|
|
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
|
-
|
|
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
|
-
|
|
889
|
+
DowncaseTags.call(tags: 'Ruby, Rails, RUBY').data
|
|
890
|
+
# => { tags: ["ruby", "rails", "ruby"] }
|
|
968
891
|
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
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
|
-
|
|
982
|
-
call(
|
|
983
|
-
on_failure {
|
|
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
|
|
906
|
+
When a flow fails, `Result#use_case` points to the step responsible:
|
|
987
907
|
|
|
988
908
|
```ruby
|
|
989
|
-
result =
|
|
909
|
+
result = NormalizeTags.call(tags: 42)
|
|
910
|
+
result.failure? # => true
|
|
911
|
+
result.use_case.is_a?(Steps::ParseTags) # => true
|
|
912
|
+
```
|
|
990
913
|
|
|
991
|
-
|
|
992
|
-
result.use_case.is_a?(Steps::ConvertTextToNumbers) # true
|
|
914
|
+
##### Composing flows together
|
|
993
915
|
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
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
|
-
|
|
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
|
-
|
|
931
|
+
##### Data accumulation through a flow
|
|
1002
932
|
|
|
1003
|
-
|
|
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
|
|
1007
|
-
class
|
|
1008
|
-
attribute :
|
|
936
|
+
module Users
|
|
937
|
+
class FindByEmail < Micro::Case
|
|
938
|
+
attribute :email
|
|
1009
939
|
|
|
1010
940
|
def call!
|
|
1011
|
-
|
|
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
|
-
|
|
1020
|
-
attribute :numbers
|
|
943
|
+
return Success result: { user: } if user
|
|
1021
944
|
|
|
1022
|
-
|
|
1023
|
-
Success result: { numbers: numbers.map { |number| number + 2 } }
|
|
945
|
+
Failure(:user_not_found)
|
|
1024
946
|
end
|
|
1025
947
|
end
|
|
1026
948
|
|
|
1027
|
-
class
|
|
1028
|
-
|
|
949
|
+
class ValidatePassword < Micro::Case::Strict
|
|
950
|
+
attributes :user, :password
|
|
1029
951
|
|
|
1030
952
|
def call!
|
|
1031
|
-
|
|
1032
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1045
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1051
|
-
Micro::Cases.flow([DoubleAllNumbers, Steps::Add2])
|
|
972
|
+
##### Inspecting execution with `result.transitions`
|
|
1052
973
|
|
|
1053
|
-
|
|
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
|
-
|
|
1057
|
-
|
|
976
|
+
```ruby
|
|
977
|
+
user_authenticated = Users::Authenticate.call(email: 'rodrigo@test.com', password: '...')
|
|
1058
978
|
|
|
1059
|
-
|
|
1060
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1021
|
+
To disable transitions globally (saves a hash per step), see [Configuration](#configuration).
|
|
1074
1022
|
|
|
1075
|
-
|
|
1023
|
+
##### Composing a flow that includes itself
|
|
1076
1024
|
|
|
1077
|
-
|
|
1025
|
+
A class can use itself as a step inside its own `flow` declaration via `self.call!`:
|
|
1078
1026
|
|
|
1079
1027
|
```ruby
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1028
|
+
class ParseTagsString < Micro::Case
|
|
1029
|
+
attribute :input
|
|
1030
|
+
def call!; Success result: { tags: input.split(',').map(&:strip) }; end
|
|
1031
|
+
end
|
|
1083
1032
|
|
|
1084
|
-
|
|
1085
|
-
|
|
1033
|
+
class JoinTagsArray < Micro::Case
|
|
1034
|
+
attribute :tags
|
|
1035
|
+
def call!; Success result: { input: tags.join(', ') }; end
|
|
1036
|
+
end
|
|
1086
1037
|
|
|
1087
|
-
|
|
1038
|
+
class CleanTags < Micro::Case
|
|
1039
|
+
flow ParseTagsString,
|
|
1040
|
+
self.call!,
|
|
1041
|
+
JoinTagsArray
|
|
1088
1042
|
|
|
1089
|
-
|
|
1090
|
-
|
|
1043
|
+
attribute :tags
|
|
1044
|
+
|
|
1045
|
+
def call!
|
|
1046
|
+
Success result: { tags: tags.map(&:downcase).uniq }
|
|
1091
1047
|
end
|
|
1092
1048
|
end
|
|
1093
1049
|
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
attributes :user, :password
|
|
1050
|
+
CleanTags.call(input: 'Ruby, RUBY, Rails').data[:input] # => "ruby, rails"
|
|
1051
|
+
```
|
|
1097
1052
|
|
|
1098
|
-
|
|
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
|
-
|
|
1103
|
-
end
|
|
1104
|
-
end
|
|
1105
|
-
end
|
|
1055
|
+
#### Internal steps — `Result#then` chains
|
|
1106
1056
|
|
|
1107
|
-
|
|
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
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
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
|
-
|
|
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
|
|
1125
|
-
attribute :
|
|
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
|
|
1129
|
-
attributes :
|
|
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
|
-
|
|
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
|
-
|
|
1115
|
+
##### `|` pipe alias
|
|
1137
1116
|
|
|
1138
|
-
|
|
1117
|
+
`|` is sugar for `.then(...)`. The previous example reads:
|
|
1139
1118
|
|
|
1140
|
-
|
|
1119
|
+
```ruby
|
|
1120
|
+
def call!
|
|
1121
|
+
validate_input | :strip_title | :slugify | CapitalizeTitle
|
|
1122
|
+
end
|
|
1123
|
+
```
|
|
1141
1124
|
|
|
1142
|
-
|
|
1125
|
+
Both forms produce identical `result.data` and `result.transitions`.
|
|
1143
1126
|
|
|
1144
|
-
|
|
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
|
-
|
|
1139
|
+
##### Lambda / `Method` forms
|
|
1147
1140
|
|
|
1148
|
-
|
|
1141
|
+
Lambdas (and bound `Method` objects) receive the accumulated data **positionally** as a single Hash:
|
|
1149
1142
|
|
|
1150
1143
|
```ruby
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1170
|
+
##### Persistence without a transaction
|
|
1218
1171
|
|
|
1219
|
-
|
|
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
|
|
1225
|
-
|
|
1175
|
+
class CreateUserWithProfileInline < Micro::Case
|
|
1176
|
+
attributes :name, :info
|
|
1226
1177
|
|
|
1227
1178
|
def call!
|
|
1228
|
-
|
|
1179
|
+
create_user.then(:create_profile)
|
|
1229
1180
|
end
|
|
1230
|
-
end
|
|
1231
1181
|
|
|
1232
|
-
|
|
1233
|
-
attribute :number
|
|
1182
|
+
private
|
|
1234
1183
|
|
|
1235
|
-
def
|
|
1236
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1248
|
-
Success result: { number: number * 2 }
|
|
1193
|
+
Success result: { user:, profile: }
|
|
1249
1194
|
end
|
|
1250
1195
|
end
|
|
1251
1196
|
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
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
|
-
|
|
1202
|
+
To roll the partial writes back, wrap the chain in a [transaction](#transactions).
|
|
1259
1203
|
|
|
1260
|
-
|
|
1204
|
+
#### Transactions
|
|
1261
1205
|
|
|
1262
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
> **
|
|
1315
|
-
>
|
|
1316
|
-
> `transaction { ... }
|
|
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
|
|
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
|
-
#####
|
|
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 { … }`
|
|
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
|
-
|
|
1412
|
-
|
|
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
|
|
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--
|
|
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
|
-
|
|
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.**
|
|
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:
|
|
1456
|
-
Success result: { 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:
|
|
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
|
|
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`
|
|
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 `
|
|
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
|
-
|
|
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
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
-
|
|
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
|
-
|
|
1388
|
+
## Configuration
|
|
1565
1389
|
|
|
1566
|
-
|
|
1390
|
+
`Micro::Case.config` exposes the gem's toggles. Set them once — typically in a Rails initializer:
|
|
1567
1391
|
|
|
1568
1392
|
```ruby
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1393
|
+
Micro::Case.config do |config|
|
|
1394
|
+
# Auto-fail use cases on ActiveModel validation errors.
|
|
1395
|
+
config.enable_activemodel_validation = false
|
|
1572
1396
|
|
|
1573
|
-
|
|
1574
|
-
|
|
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
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
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
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1430
|
+
## Performance
|
|
1611
1431
|
|
|
1612
|
-
|
|
1432
|
+
In benchmarks against comparable abstractions, `Micro::Case` is the fastest after `Dry::Monads`:
|
|
1613
1433
|
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
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
|
-
|
|
1442
|
+
For flows, the `|` pipe alias is the fastest composition style:
|
|
1641
1443
|
|
|
1642
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
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
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
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
|
-
|
|
1685
|
-
|
|
1686
|
-
[⬆️ Back to Top](#table-of-contents-)
|
|
1467
|
+
Memory profiling:
|
|
1687
1468
|
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
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
|
-
|
|
1476
|
+
### Disabling runtime checks
|
|
1701
1477
|
|
|
1702
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1486
|
+
### Comparisons
|
|
1740
1487
|
|
|
1741
|
-
|
|
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
|
-
[
|
|
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
|
-
|
|
1493
|
+
[⬆️ Back to Top](#table-of-contents-)
|
|
1748
1494
|
|
|
1749
|
-
|
|
1495
|
+
## Examples
|
|
1750
1496
|
|
|
1751
|
-
|
|
1497
|
+
### An end-to-end sign-up flow
|
|
1752
1498
|
|
|
1753
|
-
|
|
1499
|
+
Three use cases composed into a transactional flow, using `accept:` validation, result contracts, and hooks:
|
|
1754
1500
|
|
|
1755
1501
|
```ruby
|
|
1756
|
-
class
|
|
1757
|
-
|
|
1502
|
+
class NormalizeParams < Micro::Case
|
|
1503
|
+
attribute :params, accept: Hash
|
|
1758
1504
|
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
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
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
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
|
-
|
|
1514
|
+
return Failure(:invalid_params) if name.empty? || email.empty?
|
|
1788
1515
|
|
|
1789
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1802
|
-
|
|
1523
|
+
results do |on|
|
|
1524
|
+
on.success(result: [:user])
|
|
1525
|
+
on.failure(:invalid_user)
|
|
1526
|
+
end
|
|
1803
1527
|
|
|
1804
|
-
|
|
1805
|
-
|
|
1528
|
+
def call!
|
|
1529
|
+
user = User.create(name:, email:)
|
|
1806
1530
|
|
|
1807
|
-
|
|
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
|
-
|
|
1812
|
-
Success result: { number: a * b }
|
|
1533
|
+
Success result: { user: }
|
|
1813
1534
|
end
|
|
1814
1535
|
end
|
|
1815
1536
|
|
|
1816
|
-
|
|
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
|
-
|
|
1835
|
-
|
|
1540
|
+
results do |on|
|
|
1541
|
+
on.success(result: [:profile])
|
|
1542
|
+
on.failure(:invalid_profile)
|
|
1543
|
+
end
|
|
1836
1544
|
|
|
1837
1545
|
def call!
|
|
1838
|
-
|
|
1546
|
+
profile = Profile.create(user_id: user.id)
|
|
1839
1547
|
|
|
1840
|
-
|
|
1548
|
+
return Failure(:invalid_profile, result: { errors: profile.errors }) if profile.errors.any?
|
|
1841
1549
|
|
|
1842
|
-
Success result: {
|
|
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
|
-
|
|
1854
|
-
|
|
1554
|
+
SignUp = Micro::Cases.flow(transaction: true, steps: [
|
|
1555
|
+
NormalizeParams,
|
|
1556
|
+
CreateUser,
|
|
1557
|
+
CreateProfile
|
|
1558
|
+
])
|
|
1855
1559
|
|
|
1856
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1864
|
-
config.enable_transitions = true
|
|
1570
|
+
### More examples
|
|
1865
1571
|
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
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
|
-
##
|
|
1579
|
+
## Going further with `u-attributes`
|
|
1892
1580
|
|
|
1893
|
-
|
|
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
|
-
|
|
1583
|
+
### Nested attributes (block form)
|
|
1896
1584
|
|
|
1897
|
-
|
|
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
|
-
|
|
1910
|
-
|
|
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
|
-
|
|
1591
|
+
attribute :customer do
|
|
1592
|
+
attribute :name, accept: String
|
|
1593
|
+
attribute :email, accept: String
|
|
1594
|
+
end
|
|
1943
1595
|
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
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
|
-
|
|
1953
|
-
|
|
1601
|
+
CreateOrder
|
|
1602
|
+
.call(id: 42, customer: { name: 'Ada', email: 'ada@example.com' })
|
|
1603
|
+
.success? # => true
|
|
1954
1604
|
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
#
|
|
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
|
-
|
|
1610
|
+
The nested hash is accessible as `customer.name`, `customer.email`.
|
|
1990
1611
|
|
|
1991
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
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
|
-
|
|
2091
|
-
|
|
2092
|
-
./benchmarks/memory/use_case/success/without_transitions/analyze.sh
|
|
2093
|
-
```
|
|
1623
|
+
attribute :name, accept: String
|
|
1624
|
+
attribute :address, accept: Address
|
|
2094
1625
|
|
|
2095
|
-
|
|
1626
|
+
def call!
|
|
1627
|
+
Success result: { profile: Profile.create!(name:, address: address.to_h) }
|
|
1628
|
+
end
|
|
1629
|
+
end
|
|
2096
1630
|
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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](
|
|
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
|
-
|
|
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 project
|
|
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).
|