axn 0.1.0.pre.alpha.2.8.1 → 0.1.0.pre.alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.cursor/commands/pr.md +36 -0
- 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 +57 -0
- data/Rakefile +114 -4
- data/docs/.vitepress/config.mjs +19 -10
- data/docs/advanced/conventions.md +3 -3
- data/docs/advanced/mountable.md +476 -0
- data/docs/advanced/profiling.md +351 -0
- data/docs/advanced/rough.md +27 -8
- data/docs/index.md +5 -3
- data/docs/intro/about.md +1 -1
- data/docs/intro/overview.md +6 -6
- data/docs/recipes/formatting-context-for-error-tracking.md +186 -0
- data/docs/recipes/memoization.md +103 -18
- 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 +429 -0
- data/docs/reference/axn-result.md +107 -0
- data/docs/reference/class.md +225 -64
- data/docs/reference/configuration.md +366 -34
- data/docs/reference/form-object.md +252 -0
- data/docs/reference/instance.md +14 -29
- data/docs/strategies/client.md +212 -0
- data/docs/strategies/form.md +235 -0
- data/docs/strategies/index.md +21 -21
- data/docs/strategies/transaction.md +1 -1
- data/docs/usage/setup.md +16 -2
- data/docs/usage/steps.md +7 -7
- data/docs/usage/using.md +23 -12
- data/docs/usage/writing.md +191 -12
- data/lib/axn/async/adapters/active_job.rb +74 -0
- data/lib/axn/async/adapters/disabled.rb +41 -0
- data/lib/axn/async/adapters/sidekiq.rb +67 -0
- data/lib/axn/async/adapters.rb +26 -0
- data/lib/axn/async/batch_enqueue/config.rb +38 -0
- data/lib/axn/async/batch_enqueue.rb +99 -0
- data/lib/axn/async/enqueue_all_orchestrator.rb +363 -0
- data/lib/axn/async.rb +178 -0
- data/lib/axn/configuration.rb +113 -0
- data/lib/{action → axn}/context.rb +22 -4
- data/lib/axn/core/automatic_logging.rb +89 -0
- data/lib/axn/core/context/facade.rb +69 -0
- data/lib/{action → axn}/core/context/facade_inspector.rb +32 -5
- data/lib/{action → axn}/core/context/internal.rb +5 -5
- data/lib/{action → axn}/core/contract.rb +111 -73
- data/lib/{action → axn}/core/contract_for_subfields.rb +30 -35
- data/lib/{action → axn}/core/contract_validation.rb +27 -12
- data/lib/axn/core/contract_validation_for_subfields.rb +165 -0
- data/lib/axn/core/default_call.rb +63 -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 +9 -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 +23 -11
- data/lib/axn/core/flow/handlers/invoker.rb +47 -0
- data/lib/{action → axn}/core/flow/handlers/matcher.rb +9 -19
- 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 +8 -8
- data/lib/{action → axn}/core/flow.rb +4 -4
- data/lib/{action → axn}/core/hooks.rb +17 -5
- data/lib/axn/core/logging.rb +48 -0
- data/lib/axn/core/memoization.rb +53 -0
- data/lib/{action → axn}/core/nesting_tracking.rb +1 -1
- data/lib/{action → axn}/core/timing.rb +1 -1
- data/lib/axn/core/tracing.rb +90 -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/{action → axn}/core.rb +55 -55
- data/lib/{action → axn}/exceptions.rb +12 -2
- data/lib/axn/extras/strategies/client.rb +150 -0
- data/lib/axn/extras/strategies/vernier.rb +121 -0
- data/lib/axn/extras.rb +4 -0
- data/lib/axn/factory.rb +122 -34
- data/lib/axn/form_object.rb +90 -0
- data/lib/axn/internal/logging.rb +30 -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 +193 -0
- data/lib/axn/mountable/helpers/mounter.rb +33 -0
- data/lib/axn/mountable/helpers/namespace_manager.rb +38 -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 +87 -0
- data/lib/axn/mountable/mounting_strategies/axn.rb +48 -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 +119 -0
- data/lib/axn/rails/engine.rb +51 -0
- data/lib/axn/rails/generators/axn_generator.rb +86 -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 +32 -13
- data/lib/axn/strategies/form.rb +98 -0
- data/lib/axn/strategies/transaction.rb +26 -0
- data/lib/axn/strategies.rb +20 -0
- data/lib/axn/testing/spec_helpers.rb +6 -8
- data/lib/axn/util/callable.rb +120 -0
- data/lib/axn/util/contract_error_handling.rb +32 -0
- data/lib/axn/util/execution_context.rb +34 -0
- data/lib/axn/util/global_id_serialization.rb +52 -0
- data/lib/axn/util/logging.rb +87 -0
- data/lib/axn/util/memoization.rb +20 -0
- data/lib/axn/version.rb +1 -1
- data/lib/axn.rb +26 -16
- data/lib/rubocop/cop/axn/README.md +23 -23
- data/lib/rubocop/cop/axn/unchecked_result.rb +138 -17
- metadata +106 -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/configuration.rb +0 -55
- data/lib/action/core/automatic_logging.rb +0 -93
- data/lib/action/core/context/facade.rb +0 -48
- data/lib/action/core/flow/handlers/invoker.rb +0 -73
- data/lib/action/core/flow/handlers.rb +0 -20
- data/lib/action/core/logging.rb +0 -37
- 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/enqueueable/via_sidekiq.rb +0 -76
- data/lib/action/enqueueable.rb +0 -13
- data/lib/action/strategies/transaction.rb +0 -19
- 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,235 @@
|
|
|
1
|
+
# Form Strategy
|
|
2
|
+
|
|
3
|
+
The `form` strategy provides a declarative way to validate user input using form objects. It bridges the gap between raw user input (like `params`) and validated, structured data.
|
|
4
|
+
|
|
5
|
+
::: tip When to Use
|
|
6
|
+
Use the form strategy when you need to validate **user-facing input** with user-friendly error messages. This is different from `expects` validations, which validate the **developer contract** (how the action is called).
|
|
7
|
+
:::
|
|
8
|
+
|
|
9
|
+
## Basic Usage
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
class CreateUser
|
|
13
|
+
include Axn
|
|
14
|
+
|
|
15
|
+
use :form, type: CreateUser::Form
|
|
16
|
+
|
|
17
|
+
def call
|
|
18
|
+
# form is automatically validated and exposed
|
|
19
|
+
# If validation fails, the action fails with form.errors
|
|
20
|
+
User.create!(form.to_h)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class CreateUser::Form < Axn::FormObject
|
|
25
|
+
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
26
|
+
validates :name, presence: true, length: { minimum: 2 }
|
|
27
|
+
end
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Configuration Options
|
|
31
|
+
|
|
32
|
+
The form strategy accepts several configuration options:
|
|
33
|
+
|
|
34
|
+
| Option | Default | Description |
|
|
35
|
+
| ------ | ------- | ----------- |
|
|
36
|
+
| `type` | Auto-detected | The form class to use (see [Type Resolution](#type-resolution)) |
|
|
37
|
+
| `expect` | `:params` | The input field name to read from |
|
|
38
|
+
| `expose` | `:form` | The field name to expose the form object as |
|
|
39
|
+
| `inject` | `nil` | Additional context fields to inject into the form |
|
|
40
|
+
|
|
41
|
+
### Type Resolution
|
|
42
|
+
|
|
43
|
+
The `type` option determines which form class to use:
|
|
44
|
+
|
|
45
|
+
1. **Explicit class**: `use :form, type: MyFormClass`
|
|
46
|
+
2. **String constant path**: `use :form, type: "CreateUser::Form"`
|
|
47
|
+
3. **Auto-detected**: If not specified, inferred from action name + expose name (e.g., `CreateUser` + `:form` → `CreateUser::Form`)
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
# Explicit type
|
|
51
|
+
use :form, type: RegistrationForm
|
|
52
|
+
|
|
53
|
+
# String constant (useful for avoiding load order issues)
|
|
54
|
+
use :form, type: "Users::RegistrationForm"
|
|
55
|
+
|
|
56
|
+
# Auto-detected from action name
|
|
57
|
+
class CreateUser
|
|
58
|
+
include Axn
|
|
59
|
+
use :form # Uses CreateUser::Form
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Inline Form Definition
|
|
64
|
+
|
|
65
|
+
You can define the form class inline using a block:
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
class CreateUser
|
|
69
|
+
include Axn
|
|
70
|
+
|
|
71
|
+
use :form do
|
|
72
|
+
validates :email, presence: true
|
|
73
|
+
validates :name, presence: true
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def call
|
|
77
|
+
User.create!(form.to_h)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
This creates an anonymous form class that inherits from `Axn::FormObject`.
|
|
83
|
+
|
|
84
|
+
### Custom Field Names
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
class ProcessOrder
|
|
88
|
+
include Axn
|
|
89
|
+
|
|
90
|
+
# Read from :order_params, expose as :order_form
|
|
91
|
+
use :form, expect: :order_params, expose: :order_form, type: OrderForm
|
|
92
|
+
|
|
93
|
+
def call
|
|
94
|
+
# Access via order_form instead of form
|
|
95
|
+
Order.create!(order_form.to_h)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Injecting Context
|
|
101
|
+
|
|
102
|
+
Use `inject` to pass additional context fields to the form:
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
class UpdateProfile
|
|
106
|
+
include Axn
|
|
107
|
+
|
|
108
|
+
expects :user, model: User
|
|
109
|
+
use :form, type: ProfileForm, inject: [:user]
|
|
110
|
+
|
|
111
|
+
def call
|
|
112
|
+
user.update!(form.to_h)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
class ProfileForm < Axn::FormObject
|
|
117
|
+
attr_accessor :user # Injected from action context
|
|
118
|
+
|
|
119
|
+
validates :email, presence: true
|
|
120
|
+
validate :email_unique_for_other_users
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
def email_unique_for_other_users
|
|
125
|
+
return if user.nil?
|
|
126
|
+
return unless User.where.not(id: user.id).exists?(email: email)
|
|
127
|
+
|
|
128
|
+
errors.add(:email, "is already taken")
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## How It Works
|
|
134
|
+
|
|
135
|
+
When you use the form strategy, the following happens automatically:
|
|
136
|
+
|
|
137
|
+
1. **Expects params**: Adds `expects :params, type: :params` (or your custom `expect` field)
|
|
138
|
+
2. **Exposes form**: Adds `exposes :form` (or your custom `expose` field)
|
|
139
|
+
3. **Creates form**: Defines a memoized method that creates the form from params
|
|
140
|
+
4. **Validates in before hook**: Runs `form.valid?` in a before hook; if invalid, the action fails
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
# This:
|
|
144
|
+
use :form, type: MyForm
|
|
145
|
+
|
|
146
|
+
# Is roughly equivalent to:
|
|
147
|
+
expects :params, type: :params
|
|
148
|
+
exposes :form, type: MyForm
|
|
149
|
+
|
|
150
|
+
def form
|
|
151
|
+
@form ||= MyForm.new(params)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
before do
|
|
155
|
+
expose form: form
|
|
156
|
+
fail! unless form.valid?
|
|
157
|
+
end
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Error Handling
|
|
161
|
+
|
|
162
|
+
When form validation fails:
|
|
163
|
+
- The action fails (returns `ok? == false`)
|
|
164
|
+
- `result.error` contains a generic message
|
|
165
|
+
- `result.form.errors` contains the detailed validation errors
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
result = CreateUser.call(params: { email: "", name: "" })
|
|
169
|
+
|
|
170
|
+
result.ok? # => false
|
|
171
|
+
result.form.errors.full_messages
|
|
172
|
+
# => ["Email can't be blank", "Name can't be blank"]
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### User-Facing Errors
|
|
176
|
+
|
|
177
|
+
To expose user-friendly error messages, configure a custom error handler:
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
class CreateUser
|
|
181
|
+
include Axn
|
|
182
|
+
|
|
183
|
+
use :form, type: CreateUser::Form
|
|
184
|
+
|
|
185
|
+
error { form.errors.full_messages.to_sentence }
|
|
186
|
+
|
|
187
|
+
def call
|
|
188
|
+
User.create!(form.to_h)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Complete Example
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
class CreateCompanyMember
|
|
197
|
+
include Axn
|
|
198
|
+
|
|
199
|
+
expects :company, model: Company
|
|
200
|
+
use :form, type: MemberForm, inject: [:company]
|
|
201
|
+
|
|
202
|
+
exposes :member
|
|
203
|
+
|
|
204
|
+
error { form.errors.full_messages.to_sentence }
|
|
205
|
+
success { "#{member.name} has been added to #{company.name}" }
|
|
206
|
+
|
|
207
|
+
def call
|
|
208
|
+
member = company.members.create!(form.to_h)
|
|
209
|
+
expose member: member
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
class MemberForm < Axn::FormObject
|
|
214
|
+
attr_accessor :company # Injected
|
|
215
|
+
|
|
216
|
+
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
217
|
+
validates :name, presence: true
|
|
218
|
+
validates :role, presence: true, inclusion: { in: %w[admin member guest] }
|
|
219
|
+
|
|
220
|
+
validate :email_not_already_member
|
|
221
|
+
|
|
222
|
+
private
|
|
223
|
+
|
|
224
|
+
def email_not_already_member
|
|
225
|
+
return unless company&.members&.exists?(email: email)
|
|
226
|
+
|
|
227
|
+
errors.add(:email, "is already a member of this company")
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## See Also
|
|
233
|
+
|
|
234
|
+
- [Axn::FormObject](/reference/form-object) - The base class for form objects
|
|
235
|
+
- [Validating User Input](/recipes/validating-user-input) - When to use form validation vs expects validation
|
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
|
@@ -5,7 +5,7 @@ outline: deep
|
|
|
5
5
|
|
|
6
6
|
## Installation
|
|
7
7
|
|
|
8
|
-
Adding `axn` to your Gemfile is enough to start using `include
|
|
8
|
+
Adding `axn` to your Gemfile is enough to start using `include Axn`.
|
|
9
9
|
|
|
10
10
|
## Global Configuration
|
|
11
11
|
|
|
@@ -19,7 +19,21 @@ By default any swallowed errors are noted in the logs, but it's _highly recommen
|
|
|
19
19
|
|
|
20
20
|
### Metrics / Tracing
|
|
21
21
|
|
|
22
|
-
If you're using an APM provider, observability can be greatly enhanced by [configuring tracing and metrics hooks](/reference/configuration#tracing
|
|
22
|
+
If you're using an APM provider, observability can be greatly enhanced by [configuring OpenTelemetry tracing and metrics hooks](/reference/configuration#opentelemetry-tracing). Axn automatically creates OpenTelemetry spans when OpenTelemetry is available.
|
|
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.
|
|
23
37
|
|
|
24
38
|
### Code Quality (Optional)
|
|
25
39
|
|
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).
|