axn 0.1.0.pre.alpha.2.8 → 0.1.0.pre.alpha.3

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 (126) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/rules/axn-framework-patterns.mdc +43 -0
  3. data/.cursor/rules/general-coding-standards.mdc +27 -0
  4. data/.cursor/rules/spec/testing-patterns.mdc +40 -0
  5. data/CHANGELOG.md +47 -0
  6. data/Rakefile +12 -2
  7. data/docs/.vitepress/config.mjs +8 -3
  8. data/docs/advanced/conventions.md +2 -2
  9. data/docs/advanced/mountable.md +562 -0
  10. data/docs/advanced/profiling.md +355 -0
  11. data/docs/advanced/rough.md +1 -1
  12. data/docs/index.md +5 -3
  13. data/docs/intro/about.md +1 -1
  14. data/docs/intro/overview.md +5 -5
  15. data/docs/recipes/memoization.md +2 -2
  16. data/docs/recipes/rubocop-integration.md +38 -284
  17. data/docs/recipes/testing.md +14 -14
  18. data/docs/recipes/validating-user-input.md +1 -1
  19. data/docs/reference/async.md +160 -0
  20. data/docs/reference/axn-result.md +107 -0
  21. data/docs/reference/class.md +123 -25
  22. data/docs/reference/configuration.md +191 -10
  23. data/docs/reference/instance.md +14 -29
  24. data/docs/strategies/index.md +21 -21
  25. data/docs/strategies/transaction.md +1 -1
  26. data/docs/usage/setup.md +14 -0
  27. data/docs/usage/steps.md +7 -7
  28. data/docs/usage/using.md +23 -12
  29. data/docs/usage/writing.md +92 -11
  30. data/lib/axn/async/adapters/active_job.rb +65 -0
  31. data/lib/axn/async/adapters/disabled.rb +26 -0
  32. data/lib/axn/async/adapters/sidekiq.rb +74 -0
  33. data/lib/axn/async/adapters.rb +26 -0
  34. data/lib/axn/async.rb +61 -0
  35. data/lib/{action → axn}/configuration.rb +21 -3
  36. data/lib/{action → axn}/context.rb +21 -4
  37. data/lib/{action → axn}/core/automatic_logging.rb +6 -6
  38. data/lib/axn/core/context/facade.rb +69 -0
  39. data/lib/{action → axn}/core/context/facade_inspector.rb +31 -4
  40. data/lib/{action → axn}/core/context/internal.rb +5 -5
  41. data/lib/{action → axn}/core/contract.rb +43 -46
  42. data/lib/{action → axn}/core/contract_for_subfields.rb +30 -35
  43. data/lib/{action → axn}/core/contract_validation.rb +16 -6
  44. data/lib/axn/core/contract_validation_for_subfields.rb +158 -0
  45. data/lib/axn/core/field_resolvers/extract.rb +32 -0
  46. data/lib/axn/core/field_resolvers/model.rb +63 -0
  47. data/lib/axn/core/field_resolvers.rb +24 -0
  48. data/lib/{action → axn}/core/flow/callbacks.rb +7 -7
  49. data/lib/{action → axn}/core/flow/exception_execution.rb +4 -13
  50. data/lib/{action → axn}/core/flow/handlers/base_descriptor.rb +3 -2
  51. data/lib/{action → axn}/core/flow/handlers/descriptors/callback_descriptor.rb +2 -2
  52. data/lib/{action → axn}/core/flow/handlers/descriptors/message_descriptor.rb +6 -6
  53. data/lib/{action → axn}/core/flow/handlers/invoker.rb +6 -6
  54. data/lib/{action → axn}/core/flow/handlers/matcher.rb +5 -5
  55. data/lib/{action → axn}/core/flow/handlers/registry.rb +3 -1
  56. data/lib/{action → axn}/core/flow/handlers/resolvers/base_resolver.rb +1 -1
  57. data/lib/{action → axn}/core/flow/handlers/resolvers/callback_resolver.rb +2 -2
  58. data/lib/{action → axn}/core/flow/handlers/resolvers/message_resolver.rb +12 -3
  59. data/lib/axn/core/flow/handlers.rb +20 -0
  60. data/lib/{action → axn}/core/flow/messages.rb +7 -7
  61. data/lib/{action → axn}/core/flow.rb +4 -4
  62. data/lib/{action → axn}/core/hooks.rb +16 -5
  63. data/lib/{action → axn}/core/logging.rb +3 -3
  64. data/lib/{action → axn}/core/nesting_tracking.rb +1 -1
  65. data/lib/axn/core/profiling.rb +124 -0
  66. data/lib/{action → axn}/core/timing.rb +1 -1
  67. data/lib/axn/core/tracing.rb +17 -0
  68. data/lib/axn/core/use_strategy.rb +29 -0
  69. data/lib/{action → axn}/core/validation/fields.rb +26 -2
  70. data/lib/{action → axn}/core/validation/subfields.rb +14 -12
  71. data/lib/axn/core/validation/validators/model_validator.rb +36 -0
  72. data/lib/axn/core/validation/validators/type_validator.rb +80 -0
  73. data/lib/{action → axn}/core/validation/validators/validate_validator.rb +12 -2
  74. data/lib/axn/core.rb +123 -0
  75. data/lib/{action → axn}/exceptions.rb +12 -2
  76. data/lib/axn/factory.rb +102 -34
  77. data/lib/axn/internal/logging.rb +26 -0
  78. data/lib/axn/internal/registry.rb +87 -0
  79. data/lib/axn/mountable/descriptor.rb +76 -0
  80. data/lib/axn/mountable/helpers/class_builder.rb +162 -0
  81. data/lib/axn/mountable/helpers/mounter.rb +33 -0
  82. data/lib/axn/mountable/helpers/namespace_manager.rb +66 -0
  83. data/lib/axn/mountable/helpers/validator.rb +112 -0
  84. data/lib/axn/mountable/inherit_profiles.rb +72 -0
  85. data/lib/axn/mountable/mounting_strategies/_base.rb +83 -0
  86. data/lib/axn/mountable/mounting_strategies/axn.rb +48 -0
  87. data/lib/axn/mountable/mounting_strategies/enqueue_all.rb +55 -0
  88. data/lib/axn/mountable/mounting_strategies/method.rb +95 -0
  89. data/lib/axn/mountable/mounting_strategies/step.rb +69 -0
  90. data/lib/axn/mountable/mounting_strategies.rb +32 -0
  91. data/lib/axn/mountable.rb +85 -0
  92. data/lib/axn/rails/engine.rb +51 -0
  93. data/lib/axn/rails/generators/axn_generator.rb +68 -0
  94. data/lib/axn/rails/generators/templates/action.rb.erb +17 -0
  95. data/lib/axn/rails/generators/templates/action_spec.rb.erb +25 -0
  96. data/lib/{action → axn}/result.rb +30 -11
  97. data/lib/{action → axn}/strategies/transaction.rb +1 -1
  98. data/lib/axn/strategies.rb +20 -0
  99. data/lib/axn/testing/spec_helpers.rb +6 -8
  100. data/lib/axn/util/memoization.rb +20 -0
  101. data/lib/axn/version.rb +1 -1
  102. data/lib/axn.rb +17 -16
  103. data/lib/rubocop/cop/axn/README.md +23 -23
  104. data/lib/rubocop/cop/axn/unchecked_result.rb +138 -17
  105. metadata +88 -64
  106. data/.rspec +0 -3
  107. data/.rubocop.yml +0 -76
  108. data/.tool-versions +0 -1
  109. data/docs/reference/action-result.md +0 -37
  110. data/lib/action/attachable/base.rb +0 -43
  111. data/lib/action/attachable/steps.rb +0 -63
  112. data/lib/action/attachable/subactions.rb +0 -70
  113. data/lib/action/attachable.rb +0 -17
  114. data/lib/action/core/context/facade.rb +0 -48
  115. data/lib/action/core/flow/handlers.rb +0 -20
  116. data/lib/action/core/tracing.rb +0 -17
  117. data/lib/action/core/use_strategy.rb +0 -30
  118. data/lib/action/core/validation/validators/model_validator.rb +0 -34
  119. data/lib/action/core/validation/validators/type_validator.rb +0 -30
  120. data/lib/action/core.rb +0 -108
  121. data/lib/action/enqueueable/via_sidekiq.rb +0 -76
  122. data/lib/action/enqueueable.rb +0 -13
  123. data/lib/action/strategies.rb +0 -48
  124. data/lib/axn/util.rb +0 -24
  125. data/package.json +0 -10
  126. data/yarn.lock +0 -1166
@@ -0,0 +1,107 @@
1
+ # `Axn::Result`
2
+
3
+ Every `call` invocation on an Axn will return an `Axn::Result` instance, which provides a consistent interface:
4
+
5
+ | Method | Description |
6
+ | -- | -- |
7
+ | `ok?` | `true` if the call succeeded, `false` if not.
8
+ | `error` | User-facing error message (string), if not `ok?` (else nil)
9
+ | `success` | User-facing success message (string), if `ok?` (else nil)
10
+ | `message` | User-facing message (string), always defined (`ok? ? success : error`)
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 string inquirer (`success?`, `failure?`, `exception?`)
13
+ | `elapsed_time` | Execution time in milliseconds (Float)
14
+ | `finalized?` | `true` if the result has completed execution (either successfully or with an exception), `false` if still in progress
15
+ | 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)
16
+
17
+ NOTE: `success` and `error` (and so implicitly `message`) can be configured per-action via [the `success` and `error` declarations](/reference/class#success-and-error).
18
+
19
+ ### Clarification of exposed values
20
+
21
+ In addition to the core interface, your Action's Result class will have methods defined to read the values of any attributes that were explicitly exposed. For example, given this action and result:
22
+
23
+
24
+ ```ruby
25
+ class Foo
26
+ include Axn
27
+
28
+ exposes :bar, :baz # [!code focus]
29
+
30
+ def call
31
+ expose bar: 1, baz: 2
32
+ end
33
+ end
34
+
35
+ result = Foo.call # [!code focus]
36
+ ```
37
+
38
+ `result` will have both `bar` and `baz` reader methods (which will return 1 and 2, respectively).
39
+
40
+ ## Pattern Matching Support
41
+
42
+ `Axn::Result` supports Ruby 3's pattern matching feature, allowing you to destructure results in a more expressive way:
43
+
44
+ ```ruby
45
+ case SomeAction.call
46
+ in ok: true, success: String => message, user:, order:
47
+ process_success(user, order, message)
48
+ in ok: false, error: String => message
49
+ handle_error(message)
50
+ end
51
+ ```
52
+
53
+ ### Available Pattern Matching Keys
54
+
55
+ When pattern matching, the following keys are available:
56
+
57
+ - `ok` - Boolean success state (`true` for success, `false` for failure)
58
+ - `success` - Success message string (only present when `ok` is `true`)
59
+ - `error` - Error message string (only present when `ok` is `false`)
60
+ - `message` - Always present message string (success or error)
61
+ - `outcome` - Symbol indicating the execution outcome (`:success`, `:failure`, or `:exception`)
62
+ - `finalized` - Boolean indicating if execution completed
63
+ - Any exposed values from the action
64
+
65
+ ### Pattern Matching Examples
66
+
67
+ **Basic Success/Failure Matching:**
68
+ ```ruby
69
+ case result
70
+ in ok: true, user: User => user
71
+ puts "User created: #{user.name}"
72
+ in ok: false, error: String => message
73
+ puts "Error: #{message}"
74
+ end
75
+ ```
76
+
77
+ **Outcome-Based Matching:**
78
+ ```ruby
79
+ case result
80
+ in ok: true, outcome: :success, data: { id: Integer => id }
81
+ puts "Success with ID: #{id}"
82
+ in ok: false, outcome: :failure, error: String => message
83
+ puts "Business logic failure: #{message}"
84
+ in ok: false, outcome: :exception, error: String => message
85
+ puts "System error: #{message}"
86
+ end
87
+ ```
88
+
89
+ **Complex Nested Data Matching:**
90
+ ```ruby
91
+ case result
92
+ in ok: true, order: { id: Integer => order_id, items: [{ name: String => item_name }] }
93
+ puts "Order #{order_id} created with #{item_name}"
94
+ in ok: false, error: String => message, field: String => field
95
+ puts "Validation failed on #{field}: #{message}"
96
+ end
97
+ ```
98
+
99
+ **Type Guards and Variable Binding:**
100
+ ```ruby
101
+ case result
102
+ in ok: true, success: String => message, user: { email: String => email }
103
+ send_notification(email, message)
104
+ in ok: false, error: String => message, code: String => code
105
+ log_error(code: code, message: message)
106
+ end
107
+ ```
@@ -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) }
@@ -130,7 +155,7 @@ end
130
155
  **Correct order:**
131
156
  ```ruby
132
157
  class MyAction
133
- include Action
158
+ include Axn
134
159
 
135
160
  # Define static fallback first
136
161
  success "Default success message"
@@ -145,7 +170,7 @@ end
145
170
  **Incorrect order (conditional messages will be shadowed):**
146
171
  ```ruby
147
172
  class MyAction
148
- include Action
173
+ include Axn
149
174
 
150
175
  # These conditional messages will never be reached!
151
176
  success "Special success", if: :special_condition?
@@ -163,7 +188,7 @@ end
163
188
  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
189
 
165
190
  - an exception class (e.g., `ArgumentError`)
166
- - a class name string (e.g., `"Action::InboundValidationError"`)
191
+ - a class name string (e.g., `"Axn::InboundValidationError"`)
167
192
  - a symbol referencing a local instance method predicate (arity 0 or 1, or keyword `exception:`), e.g. `:bad_input?`
168
193
  - a callable (arity 0 or 1, or keyword `exception:`)
169
194
 
@@ -228,7 +253,7 @@ When using `from:`, the error handler receives the exception from the child acti
228
253
 
229
254
  ```ruby
230
255
  class InnerAction
231
- include Action
256
+ include Axn
232
257
 
233
258
  error "Something went wrong in the inner action"
234
259
 
@@ -238,7 +263,7 @@ class InnerAction
238
263
  end
239
264
 
240
265
  class OuterAction
241
- include Action
266
+ include Axn
242
267
 
243
268
  # Customize error messages from InnerAction
244
269
  error from: InnerAction do |e|
@@ -267,7 +292,7 @@ You can also combine the `from:` parameter with the `prefix:` keyword to create
267
292
 
268
293
  ```ruby
269
294
  class OuterAction
270
- include Action
295
+ include Axn
271
296
 
272
297
  # Add prefix to error messages from InnerAction
273
298
  error from: InnerAction, prefix: "API Error: " do |e|
@@ -293,7 +318,7 @@ Messages are evaluated in **last-defined-first** order, meaning the most recentl
293
318
 
294
319
  ```ruby
295
320
  class ParentAction
296
- include Action
321
+ include Axn
297
322
 
298
323
  success "Parent success message"
299
324
  error "Parent error message"
@@ -309,7 +334,7 @@ Within a single class, later definitions override earlier ones:
309
334
 
310
335
  ```ruby
311
336
  class MyAction
312
- include Action
337
+ include Axn
313
338
 
314
339
  success "First success message" # Ignored
315
340
  success "Second success message" # Ignored
@@ -327,6 +352,79 @@ The system evaluates handlers in the order they were defined until it finds one
327
352
  **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
353
  :::
329
354
 
355
+ ## `.async`
356
+
357
+ Configures the async execution behavior for the action. This determines how the action will be executed when `call_async` is called.
358
+
359
+ ```ruby
360
+ class MyAction
361
+ include Axn
362
+
363
+ # Configure Sidekiq
364
+ async :sidekiq do
365
+ sidekiq_options queue: "high_priority", retry: 5, priority: 10
366
+ end
367
+
368
+ # Or use keyword arguments (shorthand)
369
+ async :sidekiq, queue: "high_priority", retry: 5
370
+
371
+ # Configure ActiveJob
372
+ async :active_job do
373
+ queue_as "data_processing"
374
+ self.priority = 10
375
+ self.wait = 5.minutes
376
+ end
377
+
378
+ # Disable async execution
379
+ async false
380
+
381
+ expects :input
382
+
383
+ def call
384
+ # Action logic here
385
+ end
386
+ end
387
+ ```
388
+
389
+ ### Available Adapters
390
+
391
+ **`:sidekiq`** - Integrates with Sidekiq background job processing
392
+ - Supports all Sidekiq configuration options via `sidekiq_options`
393
+ - Supports keyword argument shorthand for common options (`queue`, `retry`, `priority`)
394
+
395
+ **`:active_job`** - Integrates with Rails' ActiveJob framework
396
+ - Supports all ActiveJob configuration options
397
+ - Works with any ActiveJob backend (Sidekiq, Delayed Job, etc.)
398
+
399
+ **`false`** - Disables async execution
400
+ - `call_async` will raise a `NotImplementedError`
401
+
402
+ ### Inheritance
403
+
404
+ Async configuration is inherited from parent classes. Child classes can override the parent's configuration:
405
+
406
+ ```ruby
407
+ class ParentAction
408
+ include Axn
409
+
410
+ async :sidekiq do
411
+ sidekiq_options queue: "parent_queue"
412
+ end
413
+ end
414
+
415
+ class ChildAction < ParentAction
416
+ # Inherits parent's Sidekiq configuration
417
+ # Can override with its own configuration
418
+ async :active_job do
419
+ queue_as "child_queue"
420
+ end
421
+ end
422
+ ```
423
+
424
+ ### Default Configuration
425
+
426
+ 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.
427
+
330
428
  ## Callbacks
331
429
 
332
430
  In addition to the [global exception handler](/reference/configuration#on-exception), a number of custom callback are available for you as well, if you want to take specific actions when a given Axn succeeds or fails.
@@ -371,7 +469,7 @@ Much like the [globally-configured on_exception hook](/reference/configuration#o
371
469
 
372
470
  ```ruby
373
471
  class Foo
374
- include Action
472
+ include Axn
375
473
 
376
474
  on_exception do |exception| # [!code focus:3]
377
475
  # e.g. trigger a slack error
@@ -383,7 +481,7 @@ Note that by default the `on_exception` block will be applied to _any_ `Standard
383
481
 
384
482
  ```ruby
385
483
  class Foo
386
- include Action
484
+ include Axn
387
485
 
388
486
  on_exception(if: NoMethodError) do |exception| # [!code focus]
389
487
  # e.g. trigger a slack error
@@ -1,9 +1,9 @@
1
1
  # Configuration
2
2
 
3
- Somewhere at boot (e.g. `config/initializers/actions.rb` in Rails), you can call `Action.configure` to adjust a few global settings.
3
+ Somewhere at boot (e.g. `config/initializers/actions.rb` in Rails), you can call `Axn.configure` to adjust a few global settings.
4
4
 
5
5
  ```ruby
6
- Action.configure do |c|
6
+ Axn.configure do |c|
7
7
  c.log_level = :info
8
8
  c.logger = ...
9
9
  c.on_exception = proc do |e, action:, context:|
@@ -22,7 +22,7 @@ By default any swallowed errors are noted in the logs, but it's _highly recommen
22
22
  For example, if you're using Honeybadger this could look something like:
23
23
 
24
24
  ```ruby
25
- Action.configure do |c|
25
+ Axn.configure do |c|
26
26
  c.on_exception = proc do |e, action:, context:|
27
27
  message = "[#{action.class.name}] Failing due to #{e.class.name}: #{e.message}"
28
28
 
@@ -56,7 +56,7 @@ A couple notes:
56
56
 
57
57
  ## `wrap_with_trace` and `emit_metrics`
58
58
 
59
- If you're using an APM provider, observability can be greatly enhanced by adding automatic _tracing_ of Action calls and/or emitting count metrics after each call completes.
59
+ If you're using an APM provider, observability can be greatly enhanced by adding automatic _tracing_ of Axn calls and/or emitting count metrics after each call completes.
60
60
 
61
61
  The framework provides two distinct hooks for observability:
62
62
 
@@ -66,7 +66,7 @@ The framework provides two distinct hooks for observability:
66
66
  For example, to wire up Datadog:
67
67
 
68
68
  ```ruby
69
- Action.configure do |c|
69
+ Axn.configure do |c|
70
70
  c.wrap_with_trace = proc do |resource, &action|
71
71
  Datadog::Tracing.trace("Action", resource:) do
72
72
  action.call
@@ -91,6 +91,20 @@ 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
+ ### Background Job Logging
95
+
96
+ When using background jobs, you may want different loggers for web requests vs. background job execution. Here's a recommended pattern:
97
+
98
+ ```ruby
99
+ Axn.configure do |c|
100
+ # Use Sidekiq's logger when running in Sidekiq workers, otherwise use Rails logger
101
+ c.logger = (defined?(Sidekiq) && Sidekiq.server?) ? Sidekiq.logger : Rails.logger
102
+ end
103
+ ```
104
+
105
+ This ensures that:
106
+ - Web requests log to `Rails.logger` (typically `log/production.log`)
107
+ - Background jobs log to `Sidekiq.logger` (typically STDOUT or a separate log file)
94
108
 
95
109
 
96
110
  ## `additional_includes`
@@ -100,7 +114,7 @@ This is much less critical than the preceding options, but on the off chance you
100
114
  For example:
101
115
 
102
116
  ```ruby
103
- Action.configure do |c|
117
+ Axn.configure do |c|
104
118
  c.additional_includes = [SomeFancyCustomModule]
105
119
  end
106
120
  ```
@@ -115,6 +129,105 @@ Sets the log level used when you call `log "Some message"` in your Action. Note
115
129
 
116
130
  Automatically detects the environment from `RACK_ENV` or `RAILS_ENV`, defaulting to `"development"`. This is used internally for conditional behavior (e.g., more verbose logging in non-production environments).
117
131
 
132
+ ## `set_default_async`
133
+
134
+ Configures the default async adapter and settings for all actions that don't explicitly specify their own async configuration.
135
+
136
+ ```ruby
137
+ Axn.configure do |c|
138
+ # Set default async adapter with configuration
139
+ c.set_default_async(:sidekiq, queue: "default", retry: 3) do
140
+ sidekiq_options priority: 5
141
+ end
142
+
143
+ # Set default async adapter with just configuration
144
+ c.set_default_async(:active_job) do
145
+ queue_as "default"
146
+ self.priority = 5
147
+ end
148
+
149
+ # Disable async by default
150
+ c.set_default_async(false)
151
+ end
152
+ ```
153
+
154
+ ### Async Configuration
155
+
156
+ Axn supports asynchronous execution through background job processing libraries. You can configure async behavior globally or per-action.
157
+
158
+ **Available adapters:**
159
+ - `:sidekiq` - Sidekiq background job processing
160
+ - `:active_job` - Rails ActiveJob framework
161
+ - `false` - Disable async execution
162
+
163
+ **Basic usage:**
164
+ ```ruby
165
+ # Configure per-action
166
+ async :sidekiq, queue: "high_priority"
167
+
168
+ # Configure globally
169
+ Axn.configure do |c|
170
+ c.set_default_async(:sidekiq, queue: "default")
171
+ end
172
+ ```
173
+
174
+ For detailed information about async execution, including delayed execution, adapter configuration options, and best practices, see the [Async Execution documentation](/reference/async).
175
+
176
+ #### Disabled
177
+
178
+ Disables async execution entirely. The action will raise a `NotImplementedError` when `call_async` is called.
179
+
180
+ ```ruby
181
+ # In your action class
182
+ async false
183
+ ```
184
+
185
+ ### Default Configuration
186
+
187
+ By default, async execution is disabled (`false`). You can set a default configuration that will be applied to all actions that don't explicitly configure their own async behavior:
188
+
189
+ ```ruby
190
+ Axn.configure do |c|
191
+ # Set a default async configuration
192
+ c.set_default_async(:sidekiq, queue: "default") do
193
+ sidekiq_options retry: 3
194
+ end
195
+ end
196
+
197
+ # Now all actions will use Sidekiq by default
198
+ class MyAction
199
+ include Axn
200
+ # No async configuration needed - uses default
201
+ end
202
+ ```
203
+
204
+ ## Rails-specific Configuration
205
+
206
+ When using Axn in a Rails application, additional configuration options are available under `Axn.config.rails`:
207
+
208
+ ### `app_actions_autoload_namespace`
209
+
210
+ Controls the namespace for actions in `app/actions`. Defaults to `nil` (no namespace).
211
+
212
+ ```ruby
213
+ Axn.configure do |c|
214
+ # No namespace (default behavior)
215
+ c.rails.app_actions_autoload_namespace = nil
216
+
217
+ # Use Actions namespace
218
+ c.rails.app_actions_autoload_namespace = :Actions
219
+
220
+ # Use any other namespace
221
+ c.rails.app_actions_autoload_namespace = :MyApp
222
+ end
223
+ ```
224
+
225
+ When `nil` (default), actions in `app/actions/user_management/create_user.rb` will be available as `UserManagement::CreateUser`.
226
+
227
+ When set to `:Actions`, the same action will be available as `Actions::UserManagement::CreateUser`.
228
+
229
+ When set to any other symbol (e.g., `:MyApp`), the action will be available as `MyApp::UserManagement::CreateUser`.
230
+
118
231
  ## Automatic Logging
119
232
 
120
233
  By default, every `action.call` will emit log lines when it is called and after it completes:
@@ -124,11 +237,11 @@ By default, every `action.call` will emit log lines when it is called and after
124
237
  [YourCustomAction] Execution completed (with outcome: success) in 0.957 milliseconds
125
238
  ```
126
239
 
127
- Automatic logging will log at `Action.config.log_level` by default, but can be overridden or disabled using the declarative `auto_log` method:
240
+ Automatic logging will log at `Axn.config.log_level` by default, but can be overridden or disabled using the declarative `auto_log` method:
128
241
 
129
242
  ```ruby
130
243
  # Set default for all actions (affects both explicit logging and automatic logging)
131
- Action.configure do |c|
244
+ Axn.configure do |c|
132
245
  c.log_level = :debug
133
246
  end
134
247
 
@@ -143,18 +256,77 @@ end
143
256
 
144
257
  # Use default level (no auto_log call needed)
145
258
  class DefaultAction
146
- # Uses Action.config.log_level
259
+ # Uses Axn.config.log_level
147
260
  end
148
261
  ```
149
262
 
150
263
  The `auto_log` method supports inheritance, so subclasses will inherit the setting from their parent class unless explicitly overridden.
151
264
 
265
+ ## Profiling
266
+
267
+ Axn supports performance profiling using [Vernier](https://github.com/Shopify/vernier), a Ruby sampling profiler. Profiling is enabled per-action by calling the `profile` method.
268
+
269
+ ### Usage
270
+
271
+ Enable profiling on specific actions using the `profile` method:
272
+
273
+ ```ruby
274
+ class MyAction
275
+ include Axn
276
+
277
+ # Profile conditionally (only one profile call per action)
278
+ profile if: -> { debug_mode }
279
+
280
+ expects :name, :debug_mode
281
+
282
+ def call
283
+ "Hello, #{name}!"
284
+ end
285
+ end
286
+ ```
287
+
288
+ ### Configuration Options
289
+
290
+ The `profile` method accepts several options:
291
+
292
+ ```ruby
293
+ class MyAction
294
+ include Axn
295
+
296
+ # Profile with custom options
297
+ profile(
298
+ if: -> { debug_mode },
299
+ sample_rate: 0.1, # Sampling rate (0.0 to 1.0, default: 0.1)
300
+ output_dir: "tmp/profiles" # Output directory (default: Rails.root/tmp/profiles or tmp/profiles)
301
+ )
302
+
303
+ def call
304
+ # Action logic
305
+ end
306
+ end
307
+ ```
308
+
309
+ **Important**:
310
+ - You can only call `profile` **once per action** - subsequent calls will override the previous one
311
+ - This prevents accidental profiling of all actions and ensures you only profile what you intend to analyze
312
+
313
+ ### Viewing Profiles
314
+
315
+ Profiles are saved as JSON files that can be viewed in the [Firefox Profiler](https://profiler.firefox.com/):
316
+
317
+ 1. Run your action with profiling enabled
318
+ 2. Find the generated profile file in your `profiling_output_dir`
319
+ 3. Upload the JSON file to [profiler.firefox.com](https://profiler.firefox.com/)
320
+ 4. Analyze the performance data
321
+
322
+ For more detailed information, see the [Profiling guide](/advanced/profiling).
323
+
152
324
  ## Complete Configuration Example
153
325
 
154
326
  Here's a complete example showing all available configuration options:
155
327
 
156
328
  ```ruby
157
- Action.configure do |c|
329
+ Axn.configure do |c|
158
330
  # Logging
159
331
  c.log_level = :info
160
332
  c.logger = Rails.logger
@@ -178,7 +350,16 @@ Action.configure do |c|
178
350
  Datadog::Metrics.histogram("action.duration", result.elapsed_time, tags: { resource: })
179
351
  end
180
352
 
353
+
354
+ # Async configuration
355
+ c.set_default_async(:sidekiq, queue: "default") do
356
+ sidekiq_options retry: 3, priority: 5
357
+ end
358
+
181
359
  # Global includes
182
360
  c.additional_includes = [MyCustomModule]
361
+
362
+ # Rails-specific configuration
363
+ c.rails.app_actions_autoload_namespace = :Actions
183
364
  end
184
365
  ```