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