axn 0.1.0.pre.alpha.2.8.1 → 0.1.0.pre.alpha.4

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 (148) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/commands/pr.md +36 -0
  3. data/.cursor/rules/axn-framework-patterns.mdc +43 -0
  4. data/.cursor/rules/general-coding-standards.mdc +27 -0
  5. data/.cursor/rules/spec/testing-patterns.mdc +40 -0
  6. data/CHANGELOG.md +57 -0
  7. data/Rakefile +114 -4
  8. data/docs/.vitepress/config.mjs +19 -10
  9. data/docs/advanced/conventions.md +3 -3
  10. data/docs/advanced/mountable.md +476 -0
  11. data/docs/advanced/profiling.md +351 -0
  12. data/docs/advanced/rough.md +27 -8
  13. data/docs/index.md +5 -3
  14. data/docs/intro/about.md +1 -1
  15. data/docs/intro/overview.md +6 -6
  16. data/docs/recipes/formatting-context-for-error-tracking.md +186 -0
  17. data/docs/recipes/memoization.md +103 -18
  18. data/docs/recipes/rubocop-integration.md +38 -284
  19. data/docs/recipes/testing.md +14 -14
  20. data/docs/recipes/validating-user-input.md +1 -1
  21. data/docs/reference/async.md +429 -0
  22. data/docs/reference/axn-result.md +107 -0
  23. data/docs/reference/class.md +225 -64
  24. data/docs/reference/configuration.md +366 -34
  25. data/docs/reference/form-object.md +252 -0
  26. data/docs/reference/instance.md +14 -29
  27. data/docs/strategies/client.md +212 -0
  28. data/docs/strategies/form.md +235 -0
  29. data/docs/strategies/index.md +21 -21
  30. data/docs/strategies/transaction.md +1 -1
  31. data/docs/usage/setup.md +16 -2
  32. data/docs/usage/steps.md +7 -7
  33. data/docs/usage/using.md +23 -12
  34. data/docs/usage/writing.md +191 -12
  35. data/lib/axn/async/adapters/active_job.rb +74 -0
  36. data/lib/axn/async/adapters/disabled.rb +41 -0
  37. data/lib/axn/async/adapters/sidekiq.rb +67 -0
  38. data/lib/axn/async/adapters.rb +26 -0
  39. data/lib/axn/async/batch_enqueue/config.rb +38 -0
  40. data/lib/axn/async/batch_enqueue.rb +99 -0
  41. data/lib/axn/async/enqueue_all_orchestrator.rb +363 -0
  42. data/lib/axn/async.rb +178 -0
  43. data/lib/axn/configuration.rb +113 -0
  44. data/lib/{action → axn}/context.rb +22 -4
  45. data/lib/axn/core/automatic_logging.rb +89 -0
  46. data/lib/axn/core/context/facade.rb +69 -0
  47. data/lib/{action → axn}/core/context/facade_inspector.rb +32 -5
  48. data/lib/{action → axn}/core/context/internal.rb +5 -5
  49. data/lib/{action → axn}/core/contract.rb +111 -73
  50. data/lib/{action → axn}/core/contract_for_subfields.rb +30 -35
  51. data/lib/{action → axn}/core/contract_validation.rb +27 -12
  52. data/lib/axn/core/contract_validation_for_subfields.rb +165 -0
  53. data/lib/axn/core/default_call.rb +63 -0
  54. data/lib/axn/core/field_resolvers/extract.rb +32 -0
  55. data/lib/axn/core/field_resolvers/model.rb +63 -0
  56. data/lib/axn/core/field_resolvers.rb +24 -0
  57. data/lib/{action → axn}/core/flow/callbacks.rb +7 -7
  58. data/lib/{action → axn}/core/flow/exception_execution.rb +9 -13
  59. data/lib/{action → axn}/core/flow/handlers/base_descriptor.rb +3 -2
  60. data/lib/{action → axn}/core/flow/handlers/descriptors/callback_descriptor.rb +2 -2
  61. data/lib/{action → axn}/core/flow/handlers/descriptors/message_descriptor.rb +23 -11
  62. data/lib/axn/core/flow/handlers/invoker.rb +47 -0
  63. data/lib/{action → axn}/core/flow/handlers/matcher.rb +9 -19
  64. data/lib/{action → axn}/core/flow/handlers/registry.rb +3 -1
  65. data/lib/{action → axn}/core/flow/handlers/resolvers/base_resolver.rb +1 -1
  66. data/lib/{action → axn}/core/flow/handlers/resolvers/callback_resolver.rb +2 -2
  67. data/lib/{action → axn}/core/flow/handlers/resolvers/message_resolver.rb +12 -3
  68. data/lib/axn/core/flow/handlers.rb +20 -0
  69. data/lib/{action → axn}/core/flow/messages.rb +8 -8
  70. data/lib/{action → axn}/core/flow.rb +4 -4
  71. data/lib/{action → axn}/core/hooks.rb +17 -5
  72. data/lib/axn/core/logging.rb +48 -0
  73. data/lib/axn/core/memoization.rb +53 -0
  74. data/lib/{action → axn}/core/nesting_tracking.rb +1 -1
  75. data/lib/{action → axn}/core/timing.rb +1 -1
  76. data/lib/axn/core/tracing.rb +90 -0
  77. data/lib/axn/core/use_strategy.rb +29 -0
  78. data/lib/{action → axn}/core/validation/fields.rb +26 -2
  79. data/lib/{action → axn}/core/validation/subfields.rb +14 -12
  80. data/lib/axn/core/validation/validators/model_validator.rb +36 -0
  81. data/lib/axn/core/validation/validators/type_validator.rb +80 -0
  82. data/lib/{action → axn}/core/validation/validators/validate_validator.rb +12 -2
  83. data/lib/{action → axn}/core.rb +55 -55
  84. data/lib/{action → axn}/exceptions.rb +12 -2
  85. data/lib/axn/extras/strategies/client.rb +150 -0
  86. data/lib/axn/extras/strategies/vernier.rb +121 -0
  87. data/lib/axn/extras.rb +4 -0
  88. data/lib/axn/factory.rb +122 -34
  89. data/lib/axn/form_object.rb +90 -0
  90. data/lib/axn/internal/logging.rb +30 -0
  91. data/lib/axn/internal/registry.rb +87 -0
  92. data/lib/axn/mountable/descriptor.rb +76 -0
  93. data/lib/axn/mountable/helpers/class_builder.rb +193 -0
  94. data/lib/axn/mountable/helpers/mounter.rb +33 -0
  95. data/lib/axn/mountable/helpers/namespace_manager.rb +38 -0
  96. data/lib/axn/mountable/helpers/validator.rb +112 -0
  97. data/lib/axn/mountable/inherit_profiles.rb +72 -0
  98. data/lib/axn/mountable/mounting_strategies/_base.rb +87 -0
  99. data/lib/axn/mountable/mounting_strategies/axn.rb +48 -0
  100. data/lib/axn/mountable/mounting_strategies/method.rb +95 -0
  101. data/lib/axn/mountable/mounting_strategies/step.rb +69 -0
  102. data/lib/axn/mountable/mounting_strategies.rb +32 -0
  103. data/lib/axn/mountable.rb +119 -0
  104. data/lib/axn/rails/engine.rb +51 -0
  105. data/lib/axn/rails/generators/axn_generator.rb +86 -0
  106. data/lib/axn/rails/generators/templates/action.rb.erb +17 -0
  107. data/lib/axn/rails/generators/templates/action_spec.rb.erb +25 -0
  108. data/lib/{action → axn}/result.rb +32 -13
  109. data/lib/axn/strategies/form.rb +98 -0
  110. data/lib/axn/strategies/transaction.rb +26 -0
  111. data/lib/axn/strategies.rb +20 -0
  112. data/lib/axn/testing/spec_helpers.rb +6 -8
  113. data/lib/axn/util/callable.rb +120 -0
  114. data/lib/axn/util/contract_error_handling.rb +32 -0
  115. data/lib/axn/util/execution_context.rb +34 -0
  116. data/lib/axn/util/global_id_serialization.rb +52 -0
  117. data/lib/axn/util/logging.rb +87 -0
  118. data/lib/axn/util/memoization.rb +20 -0
  119. data/lib/axn/version.rb +1 -1
  120. data/lib/axn.rb +26 -16
  121. data/lib/rubocop/cop/axn/README.md +23 -23
  122. data/lib/rubocop/cop/axn/unchecked_result.rb +138 -17
  123. metadata +106 -64
  124. data/.rspec +0 -3
  125. data/.rubocop.yml +0 -76
  126. data/.tool-versions +0 -1
  127. data/docs/reference/action-result.md +0 -37
  128. data/lib/action/attachable/base.rb +0 -43
  129. data/lib/action/attachable/steps.rb +0 -63
  130. data/lib/action/attachable/subactions.rb +0 -70
  131. data/lib/action/attachable.rb +0 -17
  132. data/lib/action/configuration.rb +0 -55
  133. data/lib/action/core/automatic_logging.rb +0 -93
  134. data/lib/action/core/context/facade.rb +0 -48
  135. data/lib/action/core/flow/handlers/invoker.rb +0 -73
  136. data/lib/action/core/flow/handlers.rb +0 -20
  137. data/lib/action/core/logging.rb +0 -37
  138. data/lib/action/core/tracing.rb +0 -17
  139. data/lib/action/core/use_strategy.rb +0 -30
  140. data/lib/action/core/validation/validators/model_validator.rb +0 -34
  141. data/lib/action/core/validation/validators/type_validator.rb +0 -30
  142. data/lib/action/enqueueable/via_sidekiq.rb +0 -76
  143. data/lib/action/enqueueable.rb +0 -13
  144. data/lib/action/strategies/transaction.rb +0 -19
  145. data/lib/action/strategies.rb +0 -48
  146. data/lib/axn/util.rb +0 -24
  147. data/package.json +0 -10
  148. data/yarn.lock +0 -1166
@@ -13,9 +13,10 @@ Both `expects` and `exposes` support the same core options:
13
13
  | Option | Example (same for `exposes`) | Meaning |
14
14
  | -- | -- | -- |
15
15
  | `sensitive` | `expects :password, sensitive: true` | Filters the field's value when logging, reporting errors, or calling `inspect`
16
- | `default` | `expects :foo, default: 123` | If `foo` isn't explicitly set, it'll default to this value
17
- | `allow_nil` | `expects :foo, allow_nil: true` | Don't fail if the value is `nil`
18
- | `allow_blank` | `expects :foo, allow_blank: true` | Don't fail if the value is blank
16
+ | `default` | `expects :foo, default: 123` | If `foo` is missing or explicitly `nil`, it'll default to this value (not applied for blank values)
17
+ | `optional` | `expects :foo, optional: true` | **Recommended**: Don't fail if the value is missing, nil, or blank. Equivalent to `allow_blank: true`
18
+ | `allow_nil` | `expects :foo, allow_nil: true` | Don't fail if the value is `nil` (but will fail for blank strings)
19
+ | `allow_blank` | `expects :foo, allow_blank: true` | Don't fail if the value is blank (nil, empty string, whitespace, etc.)
19
20
  | `type` | `expects :foo, type: String` | Custom type validation -- fail unless `name.is_a?(String)`
20
21
  | anything else | `expects :foo, inclusion: { in: [:apple, :peach] }` | Any other arguments will be processed [as ActiveModel validations](https://guides.rubyonrails.org/active_record_validations.html) (i.e. as if passed to `validates :foo, <...>` on an ActiveRecord model)
21
22
 
@@ -26,33 +27,52 @@ Both `expects` and `exposes` support the same core options:
26
27
  While we _support_ complex interface validations, in practice you usually just want a `type`, if anything. Remember this is your validation about how the action is called, _not_ pretty user-facing errors (there's [a different pattern for that](/recipes/validating-user-input)).
27
28
  :::
28
29
 
29
- In addition to the [standard ActiveModel validations](https://guides.rubyonrails.org/active_record_validations.html), we also support three additional custom validators:
30
+ In addition to the [standard ActiveModel validations](https://guides.rubyonrails.org/active_record_validations.html), we also support four additional custom validators:
30
31
  * `type: Foo` - fails unless the provided value `.is_a?(Foo)`
31
32
  * Edge case: use `type: :boolean` to handle a boolean field (since ruby doesn't have a Boolean class to pass in directly)
32
33
  * Edge case: use `type: :uuid` to handle a confirming given string is a UUID (with or without `-` chars)
34
+ * Edge case: use `type: :params` to accept either a Hash or ActionController::Parameters (Rails-compatible)
33
35
  * `validate: [callable]` - Support custom validations (fails if any string is returned OR if it raises an exception)
34
36
  * Example:
35
37
  ```ruby
36
38
  expects :foo, validate: ->(value) { "must be pretty big" unless value > 10 }
37
39
  ```
38
- * `model: true` (or `model: TheModelClass`) - allows auto-hydrating a record when only given its ID
40
+ * `model: true` (or `model: TheModelClass` or `model: { klass: TheModelClass, finder: :find }`) - allows auto-hydrating a record when only given its ID
39
41
  * Example:
40
42
  ```ruby
41
- expects :user_id, model: true
43
+ expects :user, model: true
44
+ # or
45
+ expects :user, model: User
46
+ # or with custom finder
47
+ expects :user, model: { klass: User, finder: :find }
42
48
  ```
43
49
  This line will add expectations that:
44
- * `user_id` is provided
45
- * `User.find(user_id)` returns a record
50
+ * `user_id` is provided (automatically derived from field name)
51
+ * `User.find(user_id)` (or custom finder) returns a record
46
52
 
47
- And, when used on `expects`, will create two reader methods for you:
48
- * `user_id` (normal), _and_
49
- * `user` (for the auto-found record)
53
+ And, when used on `expects`, will create a reader method for you:
54
+ * `user` (the auto-found record)
50
55
 
51
56
  ::: info NOTES
52
- * The field name must end in `_id`
53
- * This was designed for ActiveRecord models, but will work on any class that returns an instance from `find_by(id: <the provided ID>)`
57
+ * The system automatically looks for `#{field}_id` (e.g., `:user` → `:user_id`)
58
+ * The `klass` option defaults to the field name classified (e.g., `:user` `User`)
59
+ * The `finder` option defaults to `:find` but can be any method that takes an ID directly
60
+ * This works with any class that has a finder method (e.g., `User.find`, `ApiService.find_by_id`, etc.)
61
+ * For external APIs, you can pass a `Method` object as the finder
54
62
  :::
55
63
 
64
+ #### How `optional`, `allow_blank` and `allow_nil` work with validators
65
+
66
+ When you specify `optional: true`, `allow_blank: true`, or `allow_nil: true` on a field, these options are automatically passed through to **all validators** applied to that field. This means:
67
+
68
+ - **ActiveModel validations** (like `inclusion`, `length`, etc.) will respect these options
69
+ - **Custom validators** (`type`, `validate`, `model`) will also respect these options
70
+ - **Type validator edge case**: Note passing `allow_blank` is nonsensical for type: :params and type: :boolean
71
+
72
+ **Recommended approach**: Use `optional: true` instead of `allow_blank: true` for better clarity. The `optional` parameter is equivalent to `allow_blank: true` and makes the intent clearer.
73
+
74
+ If neither `optional`, `allow_blank` nor `allow_nil` is specified, a default presence validation is automatically added (unless the type is `:boolean` or `:params`, which have their own validation logic as described above).
75
+
56
76
  ### Details specific to `.exposes`
57
77
 
58
78
  Remember that you'll need [a corresponding `expose` call](/reference/instance#expose) for every variable you declare via `exposes`.
@@ -62,13 +82,14 @@ Remember that you'll need [a corresponding `expose` call](/reference/instance#ex
62
82
 
63
83
  #### Nested/Subfield expectations
64
84
 
65
- `expects` is for defining the inbound interface. Usually it's enough to declare the top-level fields you receive, but sometimes you want to make expectations about the shape of that data, and/or to define easy accessor methods for deeply nested fields. `expects` supports the `on` option for this (all the normal attributes can be applied as well, _except default, preprocess, and sensitive_):
85
+ `expects` is for defining the inbound interface. Usually it's enough to declare the top-level fields you receive, but sometimes you want to make expectations about the shape of that data, and/or to define easy accessor methods for deeply nested fields. `expects` supports the `on` option for this (all the normal attributes can be applied as well):
66
86
 
67
87
  ```ruby
68
88
  class Foo
69
89
  expects :event
70
90
  expects :data, type: Hash, on: :event # [!code focus:2]
71
91
  expects :some, :random, :fields, on: :data
92
+ expects :optional_field, on: :data, default: "default value" # [!code focus]
72
93
 
73
94
  def call
74
95
  puts "THe event.data.random field's value is: #{random}"
@@ -76,8 +97,12 @@ class Foo
76
97
  end
77
98
  ```
78
99
 
100
+ ::: tip Subfield Defaults
101
+ Defaults work the same way for subfields as they do for top-level fields - they are applied when the subfield is missing or explicitly `nil`, but not for blank values.
102
+ :::
103
+
79
104
  #### `preprocess`
80
- `expects` also supports a `preprocess` option that, if set to a callable, will be executed _before_ applying any validations. This can be useful for type coercion, e.g.:
105
+ `expects` also supports a `preprocess` option that, if set to a callable, will be executed _before_ applying any defaults or validations. This can be useful for type coercion, e.g.:
81
106
 
82
107
  ```ruby
83
108
  expects :date, type: Date, preprocess: ->(d) { d.is_a?(Date) ? d : Date.parse(d) }
@@ -124,46 +149,74 @@ def build_error_message(exception:)
124
149
  end
125
150
  ```
126
151
 
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.
152
+ ## Message Matching Order {#message-matching-order}
153
+
154
+ ::: danger Important: Understanding Handler Evaluation Order
155
+ Message handlers are stored in **last-defined-first** order and evaluated in that order until a match is found. This has critical implications for how you structure your message declarations.
156
+ :::
157
+
158
+ ### How It Works
159
+
160
+ 1. Handlers are registered in reverse definition order (last defined = first evaluated)
161
+ 2. The system evaluates handlers one by one until finding one that matches
162
+ 3. Static handlers (no `if:` or `unless:` condition) **always match**
163
+ 4. Once a match is found, evaluation stops
164
+
165
+ ### Correct Pattern: Static Fallbacks First
166
+
167
+ Because static handlers always match, they must be defined **first** (so they're evaluated **last**):
129
168
 
130
- **Correct order:**
131
169
  ```ruby
132
170
  class MyAction
133
- include Action
171
+ include Axn
134
172
 
135
- # Define static fallback first
136
- success "Default success message"
137
- error "Default error message"
173
+ # 1. Define static fallback FIRST (evaluated last, catches anything unmatched)
174
+ error "Something went wrong"
138
175
 
139
- # Then define conditional messages
140
- success "Special success", if: :special_condition?
141
- error "Special error", if: ArgumentError
176
+ # 2. Define conditional handlers AFTER (evaluated first, catch specific cases)
177
+ error "Invalid input provided", if: ArgumentError
178
+ error "Record not found", if: ActiveRecord::RecordNotFound
142
179
  end
143
180
  ```
144
181
 
145
- **Incorrect order (conditional messages will be shadowed):**
182
+ ### Incorrect Pattern: Conditional Messages Shadowed
183
+
184
+ If you define static handlers last, they match first and conditional handlers are never reached:
185
+
146
186
  ```ruby
147
187
  class MyAction
148
- include Action
188
+ include Axn
149
189
 
150
- # These conditional messages will never be reached!
151
- success "Special success", if: :special_condition?
152
- error "Special error", if: ArgumentError
190
+ # These will NEVER be reached!
191
+ error "Invalid input provided", if: ArgumentError
192
+ error "Record not found", if: ActiveRecord::RecordNotFound
153
193
 
154
- # Static messages defined last will always match first
155
- success "Default success message"
156
- error "Default error message"
194
+ # This static handler is evaluated FIRST and always matches
195
+ error "Something went wrong"
196
+ end
197
+ ```
198
+
199
+ ### With Inheritance
200
+
201
+ Child class handlers are evaluated before parent class handlers:
202
+
203
+ ```ruby
204
+ class ParentAction
205
+ include Axn
206
+ error "Parent error" # Evaluated last
207
+ end
208
+
209
+ class ChildAction < ParentAction
210
+ error "Child error" # Evaluated first
157
211
  end
158
212
  ```
159
- :::
160
213
 
161
214
  ## Conditional messages
162
215
 
163
216
  While `.error` and `.success` set the default messages, you can register conditional messages using an optional `if:` or `unless:` matcher. The matcher can be:
164
217
 
165
218
  - an exception class (e.g., `ArgumentError`)
166
- - a class name string (e.g., `"Action::InboundValidationError"`)
219
+ - a class name string (e.g., `"Axn::InboundValidationError"`)
167
220
  - a symbol referencing a local instance method predicate (arity 0 or 1, or keyword `exception:`), e.g. `:bad_input?`
168
221
  - a callable (arity 0 or 1, or keyword `exception:`)
169
222
 
@@ -226,9 +279,13 @@ The `from:` parameter allows you to customize error messages when an action call
226
279
 
227
280
  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
281
 
282
+ ### Basic usage
283
+
284
+ You can use `from:` with a single child action class. The prefix and custom handler are optional:
285
+
229
286
  ```ruby
230
287
  class InnerAction
231
- include Action
288
+ include Axn
232
289
 
233
290
  error "Something went wrong in the inner action"
234
291
 
@@ -238,9 +295,12 @@ class InnerAction
238
295
  end
239
296
 
240
297
  class OuterAction
241
- include Action
298
+ include Axn
299
+
300
+ # Simply inherit child's error message (no prefix or custom handler needed)
301
+ error from: InnerAction
242
302
 
243
- # Customize error messages from InnerAction
303
+ # Or customize the message
244
304
  error from: InnerAction do |e|
245
305
  "Outer action failed: #{e.message}"
246
306
  end
@@ -254,7 +314,58 @@ end
254
314
  In this example:
255
315
  - When `InnerAction` fails, `OuterAction` will catch the exception
256
316
  - 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"
317
+ - With no handler: the error message will be "Something went wrong in the inner action" (inherited directly)
318
+ - With custom handler: the error message will be "Outer action failed: Something went wrong in the inner action"
319
+
320
+ ### Matching multiple child actions
321
+
322
+ You can pass an array of child action classes to match multiple children:
323
+
324
+ ```ruby
325
+ class OuterAction
326
+ include Axn
327
+
328
+ # Match errors from multiple child actions
329
+ error from: [FirstChildAction, SecondChildAction]
330
+
331
+ # Or with custom handler
332
+ error from: [FirstChildAction, SecondChildAction] do |e|
333
+ "Parent caught: #{e.message}"
334
+ end
335
+
336
+ def call
337
+ # Calls one of the child actions
338
+ end
339
+ end
340
+ ```
341
+
342
+ You can also mix class references and string class names:
343
+
344
+ ```ruby
345
+ error from: [FirstChildAction, "SecondChildAction"]
346
+ ```
347
+
348
+ ### Matching any child action
349
+
350
+ Use `from: true` to match errors from any child action without listing them explicitly:
351
+
352
+ ```ruby
353
+ class OuterAction
354
+ include Axn
355
+
356
+ # Match errors from any child action
357
+ error from: true
358
+
359
+ # Or with custom handler
360
+ error from: true do |e|
361
+ "Any child failed: #{e.message}"
362
+ end
363
+
364
+ def call
365
+ # Can call any child action
366
+ end
367
+ end
368
+ ```
258
369
 
259
370
  This pattern is especially useful for:
260
371
  - Adding context to error messages from sub-actions
@@ -267,7 +378,7 @@ You can also combine the `from:` parameter with the `prefix:` keyword to create
267
378
 
268
379
  ```ruby
269
380
  class OuterAction
270
- include Action
381
+ include Axn
271
382
 
272
383
  # Add prefix to error messages from InnerAction
273
384
  error from: InnerAction, prefix: "API Error: " do |e|
@@ -287,45 +398,95 @@ This results in:
287
398
  - With custom message: "API Error: Request failed: Something went wrong in the inner action"
288
399
  - With prefix only: "API Error: Something went wrong in the inner action"
289
400
 
290
- ### Message ordering and inheritance
401
+ ### Message ordering with `from:`
291
402
 
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:
403
+ When using `from:` with inheritance, the same [message matching order](#message-matching-order) applies. Define your `from:` handlers after your static fallback:
293
404
 
294
405
  ```ruby
295
- class ParentAction
296
- include Action
406
+ class OuterAction
407
+ include Axn
297
408
 
298
- success "Parent success message"
299
- error "Parent error message"
300
- end
409
+ # Static fallback first
410
+ error "Something went wrong"
301
411
 
302
- class ChildAction < ParentAction
303
- success "Child success message" # This will be used when action succeeds
304
- error "Child error message" # This will be used when action fails
412
+ # Then from: handlers for specific child actions
413
+ error from: InnerAction, prefix: "Inner failed: "
414
+ error from: AnotherAction, prefix: "Another failed: "
305
415
  end
306
416
  ```
307
417
 
308
- Within a single class, later definitions override earlier ones:
418
+ ## `.async`
419
+
420
+ Configures the async execution behavior for the action. This determines how the action will be executed when `call_async` is called.
309
421
 
310
422
  ```ruby
311
423
  class MyAction
312
- include Action
424
+ include Axn
425
+
426
+ # Configure Sidekiq
427
+ async :sidekiq do
428
+ sidekiq_options queue: "high_priority", retry: 5, priority: 10
429
+ end
313
430
 
314
- success "First success message" # Ignored
315
- success "Second success message" # Ignored
316
- success "Final success message" # This will be used
431
+ # Or use keyword arguments (shorthand)
432
+ async :sidekiq, queue: "high_priority", retry: 5
433
+
434
+ # Configure ActiveJob
435
+ async :active_job do
436
+ queue_as "data_processing"
437
+ self.priority = 10
438
+ self.wait = 5.minutes
439
+ end
317
440
 
318
- error "First error message" # Ignored
319
- error "Second error message" # Ignored
320
- error "Final error message" # This will be used
441
+ # Disable async execution
442
+ async false
443
+
444
+ expects :input
445
+
446
+ def call
447
+ # Action logic here
448
+ end
321
449
  end
322
450
  ```
323
451
 
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.
452
+ ### Available Adapters
326
453
 
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
- :::
454
+ **`:sidekiq`** - Integrates with Sidekiq background job processing
455
+ - Supports all Sidekiq configuration options via `sidekiq_options`
456
+ - Supports keyword argument shorthand for common options (`queue`, `retry`, `priority`)
457
+
458
+ **`:active_job`** - Integrates with Rails' ActiveJob framework
459
+ - Supports all ActiveJob configuration options
460
+ - Works with any ActiveJob backend (Sidekiq, Delayed Job, etc.)
461
+
462
+ **`false`** - Disables async execution
463
+ - `call_async` will raise a `NotImplementedError`
464
+
465
+ ### Inheritance
466
+
467
+ Async configuration is inherited from parent classes. Child classes can override the parent's configuration:
468
+
469
+ ```ruby
470
+ class ParentAction
471
+ include Axn
472
+
473
+ async :sidekiq do
474
+ sidekiq_options queue: "parent_queue"
475
+ end
476
+ end
477
+
478
+ class ChildAction < ParentAction
479
+ # Inherits parent's Sidekiq configuration
480
+ # Can override with its own configuration
481
+ async :active_job do
482
+ queue_as "child_queue"
483
+ end
484
+ end
485
+ ```
486
+
487
+ ### Default Configuration
488
+
489
+ If no async configuration is specified, the action will use the default configuration set via `Axn.config.set_default_async`. If no default is set, async execution is disabled.
329
490
 
330
491
  ## Callbacks
331
492
 
@@ -371,7 +532,7 @@ Much like the [globally-configured on_exception hook](/reference/configuration#o
371
532
 
372
533
  ```ruby
373
534
  class Foo
374
- include Action
535
+ include Axn
375
536
 
376
537
  on_exception do |exception| # [!code focus:3]
377
538
  # e.g. trigger a slack error
@@ -383,7 +544,7 @@ Note that by default the `on_exception` block will be applied to _any_ `Standard
383
544
 
384
545
  ```ruby
385
546
  class Foo
386
- include Action
547
+ include Axn
387
548
 
388
549
  on_exception(if: NoMethodError) do |exception| # [!code focus]
389
550
  # e.g. trigger a slack error