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
data/docs/reference/instance.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
## `#expose`
|
4
4
|
|
5
|
-
Used to set a value on the
|
5
|
+
Used to set a value on the Axn::Result. Remember you can only `expose` keys that you have declared in [the class-level interface](/reference/class).
|
6
6
|
|
7
7
|
* Accepts two positional arguments (the key and value to set, respectively): `expose :some_key, 123`
|
8
8
|
* Accepts a hash with one or more key/value pairs: `expose some_key: 123, another: 456`
|
@@ -12,43 +12,28 @@ Primarily used for its side effects, but it does return a Hash with the key/valu
|
|
12
12
|
|
13
13
|
## `#fail!`
|
14
14
|
|
15
|
-
Called with a string, it immediately halts execution and sets `result.error` to the provided string.
|
15
|
+
Called with a string, it immediately halts execution and sets `result.error` to the provided string. Can also accept keyword arguments that will be exposed before halting execution.
|
16
16
|
|
17
|
-
|
18
|
-
|
19
|
-
Helper method to log (via the [configurable](/reference/configuration#logger) `Action.config.logger`) the string you provide (prefixed with the Action's class name).
|
20
|
-
|
21
|
-
* First argument (required) is a string message to log
|
22
|
-
* Also accepts a `level:` keyword argument to change the log level (defaults to `info`)
|
17
|
+
* First argument (optional) is a string error message
|
18
|
+
* Additional keyword arguments are exposed as data before halting
|
23
19
|
|
24
|
-
|
20
|
+
## `#done!`
|
25
21
|
|
26
|
-
|
22
|
+
Called with an optional string, it immediately halts execution and sets `result.success` to the provided string (or default success message if none provided). Can also accept keyword arguments that will be exposed before halting execution. Skips `after` hooks and remaining `call` method execution, but allows `around` hooks to complete normally.
|
27
23
|
|
28
|
-
|
24
|
+
* First argument (optional) is a string success message
|
25
|
+
* Additional keyword arguments are exposed as data before halting
|
29
26
|
|
30
|
-
|
31
|
-
* An explicit `fail!` call _will_ still fail the action
|
32
|
-
* Any exceptions swallowed _will_ still be reported via the `on_exception` handler
|
27
|
+
**Important:** This method is implemented internally via an exception, so it will roll back manually applied `ActiveRecord::Base.transaction` blocks. Use the [`use :transaction` strategy](/strategies/transaction) instead for transaction-safe early completion.
|
33
28
|
|
34
|
-
|
35
|
-
|
36
|
-
Example:
|
37
|
-
|
38
|
-
```ruby
|
39
|
-
class Foo
|
40
|
-
include Action
|
29
|
+
## `#log`
|
41
30
|
|
42
|
-
|
43
|
-
try { send_slack_notifications } # [!code focus]
|
44
|
-
end
|
31
|
+
Helper method to log (via the [configurable](/reference/configuration#logger) `Axn.config.logger`) the string you provide (prefixed with the Action's class name).
|
45
32
|
|
46
|
-
|
33
|
+
* First argument (required) is a string message to log
|
34
|
+
* Also accepts a `level:` keyword argument to change the log level (defaults to `info`)
|
47
35
|
|
48
|
-
|
36
|
+
Primarily used for its side effects; returns whatever the underlying `Axn.config.logger` instance returns but it does return a Hash with the key/value pair(s) you exposed.
|
49
37
|
|
50
|
-
def send_slack_notifications = ...
|
51
|
-
end
|
52
|
-
```
|
53
38
|
|
54
39
|
|
data/docs/strategies/index.md
CHANGED
@@ -19,7 +19,7 @@ To use a strategy in your action, call the `use` method with the strategy name:
|
|
19
19
|
|
20
20
|
```ruby
|
21
21
|
class CreateUser
|
22
|
-
include
|
22
|
+
include Axn
|
23
23
|
|
24
24
|
use :transaction
|
25
25
|
|
@@ -35,11 +35,11 @@ end
|
|
35
35
|
|
36
36
|
### Using Strategies with Configuration
|
37
37
|
|
38
|
-
Some strategies support configuration options. These strategies have a `
|
38
|
+
Some strategies support configuration options. These strategies have a `configure` method that accepts configuration and returns a configured module. As an _imaginary_ example:
|
39
39
|
|
40
40
|
```ruby
|
41
41
|
class ProcessPayment
|
42
|
-
include
|
42
|
+
include Axn
|
43
43
|
|
44
44
|
use :retry, max_attempts: 3, backoff: :exponential
|
45
45
|
|
@@ -55,7 +55,7 @@ end
|
|
55
55
|
|
56
56
|
## Built-in Strategies
|
57
57
|
|
58
|
-
The list of built in strategies is available via `
|
58
|
+
The list of built in strategies is available via `Axn::Strategies.built_in`.
|
59
59
|
|
60
60
|
## Registering Custom Strategies
|
61
61
|
|
@@ -79,14 +79,14 @@ end
|
|
79
79
|
Then register it with the strategies system:
|
80
80
|
|
81
81
|
```ruby
|
82
|
-
|
82
|
+
Axn::Strategies.register(:my_custom, MyCustomStrategy)
|
83
83
|
```
|
84
84
|
|
85
85
|
Now you can use it in your actions:
|
86
86
|
|
87
87
|
```ruby
|
88
88
|
class MyAction
|
89
|
-
include
|
89
|
+
include Axn
|
90
90
|
|
91
91
|
use :my_custom
|
92
92
|
|
@@ -98,13 +98,13 @@ end
|
|
98
98
|
|
99
99
|
### Configurable Strategies
|
100
100
|
|
101
|
-
For strategies that need configuration, implement a `
|
101
|
+
For strategies that need configuration, implement a `configure` method that returns a configured module:
|
102
102
|
|
103
103
|
```ruby
|
104
104
|
module RetryStrategy
|
105
105
|
extend ActiveSupport::Concern
|
106
106
|
|
107
|
-
def self.
|
107
|
+
def self.configure(max_attempts: 3, backoff: :linear, &block)
|
108
108
|
Module.new do
|
109
109
|
extend ActiveSupport::Concern
|
110
110
|
|
@@ -142,15 +142,15 @@ module RetryStrategy
|
|
142
142
|
end
|
143
143
|
|
144
144
|
# Register the strategy
|
145
|
-
|
145
|
+
Axn::Strategies.register(:retry, RetryStrategy)
|
146
146
|
```
|
147
147
|
|
148
148
|
### Strategy Registration Best Practices
|
149
149
|
|
150
150
|
1. **Register early**: Register custom strategies during application initialization
|
151
151
|
2. **Use descriptive names**: Choose strategy names that clearly indicate their purpose
|
152
|
-
3. **Handle configuration validation**: Validate configuration options in your `
|
153
|
-
4. **Return proper modules**: Always return a module from the `
|
152
|
+
3. **Handle configuration validation**: Validate configuration options in your `configure` method
|
153
|
+
4. **Return proper modules**: Always return a module from the `configure` method
|
154
154
|
5. **Document your strategies**: Include clear documentation for how to use your custom strategies
|
155
155
|
|
156
156
|
### Example: Complete Custom Strategy
|
@@ -161,7 +161,7 @@ Here's a complete example of a custom strategy that adds performance monitoring
|
|
161
161
|
module PerformanceMonitoringStrategy
|
162
162
|
extend ActiveSupport::Concern
|
163
163
|
|
164
|
-
def self.
|
164
|
+
def self.configure(threshold_ms: 1000, notify_slow: false, &block)
|
165
165
|
Module.new do
|
166
166
|
extend ActiveSupport::Concern
|
167
167
|
|
@@ -194,11 +194,11 @@ module PerformanceMonitoringStrategy
|
|
194
194
|
end
|
195
195
|
|
196
196
|
# Register the strategy
|
197
|
-
|
197
|
+
Axn::Strategies.register(:performance_monitoring, PerformanceMonitoringStrategy)
|
198
198
|
|
199
199
|
# Use it in an action
|
200
200
|
class ExpensiveCalculation
|
201
|
-
include
|
201
|
+
include Axn
|
202
202
|
|
203
203
|
use :performance_monitoring, threshold_ms: 500, notify_slow: true
|
204
204
|
|
@@ -227,7 +227,7 @@ end
|
|
227
227
|
You can inspect all registered strategies:
|
228
228
|
|
229
229
|
```ruby
|
230
|
-
|
230
|
+
Axn::Strategies.all
|
231
231
|
# Returns a hash of strategy names to their modules
|
232
232
|
```
|
233
233
|
|
@@ -236,11 +236,11 @@ Action::Strategies.all
|
|
236
236
|
To find a specific strategy by name:
|
237
237
|
|
238
238
|
```ruby
|
239
|
-
|
239
|
+
Axn::Strategies.find(:transaction)
|
240
240
|
# Returns the strategy module for the transaction strategy
|
241
241
|
|
242
|
-
|
243
|
-
# Raises
|
242
|
+
Axn::Strategies.find(:nonexistent)
|
243
|
+
# Raises Axn::StrategyNotFound: Strategy 'nonexistent' not found
|
244
244
|
```
|
245
245
|
|
246
246
|
The `find` method is useful when you need to programmatically access a strategy module or verify that a strategy exists before using it.
|
@@ -250,15 +250,15 @@ The `find` method is useful when you need to programmatically access a strategy
|
|
250
250
|
To reset strategies to only built-in ones (useful in tests):
|
251
251
|
|
252
252
|
```ruby
|
253
|
-
|
253
|
+
Axn::Strategies.clear!
|
254
254
|
```
|
255
255
|
|
256
256
|
### Strategy Errors
|
257
257
|
|
258
258
|
The following errors may be raised when using strategies:
|
259
259
|
|
260
|
-
- `
|
261
|
-
- `
|
260
|
+
- `Axn::StrategyNotFound`: When trying to use a strategy that hasn't been registered
|
261
|
+
- `Axn::DuplicateStrategyError`: When trying to register a strategy with a name that's already taken
|
262
262
|
- `ArgumentError`: When providing configuration to a strategy that doesn't support it
|
263
263
|
|
264
264
|
## Best Practices
|
data/docs/usage/setup.md
CHANGED
@@ -21,6 +21,20 @@ By default any swallowed errors are noted in the logs, but it's _highly recommen
|
|
21
21
|
|
22
22
|
If you're using an APM provider, observability can be greatly enhanced by [configuring tracing and metrics hooks](/reference/configuration#tracing-and-metrics).
|
23
23
|
|
24
|
+
### Rails Integration (Optional)
|
25
|
+
|
26
|
+
When using Axn in a Rails application, you can configure how actions are autoloaded from the `app/actions` directory. By default, actions are loaded without any namespace, but you can configure a namespace to help differentiate them from existing service objects:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
# config/initializers/axn.rb
|
30
|
+
Axn.configure do |c|
|
31
|
+
# Use :Actions namespace to differentiate from existing service objects
|
32
|
+
c.rails.app_actions_autoload_namespace = :Actions
|
33
|
+
end
|
34
|
+
```
|
35
|
+
|
36
|
+
This is particularly useful when migrating from existing service object patterns, as it makes it easy to distinguish between new Axn actions and legacy service objects when you see `action.call` in your codebase.
|
37
|
+
|
24
38
|
### Code Quality (Optional)
|
25
39
|
|
26
40
|
For teams using RuboCop, Axn provides custom cops to enforce best practices. See the [RuboCop Integration guide](/recipes/rubocop-integration) for setup instructions.
|
data/docs/usage/steps.md
CHANGED
@@ -31,7 +31,7 @@ The `step` method allows you to define steps inline with blocks:
|
|
31
31
|
|
32
32
|
```ruby
|
33
33
|
class UserRegistration
|
34
|
-
include
|
34
|
+
include Axn
|
35
35
|
expects :email, :password, :name
|
36
36
|
exposes :user_id, :welcome_message
|
37
37
|
|
@@ -66,7 +66,7 @@ The `steps` method allows you to compose existing action classes:
|
|
66
66
|
|
67
67
|
```ruby
|
68
68
|
class ValidateInput
|
69
|
-
include
|
69
|
+
include Axn
|
70
70
|
expects :email, :password, :name
|
71
71
|
exposes :validated_data
|
72
72
|
|
@@ -80,7 +80,7 @@ class ValidateInput
|
|
80
80
|
end
|
81
81
|
|
82
82
|
class CreateUser
|
83
|
-
include
|
83
|
+
include Axn
|
84
84
|
expects :validated_data
|
85
85
|
exposes :user_id
|
86
86
|
|
@@ -91,7 +91,7 @@ class CreateUser
|
|
91
91
|
end
|
92
92
|
|
93
93
|
class SendWelcome
|
94
|
-
include
|
94
|
+
include Axn
|
95
95
|
expects :user_id, :validated_data
|
96
96
|
exposes :welcome_message
|
97
97
|
|
@@ -102,7 +102,7 @@ class SendWelcome
|
|
102
102
|
end
|
103
103
|
|
104
104
|
class UserRegistration
|
105
|
-
include
|
105
|
+
include Axn
|
106
106
|
expects :email, :password, :name
|
107
107
|
exposes :user_id, :welcome_message
|
108
108
|
|
@@ -117,7 +117,7 @@ You can combine both approaches:
|
|
117
117
|
|
118
118
|
```ruby
|
119
119
|
class UserRegistration
|
120
|
-
include
|
120
|
+
include Axn
|
121
121
|
expects :email, :password, :name
|
122
122
|
exposes :user_id, :welcome_message
|
123
123
|
|
@@ -292,7 +292,7 @@ end
|
|
292
292
|
|
293
293
|
```ruby
|
294
294
|
class ProcessAPIRequest
|
295
|
-
include
|
295
|
+
include Axn
|
296
296
|
expects :request_data
|
297
297
|
exposes :response_data
|
298
298
|
|
data/docs/usage/using.md
CHANGED
@@ -7,9 +7,9 @@ outline: deep
|
|
7
7
|
|
8
8
|
## Common Case
|
9
9
|
|
10
|
-
An action executed via `#call` _always_ returns an instance of the `
|
10
|
+
An action executed via `#call` _always_ returns an instance of the `Axn::Result` class.
|
11
11
|
|
12
|
-
This means the result _always_ implements a consistent interface, including `ok?` and `error` (see [full details](/reference/
|
12
|
+
This means the result _always_ implements a consistent interface, including `ok?` and `error` (see [full details](/reference/axn-result)) as well as any variables that the action `exposes`.
|
13
13
|
|
14
14
|
As a consumer, you usually want a conditional that surfaces `error` unless the result is `ok?` (remember that any exceptions have been swallowed), and otherwise takes whatever success action is relevant.
|
15
15
|
|
@@ -38,20 +38,31 @@ end
|
|
38
38
|
|
39
39
|
### `#call!`
|
40
40
|
|
41
|
-
An action executed via `#call!` (note the `!`) does _not_ swallow exceptions -- a _successful_ action will return an `
|
41
|
+
An action executed via `#call!` (note the `!`) does _not_ swallow exceptions -- a _successful_ action will return an `Axn::Result` just like `call`, but any exceptions will bubble up uncaught (note: technically they _will_ be caught, your on_exception handler triggered, and then re-raised) and any explicit `fail!` calls will raise an `Axn::Failure` exception with your custom message.
|
42
42
|
|
43
43
|
This is a much less common pattern, as you're giving up the benefits of error swallowing and the consistent return interface guarantee, but it can be useful in limited contexts (usually for smaller, one-off scripts where it's easier to just let a failure bubble up rather than worry about adding conditionals for error handling).
|
44
44
|
|
45
45
|
|
46
|
-
### `#
|
46
|
+
### `#call_async`
|
47
47
|
|
48
|
-
Before adopting this library, our code was littered with one-line workers whose only job was to fire off a service on a background job.
|
48
|
+
Before adopting this library, our code was littered with one-line workers whose only job was to fire off a service on a background job. We were able to remove that entire glue layer by directly supporting async execution via background jobs from the Axn itself.
|
49
49
|
|
50
|
-
|
51
|
-
|
52
|
-
|
50
|
+
```ruby
|
51
|
+
class ProcessDataAction
|
52
|
+
include Axn
|
53
|
+
|
54
|
+
expects :data
|
55
|
+
|
56
|
+
def call
|
57
|
+
# Process data logic here
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Execute synchronously
|
62
|
+
result = ProcessDataAction.call(data: large_dataset)
|
63
|
+
|
64
|
+
# Execute asynchronously
|
65
|
+
ProcessDataAction.call_async(data: large_dataset)
|
66
|
+
```
|
53
67
|
|
54
|
-
|
55
|
-
* enqueue will not retry even if fails
|
56
|
-
* enqueue! will go through normal sidekiq retries on any failure (including user-facing `fail!`)
|
57
|
-
* Note implicit GlobalID support (if not serializable, will get ArgumentError at callsite)
|
68
|
+
For detailed information about configuring async adapters (Sidekiq, ActiveJob, etc.), see the [Async Execution documentation](/reference/async).
|
data/docs/usage/writing.md
CHANGED
@@ -8,7 +8,7 @@ The core boilerplate is pretty minimal:
|
|
8
8
|
|
9
9
|
```ruby
|
10
10
|
class Foo
|
11
|
-
include
|
11
|
+
include Axn
|
12
12
|
|
13
13
|
def call
|
14
14
|
# ... do some stuff here?
|
@@ -22,14 +22,15 @@ The first step is to determine what arguments you expect to be passed into `call
|
|
22
22
|
|
23
23
|
If you want to expose any results to the caller, declare that via the `exposes` keyword.
|
24
24
|
|
25
|
-
Both of these optionally accept `type:`, `allow_nil:`, `allow_blank:`, and any other ActiveModel validation (see: [reference](/reference/class)).
|
25
|
+
Both of these optionally accept `type:`, `optional:`, `allow_nil:`, `allow_blank:`, and any other ActiveModel validation (see: [reference](/reference/class)).
|
26
26
|
|
27
27
|
|
28
28
|
```ruby
|
29
29
|
class Foo
|
30
|
-
include
|
30
|
+
include Axn
|
31
31
|
|
32
32
|
expects :name, type: String # [!code focus:2]
|
33
|
+
expects :email, type: String, optional: true # [!code focus:2]
|
33
34
|
exposes :meaning_of_life
|
34
35
|
|
35
36
|
def call
|
@@ -42,13 +43,15 @@ end
|
|
42
43
|
|
43
44
|
Once the interface is defined, you're primarily focused on defining the `call` method.
|
44
45
|
|
45
|
-
To abort execution with a specific error message, call `fail!`.
|
46
|
+
To abort execution with a specific error message, call `fail!`. You can also provide exposures as keyword arguments.
|
47
|
+
|
48
|
+
To complete execution early with a success result, call `done!` with an optional success message and exposures as keyword arguments.
|
46
49
|
|
47
50
|
If you declare that your action `exposes` anything, you need to actually `expose` it.
|
48
51
|
|
49
52
|
```ruby
|
50
53
|
class Foo
|
51
|
-
include
|
54
|
+
include Axn
|
52
55
|
|
53
56
|
expects :name, type: String
|
54
57
|
exposes :meaning_of_life
|
@@ -64,6 +67,84 @@ end
|
|
64
67
|
|
65
68
|
See [the reference doc](/reference/instance) for a few more handy helper methods (e.g. `#log`).
|
66
69
|
|
70
|
+
### Convenient failure with context
|
71
|
+
|
72
|
+
Both `fail!` and `done!` can accept keyword arguments to expose data before halting execution:
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
class UserValidator
|
76
|
+
include Axn
|
77
|
+
|
78
|
+
expects :email
|
79
|
+
exposes :error_code, :field
|
80
|
+
|
81
|
+
def call
|
82
|
+
if email.blank?
|
83
|
+
fail!("Email is required", error_code: 422, field: "email")
|
84
|
+
end
|
85
|
+
|
86
|
+
# ... validation logic
|
87
|
+
end
|
88
|
+
end
|
89
|
+
```
|
90
|
+
|
91
|
+
## Early completion with `done!`
|
92
|
+
|
93
|
+
The `done!` method allows you to complete an action early with a success result, bypassing the rest of the execution:
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
class UserLookup
|
97
|
+
include Axn
|
98
|
+
|
99
|
+
expects :user_id
|
100
|
+
exposes :user, :cached
|
101
|
+
|
102
|
+
def call
|
103
|
+
# Check cache first
|
104
|
+
cached_user = Rails.cache.read("user:#{user_id}")
|
105
|
+
if cached_user
|
106
|
+
done!("User found in cache", user: cached_user, cached: true) # Early completion with exposures
|
107
|
+
end
|
108
|
+
|
109
|
+
# This won't execute if done! was called above
|
110
|
+
user = User.find(user_id)
|
111
|
+
expose user: user, cached: false
|
112
|
+
end
|
113
|
+
end
|
114
|
+
```
|
115
|
+
|
116
|
+
### Important behavior notes
|
117
|
+
|
118
|
+
**Hook execution:**
|
119
|
+
- `done!` **skips** any `after` hooks (or `call` method if called from a `before` hook)
|
120
|
+
- `around` hooks **will complete** normally, allowing transactions and tracing to finish properly
|
121
|
+
- If you want code that executes on both normal AND early success, use an `on_success` callback instead of an `after` hook
|
122
|
+
|
123
|
+
**Transaction handling:**
|
124
|
+
- `done!` is implemented internally via an exception, so it **will roll back** manually applied `ActiveRecord::Base.transaction` blocks
|
125
|
+
- Use the [`use :transaction` strategy](/strategies/transaction) instead - transactions applied via this strategy will **NOT** be rolled back by `done!`
|
126
|
+
- This ensures database consistency while allowing early completion
|
127
|
+
|
128
|
+
**Validation:**
|
129
|
+
- Outbound validation (required `exposes`) still runs even with early completion
|
130
|
+
- If required fields are not provided, the action will fail despite the early completion
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
class BadExample
|
134
|
+
include Axn
|
135
|
+
|
136
|
+
expects :user_id
|
137
|
+
exposes :user # Required field
|
138
|
+
|
139
|
+
def call
|
140
|
+
done!("Early completion") # This will FAIL - user not exposed
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
BadExample.call(user_id: 123).ok? # => false
|
145
|
+
BadExample.call(user_id: 123).exception # => Axn::OutboundValidationError
|
146
|
+
```
|
147
|
+
|
67
148
|
## Customizing messages
|
68
149
|
|
69
150
|
The default `error` and `success` message strings ("Something went wrong" / "Action completed successfully", respectively) _are_ technically safe to show users, but you'll often want to set them to something more useful.
|
@@ -74,7 +155,7 @@ For instance, configuring the action like this:
|
|
74
155
|
|
75
156
|
```ruby
|
76
157
|
class Foo
|
77
|
-
include
|
158
|
+
include Axn
|
78
159
|
|
79
160
|
expects :name, type: String
|
80
161
|
exposes :meaning_of_life
|
@@ -106,7 +187,7 @@ You can also use conditional error messages with the `prefix:` keyword and combi
|
|
106
187
|
|
107
188
|
```ruby
|
108
189
|
class ValidationAction
|
109
|
-
include
|
190
|
+
include Axn
|
110
191
|
|
111
192
|
expects :input
|
112
193
|
|
@@ -123,7 +204,7 @@ class ValidationAction
|
|
123
204
|
end
|
124
205
|
|
125
206
|
class ApiAction
|
126
|
-
include
|
207
|
+
include Axn
|
127
208
|
|
128
209
|
expects :data
|
129
210
|
|
@@ -152,7 +233,7 @@ This configuration provides:
|
|
152
233
|
**Correct order:**
|
153
234
|
```ruby
|
154
235
|
class Foo
|
155
|
-
include
|
236
|
+
include Axn
|
156
237
|
|
157
238
|
# Static fallback messages first
|
158
239
|
success "Default success message"
|
@@ -175,13 +256,13 @@ In addition to `#call`, there are a few additional pieces to be aware of:
|
|
175
256
|
|
176
257
|
`before`, `after`, and `around` hooks are supported. They can receive a block directly, or the symbol name of a local method.
|
177
258
|
|
178
|
-
Note execution is halted whenever `fail!` is called or an exception is raised (so a `before` block failure won't execute `call` or `after`, while an `after` block failure will make `result.ok?` be false even though `call` completed successfully).
|
259
|
+
Note execution is halted whenever `fail!` is called, `done!` is called, or an exception is raised (so a `before` block failure won't execute `call` or `after`, while an `after` block failure will make `result.ok?` be false even though `call` completed successfully). The `done!` method specifically skips `after` hooks and any remaining `call` method execution, but allows `around` hooks to complete normally.
|
179
260
|
|
180
261
|
For instance, given this configuration:
|
181
262
|
|
182
263
|
```ruby
|
183
264
|
class Foo
|
184
|
-
include
|
265
|
+
include Axn
|
185
266
|
|
186
267
|
before { log("before hook") } # [!code focus:2]
|
187
268
|
after :log_after
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Axn
|
4
|
+
module Async
|
5
|
+
class Adapters
|
6
|
+
module ActiveJob
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
raise LoadError, "ActiveJob is not available. Please add 'activejob' to your Gemfile." unless defined?(::ActiveJob::Base)
|
11
|
+
|
12
|
+
# Validate that kwargs are not provided for ActiveJob
|
13
|
+
if _async_config&.any?
|
14
|
+
raise ArgumentError, "ActiveJob adapter requires a configuration block. Use `async :active_job do ... end` instead of passing keyword arguments."
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class_methods do
|
19
|
+
def call_async(**kwargs)
|
20
|
+
job = active_job_proxy_class
|
21
|
+
|
22
|
+
if kwargs[:_async].is_a?(Hash)
|
23
|
+
options = kwargs.delete(:_async)
|
24
|
+
if options[:wait_until]
|
25
|
+
job = job.set(wait_until: options[:wait_until])
|
26
|
+
elsif options[:wait]
|
27
|
+
job = job.set(wait: options[:wait])
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
job.perform_later(**kwargs)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def active_job_proxy_class
|
37
|
+
@active_job_proxy_class ||= create_active_job_proxy_class
|
38
|
+
end
|
39
|
+
|
40
|
+
def create_active_job_proxy_class
|
41
|
+
# Store reference to the original action class
|
42
|
+
action_class = self
|
43
|
+
|
44
|
+
# Create the ActiveJob proxy class
|
45
|
+
Class.new(::ActiveJob::Base).tap do |proxy|
|
46
|
+
# Give the job class a meaningful name for logging and debugging
|
47
|
+
job_name = "#{name}::ActiveJobProxy"
|
48
|
+
const_set("ActiveJobProxy", proxy)
|
49
|
+
proxy.define_singleton_method(:name) { job_name }
|
50
|
+
|
51
|
+
# Apply the async configuration block if it exists
|
52
|
+
proxy.class_eval(&_async_config_block) if _async_config_block
|
53
|
+
|
54
|
+
# Define the perform method
|
55
|
+
proxy.define_method(:perform) do |job_context = {}|
|
56
|
+
# Call the original action class with the job context
|
57
|
+
action_class.call!(**job_context)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Axn
|
4
|
+
module Async
|
5
|
+
class Adapters
|
6
|
+
module Disabled
|
7
|
+
def self.included(base)
|
8
|
+
base.class_eval do
|
9
|
+
# Validate that kwargs are not provided for Disabled adapter
|
10
|
+
raise ArgumentError, "Disabled adapter does not accept configuration options." if _async_config&.any?
|
11
|
+
raise ArgumentError, "Disabled adapter does not accept configuration block." if _async_config_block
|
12
|
+
|
13
|
+
def self.call_async(**kwargs)
|
14
|
+
# Remove _async parameter to avoid confusion in error message
|
15
|
+
kwargs.delete(:_async)
|
16
|
+
|
17
|
+
raise NotImplementedError,
|
18
|
+
"Async execution is explicitly disabled for #{name}. " \
|
19
|
+
"Use `async :sidekiq` or `async :active_job` to enable background processing."
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|