axn 0.1.0.pre.alpha.2.7.1 → 0.1.0.pre.alpha.2.8
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/.rubocop.yml +11 -5
- data/CHANGELOG.md +10 -0
- data/Rakefile +12 -0
- data/docs/intro/about.md +2 -2
- data/docs/intro/overview.md +18 -0
- data/docs/recipes/rubocop-integration.md +352 -0
- data/docs/reference/action-result.md +1 -1
- data/docs/reference/class.md +110 -2
- data/docs/reference/configuration.md +5 -3
- data/docs/reference/instance.md +0 -52
- data/docs/usage/setup.md +4 -0
- data/docs/usage/steps.md +335 -0
- data/docs/usage/writing.md +67 -0
- data/lib/action/attachable/steps.rb +18 -17
- data/lib/action/attachable/subactions.rb +1 -1
- data/lib/action/context.rb +10 -14
- data/lib/action/core/context/facade.rb +11 -2
- data/lib/action/core/context/facade_inspector.rb +3 -2
- data/lib/action/core/context/internal.rb +3 -11
- data/lib/action/core/contract_validation.rb +1 -1
- data/lib/action/core/flow/callbacks.rb +22 -8
- data/lib/action/core/flow/exception_execution.rb +2 -5
- data/lib/action/core/flow/handlers/{base_handler.rb → base_descriptor.rb} +7 -4
- data/lib/action/core/flow/handlers/descriptors/callback_descriptor.rb +17 -0
- data/lib/action/core/flow/handlers/descriptors/message_descriptor.rb +53 -0
- data/lib/action/core/flow/handlers/matcher.rb +41 -2
- data/lib/action/core/flow/handlers/resolvers/base_resolver.rb +28 -0
- data/lib/action/core/flow/handlers/resolvers/callback_resolver.rb +29 -0
- data/lib/action/core/flow/handlers/resolvers/message_resolver.rb +59 -0
- data/lib/action/core/flow/handlers.rb +7 -4
- data/lib/action/core/flow/messages.rb +15 -41
- data/lib/action/core/nesting_tracking.rb +31 -0
- data/lib/action/core/timing.rb +1 -1
- data/lib/action/core.rb +20 -12
- data/lib/action/exceptions.rb +20 -2
- data/lib/action/result.rb +30 -32
- data/lib/axn/factory.rb +22 -23
- data/lib/axn/rubocop.rb +10 -0
- data/lib/axn/version.rb +1 -1
- data/lib/rubocop/cop/axn/README.md +237 -0
- data/lib/rubocop/cop/axn/unchecked_result.rb +327 -0
- metadata +14 -6
- data/lib/action/core/flow/handlers/callback_handler.rb +0 -21
- data/lib/action/core/flow/handlers/message_handler.rb +0 -27
- data/lib/action/core/hoist_errors.rb +0 -58
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 309f7b8f6587d121756b8c3ad5d1bf8d5ba7689935920a5cd7f8c791f4bf38c4
|
4
|
+
data.tar.gz: '08f91a0d7344b04df8db6d9bb74bdbfb4fd095deb8dba8a105e8211dbaaa2ce0'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1abdb9efc00cf6c9be0d73094c59a1322e84ca51968cf2d66af76178693db5c2fd20e99c2e2a0ee149665b92dd69ac7a3ed886ecd8567d2389b9e447978f79ac
|
7
|
+
data.tar.gz: 241899334f89065dec935093bbf1277a07b81101855d78e4ab56511af8561814abd372506c2f9f6df762cc79f8b1971000bfc3b425c35db8f09d3faa4d70336c
|
data/.rubocop.yml
CHANGED
@@ -1,9 +1,13 @@
|
|
1
|
+
# RuboCop cops are not loaded by default in this repo
|
2
|
+
# Downstream consumers can enable them by adding:
|
3
|
+
# require:
|
4
|
+
# - axn/rubocop
|
5
|
+
|
1
6
|
AllCops:
|
2
7
|
TargetRubyVersion: 3.2
|
3
8
|
SuggestExtensions: false
|
4
9
|
NewCops: enable
|
5
10
|
|
6
|
-
|
7
11
|
Style/MultilineBlockChain:
|
8
12
|
Enabled: false
|
9
13
|
|
@@ -43,28 +47,30 @@ Metrics/ModuleLength:
|
|
43
47
|
Enabled: false
|
44
48
|
|
45
49
|
Metrics/ClassLength:
|
46
|
-
Max:
|
50
|
+
Max: 150
|
47
51
|
|
48
52
|
Metrics/MethodLength:
|
49
53
|
Max: 70
|
50
54
|
|
51
55
|
Metrics/PerceivedComplexity:
|
52
|
-
Max:
|
56
|
+
Max: 20
|
53
57
|
|
54
58
|
Metrics/AbcSize:
|
55
59
|
Max: 60
|
56
60
|
|
57
61
|
Metrics/CyclomaticComplexity:
|
58
|
-
Max:
|
62
|
+
Max: 20
|
59
63
|
|
60
64
|
Lint/EmptyBlock:
|
61
65
|
Enabled: false
|
62
66
|
|
63
67
|
Naming/MethodParameterName:
|
64
|
-
AllowedNames: e, on, id
|
68
|
+
AllowedNames: e, on, id, if
|
65
69
|
|
66
70
|
Metrics/ParameterLists:
|
67
71
|
Max: 9
|
68
72
|
|
69
73
|
Layout/LineLength:
|
70
74
|
Max: 160
|
75
|
+
|
76
|
+
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,15 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## 0.1.0-alpha.2.8
|
4
|
+
* [FEAT] Custom RuboCop cop `Axn/UncheckedResult` to enforce proper result handling in Actions with configurable nested/non-nested checking
|
5
|
+
* [FEAT] Added `prefix:` keyword support to `error` method for customizing error message prefixes
|
6
|
+
* When no block or message is provided, falls back to `e.message` with the prefix
|
7
|
+
* [BREAKING] `result.outcome` now returns a string inquirer instead of a symbol
|
8
|
+
* [BREAKING] **Message ordering change**: Static success/error messages (without conditions) should now be defined **first** in your action class, before any conditional messages. This ensures proper fallback behavior and prevents conditional messages from being shadowed by static ones.
|
9
|
+
* [CHANGE] `result.exception` will new return the internal Action::Failure, rather than nil, when user calls `fail!`
|
10
|
+
* [BREAKING] `hoist_errors` has been replaced by `error from:`
|
11
|
+
* [FEAT] Improved Axn::Factory.build support for newly-added messaging and callback descriptors
|
12
|
+
|
3
13
|
## 0.1.0-alpha.2.7.1
|
4
14
|
* [FEAT] Implemented symbol method handler support for callbacks
|
5
15
|
|
data/Rakefile
CHANGED
@@ -5,8 +5,20 @@ require "rspec/core/rake_task"
|
|
5
5
|
|
6
6
|
RSpec::Core::RakeTask.new(:spec)
|
7
7
|
|
8
|
+
# RuboCop specs (separate from main specs to avoid loading RuboCop unnecessarily)
|
9
|
+
RSpec::Core::RakeTask.new(:spec_rubocop) do |task|
|
10
|
+
task.pattern = "spec_rubocop/**/*_spec.rb"
|
11
|
+
end
|
12
|
+
|
8
13
|
require "rubocop/rake_task"
|
9
14
|
|
15
|
+
# RuboCop with Axn custom cops (targeting examples/rubocop directory)
|
16
|
+
task :rubocop_axn do
|
17
|
+
sh "bundle exec rubocop --require axn/rubocop examples/rubocop/ || true"
|
18
|
+
end
|
19
|
+
|
20
|
+
# Default RuboCop task (runs on all files)
|
10
21
|
RuboCop::RakeTask.new
|
11
22
|
|
12
23
|
task default: %i[spec rubocop]
|
24
|
+
task all_specs: %i[spec spec_rubocop]
|
data/docs/intro/about.md
CHANGED
@@ -38,8 +38,8 @@ The core library provides many benefits for individual action calls, but also ai
|
|
38
38
|
* Each layer `expects` and `exposes` its own accessor set, but internally all the values are passed down the chain (i.e. actor C can accept something A exposed that B didn’t touch and knows nothing about).
|
39
39
|
* The top-level action must `expose` it’s own layer (effectively documenting public vs private exposures, which drastically eases refactoring)
|
40
40
|
* Ad hoc (called arbitrarily from within other actions)
|
41
|
-
*
|
42
|
-
*
|
41
|
+
* Use `call!` to ensure any failure from a nested service raises an exception, or manually check `result.ok?` and handle failures appropriately
|
42
|
+
* The `from` filter on error messages can be used to distinguish failures from different nested calls
|
43
43
|
|
44
44
|
::: danger ALPHA
|
45
45
|
* TODO: add links to sections showing usage guides/examples for the more complex flows
|
data/docs/intro/overview.md
CHANGED
@@ -134,3 +134,21 @@ For _known_ failure modes, you can call `fail!("Some user-facing explanation")`
|
|
134
134
|
Any exceptions will be swallowed and the action failed (i.e. _not_ `ok?`). `result.error` will be set to a generic error message ("Something went wrong" by default, but [highly configurable](/reference/class#messages)).
|
135
135
|
|
136
136
|
The swallowed exception will be available on `result.exception` for your introspection, but it'll also be passed to your `on_exception` handler so, [with a bit of configuration](/usage/setup), you can trust that any exceptions have been logged to your error tracking service automatically (one more thing the dev doesn't need to think about).
|
137
|
+
|
138
|
+
::: tip Message Configuration Order
|
139
|
+
When configuring custom error and success messages, remember to define your static fallback messages **first**, before any conditional messages. This ensures proper fallback behavior and prevents conditional messages from being shadowed.
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
class MyAction
|
143
|
+
include Action
|
144
|
+
|
145
|
+
# Static fallback messages first
|
146
|
+
success "Default success message"
|
147
|
+
error "Default error message"
|
148
|
+
|
149
|
+
# Then conditional messages
|
150
|
+
success "Special success", if: :special_condition?
|
151
|
+
error "Special error", if: ArgumentError
|
152
|
+
end
|
153
|
+
```
|
154
|
+
:::
|
@@ -0,0 +1,352 @@
|
|
1
|
+
# RuboCop Integration
|
2
|
+
|
3
|
+
Axn provides custom RuboCop cops to help enforce best practices and maintain code quality in your Action-based codebase.
|
4
|
+
|
5
|
+
## Overview
|
6
|
+
|
7
|
+
The `Axn/UncheckedResult` cop enforces proper result handling when calling Actions. It can detect when Action results are ignored and help ensure consistent error handling patterns.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
### 1. Add to Your .rubocop.yml
|
12
|
+
|
13
|
+
```yaml
|
14
|
+
require:
|
15
|
+
- axn/rubocop
|
16
|
+
|
17
|
+
# Enable Axn's custom cop
|
18
|
+
Axn/UncheckedResult:
|
19
|
+
Enabled: true
|
20
|
+
CheckNested: true # Check nested Action calls
|
21
|
+
CheckNonNested: true # Check non-nested Action calls
|
22
|
+
Severity: warning # or error
|
23
|
+
```
|
24
|
+
|
25
|
+
### 2. Verify Installation
|
26
|
+
|
27
|
+
Run RuboCop to ensure the cop is loaded:
|
28
|
+
|
29
|
+
```bash
|
30
|
+
bundle exec rubocop --show-cops | grep Axn
|
31
|
+
```
|
32
|
+
|
33
|
+
You should see:
|
34
|
+
```
|
35
|
+
Axn/UncheckedResult
|
36
|
+
```
|
37
|
+
|
38
|
+
## Configuration Options
|
39
|
+
|
40
|
+
### CheckNested
|
41
|
+
|
42
|
+
Controls whether the cop checks Action calls that are inside other Action classes.
|
43
|
+
|
44
|
+
```yaml
|
45
|
+
Axn/UncheckedResult:
|
46
|
+
CheckNested: true # Check nested calls (default)
|
47
|
+
CheckNested: false # Skip nested calls
|
48
|
+
```
|
49
|
+
|
50
|
+
**When to use `CheckNested: false`:**
|
51
|
+
- You're gradually adopting the rule and want to focus on top-level calls first
|
52
|
+
- Your team has different standards for nested vs. non-nested calls
|
53
|
+
- You're using a different pattern for nested Action handling
|
54
|
+
|
55
|
+
### CheckNonNested
|
56
|
+
|
57
|
+
Controls whether the cop checks Action calls that are outside Action classes.
|
58
|
+
|
59
|
+
```yaml
|
60
|
+
Axn/UncheckedResult:
|
61
|
+
CheckNonNested: true # Check non-nested calls (default)
|
62
|
+
CheckNonNested: false # Skip non-nested calls
|
63
|
+
```
|
64
|
+
|
65
|
+
**When to use `CheckNonNested: false`:**
|
66
|
+
- You're only concerned about nested Action calls
|
67
|
+
- Top-level Action calls are handled by other tools or processes
|
68
|
+
- You want to focus on the most critical use case first
|
69
|
+
|
70
|
+
### Severity
|
71
|
+
|
72
|
+
Controls how violations are reported.
|
73
|
+
|
74
|
+
```yaml
|
75
|
+
Axn/UncheckedResult:
|
76
|
+
Severity: warning # Show as warnings (default)
|
77
|
+
Severity: error # Show as errors (fails CI)
|
78
|
+
```
|
79
|
+
|
80
|
+
## Common Configuration Patterns
|
81
|
+
|
82
|
+
### Full Enforcement (Recommended for New Projects)
|
83
|
+
|
84
|
+
```yaml
|
85
|
+
Axn/UncheckedResult:
|
86
|
+
Enabled: true
|
87
|
+
CheckNested: true
|
88
|
+
CheckNonNested: true
|
89
|
+
Severity: error
|
90
|
+
```
|
91
|
+
|
92
|
+
### Gradual Adoption (Recommended for Existing Projects)
|
93
|
+
|
94
|
+
```yaml
|
95
|
+
Axn/UncheckedResult:
|
96
|
+
Enabled: true
|
97
|
+
CheckNested: true # Start with nested calls
|
98
|
+
CheckNonNested: false # Add this later
|
99
|
+
Severity: warning # Start with warnings
|
100
|
+
```
|
101
|
+
|
102
|
+
### Nested-Only Focus
|
103
|
+
|
104
|
+
```yaml
|
105
|
+
Axn/UncheckedResult:
|
106
|
+
Enabled: true
|
107
|
+
CheckNested: true
|
108
|
+
CheckNonNested: false
|
109
|
+
Severity: warning
|
110
|
+
```
|
111
|
+
|
112
|
+
## What the Cop Checks
|
113
|
+
|
114
|
+
The cop analyzes your code to determine if you're:
|
115
|
+
|
116
|
+
1. **Inside an Action class** - Classes that `include Action`
|
117
|
+
2. **Inside the `call` method** - Only the main execution method
|
118
|
+
3. **Calling another Action** - Using `.call` on Action classes
|
119
|
+
4. **Properly handling the result** - One of the acceptable patterns
|
120
|
+
|
121
|
+
## What the Cop Ignores
|
122
|
+
|
123
|
+
The cop will NOT report offenses for:
|
124
|
+
|
125
|
+
- Action calls outside of Action classes (if `CheckNonNested: false`)
|
126
|
+
- Action calls in methods other than `call`
|
127
|
+
- Action calls that use `call!` (bang method)
|
128
|
+
- Action calls where the result is properly handled
|
129
|
+
|
130
|
+
## Proper Result Handling Patterns
|
131
|
+
|
132
|
+
### ✅ Using call!
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
class OuterAction
|
136
|
+
include Action
|
137
|
+
def call
|
138
|
+
InnerAction.call!(param: "value") # Exceptions bubble up
|
139
|
+
end
|
140
|
+
end
|
141
|
+
```
|
142
|
+
|
143
|
+
### ✅ Checking result.ok?
|
144
|
+
|
145
|
+
```ruby
|
146
|
+
class OuterAction
|
147
|
+
include Action
|
148
|
+
def call
|
149
|
+
result = InnerAction.call(param: "value")
|
150
|
+
return result unless result.ok?
|
151
|
+
# Process successful result...
|
152
|
+
end
|
153
|
+
end
|
154
|
+
```
|
155
|
+
|
156
|
+
### ✅ Checking result.failed?
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
class OuterAction
|
160
|
+
include Action
|
161
|
+
def call
|
162
|
+
result = InnerAction.call(param: "value")
|
163
|
+
if result.failed?
|
164
|
+
return result
|
165
|
+
end
|
166
|
+
# Process successful result...
|
167
|
+
end
|
168
|
+
end
|
169
|
+
```
|
170
|
+
|
171
|
+
### ✅ Accessing result.error
|
172
|
+
|
173
|
+
```ruby
|
174
|
+
class OuterAction
|
175
|
+
include Action
|
176
|
+
def call
|
177
|
+
result = InnerAction.call(param: "value")
|
178
|
+
if result.error
|
179
|
+
return result
|
180
|
+
end
|
181
|
+
# Process successful result...
|
182
|
+
end
|
183
|
+
end
|
184
|
+
```
|
185
|
+
|
186
|
+
### ✅ Returning the result
|
187
|
+
|
188
|
+
```ruby
|
189
|
+
class OuterAction
|
190
|
+
include Action
|
191
|
+
def call
|
192
|
+
result = InnerAction.call(param: "value")
|
193
|
+
result # Result is returned, so it's properly handled
|
194
|
+
end
|
195
|
+
end
|
196
|
+
```
|
197
|
+
|
198
|
+
### ✅ Using result in expose
|
199
|
+
|
200
|
+
```ruby
|
201
|
+
class OuterAction
|
202
|
+
include Action
|
203
|
+
exposes :nested_result
|
204
|
+
def call
|
205
|
+
result = InnerAction.call(param: "value")
|
206
|
+
expose nested_result: result # Result is used, so it's properly handled
|
207
|
+
end
|
208
|
+
end
|
209
|
+
```
|
210
|
+
|
211
|
+
### ✅ Passing result to another method
|
212
|
+
|
213
|
+
```ruby
|
214
|
+
class OuterAction
|
215
|
+
include Action
|
216
|
+
def call
|
217
|
+
result = InnerAction.call(param: "value")
|
218
|
+
process_result(result) # Result is used, so it's properly handled
|
219
|
+
end
|
220
|
+
end
|
221
|
+
```
|
222
|
+
|
223
|
+
## Common Anti-Patterns
|
224
|
+
|
225
|
+
### ❌ Ignoring the result
|
226
|
+
|
227
|
+
```ruby
|
228
|
+
class OuterAction
|
229
|
+
include Action
|
230
|
+
def call
|
231
|
+
InnerAction.call(param: "value") # Result ignored - will trigger offense
|
232
|
+
# This continues even if InnerAction fails
|
233
|
+
end
|
234
|
+
end
|
235
|
+
```
|
236
|
+
|
237
|
+
### ❌ Assigning but not using
|
238
|
+
|
239
|
+
```ruby
|
240
|
+
class OuterAction
|
241
|
+
include Action
|
242
|
+
def call
|
243
|
+
result = InnerAction.call(param: "value") # Assigned but never used
|
244
|
+
# Will trigger offense unless result is properly handled
|
245
|
+
end
|
246
|
+
end
|
247
|
+
```
|
248
|
+
|
249
|
+
### ❌ Using unrelated attributes
|
250
|
+
|
251
|
+
```ruby
|
252
|
+
class OuterAction
|
253
|
+
include Action
|
254
|
+
def call
|
255
|
+
result = InnerAction.call(param: "value")
|
256
|
+
some_other_method(result.some_other_attribute) # Not checking success/failure
|
257
|
+
# Will trigger offense - need to check result.ok? first
|
258
|
+
end
|
259
|
+
end
|
260
|
+
```
|
261
|
+
|
262
|
+
## Migration Strategies
|
263
|
+
|
264
|
+
### For New Projects
|
265
|
+
|
266
|
+
1. Enable the cop with full enforcement from the start
|
267
|
+
2. Use `Severity: error` to catch violations early
|
268
|
+
3. Train your team on the proper patterns
|
269
|
+
|
270
|
+
### For Existing Projects
|
271
|
+
|
272
|
+
1. **Phase 1**: Enable with `CheckNested: true, CheckNonNested: false, Severity: warning`
|
273
|
+
2. **Phase 2**: Fix all nested Action violations
|
274
|
+
3. **Phase 3**: Enable `CheckNonNested: true`
|
275
|
+
4. **Phase 4**: Fix all non-nested Action violations
|
276
|
+
5. **Phase 5**: Set `Severity: error`
|
277
|
+
|
278
|
+
### Using RuboCop Disable Comments
|
279
|
+
|
280
|
+
For intentional violations, you can disable the cop:
|
281
|
+
|
282
|
+
```ruby
|
283
|
+
class OuterAction
|
284
|
+
include Action
|
285
|
+
def call
|
286
|
+
# rubocop:disable Axn/UncheckedResult
|
287
|
+
InnerAction.call(param: "value") # Intentionally ignored
|
288
|
+
# rubocop:enable Axn/UncheckedResult
|
289
|
+
end
|
290
|
+
end
|
291
|
+
```
|
292
|
+
|
293
|
+
## Troubleshooting
|
294
|
+
|
295
|
+
### Cop Not Loading
|
296
|
+
|
297
|
+
If you see "uninitialized constant" errors:
|
298
|
+
|
299
|
+
1. Ensure the gem is properly installed: `bundle list | grep axn`
|
300
|
+
2. Check your `.rubocop.yml` syntax
|
301
|
+
3. Verify the require path: `require: - axn/rubocop`
|
302
|
+
|
303
|
+
### False Positives
|
304
|
+
|
305
|
+
If the cop reports violations for properly handled results:
|
306
|
+
|
307
|
+
1. Check that you're using the exact patterns shown above
|
308
|
+
2. Ensure the result variable name matches exactly
|
309
|
+
3. Verify the result is being used in an acceptable way
|
310
|
+
|
311
|
+
### Performance Issues
|
312
|
+
|
313
|
+
The cop analyzes AST nodes, so it's generally fast. If you experience slowdowns:
|
314
|
+
|
315
|
+
1. Ensure you're not running RuboCop on very large files
|
316
|
+
2. Consider using RuboCop's `--parallel` option
|
317
|
+
3. Use `.rubocop_todo.yml` for gradual adoption
|
318
|
+
|
319
|
+
## Best Practices
|
320
|
+
|
321
|
+
1. **Start Small**: Begin with warnings and nested calls only
|
322
|
+
2. **Be Consistent**: Choose one pattern and stick with it
|
323
|
+
3. **Train Your Team**: Make sure everyone understands the rules
|
324
|
+
4. **Review Regularly**: Use the cop in your CI/CD pipeline
|
325
|
+
5. **Document Exceptions**: Use disable comments sparingly and document why
|
326
|
+
|
327
|
+
## Integration with CI/CD
|
328
|
+
|
329
|
+
Add RuboCop to your CI pipeline to catch violations early:
|
330
|
+
|
331
|
+
```yaml
|
332
|
+
# .github/workflows/rubocop.yml
|
333
|
+
name: RuboCop
|
334
|
+
on: [push, pull_request]
|
335
|
+
jobs:
|
336
|
+
rubocop:
|
337
|
+
runs-on: ubuntu-latest
|
338
|
+
steps:
|
339
|
+
- uses: actions/checkout@v3
|
340
|
+
- uses: ruby/setup-ruby@v1
|
341
|
+
with:
|
342
|
+
ruby-version: 3.2
|
343
|
+
- run: bundle install
|
344
|
+
- run: bundle exec rubocop
|
345
|
+
```
|
346
|
+
|
347
|
+
## Related Resources
|
348
|
+
|
349
|
+
- [Action Result Reference](/reference/action-result)
|
350
|
+
- [Configuration Guide](/reference/configuration)
|
351
|
+
- [Testing Recipes](/recipes/testing)
|
352
|
+
- [Best Practices Guide](/advanced/conventions)
|
@@ -9,7 +9,7 @@ Every `call` invocation on an Action will return an `Action::Result` instance, w
|
|
9
9
|
| `success` | User-facing success message (string), if `ok?` (else nil)
|
10
10
|
| `message` | User-facing message (string), always defined (`ok? ? success : error`)
|
11
11
|
| `exception` | If not `ok?` because an exception was swallowed, will be set to the swallowed exception (note: rarely used outside development; prefer to let the library automatically handle exception handling for you)
|
12
|
-
| `outcome` | The execution outcome as a
|
12
|
+
| `outcome` | The execution outcome as a string inquirer (`success?`, `failure?`, `exception?`)
|
13
13
|
| `elapsed_time` | Execution time in milliseconds (Float)
|
14
14
|
| any `expose`d values | guaranteed to be set if `ok?` (since they have outgoing presence validations by default; any missing would have failed the action)
|
15
15
|
|
data/docs/reference/class.md
CHANGED
@@ -124,6 +124,40 @@ def build_error_message(exception:)
|
|
124
124
|
end
|
125
125
|
```
|
126
126
|
|
127
|
+
::: warning Message Ordering
|
128
|
+
**Important**: Static success/error messages (those without conditions) should be defined **first** in your action class. If you define conditional messages before static ones, the conditional messages will never be reached because the static message will always match first.
|
129
|
+
|
130
|
+
**Correct order:**
|
131
|
+
```ruby
|
132
|
+
class MyAction
|
133
|
+
include Action
|
134
|
+
|
135
|
+
# Define static fallback first
|
136
|
+
success "Default success message"
|
137
|
+
error "Default error message"
|
138
|
+
|
139
|
+
# Then define conditional messages
|
140
|
+
success "Special success", if: :special_condition?
|
141
|
+
error "Special error", if: ArgumentError
|
142
|
+
end
|
143
|
+
```
|
144
|
+
|
145
|
+
**Incorrect order (conditional messages will be shadowed):**
|
146
|
+
```ruby
|
147
|
+
class MyAction
|
148
|
+
include Action
|
149
|
+
|
150
|
+
# These conditional messages will never be reached!
|
151
|
+
success "Special success", if: :special_condition?
|
152
|
+
error "Special error", if: ArgumentError
|
153
|
+
|
154
|
+
# Static messages defined last will always match first
|
155
|
+
success "Default success message"
|
156
|
+
error "Default error message"
|
157
|
+
end
|
158
|
+
```
|
159
|
+
:::
|
160
|
+
|
127
161
|
## Conditional messages
|
128
162
|
|
129
163
|
While `.error` and `.success` set the default messages, you can register conditional messages using an optional `if:` or `unless:` matcher. The matcher can be:
|
@@ -145,6 +179,10 @@ error "Invalid params provided", if: ActiveRecord::InvalidRecord
|
|
145
179
|
error(if: ArgumentError) { |e| "Argument error: #{e.message}" }
|
146
180
|
error(if: -> { name == "bad" }) { "Bad input #{name}, result: #{result.status}" }
|
147
181
|
|
182
|
+
# Custom message with prefix (falls back to exception message when no block/message provided)
|
183
|
+
error(if: ArgumentError, prefix: "Foo: ") { "bar" } # Results in "Foo: bar"
|
184
|
+
error(if: StandardError, prefix: "Baz: ") # Results in "Baz: [exception message]"
|
185
|
+
|
148
186
|
# Custom message with symbol predicate (arity 0)
|
149
187
|
error "Transient error, please retry", if: :transient_error?
|
150
188
|
|
@@ -182,6 +220,73 @@ end
|
|
182
220
|
You cannot use both `if:` and `unless:` for the same message - this will raise an `ArgumentError`.
|
183
221
|
:::
|
184
222
|
|
223
|
+
## Error message inheritance with `from:`
|
224
|
+
|
225
|
+
The `from:` parameter allows you to customize error messages when an action calls another action that fails. This is particularly useful for adding context or prefixing error messages from child actions.
|
226
|
+
|
227
|
+
When using `from:`, the error handler receives the exception from the child action, and you can access the child's error message via `e.message` (which contains the `result.error` from the child action).
|
228
|
+
|
229
|
+
```ruby
|
230
|
+
class InnerAction
|
231
|
+
include Action
|
232
|
+
|
233
|
+
error "Something went wrong in the inner action"
|
234
|
+
|
235
|
+
def call
|
236
|
+
raise StandardError, "inner action failed"
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
class OuterAction
|
241
|
+
include Action
|
242
|
+
|
243
|
+
# Customize error messages from InnerAction
|
244
|
+
error from: InnerAction do |e|
|
245
|
+
"Outer action failed: #{e.message}"
|
246
|
+
end
|
247
|
+
|
248
|
+
def call
|
249
|
+
InnerAction.call!
|
250
|
+
end
|
251
|
+
end
|
252
|
+
```
|
253
|
+
|
254
|
+
In this example:
|
255
|
+
- When `InnerAction` fails, `OuterAction` will catch the exception
|
256
|
+
- The `e.message` contains the error message from `InnerAction`'s result
|
257
|
+
- The final error message will be "Outer action failed: Something went wrong in the inner action"
|
258
|
+
|
259
|
+
This pattern is especially useful for:
|
260
|
+
- Adding context to error messages from sub-actions
|
261
|
+
- Implementing consistent error message formatting across action hierarchies
|
262
|
+
- Providing user-friendly error messages that include details from underlying failures
|
263
|
+
|
264
|
+
### Combining `from:` with `prefix:`
|
265
|
+
|
266
|
+
You can also combine the `from:` parameter with the `prefix:` keyword to create consistent error message formatting:
|
267
|
+
|
268
|
+
```ruby
|
269
|
+
class OuterAction
|
270
|
+
include Action
|
271
|
+
|
272
|
+
# Add prefix to error messages from InnerAction
|
273
|
+
error from: InnerAction, prefix: "API Error: " do |e|
|
274
|
+
"Request failed: #{e.message}"
|
275
|
+
end
|
276
|
+
|
277
|
+
# Or use prefix only (falls back to exception message)
|
278
|
+
error from: InnerAction, prefix: "API Error: "
|
279
|
+
|
280
|
+
def call
|
281
|
+
InnerAction.call!
|
282
|
+
end
|
283
|
+
end
|
284
|
+
```
|
285
|
+
|
286
|
+
This results in:
|
287
|
+
- With custom message: "API Error: Request failed: Something went wrong in the inner action"
|
288
|
+
- With prefix only: "API Error: Something went wrong in the inner action"
|
289
|
+
|
185
290
|
### Message ordering and inheritance
|
186
291
|
|
187
292
|
Messages are evaluated in **last-defined-first** order, meaning the most recently defined message that matches its conditions will be used. This applies to both success and error messages:
|
@@ -216,8 +321,11 @@ class MyAction
|
|
216
321
|
end
|
217
322
|
```
|
218
323
|
|
219
|
-
|
220
|
-
|
324
|
+
::: tip Message Evaluation Order
|
325
|
+
The system evaluates handlers in the order they were defined until it finds one that matches and doesn't raise an exception. If a handler raises an exception, it falls back to the next matching handler, then to static messages, and finally to the default message.
|
326
|
+
|
327
|
+
**Key point**: Static messages (without conditions) are evaluated **first** in the order they were defined. This means you should define your static fallback messages at the top of your class, before any conditional messages, to ensure proper fallback behavior.
|
328
|
+
:::
|
221
329
|
|
222
330
|
## Callbacks
|
223
331
|
|
@@ -74,7 +74,7 @@ For example, to wire up Datadog:
|
|
74
74
|
end
|
75
75
|
|
76
76
|
c.emit_metrics = proc do |resource, result|
|
77
|
-
TS::Metrics.increment("action.#{resource.underscore}", tags: { outcome: result.outcome, resource: })
|
77
|
+
TS::Metrics.increment("action.#{resource.underscore}", tags: { outcome: result.outcome.to_s, resource: })
|
78
78
|
TS::Metrics.histogram("action.duration", result.elapsed_time, tags: { resource: })
|
79
79
|
end
|
80
80
|
end
|
@@ -83,7 +83,7 @@ For example, to wire up Datadog:
|
|
83
83
|
A couple notes:
|
84
84
|
|
85
85
|
* `Datadog::Tracing` is provided by [the datadog gem](https://rubygems.org/gems/datadog)
|
86
|
-
* `TS::Metrics` is a custom implementation to set a Datadog count metric, but the relevant part to note is that the result object provides access to the outcome (`success
|
86
|
+
* `TS::Metrics` is a custom implementation to set a Datadog count metric, but the relevant part to note is that the result object provides access to the outcome (`result.outcome.success?`, `result.outcome.failure?`, `result.outcome.exception?`) and elapsed time of the action.
|
87
87
|
* The `wrap_with_trace` hook is an around hook - you must call the provided block to execute the action
|
88
88
|
* The `emit_metrics` hook is called after execution with the result - do not call any blocks
|
89
89
|
|
@@ -91,6 +91,8 @@ A couple notes:
|
|
91
91
|
|
92
92
|
Defaults to `Rails.logger`, if present, otherwise falls back to `Logger.new($stdout)`. But can be set to a custom logger as necessary.
|
93
93
|
|
94
|
+
|
95
|
+
|
94
96
|
## `additional_includes`
|
95
97
|
|
96
98
|
This is much less critical than the preceding options, but on the off chance you want to add additional customization to _all_ your actions you can set additional modules to be included alongside `include Action`.
|
@@ -172,7 +174,7 @@ Action.configure do |c|
|
|
172
174
|
end
|
173
175
|
|
174
176
|
c.emit_metrics = proc do |resource, result|
|
175
|
-
Datadog::Metrics.increment("action.#{resource.underscore}", tags: { outcome: result.outcome })
|
177
|
+
Datadog::Metrics.increment("action.#{resource.underscore}", tags: { outcome: result.outcome.to_s })
|
176
178
|
Datadog::Metrics.histogram("action.duration", result.elapsed_time, tags: { resource: })
|
177
179
|
end
|
178
180
|
|