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
@@ -1,12 +1,16 @@
|
|
1
1
|
# RuboCop Integration
|
2
2
|
|
3
|
-
Axn provides custom RuboCop
|
3
|
+
Axn provides a custom RuboCop cop to help enforce proper result handling when calling Actions.
|
4
4
|
|
5
|
-
##
|
5
|
+
## What It Does
|
6
6
|
|
7
|
-
The `Axn/UncheckedResult` cop
|
7
|
+
The `Axn/UncheckedResult` cop detects when you call another Action from within an Action but don't properly handle the result. This helps prevent silent failures and ensures consistent error handling patterns.
|
8
8
|
|
9
|
-
|
9
|
+
> **⚠️ Warning**: This cop uses static analysis and cannot distinguish between actual Axn classes and other classes that happen to have a `call` method. If you're using legacy services or other service patterns alongside Axn, you may encounter false positives. Use RuboCop disable comments for intentional violations.
|
10
|
+
>
|
11
|
+
> **💡 Tip**: If you're using the Actions namespace (see [Rails Integration](/usage/setup#rails-integration-optional)), you can configure the cop to only check `Actions::*` classes, eliminating false positives from other service objects.
|
12
|
+
|
13
|
+
## Setup
|
10
14
|
|
11
15
|
### 1. Add to Your .rubocop.yml
|
12
16
|
|
@@ -14,137 +18,37 @@ The `Axn/UncheckedResult` cop enforces proper result handling when calling Actio
|
|
14
18
|
require:
|
15
19
|
- axn/rubocop
|
16
20
|
|
17
|
-
# Enable Axn's custom cop
|
18
21
|
Axn/UncheckedResult:
|
19
22
|
Enabled: true
|
20
|
-
|
21
|
-
CheckNonNested: true # Check non-nested Action calls
|
22
|
-
Severity: warning # or error
|
23
|
+
Severity: warning
|
23
24
|
```
|
24
25
|
|
25
26
|
### 2. Verify Installation
|
26
27
|
|
27
|
-
Run RuboCop to ensure the cop is loaded:
|
28
|
-
|
29
28
|
```bash
|
30
29
|
bundle exec rubocop --show-cops | grep Axn
|
31
30
|
```
|
32
31
|
|
33
|
-
You should see
|
34
|
-
```
|
35
|
-
Axn/UncheckedResult
|
36
|
-
```
|
37
|
-
|
38
|
-
## Configuration Options
|
39
|
-
|
40
|
-
### CheckNested
|
41
|
-
|
42
|
-
Controls whether the cop checks Action calls that are inside other Action classes.
|
43
|
-
|
44
|
-
```yaml
|
45
|
-
Axn/UncheckedResult:
|
46
|
-
CheckNested: true # Check nested calls (default)
|
47
|
-
CheckNested: false # Skip nested calls
|
48
|
-
```
|
49
|
-
|
50
|
-
**When to use `CheckNested: false`:**
|
51
|
-
- You're gradually adopting the rule and want to focus on top-level calls first
|
52
|
-
- Your team has different standards for nested vs. non-nested calls
|
53
|
-
- You're using a different pattern for nested Action handling
|
54
|
-
|
55
|
-
### CheckNonNested
|
56
|
-
|
57
|
-
Controls whether the cop checks Action calls that are outside Action classes.
|
58
|
-
|
59
|
-
```yaml
|
60
|
-
Axn/UncheckedResult:
|
61
|
-
CheckNonNested: true # Check non-nested calls (default)
|
62
|
-
CheckNonNested: false # Skip non-nested calls
|
63
|
-
```
|
64
|
-
|
65
|
-
**When to use `CheckNonNested: false`:**
|
66
|
-
- You're only concerned about nested Action calls
|
67
|
-
- Top-level Action calls are handled by other tools or processes
|
68
|
-
- You want to focus on the most critical use case first
|
69
|
-
|
70
|
-
### Severity
|
71
|
-
|
72
|
-
Controls how violations are reported.
|
73
|
-
|
74
|
-
```yaml
|
75
|
-
Axn/UncheckedResult:
|
76
|
-
Severity: warning # Show as warnings (default)
|
77
|
-
Severity: error # Show as errors (fails CI)
|
78
|
-
```
|
79
|
-
|
80
|
-
## Common Configuration Patterns
|
32
|
+
You should see `Axn/UncheckedResult` in the output.
|
81
33
|
|
82
|
-
|
34
|
+
## Basic Usage
|
83
35
|
|
84
|
-
|
85
|
-
Axn/UncheckedResult:
|
86
|
-
Enabled: true
|
87
|
-
CheckNested: true
|
88
|
-
CheckNonNested: true
|
89
|
-
Severity: error
|
90
|
-
```
|
91
|
-
|
92
|
-
### Gradual Adoption (Recommended for Existing Projects)
|
93
|
-
|
94
|
-
```yaml
|
95
|
-
Axn/UncheckedResult:
|
96
|
-
Enabled: true
|
97
|
-
CheckNested: true # Start with nested calls
|
98
|
-
CheckNonNested: false # Add this later
|
99
|
-
Severity: warning # Start with warnings
|
100
|
-
```
|
101
|
-
|
102
|
-
### Nested-Only Focus
|
103
|
-
|
104
|
-
```yaml
|
105
|
-
Axn/UncheckedResult:
|
106
|
-
Enabled: true
|
107
|
-
CheckNested: true
|
108
|
-
CheckNonNested: false
|
109
|
-
Severity: warning
|
110
|
-
```
|
111
|
-
|
112
|
-
## What the Cop Checks
|
113
|
-
|
114
|
-
The cop analyzes your code to determine if you're:
|
115
|
-
|
116
|
-
1. **Inside an Action class** - Classes that `include Action`
|
117
|
-
2. **Inside the `call` method** - Only the main execution method
|
118
|
-
3. **Calling another Action** - Using `.call` on Action classes
|
119
|
-
4. **Properly handling the result** - One of the acceptable patterns
|
120
|
-
|
121
|
-
## What the Cop Ignores
|
122
|
-
|
123
|
-
The cop will NOT report offenses for:
|
124
|
-
|
125
|
-
- Action calls outside of Action classes (if `CheckNonNested: false`)
|
126
|
-
- Action calls in methods other than `call`
|
127
|
-
- Action calls that use `call!` (bang method)
|
128
|
-
- Action calls where the result is properly handled
|
129
|
-
|
130
|
-
## Proper Result Handling Patterns
|
131
|
-
|
132
|
-
### ✅ Using call!
|
36
|
+
### ✅ Good - Using call!
|
133
37
|
|
134
38
|
```ruby
|
135
39
|
class OuterAction
|
136
|
-
include
|
40
|
+
include Axn
|
137
41
|
def call
|
138
42
|
InnerAction.call!(param: "value") # Exceptions bubble up
|
139
43
|
end
|
140
44
|
end
|
141
45
|
```
|
142
46
|
|
143
|
-
### ✅ Checking result
|
47
|
+
### ✅ Good - Checking the result
|
144
48
|
|
145
49
|
```ruby
|
146
50
|
class OuterAction
|
147
|
-
include
|
51
|
+
include Axn
|
148
52
|
def call
|
149
53
|
result = InnerAction.call(param: "value")
|
150
54
|
return result unless result.ok?
|
@@ -153,200 +57,50 @@ class OuterAction
|
|
153
57
|
end
|
154
58
|
```
|
155
59
|
|
156
|
-
###
|
157
|
-
|
158
|
-
```ruby
|
159
|
-
class OuterAction
|
160
|
-
include Action
|
161
|
-
def call
|
162
|
-
result = InnerAction.call(param: "value")
|
163
|
-
if result.failed?
|
164
|
-
return result
|
165
|
-
end
|
166
|
-
# Process successful result...
|
167
|
-
end
|
168
|
-
end
|
169
|
-
```
|
170
|
-
|
171
|
-
### ✅ Accessing result.error
|
172
|
-
|
173
|
-
```ruby
|
174
|
-
class OuterAction
|
175
|
-
include Action
|
176
|
-
def call
|
177
|
-
result = InnerAction.call(param: "value")
|
178
|
-
if result.error
|
179
|
-
return result
|
180
|
-
end
|
181
|
-
# Process successful result...
|
182
|
-
end
|
183
|
-
end
|
184
|
-
```
|
185
|
-
|
186
|
-
### ✅ Returning the result
|
187
|
-
|
188
|
-
```ruby
|
189
|
-
class OuterAction
|
190
|
-
include Action
|
191
|
-
def call
|
192
|
-
result = InnerAction.call(param: "value")
|
193
|
-
result # Result is returned, so it's properly handled
|
194
|
-
end
|
195
|
-
end
|
196
|
-
```
|
197
|
-
|
198
|
-
### ✅ Using result in expose
|
199
|
-
|
200
|
-
```ruby
|
201
|
-
class OuterAction
|
202
|
-
include Action
|
203
|
-
exposes :nested_result
|
204
|
-
def call
|
205
|
-
result = InnerAction.call(param: "value")
|
206
|
-
expose nested_result: result # Result is used, so it's properly handled
|
207
|
-
end
|
208
|
-
end
|
209
|
-
```
|
210
|
-
|
211
|
-
### ✅ Passing result to another method
|
212
|
-
|
213
|
-
```ruby
|
214
|
-
class OuterAction
|
215
|
-
include Action
|
216
|
-
def call
|
217
|
-
result = InnerAction.call(param: "value")
|
218
|
-
process_result(result) # Result is used, so it's properly handled
|
219
|
-
end
|
220
|
-
end
|
221
|
-
```
|
222
|
-
|
223
|
-
## Common Anti-Patterns
|
224
|
-
|
225
|
-
### ❌ Ignoring the result
|
60
|
+
### ❌ Bad - Ignoring the result
|
226
61
|
|
227
62
|
```ruby
|
228
63
|
class OuterAction
|
229
|
-
include
|
64
|
+
include Axn
|
230
65
|
def call
|
231
|
-
InnerAction.call(param: "value") #
|
66
|
+
InnerAction.call(param: "value") # Will trigger offense
|
232
67
|
# This continues even if InnerAction fails
|
233
68
|
end
|
234
69
|
end
|
235
70
|
```
|
236
71
|
|
237
|
-
|
72
|
+
## Configuration
|
238
73
|
|
239
|
-
|
240
|
-
class OuterAction
|
241
|
-
include Action
|
242
|
-
def call
|
243
|
-
result = InnerAction.call(param: "value") # Assigned but never used
|
244
|
-
# Will trigger offense unless result is properly handled
|
245
|
-
end
|
246
|
-
end
|
247
|
-
```
|
74
|
+
The cop supports flexible configuration:
|
248
75
|
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
some_other_method(result.some_other_attribute) # Not checking success/failure
|
257
|
-
# Will trigger offense - need to check result.ok? first
|
258
|
-
end
|
259
|
-
end
|
76
|
+
```yaml
|
77
|
+
Axn/UncheckedResult:
|
78
|
+
Enabled: true
|
79
|
+
CheckNested: true # Check nested Axn calls (default: true)
|
80
|
+
CheckNonNested: true # Check non-nested Axn calls (default: true)
|
81
|
+
ActionsNamespace: "Actions" # Only check Actions::* classes (optional)
|
82
|
+
Severity: warning # or error
|
260
83
|
```
|
261
84
|
|
262
|
-
|
263
|
-
|
264
|
-
### For New Projects
|
265
|
-
|
266
|
-
1. Enable the cop with full enforcement from the start
|
267
|
-
2. Use `Severity: error` to catch violations early
|
268
|
-
3. Train your team on the proper patterns
|
269
|
-
|
270
|
-
### For Existing Projects
|
85
|
+
### ActionsNamespace Configuration
|
271
86
|
|
272
|
-
|
273
|
-
2. **Phase 2**: Fix all nested Action violations
|
274
|
-
3. **Phase 3**: Enable `CheckNonNested: true`
|
275
|
-
4. **Phase 4**: Fix all non-nested Action violations
|
276
|
-
5. **Phase 5**: Set `Severity: error`
|
87
|
+
When using the Actions namespace, you can configure the cop to only check calls on `Actions::*` classes:
|
277
88
|
|
278
|
-
|
89
|
+
```yaml
|
90
|
+
Axn/UncheckedResult:
|
91
|
+
ActionsNamespace: "Actions"
|
92
|
+
```
|
279
93
|
|
280
|
-
|
94
|
+
This eliminates false positives from other service objects while still catching unchecked Axn action calls:
|
281
95
|
|
282
96
|
```ruby
|
283
97
|
class OuterAction
|
284
|
-
include
|
98
|
+
include Axn
|
285
99
|
def call
|
286
|
-
#
|
287
|
-
InnerAction.call(param: "value") #
|
288
|
-
# rubocop:enable Axn/UncheckedResult
|
100
|
+
SomeService.call(param: "value") # Won't trigger cop
|
101
|
+
Actions::InnerAction.call(param: "value") # Will trigger cop
|
289
102
|
end
|
290
103
|
end
|
291
104
|
```
|
292
105
|
|
293
|
-
|
294
|
-
|
295
|
-
### Cop Not Loading
|
296
|
-
|
297
|
-
If you see "uninitialized constant" errors:
|
298
|
-
|
299
|
-
1. Ensure the gem is properly installed: `bundle list | grep axn`
|
300
|
-
2. Check your `.rubocop.yml` syntax
|
301
|
-
3. Verify the require path: `require: - axn/rubocop`
|
302
|
-
|
303
|
-
### False Positives
|
304
|
-
|
305
|
-
If the cop reports violations for properly handled results:
|
306
|
-
|
307
|
-
1. Check that you're using the exact patterns shown above
|
308
|
-
2. Ensure the result variable name matches exactly
|
309
|
-
3. Verify the result is being used in an acceptable way
|
310
|
-
|
311
|
-
### Performance Issues
|
312
|
-
|
313
|
-
The cop analyzes AST nodes, so it's generally fast. If you experience slowdowns:
|
314
|
-
|
315
|
-
1. Ensure you're not running RuboCop on very large files
|
316
|
-
2. Consider using RuboCop's `--parallel` option
|
317
|
-
3. Use `.rubocop_todo.yml` for gradual adoption
|
318
|
-
|
319
|
-
## Best Practices
|
320
|
-
|
321
|
-
1. **Start Small**: Begin with warnings and nested calls only
|
322
|
-
2. **Be Consistent**: Choose one pattern and stick with it
|
323
|
-
3. **Train Your Team**: Make sure everyone understands the rules
|
324
|
-
4. **Review Regularly**: Use the cop in your CI/CD pipeline
|
325
|
-
5. **Document Exceptions**: Use disable comments sparingly and document why
|
326
|
-
|
327
|
-
## Integration with CI/CD
|
328
|
-
|
329
|
-
Add RuboCop to your CI pipeline to catch violations early:
|
330
|
-
|
331
|
-
```yaml
|
332
|
-
# .github/workflows/rubocop.yml
|
333
|
-
name: RuboCop
|
334
|
-
on: [push, pull_request]
|
335
|
-
jobs:
|
336
|
-
rubocop:
|
337
|
-
runs-on: ubuntu-latest
|
338
|
-
steps:
|
339
|
-
- uses: actions/checkout@v3
|
340
|
-
- uses: ruby/setup-ruby@v1
|
341
|
-
with:
|
342
|
-
ruby-version: 3.2
|
343
|
-
- run: bundle install
|
344
|
-
- run: bundle exec rubocop
|
345
|
-
```
|
346
|
-
|
347
|
-
## Related Resources
|
348
|
-
|
349
|
-
- [Action Result Reference](/reference/action-result)
|
350
|
-
- [Configuration Guide](/reference/configuration)
|
351
|
-
- [Testing Recipes](/recipes/testing)
|
352
|
-
- [Best Practices Guide](/advanced/conventions)
|
106
|
+
For detailed configuration options, usage patterns, and troubleshooting, see the [technical documentation](https://github.com/teamshares/axn/blob/main/lib/rubocop/cop/axn/README.md).
|
data/docs/recipes/testing.md
CHANGED
@@ -8,23 +8,23 @@
|
|
8
8
|
|
9
9
|
Say you're writing unit specs for PrimaryAction that calls Subaction, and you want to mock out the Subaction call.
|
10
10
|
|
11
|
-
To generate a successful
|
11
|
+
To generate a successful Axn::Result:
|
12
12
|
|
13
|
-
* Base case: `
|
14
|
-
* [Optional] Custom message: `
|
15
|
-
* [Optional] Custom exposures: `
|
13
|
+
* Base case: `Axn::Result.ok`
|
14
|
+
* [Optional] Custom message: `Axn::Result.ok("It went awesome")`
|
15
|
+
* [Optional] Custom exposures: `Axn::Result.ok("It went awesome", some_var: 123)`
|
16
16
|
|
17
|
-
To generate a failed
|
17
|
+
To generate a failed Axn::Result:
|
18
18
|
|
19
|
-
* Base case: `
|
20
|
-
* [Optional] Custom message: `
|
21
|
-
* [Optional] Custom exposures: `
|
22
|
-
* [Optional] Custom exception: `
|
19
|
+
* Base case: `Axn::Result.error`
|
20
|
+
* [Optional] Custom message: `Axn::Result.error("It went poorly")`
|
21
|
+
* [Optional] Custom exposures: `Axn::Result.error("It went poorly", some_var: 123)`
|
22
|
+
* [Optional] Custom exception: `Axn::Result.error(some_var: 123) { raise FooBarException.new("bad thing") }`
|
23
23
|
|
24
24
|
Either way, using those to mock an actual call would look something like this in your rspec:
|
25
25
|
|
26
26
|
```ruby
|
27
|
-
let(:subaction_response) {
|
27
|
+
let(:subaction_response) { Axn::Result.ok("custom message", foo: 1) }
|
28
28
|
|
29
29
|
before do
|
30
30
|
expect(Subaction).to receive(:call).and_return(subaction_response)
|
@@ -38,7 +38,7 @@ The semantics of call-bang are a little different -- if Subaction is called via
|
|
38
38
|
### Success
|
39
39
|
|
40
40
|
```ruby
|
41
|
-
let(:subaction_response) {
|
41
|
+
let(:subaction_response) { Axn::Result.ok("custom message", foo: 1) }
|
42
42
|
|
43
43
|
before do
|
44
44
|
expect(Subaction).to receive(:call!).and_return(subaction_response)
|
@@ -57,18 +57,18 @@ before do
|
|
57
57
|
end
|
58
58
|
```
|
59
59
|
|
60
|
-
NOTE: to mock subaction failing via explicit `fail!` call, you'd use an `
|
60
|
+
NOTE: to mock subaction failing via explicit `fail!` call, you'd use an `Axn::Failure` exception class.
|
61
61
|
|
62
62
|
## Mocking Axn arguments
|
63
63
|
|
64
|
-
Be aware that in order to improve testing ergonomics, the `type` validation will return `true` for _any_ `RSpec::Mocks::` subclass _as long as `
|
64
|
+
Be aware that in order to improve testing ergonomics, the `type` validation will return `true` for _any_ `RSpec::Mocks::` subclass _as long as `Axn.config.env.test?` is `true`_.
|
65
65
|
|
66
66
|
This makes it much easier to test Axns, as you can pass in mocks without immediately failing the inbound validation.
|
67
67
|
|
68
68
|
```ruby
|
69
69
|
subject(:result) { action.call!(sym:) }
|
70
70
|
|
71
|
-
let(:action) {
|
71
|
+
let(:action) { build_axn { expects :sym, type: Symbol } }
|
72
72
|
|
73
73
|
context "with a symbol" do
|
74
74
|
let(:sym) { :hello }
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# Validating _user_ input
|
2
2
|
|
3
3
|
::: danger ALPHA
|
4
|
-
This has not yet been fully fleshed out. For now, the general idea is that user-facing validation is a _separate layer_ from the declarative expectations about what inputs your
|
4
|
+
This has not yet been fully fleshed out. For now, the general idea is that user-facing validation is a _separate layer_ from the declarative expectations about what inputs your Axn takes (e.g. `expects :params` and pass to a form object, rather than accepting field-level params directly).
|
5
5
|
:::
|
6
6
|
|
7
7
|
|
@@ -0,0 +1,160 @@
|
|
1
|
+
# Async Execution
|
2
|
+
|
3
|
+
Axn provides built-in support for asynchronous execution through background job processing libraries. This allows you to execute actions in the background without blocking the main thread.
|
4
|
+
|
5
|
+
## Overview
|
6
|
+
|
7
|
+
Async execution in Axn is designed to be simple and consistent across different background job libraries. You can configure async behavior globally or per-action, and all async adapters support the same interface.
|
8
|
+
|
9
|
+
## Basic Usage
|
10
|
+
|
11
|
+
### Configuring Async Adapters
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
class EmailAction
|
15
|
+
include Axn
|
16
|
+
|
17
|
+
# Configure async adapter
|
18
|
+
async :sidekiq
|
19
|
+
|
20
|
+
expects :user, :message
|
21
|
+
|
22
|
+
def call
|
23
|
+
# Send email logic
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Execute immediately (synchronous)
|
28
|
+
result = EmailAction.call(user: user, message: "Welcome!")
|
29
|
+
|
30
|
+
# Execute asynchronously (background)
|
31
|
+
EmailAction.call_async(user: user, message: "Welcome!")
|
32
|
+
```
|
33
|
+
|
34
|
+
### Available Async Adapters
|
35
|
+
|
36
|
+
#### Sidekiq
|
37
|
+
|
38
|
+
The Sidekiq adapter provides integration with the Sidekiq background job processing library.
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
# In your action class
|
42
|
+
async :sidekiq do
|
43
|
+
sidekiq_options queue: "high_priority", retry: 5, priority: 10
|
44
|
+
end
|
45
|
+
|
46
|
+
# Or with keyword arguments (shorthand)
|
47
|
+
async :sidekiq, queue: "high_priority", retry: 5
|
48
|
+
```
|
49
|
+
|
50
|
+
**Configuration options:**
|
51
|
+
- `queue`: The Sidekiq queue name (default: "default")
|
52
|
+
- `retry`: Number of retry attempts (default: 25)
|
53
|
+
- `priority`: Job priority (default: 0)
|
54
|
+
- Any other Sidekiq options supported by `sidekiq_options`
|
55
|
+
|
56
|
+
#### ActiveJob
|
57
|
+
|
58
|
+
The ActiveJob adapter provides integration with Rails' ActiveJob framework.
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
# In your action class
|
62
|
+
async :active_job do
|
63
|
+
queue_as "high_priority"
|
64
|
+
self.priority = 10
|
65
|
+
self.wait = 5.minutes
|
66
|
+
end
|
67
|
+
```
|
68
|
+
|
69
|
+
**Configuration options:**
|
70
|
+
- `queue_as`: The ActiveJob queue name
|
71
|
+
- `priority`: Job priority
|
72
|
+
- `wait`: Delay before execution
|
73
|
+
- Any other ActiveJob options
|
74
|
+
|
75
|
+
#### Disabled
|
76
|
+
|
77
|
+
Disables async execution entirely. The action will raise a `NotImplementedError` when `call_async` is called.
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
# In your action class
|
81
|
+
async false
|
82
|
+
```
|
83
|
+
|
84
|
+
## Delayed Execution
|
85
|
+
|
86
|
+
All async adapters support delayed execution using the `_async` parameter in `call_async`. This allows you to schedule actions to run at specific future times without changing the interface.
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
class EmailAction
|
90
|
+
include Axn
|
91
|
+
async :sidekiq
|
92
|
+
|
93
|
+
expects :user, :message
|
94
|
+
|
95
|
+
def call
|
96
|
+
# Send email logic
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Immediate execution
|
101
|
+
EmailAction.call_async(user: user, message: "Welcome!")
|
102
|
+
|
103
|
+
# Delayed execution - wait 1 hour
|
104
|
+
EmailAction.call_async(user: user, message: "Follow up", _async: { wait: 1.hour })
|
105
|
+
|
106
|
+
# Scheduled execution - run at specific time
|
107
|
+
EmailAction.call_async(user: user, message: "Reminder", _async: { wait_until: 1.week.from_now })
|
108
|
+
```
|
109
|
+
|
110
|
+
### Supported Scheduling Options
|
111
|
+
|
112
|
+
- `wait`: Execute after a specific time interval (e.g., `1.hour`, `30.minutes`)
|
113
|
+
- `wait_until`: Execute at a specific future time (e.g., `1.hour.from_now`, `Time.parse("2024-01-01 12:00:00")`)
|
114
|
+
|
115
|
+
### Adapter-Specific Behavior
|
116
|
+
|
117
|
+
- **Sidekiq**: Uses `perform_in` for `wait` and `perform_at` for `wait_until`
|
118
|
+
- **ActiveJob**: Uses `set(wait:)` for `wait` and `set(wait_until:)` for `wait_until`
|
119
|
+
- **Disabled**: Ignores scheduling options and raises `NotImplementedError`
|
120
|
+
|
121
|
+
### Parameter Name Safety
|
122
|
+
|
123
|
+
The `_async` parameter is reserved for scheduling options.
|
124
|
+
|
125
|
+
## Global Configuration
|
126
|
+
|
127
|
+
You can set default async configuration that will be applied to all actions that don't explicitly configure their own async behavior:
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
Axn.configure do |c|
|
131
|
+
# Set a default async configuration
|
132
|
+
c.set_default_async(:sidekiq, queue: "default") do
|
133
|
+
sidekiq_options retry: 3
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Now all actions will use Sidekiq by default
|
138
|
+
class MyAction
|
139
|
+
include Axn
|
140
|
+
# No async configuration needed - uses default
|
141
|
+
end
|
142
|
+
```
|
143
|
+
|
144
|
+
## Error Handling
|
145
|
+
|
146
|
+
Async actions trigger via `call!` internally, so they raise on failure, which means the background job system can seamlessly handle retries.
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
class FailingAction
|
150
|
+
include Axn
|
151
|
+
async :sidekiq, retry: 3
|
152
|
+
|
153
|
+
def call
|
154
|
+
fail! "Something went wrong"
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# The job will be retried up to 3 times before giving up
|
159
|
+
FailingAction.call_async(data: "test")
|
160
|
+
```
|