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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/rules/axn-framework-patterns.mdc +43 -0
  3. data/.cursor/rules/general-coding-standards.mdc +27 -0
  4. data/.cursor/rules/spec/testing-patterns.mdc +40 -0
  5. data/CHANGELOG.md +47 -0
  6. data/Rakefile +12 -2
  7. data/docs/.vitepress/config.mjs +8 -3
  8. data/docs/advanced/conventions.md +2 -2
  9. data/docs/advanced/mountable.md +562 -0
  10. data/docs/advanced/profiling.md +355 -0
  11. data/docs/advanced/rough.md +1 -1
  12. data/docs/index.md +5 -3
  13. data/docs/intro/about.md +1 -1
  14. data/docs/intro/overview.md +5 -5
  15. data/docs/recipes/memoization.md +2 -2
  16. data/docs/recipes/rubocop-integration.md +38 -284
  17. data/docs/recipes/testing.md +14 -14
  18. data/docs/recipes/validating-user-input.md +1 -1
  19. data/docs/reference/async.md +160 -0
  20. data/docs/reference/axn-result.md +107 -0
  21. data/docs/reference/class.md +123 -25
  22. data/docs/reference/configuration.md +191 -10
  23. data/docs/reference/instance.md +14 -29
  24. data/docs/strategies/index.md +21 -21
  25. data/docs/strategies/transaction.md +1 -1
  26. data/docs/usage/setup.md +14 -0
  27. data/docs/usage/steps.md +7 -7
  28. data/docs/usage/using.md +23 -12
  29. data/docs/usage/writing.md +92 -11
  30. data/lib/axn/async/adapters/active_job.rb +65 -0
  31. data/lib/axn/async/adapters/disabled.rb +26 -0
  32. data/lib/axn/async/adapters/sidekiq.rb +74 -0
  33. data/lib/axn/async/adapters.rb +26 -0
  34. data/lib/axn/async.rb +61 -0
  35. data/lib/{action → axn}/configuration.rb +21 -3
  36. data/lib/{action → axn}/context.rb +21 -4
  37. data/lib/{action → axn}/core/automatic_logging.rb +6 -6
  38. data/lib/axn/core/context/facade.rb +69 -0
  39. data/lib/{action → axn}/core/context/facade_inspector.rb +31 -4
  40. data/lib/{action → axn}/core/context/internal.rb +5 -5
  41. data/lib/{action → axn}/core/contract.rb +43 -46
  42. data/lib/{action → axn}/core/contract_for_subfields.rb +30 -35
  43. data/lib/{action → axn}/core/contract_validation.rb +16 -6
  44. data/lib/axn/core/contract_validation_for_subfields.rb +158 -0
  45. data/lib/axn/core/field_resolvers/extract.rb +32 -0
  46. data/lib/axn/core/field_resolvers/model.rb +63 -0
  47. data/lib/axn/core/field_resolvers.rb +24 -0
  48. data/lib/{action → axn}/core/flow/callbacks.rb +7 -7
  49. data/lib/{action → axn}/core/flow/exception_execution.rb +4 -13
  50. data/lib/{action → axn}/core/flow/handlers/base_descriptor.rb +3 -2
  51. data/lib/{action → axn}/core/flow/handlers/descriptors/callback_descriptor.rb +2 -2
  52. data/lib/{action → axn}/core/flow/handlers/descriptors/message_descriptor.rb +6 -6
  53. data/lib/{action → axn}/core/flow/handlers/invoker.rb +6 -6
  54. data/lib/{action → axn}/core/flow/handlers/matcher.rb +5 -5
  55. data/lib/{action → axn}/core/flow/handlers/registry.rb +3 -1
  56. data/lib/{action → axn}/core/flow/handlers/resolvers/base_resolver.rb +1 -1
  57. data/lib/{action → axn}/core/flow/handlers/resolvers/callback_resolver.rb +2 -2
  58. data/lib/{action → axn}/core/flow/handlers/resolvers/message_resolver.rb +12 -3
  59. data/lib/axn/core/flow/handlers.rb +20 -0
  60. data/lib/{action → axn}/core/flow/messages.rb +7 -7
  61. data/lib/{action → axn}/core/flow.rb +4 -4
  62. data/lib/{action → axn}/core/hooks.rb +16 -5
  63. data/lib/{action → axn}/core/logging.rb +3 -3
  64. data/lib/{action → axn}/core/nesting_tracking.rb +1 -1
  65. data/lib/axn/core/profiling.rb +124 -0
  66. data/lib/{action → axn}/core/timing.rb +1 -1
  67. data/lib/axn/core/tracing.rb +17 -0
  68. data/lib/axn/core/use_strategy.rb +29 -0
  69. data/lib/{action → axn}/core/validation/fields.rb +26 -2
  70. data/lib/{action → axn}/core/validation/subfields.rb +14 -12
  71. data/lib/axn/core/validation/validators/model_validator.rb +36 -0
  72. data/lib/axn/core/validation/validators/type_validator.rb +80 -0
  73. data/lib/{action → axn}/core/validation/validators/validate_validator.rb +12 -2
  74. data/lib/axn/core.rb +123 -0
  75. data/lib/{action → axn}/exceptions.rb +12 -2
  76. data/lib/axn/factory.rb +102 -34
  77. data/lib/axn/internal/logging.rb +26 -0
  78. data/lib/axn/internal/registry.rb +87 -0
  79. data/lib/axn/mountable/descriptor.rb +76 -0
  80. data/lib/axn/mountable/helpers/class_builder.rb +162 -0
  81. data/lib/axn/mountable/helpers/mounter.rb +33 -0
  82. data/lib/axn/mountable/helpers/namespace_manager.rb +66 -0
  83. data/lib/axn/mountable/helpers/validator.rb +112 -0
  84. data/lib/axn/mountable/inherit_profiles.rb +72 -0
  85. data/lib/axn/mountable/mounting_strategies/_base.rb +83 -0
  86. data/lib/axn/mountable/mounting_strategies/axn.rb +48 -0
  87. data/lib/axn/mountable/mounting_strategies/enqueue_all.rb +55 -0
  88. data/lib/axn/mountable/mounting_strategies/method.rb +95 -0
  89. data/lib/axn/mountable/mounting_strategies/step.rb +69 -0
  90. data/lib/axn/mountable/mounting_strategies.rb +32 -0
  91. data/lib/axn/mountable.rb +85 -0
  92. data/lib/axn/rails/engine.rb +51 -0
  93. data/lib/axn/rails/generators/axn_generator.rb +68 -0
  94. data/lib/axn/rails/generators/templates/action.rb.erb +17 -0
  95. data/lib/axn/rails/generators/templates/action_spec.rb.erb +25 -0
  96. data/lib/{action → axn}/result.rb +30 -11
  97. data/lib/{action → axn}/strategies/transaction.rb +1 -1
  98. data/lib/axn/strategies.rb +20 -0
  99. data/lib/axn/testing/spec_helpers.rb +6 -8
  100. data/lib/axn/util/memoization.rb +20 -0
  101. data/lib/axn/version.rb +1 -1
  102. data/lib/axn.rb +17 -16
  103. data/lib/rubocop/cop/axn/README.md +23 -23
  104. data/lib/rubocop/cop/axn/unchecked_result.rb +138 -17
  105. metadata +88 -64
  106. data/.rspec +0 -3
  107. data/.rubocop.yml +0 -76
  108. data/.tool-versions +0 -1
  109. data/docs/reference/action-result.md +0 -37
  110. data/lib/action/attachable/base.rb +0 -43
  111. data/lib/action/attachable/steps.rb +0 -63
  112. data/lib/action/attachable/subactions.rb +0 -70
  113. data/lib/action/attachable.rb +0 -17
  114. data/lib/action/core/context/facade.rb +0 -48
  115. data/lib/action/core/flow/handlers.rb +0 -20
  116. data/lib/action/core/tracing.rb +0 -17
  117. data/lib/action/core/use_strategy.rb +0 -30
  118. data/lib/action/core/validation/validators/model_validator.rb +0 -34
  119. data/lib/action/core/validation/validators/type_validator.rb +0 -30
  120. data/lib/action/core.rb +0 -108
  121. data/lib/action/enqueueable/via_sidekiq.rb +0 -76
  122. data/lib/action/enqueueable.rb +0 -13
  123. data/lib/action/strategies.rb +0 -48
  124. data/lib/axn/util.rb +0 -24
  125. data/package.json +0 -10
  126. data/yarn.lock +0 -1166
@@ -1,12 +1,16 @@
1
1
  # RuboCop Integration
2
2
 
3
- Axn provides custom RuboCop cops to help enforce best practices and maintain code quality in your Action-based codebase.
3
+ Axn provides a custom RuboCop cop to help enforce proper result handling when calling Actions.
4
4
 
5
- ## Overview
5
+ ## What It Does
6
6
 
7
- The `Axn/UncheckedResult` cop enforces proper result handling when calling Actions. It can detect when Action results are ignored and help ensure consistent error handling patterns.
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
- ## Installation
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
- CheckNested: true # Check nested Action calls
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
- ### Full Enforcement (Recommended for New Projects)
34
+ ## Basic Usage
83
35
 
84
- ```yaml
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 Action
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.ok?
47
+ ### ✅ Good - Checking the result
144
48
 
145
49
  ```ruby
146
50
  class OuterAction
147
- include Action
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
- ### Checking result.failed?
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 Action
64
+ include Axn
230
65
  def call
231
- InnerAction.call(param: "value") # Result ignored - will trigger offense
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
- ### ❌ Assigning but not using
72
+ ## Configuration
238
73
 
239
- ```ruby
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
- ### ❌ Using unrelated attributes
250
-
251
- ```ruby
252
- class OuterAction
253
- include Action
254
- def call
255
- result = InnerAction.call(param: "value")
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
- ## Migration Strategies
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
- 1. **Phase 1**: Enable with `CheckNested: true, CheckNonNested: false, Severity: warning`
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
- ### Using RuboCop Disable Comments
89
+ ```yaml
90
+ Axn/UncheckedResult:
91
+ ActionsNamespace: "Actions"
92
+ ```
279
93
 
280
- For intentional violations, you can disable the cop:
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 Action
98
+ include Axn
285
99
  def call
286
- # rubocop:disable Axn/UncheckedResult
287
- InnerAction.call(param: "value") # Intentionally ignored
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
- ## Troubleshooting
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).
@@ -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 Action::Result:
11
+ To generate a successful Axn::Result:
12
12
 
13
- * Base case: `Action::Result.ok`
14
- * [Optional] Custom message: `Action::Result.ok("It went awesome")`
15
- * [Optional] Custom exposures: `Action::Result.ok("It went awesome", some_var: 123)`
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 Action::Result:
17
+ To generate a failed Axn::Result:
18
18
 
19
- * Base case: `Action::Result.error`
20
- * [Optional] Custom message: `Action::Result.error("It went poorly")`
21
- * [Optional] Custom exposures: `Action::Result.error("It went poorly", some_var: 123)`
22
- * [Optional] Custom exception: `Action::Result.error(some_var: 123) { raise FooBarException.new("bad thing") }`
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) { Action::Result.ok("custom message", foo: 1) }
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) { Action::Result.ok("custom message", foo: 1) }
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 `Action::Failure` exception class.
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 `Action.config.env.test?` is `true`_.
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) { build_action { expects :sym, type: Symbol } }
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 Action takes (e.g. `expects :params` and pass to a form object, rather than accepting field-level params directly).
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
+ ```