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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +11 -5
  3. data/CHANGELOG.md +10 -0
  4. data/Rakefile +12 -0
  5. data/docs/intro/about.md +2 -2
  6. data/docs/intro/overview.md +18 -0
  7. data/docs/recipes/rubocop-integration.md +352 -0
  8. data/docs/reference/action-result.md +1 -1
  9. data/docs/reference/class.md +110 -2
  10. data/docs/reference/configuration.md +5 -3
  11. data/docs/reference/instance.md +0 -52
  12. data/docs/usage/setup.md +4 -0
  13. data/docs/usage/steps.md +335 -0
  14. data/docs/usage/writing.md +67 -0
  15. data/lib/action/attachable/steps.rb +18 -17
  16. data/lib/action/attachable/subactions.rb +1 -1
  17. data/lib/action/context.rb +10 -14
  18. data/lib/action/core/context/facade.rb +11 -2
  19. data/lib/action/core/context/facade_inspector.rb +3 -2
  20. data/lib/action/core/context/internal.rb +3 -11
  21. data/lib/action/core/contract_validation.rb +1 -1
  22. data/lib/action/core/flow/callbacks.rb +22 -8
  23. data/lib/action/core/flow/exception_execution.rb +2 -5
  24. data/lib/action/core/flow/handlers/{base_handler.rb → base_descriptor.rb} +7 -4
  25. data/lib/action/core/flow/handlers/descriptors/callback_descriptor.rb +17 -0
  26. data/lib/action/core/flow/handlers/descriptors/message_descriptor.rb +53 -0
  27. data/lib/action/core/flow/handlers/matcher.rb +41 -2
  28. data/lib/action/core/flow/handlers/resolvers/base_resolver.rb +28 -0
  29. data/lib/action/core/flow/handlers/resolvers/callback_resolver.rb +29 -0
  30. data/lib/action/core/flow/handlers/resolvers/message_resolver.rb +59 -0
  31. data/lib/action/core/flow/handlers.rb +7 -4
  32. data/lib/action/core/flow/messages.rb +15 -41
  33. data/lib/action/core/nesting_tracking.rb +31 -0
  34. data/lib/action/core/timing.rb +1 -1
  35. data/lib/action/core.rb +20 -12
  36. data/lib/action/exceptions.rb +20 -2
  37. data/lib/action/result.rb +30 -32
  38. data/lib/axn/factory.rb +22 -23
  39. data/lib/axn/rubocop.rb +10 -0
  40. data/lib/axn/version.rb +1 -1
  41. data/lib/rubocop/cop/axn/README.md +237 -0
  42. data/lib/rubocop/cop/axn/unchecked_result.rb +327 -0
  43. metadata +14 -6
  44. data/lib/action/core/flow/handlers/callback_handler.rb +0 -21
  45. data/lib/action/core/flow/handlers/message_handler.rb +0 -27
  46. data/lib/action/core/hoist_errors.rb +0 -58
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 57d67670bb1cf60960aa2bb8234a5894a611801b69a89957057bfffd07ace8cd
4
- data.tar.gz: 9e0025cbe4bc367e1351ea867d630b9fdb3d706547d676b9cd2162e8fda0ef38
3
+ metadata.gz: 309f7b8f6587d121756b8c3ad5d1bf8d5ba7689935920a5cd7f8c791f4bf38c4
4
+ data.tar.gz: '08f91a0d7344b04df8db6d9bb74bdbfb4fd095deb8dba8a105e8211dbaaa2ce0'
5
5
  SHA512:
6
- metadata.gz: 70cc7c2851d95956278d43c746190dd5173ddda62a9a931ae9538374cf75b72523cc7f9b3524a6df6b8b8d57c969fb5510c07fad56b36312130ae86c59e48a15
7
- data.tar.gz: f3f961781373a05284ddc54072baaf13998d235528110bf482c221e5f27d6142ab261516d8237bd0689d1161887eef6b567e8ecb4688d400a20ec32d1ae33837
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: 110
50
+ Max: 150
47
51
 
48
52
  Metrics/MethodLength:
49
53
  Max: 70
50
54
 
51
55
  Metrics/PerceivedComplexity:
52
- Max: 16
56
+ Max: 20
53
57
 
54
58
  Metrics/AbcSize:
55
59
  Max: 60
56
60
 
57
61
  Metrics/CyclomaticComplexity:
58
- Max: 16
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
- * `hoist_errors` (usage: `hoist_errors { Nested::Action.call }`) ensures any failure from a nested service is bubbled up to the top level (by default, as if the failure had happened there directly).
42
- * Allows configurable handling at call site (e.g. setting `prefix`, so identical failures from different nested calls are distinguishable)
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
@@ -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 symbol (`:success`, `:failure`, or `:exception`)
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
 
@@ -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
- When using conditional messages, the system evaluates handlers in the order defined above 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.
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`, `failure`, `exception`) and elapsed time of the action.
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