axn 0.1.0.pre.alpha.2.8.1 → 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.
- checksums.yaml +4 -4
- data/.cursor/rules/axn-framework-patterns.mdc +43 -0
- data/.cursor/rules/general-coding-standards.mdc +27 -0
- data/.cursor/rules/spec/testing-patterns.mdc +40 -0
- data/CHANGELOG.md +43 -0
- data/Rakefile +12 -2
- data/docs/.vitepress/config.mjs +8 -3
- data/docs/advanced/conventions.md +2 -2
- data/docs/advanced/mountable.md +562 -0
- data/docs/advanced/profiling.md +355 -0
- data/docs/advanced/rough.md +1 -1
- data/docs/index.md +5 -3
- data/docs/intro/about.md +1 -1
- data/docs/intro/overview.md +5 -5
- data/docs/recipes/memoization.md +2 -2
- data/docs/recipes/rubocop-integration.md +38 -284
- data/docs/recipes/testing.md +14 -14
- data/docs/recipes/validating-user-input.md +1 -1
- data/docs/reference/async.md +160 -0
- data/docs/reference/axn-result.md +107 -0
- data/docs/reference/class.md +123 -25
- data/docs/reference/configuration.md +191 -10
- data/docs/reference/instance.md +14 -29
- data/docs/strategies/index.md +21 -21
- data/docs/strategies/transaction.md +1 -1
- data/docs/usage/setup.md +14 -0
- data/docs/usage/steps.md +7 -7
- data/docs/usage/using.md +23 -12
- data/docs/usage/writing.md +92 -11
- data/lib/axn/async/adapters/active_job.rb +65 -0
- data/lib/axn/async/adapters/disabled.rb +26 -0
- data/lib/axn/async/adapters/sidekiq.rb +74 -0
- data/lib/axn/async/adapters.rb +26 -0
- data/lib/axn/async.rb +61 -0
- data/lib/{action → axn}/configuration.rb +21 -3
- data/lib/{action → axn}/context.rb +21 -4
- data/lib/{action → axn}/core/automatic_logging.rb +6 -6
- data/lib/axn/core/context/facade.rb +69 -0
- data/lib/{action → axn}/core/context/facade_inspector.rb +31 -4
- data/lib/{action → axn}/core/context/internal.rb +5 -5
- data/lib/{action → axn}/core/contract.rb +41 -46
- data/lib/{action → axn}/core/contract_for_subfields.rb +30 -35
- data/lib/{action → axn}/core/contract_validation.rb +16 -6
- data/lib/axn/core/contract_validation_for_subfields.rb +158 -0
- data/lib/axn/core/field_resolvers/extract.rb +32 -0
- data/lib/axn/core/field_resolvers/model.rb +63 -0
- data/lib/axn/core/field_resolvers.rb +24 -0
- data/lib/{action → axn}/core/flow/callbacks.rb +7 -7
- data/lib/{action → axn}/core/flow/exception_execution.rb +4 -13
- data/lib/{action → axn}/core/flow/handlers/base_descriptor.rb +3 -2
- data/lib/{action → axn}/core/flow/handlers/descriptors/callback_descriptor.rb +2 -2
- data/lib/{action → axn}/core/flow/handlers/descriptors/message_descriptor.rb +6 -6
- data/lib/{action → axn}/core/flow/handlers/invoker.rb +2 -2
- data/lib/{action → axn}/core/flow/handlers/matcher.rb +5 -5
- data/lib/{action → axn}/core/flow/handlers/registry.rb +3 -1
- data/lib/{action → axn}/core/flow/handlers/resolvers/base_resolver.rb +1 -1
- data/lib/{action → axn}/core/flow/handlers/resolvers/callback_resolver.rb +2 -2
- data/lib/{action → axn}/core/flow/handlers/resolvers/message_resolver.rb +12 -3
- data/lib/axn/core/flow/handlers.rb +20 -0
- data/lib/{action → axn}/core/flow/messages.rb +7 -7
- data/lib/{action → axn}/core/flow.rb +4 -4
- data/lib/{action → axn}/core/hooks.rb +16 -5
- data/lib/{action → axn}/core/logging.rb +3 -3
- data/lib/{action → axn}/core/nesting_tracking.rb +1 -1
- data/lib/axn/core/profiling.rb +124 -0
- data/lib/{action → axn}/core/timing.rb +1 -1
- data/lib/axn/core/tracing.rb +17 -0
- data/lib/axn/core/use_strategy.rb +29 -0
- data/lib/{action → axn}/core/validation/fields.rb +26 -2
- data/lib/{action → axn}/core/validation/subfields.rb +14 -12
- data/lib/axn/core/validation/validators/model_validator.rb +36 -0
- data/lib/axn/core/validation/validators/type_validator.rb +80 -0
- data/lib/{action → axn}/core/validation/validators/validate_validator.rb +12 -2
- data/lib/axn/core.rb +123 -0
- data/lib/{action → axn}/exceptions.rb +12 -2
- data/lib/axn/factory.rb +102 -34
- data/lib/axn/internal/logging.rb +26 -0
- data/lib/axn/internal/registry.rb +87 -0
- data/lib/axn/mountable/descriptor.rb +76 -0
- data/lib/axn/mountable/helpers/class_builder.rb +162 -0
- data/lib/axn/mountable/helpers/mounter.rb +33 -0
- data/lib/axn/mountable/helpers/namespace_manager.rb +66 -0
- data/lib/axn/mountable/helpers/validator.rb +112 -0
- data/lib/axn/mountable/inherit_profiles.rb +72 -0
- data/lib/axn/mountable/mounting_strategies/_base.rb +83 -0
- data/lib/axn/mountable/mounting_strategies/axn.rb +48 -0
- data/lib/axn/mountable/mounting_strategies/enqueue_all.rb +55 -0
- data/lib/axn/mountable/mounting_strategies/method.rb +95 -0
- data/lib/axn/mountable/mounting_strategies/step.rb +69 -0
- data/lib/axn/mountable/mounting_strategies.rb +32 -0
- data/lib/axn/mountable.rb +85 -0
- data/lib/axn/rails/engine.rb +51 -0
- data/lib/axn/rails/generators/axn_generator.rb +68 -0
- data/lib/axn/rails/generators/templates/action.rb.erb +17 -0
- data/lib/axn/rails/generators/templates/action_spec.rb.erb +25 -0
- data/lib/{action → axn}/result.rb +30 -11
- data/lib/{action → axn}/strategies/transaction.rb +1 -1
- data/lib/axn/strategies.rb +20 -0
- data/lib/axn/testing/spec_helpers.rb +6 -8
- data/lib/axn/util/memoization.rb +20 -0
- data/lib/axn/version.rb +1 -1
- data/lib/axn.rb +17 -16
- data/lib/rubocop/cop/axn/README.md +23 -23
- data/lib/rubocop/cop/axn/unchecked_result.rb +138 -17
- metadata +88 -64
- data/.rspec +0 -3
- data/.rubocop.yml +0 -76
- data/.tool-versions +0 -1
- data/docs/reference/action-result.md +0 -37
- data/lib/action/attachable/base.rb +0 -43
- data/lib/action/attachable/steps.rb +0 -63
- data/lib/action/attachable/subactions.rb +0 -70
- data/lib/action/attachable.rb +0 -17
- data/lib/action/core/context/facade.rb +0 -48
- data/lib/action/core/flow/handlers.rb +0 -20
- data/lib/action/core/tracing.rb +0 -17
- data/lib/action/core/use_strategy.rb +0 -30
- data/lib/action/core/validation/validators/model_validator.rb +0 -34
- data/lib/action/core/validation/validators/type_validator.rb +0 -30
- data/lib/action/core.rb +0 -108
- data/lib/action/enqueueable/via_sidekiq.rb +0 -76
- data/lib/action/enqueueable.rb +0 -13
- data/lib/action/strategies.rb +0 -48
- data/lib/axn/util.rb +0 -24
- data/package.json +0 -10
- 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
|
+
```
|
data/docs/reference/class.md
CHANGED
@@ -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`
|
17
|
-
| `
|
18
|
-
| `
|
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
|
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 :
|
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
|
48
|
-
* `
|
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
|
53
|
-
*
|
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
|
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
|
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
|
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., `"
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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 `
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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 `
|
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
|
-
|
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
|
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
|
-
|
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
|
```
|