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.
Files changed (148) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/commands/pr.md +36 -0
  3. data/.cursor/rules/axn-framework-patterns.mdc +43 -0
  4. data/.cursor/rules/general-coding-standards.mdc +27 -0
  5. data/.cursor/rules/spec/testing-patterns.mdc +40 -0
  6. data/CHANGELOG.md +57 -0
  7. data/Rakefile +114 -4
  8. data/docs/.vitepress/config.mjs +19 -10
  9. data/docs/advanced/conventions.md +3 -3
  10. data/docs/advanced/mountable.md +476 -0
  11. data/docs/advanced/profiling.md +351 -0
  12. data/docs/advanced/rough.md +27 -8
  13. data/docs/index.md +5 -3
  14. data/docs/intro/about.md +1 -1
  15. data/docs/intro/overview.md +6 -6
  16. data/docs/recipes/formatting-context-for-error-tracking.md +186 -0
  17. data/docs/recipes/memoization.md +103 -18
  18. data/docs/recipes/rubocop-integration.md +38 -284
  19. data/docs/recipes/testing.md +14 -14
  20. data/docs/recipes/validating-user-input.md +1 -1
  21. data/docs/reference/async.md +429 -0
  22. data/docs/reference/axn-result.md +107 -0
  23. data/docs/reference/class.md +225 -64
  24. data/docs/reference/configuration.md +366 -34
  25. data/docs/reference/form-object.md +252 -0
  26. data/docs/reference/instance.md +14 -29
  27. data/docs/strategies/client.md +212 -0
  28. data/docs/strategies/form.md +235 -0
  29. data/docs/strategies/index.md +21 -21
  30. data/docs/strategies/transaction.md +1 -1
  31. data/docs/usage/setup.md +16 -2
  32. data/docs/usage/steps.md +7 -7
  33. data/docs/usage/using.md +23 -12
  34. data/docs/usage/writing.md +191 -12
  35. data/lib/axn/async/adapters/active_job.rb +74 -0
  36. data/lib/axn/async/adapters/disabled.rb +41 -0
  37. data/lib/axn/async/adapters/sidekiq.rb +67 -0
  38. data/lib/axn/async/adapters.rb +26 -0
  39. data/lib/axn/async/batch_enqueue/config.rb +38 -0
  40. data/lib/axn/async/batch_enqueue.rb +99 -0
  41. data/lib/axn/async/enqueue_all_orchestrator.rb +363 -0
  42. data/lib/axn/async.rb +178 -0
  43. data/lib/axn/configuration.rb +113 -0
  44. data/lib/{action → axn}/context.rb +22 -4
  45. data/lib/axn/core/automatic_logging.rb +89 -0
  46. data/lib/axn/core/context/facade.rb +69 -0
  47. data/lib/{action → axn}/core/context/facade_inspector.rb +32 -5
  48. data/lib/{action → axn}/core/context/internal.rb +5 -5
  49. data/lib/{action → axn}/core/contract.rb +111 -73
  50. data/lib/{action → axn}/core/contract_for_subfields.rb +30 -35
  51. data/lib/{action → axn}/core/contract_validation.rb +27 -12
  52. data/lib/axn/core/contract_validation_for_subfields.rb +165 -0
  53. data/lib/axn/core/default_call.rb +63 -0
  54. data/lib/axn/core/field_resolvers/extract.rb +32 -0
  55. data/lib/axn/core/field_resolvers/model.rb +63 -0
  56. data/lib/axn/core/field_resolvers.rb +24 -0
  57. data/lib/{action → axn}/core/flow/callbacks.rb +7 -7
  58. data/lib/{action → axn}/core/flow/exception_execution.rb +9 -13
  59. data/lib/{action → axn}/core/flow/handlers/base_descriptor.rb +3 -2
  60. data/lib/{action → axn}/core/flow/handlers/descriptors/callback_descriptor.rb +2 -2
  61. data/lib/{action → axn}/core/flow/handlers/descriptors/message_descriptor.rb +23 -11
  62. data/lib/axn/core/flow/handlers/invoker.rb +47 -0
  63. data/lib/{action → axn}/core/flow/handlers/matcher.rb +9 -19
  64. data/lib/{action → axn}/core/flow/handlers/registry.rb +3 -1
  65. data/lib/{action → axn}/core/flow/handlers/resolvers/base_resolver.rb +1 -1
  66. data/lib/{action → axn}/core/flow/handlers/resolvers/callback_resolver.rb +2 -2
  67. data/lib/{action → axn}/core/flow/handlers/resolvers/message_resolver.rb +12 -3
  68. data/lib/axn/core/flow/handlers.rb +20 -0
  69. data/lib/{action → axn}/core/flow/messages.rb +8 -8
  70. data/lib/{action → axn}/core/flow.rb +4 -4
  71. data/lib/{action → axn}/core/hooks.rb +17 -5
  72. data/lib/axn/core/logging.rb +48 -0
  73. data/lib/axn/core/memoization.rb +53 -0
  74. data/lib/{action → axn}/core/nesting_tracking.rb +1 -1
  75. data/lib/{action → axn}/core/timing.rb +1 -1
  76. data/lib/axn/core/tracing.rb +90 -0
  77. data/lib/axn/core/use_strategy.rb +29 -0
  78. data/lib/{action → axn}/core/validation/fields.rb +26 -2
  79. data/lib/{action → axn}/core/validation/subfields.rb +14 -12
  80. data/lib/axn/core/validation/validators/model_validator.rb +36 -0
  81. data/lib/axn/core/validation/validators/type_validator.rb +80 -0
  82. data/lib/{action → axn}/core/validation/validators/validate_validator.rb +12 -2
  83. data/lib/{action → axn}/core.rb +55 -55
  84. data/lib/{action → axn}/exceptions.rb +12 -2
  85. data/lib/axn/extras/strategies/client.rb +150 -0
  86. data/lib/axn/extras/strategies/vernier.rb +121 -0
  87. data/lib/axn/extras.rb +4 -0
  88. data/lib/axn/factory.rb +122 -34
  89. data/lib/axn/form_object.rb +90 -0
  90. data/lib/axn/internal/logging.rb +30 -0
  91. data/lib/axn/internal/registry.rb +87 -0
  92. data/lib/axn/mountable/descriptor.rb +76 -0
  93. data/lib/axn/mountable/helpers/class_builder.rb +193 -0
  94. data/lib/axn/mountable/helpers/mounter.rb +33 -0
  95. data/lib/axn/mountable/helpers/namespace_manager.rb +38 -0
  96. data/lib/axn/mountable/helpers/validator.rb +112 -0
  97. data/lib/axn/mountable/inherit_profiles.rb +72 -0
  98. data/lib/axn/mountable/mounting_strategies/_base.rb +87 -0
  99. data/lib/axn/mountable/mounting_strategies/axn.rb +48 -0
  100. data/lib/axn/mountable/mounting_strategies/method.rb +95 -0
  101. data/lib/axn/mountable/mounting_strategies/step.rb +69 -0
  102. data/lib/axn/mountable/mounting_strategies.rb +32 -0
  103. data/lib/axn/mountable.rb +119 -0
  104. data/lib/axn/rails/engine.rb +51 -0
  105. data/lib/axn/rails/generators/axn_generator.rb +86 -0
  106. data/lib/axn/rails/generators/templates/action.rb.erb +17 -0
  107. data/lib/axn/rails/generators/templates/action_spec.rb.erb +25 -0
  108. data/lib/{action → axn}/result.rb +32 -13
  109. data/lib/axn/strategies/form.rb +98 -0
  110. data/lib/axn/strategies/transaction.rb +26 -0
  111. data/lib/axn/strategies.rb +20 -0
  112. data/lib/axn/testing/spec_helpers.rb +6 -8
  113. data/lib/axn/util/callable.rb +120 -0
  114. data/lib/axn/util/contract_error_handling.rb +32 -0
  115. data/lib/axn/util/execution_context.rb +34 -0
  116. data/lib/axn/util/global_id_serialization.rb +52 -0
  117. data/lib/axn/util/logging.rb +87 -0
  118. data/lib/axn/util/memoization.rb +20 -0
  119. data/lib/axn/version.rb +1 -1
  120. data/lib/axn.rb +26 -16
  121. data/lib/rubocop/cop/axn/README.md +23 -23
  122. data/lib/rubocop/cop/axn/unchecked_result.rb +138 -17
  123. metadata +106 -64
  124. data/.rspec +0 -3
  125. data/.rubocop.yml +0 -76
  126. data/.tool-versions +0 -1
  127. data/docs/reference/action-result.md +0 -37
  128. data/lib/action/attachable/base.rb +0 -43
  129. data/lib/action/attachable/steps.rb +0 -63
  130. data/lib/action/attachable/subactions.rb +0 -70
  131. data/lib/action/attachable.rb +0 -17
  132. data/lib/action/configuration.rb +0 -55
  133. data/lib/action/core/automatic_logging.rb +0 -93
  134. data/lib/action/core/context/facade.rb +0 -48
  135. data/lib/action/core/flow/handlers/invoker.rb +0 -73
  136. data/lib/action/core/flow/handlers.rb +0 -20
  137. data/lib/action/core/logging.rb +0 -37
  138. data/lib/action/core/tracing.rb +0 -17
  139. data/lib/action/core/use_strategy.rb +0 -30
  140. data/lib/action/core/validation/validators/model_validator.rb +0 -34
  141. data/lib/action/core/validation/validators/type_validator.rb +0 -30
  142. data/lib/action/enqueueable/via_sidekiq.rb +0 -76
  143. data/lib/action/enqueueable.rb +0 -13
  144. data/lib/action/strategies/transaction.rb +0 -19
  145. data/lib/action/strategies.rb +0 -48
  146. data/lib/axn/util.rb +0 -24
  147. data/package.json +0 -10
  148. data/yarn.lock +0 -1166
@@ -0,0 +1,429 @@
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
+ ```
161
+
162
+ ## Batch Enqueueing with `enqueues_each`
163
+
164
+ The `enqueues_each` method provides a declarative way to set up batch enqueueing. It automatically iterates over collections and enqueues each item as a separate background job.
165
+
166
+ ### Basic Usage
167
+
168
+ ```ruby
169
+ class SyncForCompany
170
+ include Axn
171
+ async :sidekiq
172
+
173
+ expects :company, model: Company
174
+
175
+ def call
176
+ puts "Syncing data for company: #{company.name}"
177
+ # Sync individual company data
178
+ end
179
+
180
+ # No enqueues_each needed! Source is auto-inferred from model: Company
181
+ end
182
+
183
+ # Usage
184
+ SyncForCompany.enqueue_all # Automatically iterates Company.all and enqueues each company
185
+ ```
186
+
187
+ **How it works:**
188
+ 1. `enqueue_all` validates configuration upfront (async configured, static args present)
189
+ 2. If all arguments are serializable, enqueues an `EnqueueAllOrchestrator` job in the background
190
+ 3. If any argument is unserializable (e.g., an AR relation override), executes in the foreground instead
191
+ 4. During execution, iterates over the source collection and enqueues individual jobs
192
+ 5. Model-based iterations (using `find_each`) are processed first for memory efficiency
193
+
194
+ ### Auto-Inference from `model:` Declarations
195
+
196
+ If a field has a `model:` declaration and the model class responds to `find_each`, you **don't need to explicitly declare `enqueues_each`**. The source collection is automatically inferred:
197
+
198
+ ```ruby
199
+ class SyncForCompany
200
+ include Axn
201
+ async :sidekiq
202
+
203
+ expects :company, model: Company # Auto-inferred: Company.all
204
+
205
+ def call
206
+ # ... sync logic
207
+ end
208
+
209
+ # No enqueues_each needed - automatically iterates Company.all
210
+ end
211
+
212
+ SyncForCompany.enqueue_all # Works without explicit enqueues_each!
213
+ ```
214
+
215
+ ### Explicit Configuration with `enqueues_each`
216
+
217
+ Use `enqueues_each` when you need to:
218
+ - Override the default source (e.g., `Company.active` instead of `Company.all`)
219
+ - Add filtering logic
220
+ - Extract specific attributes
221
+ - Iterate over fields without `model:` declarations
222
+
223
+ ```ruby
224
+ class SyncForCompany
225
+ include Axn
226
+ async :sidekiq
227
+
228
+ expects :company, model: Company
229
+
230
+ def call
231
+ # ... sync logic
232
+ end
233
+
234
+ # Override default source
235
+ enqueues_each :company, from: -> { Company.active }
236
+
237
+ # With extraction (passes company_id instead of company object)
238
+ enqueues_each :company_id, from: -> { Company.active }, via: :id
239
+
240
+ # With filter block
241
+ enqueues_each :company do |company|
242
+ company.active? && !company.in_exit?
243
+ end
244
+
245
+ # Method name as source
246
+ enqueues_each :company, from: :active_companies
247
+ end
248
+ ```
249
+
250
+ ### Overriding on `enqueue_all` Call
251
+
252
+ You can override iteration sources or make fields static when calling `enqueue_all`:
253
+
254
+ ```ruby
255
+ class SyncForCompany
256
+ include Axn
257
+ async :sidekiq
258
+
259
+ expects :company, model: Company
260
+ expects :user, model: User
261
+
262
+ def call
263
+ # ... sync logic
264
+ end
265
+
266
+ # Default: iterates Company.all
267
+ enqueues_each :company
268
+ end
269
+
270
+ # Override with a subset (enumerable kwarg replaces source)
271
+ SyncForCompany.enqueue_all(company: Company.active.limit(10))
272
+
273
+ # Override with a single value (scalar kwarg makes it static, no iteration)
274
+ SyncForCompany.enqueue_all(company: Company.find(123))
275
+
276
+ # Mix static and iterated fields
277
+ SyncForCompany.enqueue_all(
278
+ company: Company.active, # Iterates over active companies
279
+ user: User.find(1) # Static: same user for all jobs
280
+ )
281
+ ```
282
+
283
+ ### Dynamic Iteration via Kwargs
284
+
285
+ You can iterate over fields without any `enqueues_each` declaration by passing enumerables directly:
286
+
287
+ ```ruby
288
+ class ProcessFormats
289
+ include Axn
290
+ async :sidekiq
291
+
292
+ expects :format
293
+ expects :mode
294
+
295
+ def call
296
+ # ... process logic
297
+ end
298
+ end
299
+
300
+ # Pass enumerables to create cross-product iteration
301
+ ProcessFormats.enqueue_all(
302
+ format: [:csv, :json, :xml], # Iterates: 3 jobs
303
+ mode: :full # Static: same mode for all
304
+ )
305
+
306
+ # Multiple enumerables create cross-product
307
+ ProcessFormats.enqueue_all(
308
+ format: [:csv, :json], # 2 formats
309
+ mode: [:full, :incremental] # 2 modes
310
+ )
311
+ # Result: 2 × 2 = 4 jobs total
312
+ ```
313
+
314
+ **Note:** Arrays and Sets are treated as static values (not iterated) when the field expects an enumerable type:
315
+
316
+ ```ruby
317
+ expects :tags, type: Array
318
+
319
+ # This passes the entire array as a static value
320
+ ProcessTags.enqueue_all(tags: ["ruby", "rails", "testing"])
321
+ ```
322
+
323
+ ### Multi-Field Cross-Product Iteration
324
+
325
+ Multiple `enqueues_each` declarations create a cross-product of all combinations:
326
+
327
+ ```ruby
328
+ class SyncForUserAndCompany
329
+ include Axn
330
+ async :sidekiq
331
+
332
+ expects :user, model: User
333
+ expects :company, model: Company
334
+
335
+ def call
336
+ # ... sync logic for user + company combination
337
+ end
338
+
339
+ enqueues_each :user, from: -> { User.active }
340
+ enqueues_each :company, from: -> { Company.active }
341
+ end
342
+
343
+ # Creates user_count × company_count jobs
344
+ # Each combination of (user, company) gets its own job
345
+ SyncForUserAndCompany.enqueue_all
346
+ ```
347
+
348
+ ### Static Fields
349
+
350
+ Fields declared with `expects` but not covered by `enqueues_each` (or auto-inference) become static fields that must be passed to `enqueue_all`:
351
+
352
+ ```ruby
353
+ class SyncWithMode
354
+ include Axn
355
+ async :sidekiq
356
+
357
+ expects :company, model: Company # Auto-inferred, will iterate
358
+ expects :sync_mode # Static, must be provided
359
+
360
+ def call
361
+ # Uses both company (iterated) and sync_mode (static)
362
+ end
363
+ end
364
+
365
+ # sync_mode must be provided - it's passed to every enqueued job
366
+ SyncWithMode.enqueue_all(sync_mode: :full)
367
+ ```
368
+
369
+ ### Memory Efficiency
370
+
371
+ For optimal memory usage, model-based configs (using `find_each`) are automatically processed first in nested iterations. This ensures ActiveRecord-style batch processing happens before loading potentially large enumerables into memory.
372
+
373
+ ```ruby
374
+ # Model-based iteration uses find_each (memory efficient)
375
+ expects :company, model: Company # Processed first
376
+
377
+ # Array-based iteration uses each (loads all into memory)
378
+ enqueues_each :format, from: -> { [:csv, :json, :xml] } # Processed second
379
+ ```
380
+
381
+ ### Iteration Method Selection
382
+
383
+ - **`find_each`**: Used when the source responds to `find_each` (ActiveRecord collections) - processes in batches for memory efficiency
384
+ - **`each`**: Used for plain arrays and other enumerables - loads all items into memory
385
+
386
+ ### Background vs Foreground Execution
387
+
388
+ By default, `enqueue_all` enqueues an `EnqueueAllOrchestrator` job to perform the iteration and individual job enqueueing in the background. This makes it safe to call directly from clock processes (e.g., Heroku scheduler) without risking memory bloat from loading large collections.
389
+
390
+ However, if you pass an **enumerable override** (like an ActiveRecord relation or array), `enqueue_all` automatically falls back to foreground execution—iterating and enqueueing immediately in the current process. This is because the iteration source (a lambda wrapping the enumerable) cannot be serialized for background execution.
391
+
392
+ ```ruby
393
+ class SyncForCompany
394
+ include Axn
395
+ async :sidekiq
396
+
397
+ expects :company, model: Company
398
+
399
+ def call
400
+ # ... sync logic
401
+ end
402
+
403
+ enqueues_each :company, from: -> { Company.active }
404
+ end
405
+
406
+ # Default: enqueues orchestrator job in background (safe for clock processes)
407
+ SyncForCompany.enqueue_all
408
+
409
+ # Override with a relation: executes in foreground (relation can't be serialized)
410
+ SyncForCompany.enqueue_all(company: Company.where(plan: "enterprise"))
411
+
412
+ # Override with a single value: runs in background (GlobalID-serializable scalar)
413
+ SyncForCompany.enqueue_all(company: Company.find(123))
414
+ ```
415
+
416
+ **Why this matters:**
417
+ - Configure your default iteration source via `enqueues_each` for scheduled/recurring jobs
418
+ - By default, `enqueue_all` runs safely in the background without loading your entire dataset into memory
419
+ - For one-off manual calls with a filtered subset, pass an enumerable override—foreground execution handles it automatically
420
+ - Scalar overrides (single objects) are serialized via GlobalID and still run in the background
421
+
422
+ This design lets you use the same action class for both scheduled batch processing and ad-hoc targeted runs.
423
+
424
+ ### Edge Cases and Limitations
425
+
426
+ 1. **Fields expecting enumerable types**: If a field expects `Array` or `Set`, arrays/sets passed to `enqueue_all` are treated as static values (not iterated)
427
+ 2. **Strings and Hashes**: Always treated as static values, even though they respond to `:each`
428
+ 3. **No model or source**: If a field has no `model:` declaration and no `enqueues_each` with `from:`, you must pass it as a kwarg to `enqueue_all` or it will raise an error
429
+ 4. **Required static fields**: Fields without defaults that aren't covered by iteration must be provided to `enqueue_all`
@@ -0,0 +1,107 @@
1
+ # `Axn::Result`
2
+
3
+ Every `call` invocation on an Axn will return an `Axn::Result` instance, which provides a consistent interface:
4
+
5
+ | Method | Description |
6
+ | -- | -- |
7
+ | `ok?` | `true` if the call succeeded, `false` if not.
8
+ | `error` | User-facing error message (string), if not `ok?` (else nil)
9
+ | `success` | User-facing success message (string), if `ok?` (else nil)
10
+ | `message` | User-facing message (string), always defined (`ok? ? success : error`)
11
+ | `exception` | If not `ok?` because an exception was swallowed, will be set to the swallowed exception (note: rarely used outside development; prefer to let the library automatically handle exception handling for you)
12
+ | `outcome` | The execution outcome as a string inquirer (`success?`, `failure?`, `exception?`)
13
+ | `elapsed_time` | Execution time in milliseconds (Float)
14
+ | `finalized?` | `true` if the result has completed execution (either successfully or with an exception), `false` if still in progress
15
+ | any `expose`d values | guaranteed to be set if `ok?` (since they have outgoing presence validations by default; any missing would have failed the action)
16
+
17
+ NOTE: `success` and `error` (and so implicitly `message`) can be configured per-action via [the `success` and `error` declarations](/reference/class#success-and-error).
18
+
19
+ ### Clarification of exposed values
20
+
21
+ In addition to the core interface, your Action's Result class will have methods defined to read the values of any attributes that were explicitly exposed. For example, given this action and result:
22
+
23
+
24
+ ```ruby
25
+ class Foo
26
+ include Axn
27
+
28
+ exposes :bar, :baz # [!code focus]
29
+
30
+ def call
31
+ expose bar: 1, baz: 2
32
+ end
33
+ end
34
+
35
+ result = Foo.call # [!code focus]
36
+ ```
37
+
38
+ `result` will have both `bar` and `baz` reader methods (which will return 1 and 2, respectively).
39
+
40
+ ## Pattern Matching Support
41
+
42
+ `Axn::Result` supports Ruby 3's pattern matching feature, allowing you to destructure results in a more expressive way:
43
+
44
+ ```ruby
45
+ case SomeAction.call
46
+ in ok: true, success: String => message, user:, order:
47
+ process_success(user, order, message)
48
+ in ok: false, error: String => message
49
+ handle_error(message)
50
+ end
51
+ ```
52
+
53
+ ### Available Pattern Matching Keys
54
+
55
+ When pattern matching, the following keys are available:
56
+
57
+ - `ok` - Boolean success state (`true` for success, `false` for failure)
58
+ - `success` - Success message string (only present when `ok` is `true`)
59
+ - `error` - Error message string (only present when `ok` is `false`)
60
+ - `message` - Always present message string (success or error)
61
+ - `outcome` - Symbol indicating the execution outcome (`:success`, `:failure`, or `:exception`)
62
+ - `finalized` - Boolean indicating if execution completed
63
+ - Any exposed values from the action
64
+
65
+ ### Pattern Matching Examples
66
+
67
+ **Basic Success/Failure Matching:**
68
+ ```ruby
69
+ case result
70
+ in ok: true, user: User => user
71
+ puts "User created: #{user.name}"
72
+ in ok: false, error: String => message
73
+ puts "Error: #{message}"
74
+ end
75
+ ```
76
+
77
+ **Outcome-Based Matching:**
78
+ ```ruby
79
+ case result
80
+ in ok: true, outcome: :success, data: { id: Integer => id }
81
+ puts "Success with ID: #{id}"
82
+ in ok: false, outcome: :failure, error: String => message
83
+ puts "Business logic failure: #{message}"
84
+ in ok: false, outcome: :exception, error: String => message
85
+ puts "System error: #{message}"
86
+ end
87
+ ```
88
+
89
+ **Complex Nested Data Matching:**
90
+ ```ruby
91
+ case result
92
+ in ok: true, order: { id: Integer => order_id, items: [{ name: String => item_name }] }
93
+ puts "Order #{order_id} created with #{item_name}"
94
+ in ok: false, error: String => message, field: String => field
95
+ puts "Validation failed on #{field}: #{message}"
96
+ end
97
+ ```
98
+
99
+ **Type Guards and Variable Binding:**
100
+ ```ruby
101
+ case result
102
+ in ok: true, success: String => message, user: { email: String => email }
103
+ send_notification(email, message)
104
+ in ok: false, error: String => message, code: String => code
105
+ log_error(code: code, message: message)
106
+ end
107
+ ```