axn 0.1.0.pre.alpha.3 → 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/CHANGELOG.md +15 -1
- data/Rakefile +102 -2
- data/docs/.vitepress/config.mjs +12 -8
- data/docs/advanced/conventions.md +1 -1
- data/docs/advanced/mountable.md +4 -90
- data/docs/advanced/profiling.md +26 -30
- data/docs/advanced/rough.md +27 -8
- data/docs/intro/overview.md +1 -1
- data/docs/recipes/formatting-context-for-error-tracking.md +186 -0
- data/docs/recipes/memoization.md +102 -17
- data/docs/reference/async.md +269 -0
- data/docs/reference/class.md +113 -50
- data/docs/reference/configuration.md +226 -75
- data/docs/reference/form-object.md +252 -0
- data/docs/strategies/client.md +212 -0
- data/docs/strategies/form.md +235 -0
- data/docs/usage/setup.md +2 -2
- data/docs/usage/writing.md +99 -1
- data/lib/axn/async/adapters/active_job.rb +19 -10
- data/lib/axn/async/adapters/disabled.rb +15 -0
- data/lib/axn/async/adapters/sidekiq.rb +25 -32
- 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 +121 -4
- data/lib/axn/configuration.rb +53 -13
- data/lib/axn/context.rb +1 -0
- data/lib/axn/core/automatic_logging.rb +47 -51
- data/lib/axn/core/context/facade_inspector.rb +1 -1
- data/lib/axn/core/contract.rb +73 -30
- data/lib/axn/core/contract_for_subfields.rb +1 -1
- data/lib/axn/core/contract_validation.rb +14 -9
- data/lib/axn/core/contract_validation_for_subfields.rb +14 -7
- data/lib/axn/core/default_call.rb +63 -0
- data/lib/axn/core/flow/exception_execution.rb +5 -0
- data/lib/axn/core/flow/handlers/descriptors/message_descriptor.rb +19 -7
- data/lib/axn/core/flow/handlers/invoker.rb +4 -30
- data/lib/axn/core/flow/handlers/matcher.rb +4 -14
- data/lib/axn/core/flow/messages.rb +1 -1
- data/lib/axn/core/hooks.rb +1 -0
- data/lib/axn/core/logging.rb +16 -5
- data/lib/axn/core/memoization.rb +53 -0
- data/lib/axn/core/tracing.rb +77 -4
- data/lib/axn/core/validation/validators/type_validator.rb +1 -1
- data/lib/axn/core.rb +31 -46
- 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 +22 -2
- data/lib/axn/form_object.rb +90 -0
- data/lib/axn/internal/logging.rb +5 -1
- data/lib/axn/mountable/helpers/class_builder.rb +41 -10
- data/lib/axn/mountable/helpers/namespace_manager.rb +6 -34
- data/lib/axn/mountable/inherit_profiles.rb +2 -2
- data/lib/axn/mountable/mounting_strategies/_base.rb +10 -6
- data/lib/axn/mountable/mounting_strategies/method.rb +2 -2
- data/lib/axn/mountable.rb +41 -7
- data/lib/axn/rails/generators/axn_generator.rb +19 -1
- data/lib/axn/rails/generators/templates/action.rb.erb +1 -1
- data/lib/axn/result.rb +2 -2
- data/lib/axn/strategies/form.rb +98 -0
- data/lib/axn/strategies/transaction.rb +7 -0
- 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/version.rb +1 -1
- data/lib/axn.rb +9 -0
- metadata +22 -4
- data/lib/axn/core/profiling.rb +0 -124
- data/lib/axn/mountable/mounting_strategies/enqueue_all.rb +0 -55
data/docs/usage/writing.md
CHANGED
|
@@ -208,7 +208,10 @@ class ApiAction
|
|
|
208
208
|
|
|
209
209
|
expects :data
|
|
210
210
|
|
|
211
|
-
#
|
|
211
|
+
# Simply inherit child's error (prefix and handler are optional)
|
|
212
|
+
error from: ValidationAction
|
|
213
|
+
|
|
214
|
+
# Or combine prefix with from for consistent error formatting
|
|
212
215
|
error from: ValidationAction, prefix: "API Error: " do |e|
|
|
213
216
|
"Request validation failed: #{e.message}"
|
|
214
217
|
end
|
|
@@ -216,6 +219,12 @@ class ApiAction
|
|
|
216
219
|
# Or use prefix only (falls back to exception message)
|
|
217
220
|
error from: ValidationAction, prefix: "API Error: "
|
|
218
221
|
|
|
222
|
+
# Match multiple child actions
|
|
223
|
+
error from: [ValidationAction, AnotherAction]
|
|
224
|
+
|
|
225
|
+
# Match any child action
|
|
226
|
+
error from: true
|
|
227
|
+
|
|
219
228
|
def call
|
|
220
229
|
ValidationAction.call!(input: data)
|
|
221
230
|
end
|
|
@@ -223,9 +232,11 @@ end
|
|
|
223
232
|
```
|
|
224
233
|
|
|
225
234
|
This configuration provides:
|
|
235
|
+
- Simple error message inheritance without requiring prefix or handler
|
|
226
236
|
- Consistent error message formatting with prefixes
|
|
227
237
|
- Automatic fallback to exception messages when no custom message is provided
|
|
228
238
|
- Proper error message inheritance from nested actions
|
|
239
|
+
- Support for matching multiple child actions or any child action
|
|
229
240
|
|
|
230
241
|
::: warning Message Ordering
|
|
231
242
|
**Important**: When using conditional messages, always define your static fallback messages **first** in your class, before any conditional messages. This ensures proper fallback behavior.
|
|
@@ -258,6 +269,37 @@ In addition to `#call`, there are a few additional pieces to be aware of:
|
|
|
258
269
|
|
|
259
270
|
Note execution is halted whenever `fail!` is called, `done!` is called, or an exception is raised (so a `before` block failure won't execute `call` or `after`, while an `after` block failure will make `result.ok?` be false even though `call` completed successfully). The `done!` method specifically skips `after` hooks and any remaining `call` method execution, but allows `around` hooks to complete normally.
|
|
260
271
|
|
|
272
|
+
#### Around hooks
|
|
273
|
+
|
|
274
|
+
Around hooks wrap the entire action execution, including before and after hooks. They receive a block that represents the next step in the chain:
|
|
275
|
+
|
|
276
|
+
```ruby
|
|
277
|
+
class Foo
|
|
278
|
+
include Axn
|
|
279
|
+
|
|
280
|
+
around :with_timing
|
|
281
|
+
around do |chain|
|
|
282
|
+
log("outer around start")
|
|
283
|
+
chain.call
|
|
284
|
+
log("outer around end")
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def call
|
|
288
|
+
log("in call")
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
private
|
|
292
|
+
|
|
293
|
+
def with_timing(chain)
|
|
294
|
+
start = Time.current
|
|
295
|
+
chain.call
|
|
296
|
+
log("Took #{Time.current - start}s")
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
#### Before/After example
|
|
302
|
+
|
|
261
303
|
For instance, given this configuration:
|
|
262
304
|
|
|
263
305
|
```ruby
|
|
@@ -301,6 +343,62 @@ This follows the natural pattern of setup (general → specific) and teardown (s
|
|
|
301
343
|
A number of custom callback are available for you as well, if you want to take specific actions when a given Axn succeeds or fails. See the [Class Interface docs](/reference/class#callbacks) for details.
|
|
302
344
|
|
|
303
345
|
## Strategies
|
|
346
|
+
|
|
304
347
|
A number of [Strategies](/strategies/index), which are <abbr title="Don't Repeat Yourself">DRY</abbr>ed bits of commonly-used configuration, are available for your use as well.
|
|
305
348
|
|
|
349
|
+
::: info Optional Peer Libraries
|
|
350
|
+
Axn provides enhanced functionality when certain peer libraries are available:
|
|
351
|
+
|
|
352
|
+
- **Rails**: Automatic engine loading, autoloading for `app/actions`, and generators
|
|
353
|
+
- **Faraday**: Enables the [Client Strategy](/strategies/client) for HTTP API integrations
|
|
354
|
+
- **memo_wise**: Extends built-in `memo` helper to support methods with arguments (see [Memoization recipe](/recipes/memoization))
|
|
355
|
+
|
|
356
|
+
These are all optional—Axn works great without them, but they unlock additional features when present.
|
|
357
|
+
:::
|
|
358
|
+
|
|
359
|
+
## Advanced: Default call behavior
|
|
360
|
+
|
|
361
|
+
::: tip For Experienced Users
|
|
362
|
+
This section covers an advanced shortcut. If you're new to Axn, start by explicitly defining your `call` method.
|
|
363
|
+
:::
|
|
364
|
+
|
|
365
|
+
If you don't define a `call` method, Axn provides a default implementation that automatically exposes all declared exposures by calling methods with matching names. This allows you to simplify actions that only need to compute and expose values:
|
|
366
|
+
|
|
367
|
+
```ruby
|
|
368
|
+
class CertificatesByDestination
|
|
369
|
+
include Axn
|
|
370
|
+
exposes :certs_by_destination, type: Hash
|
|
371
|
+
|
|
372
|
+
private
|
|
373
|
+
|
|
374
|
+
def certs_by_destination
|
|
375
|
+
# Your logic here - automatically exposed
|
|
376
|
+
{ "dest1" => "cert1", "dest2" => "cert2" }
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
This is equivalent to:
|
|
382
|
+
|
|
383
|
+
```ruby
|
|
384
|
+
class CertificatesByDestination
|
|
385
|
+
include Axn
|
|
386
|
+
exposes :certs_by_destination, type: Hash
|
|
387
|
+
|
|
388
|
+
def call
|
|
389
|
+
expose certs_by_destination: certs_by_destination
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
private
|
|
393
|
+
|
|
394
|
+
def certs_by_destination
|
|
395
|
+
{ "dest1" => "cert1", "dest2" => "cert2" }
|
|
396
|
+
end
|
|
397
|
+
end
|
|
306
398
|
```
|
|
399
|
+
|
|
400
|
+
**Important notes:**
|
|
401
|
+
- The default `call` requires a method matching each declared exposure (unless a `default` is provided)
|
|
402
|
+
- If a method is missing and no default is provided, the action will fail with a helpful error message
|
|
403
|
+
- You can still override `call` to implement custom logic when needed
|
|
404
|
+
- If a method returns `nil` for an exposed-only field with no default, it's treated as missing (user-defined methods that legitimately return `nil` should use `expose` explicitly or provide a default)
|
|
@@ -6,6 +6,10 @@ module Axn
|
|
|
6
6
|
module ActiveJob
|
|
7
7
|
extend ActiveSupport::Concern
|
|
8
8
|
|
|
9
|
+
def self._running_in_background?
|
|
10
|
+
defined?(ActiveJob) && ActiveJob::Base.current_job.present?
|
|
11
|
+
end
|
|
12
|
+
|
|
9
13
|
included do
|
|
10
14
|
raise LoadError, "ActiveJob is not available. Please add 'activejob' to your Gemfile." unless defined?(::ActiveJob::Base)
|
|
11
15
|
|
|
@@ -16,23 +20,28 @@ module Axn
|
|
|
16
20
|
end
|
|
17
21
|
|
|
18
22
|
class_methods do
|
|
19
|
-
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# Implements adapter-specific enqueueing logic for ActiveJob.
|
|
26
|
+
# Note: Adapters must implement _enqueue_async_job and must NOT override call_async.
|
|
27
|
+
def _enqueue_async_job(kwargs)
|
|
20
28
|
job = active_job_proxy_class
|
|
21
29
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
# Extract and normalize _async options (removes _async from kwargs)
|
|
31
|
+
normalized_options = _extract_and_normalize_async_options(kwargs)
|
|
32
|
+
|
|
33
|
+
# Process normalized async options if present
|
|
34
|
+
if normalized_options
|
|
35
|
+
if normalized_options["wait_until"]
|
|
36
|
+
job = job.set(wait_until: normalized_options["wait_until"])
|
|
37
|
+
elsif normalized_options["wait"]
|
|
38
|
+
job = job.set(wait: normalized_options["wait"])
|
|
28
39
|
end
|
|
29
40
|
end
|
|
30
41
|
|
|
31
|
-
job.perform_later(
|
|
42
|
+
job.perform_later(kwargs)
|
|
32
43
|
end
|
|
33
44
|
|
|
34
|
-
private
|
|
35
|
-
|
|
36
45
|
def active_job_proxy_class
|
|
37
46
|
@active_job_proxy_class ||= create_active_job_proxy_class
|
|
38
47
|
end
|
|
@@ -4,16 +4,31 @@ module Axn
|
|
|
4
4
|
module Async
|
|
5
5
|
class Adapters
|
|
6
6
|
module Disabled
|
|
7
|
+
def self._running_in_background?
|
|
8
|
+
false
|
|
9
|
+
end
|
|
10
|
+
|
|
7
11
|
def self.included(base)
|
|
8
12
|
base.class_eval do
|
|
9
13
|
# Validate that kwargs are not provided for Disabled adapter
|
|
10
14
|
raise ArgumentError, "Disabled adapter does not accept configuration options." if _async_config&.any?
|
|
11
15
|
raise ArgumentError, "Disabled adapter does not accept configuration block." if _async_config_block
|
|
12
16
|
|
|
17
|
+
# Exception to the adapter pattern: Disabled adapter overrides call_async directly
|
|
18
|
+
# to raise immediately without emitting notifications or logging.
|
|
19
|
+
# Other adapters must NOT override call_async and should only implement _enqueue_async_job.
|
|
13
20
|
def self.call_async(**kwargs)
|
|
14
21
|
# Remove _async parameter to avoid confusion in error message
|
|
15
22
|
kwargs.delete(:_async)
|
|
16
23
|
|
|
24
|
+
# Don't emit notification or log - just raise immediately
|
|
25
|
+
raise NotImplementedError,
|
|
26
|
+
"Async execution is explicitly disabled for #{name}. " \
|
|
27
|
+
"Use `async :sidekiq` or `async :active_job` to enable background processing."
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self._enqueue_async_job(kwargs)
|
|
31
|
+
# This should never be called since call_async raises, but define it for completeness
|
|
17
32
|
raise NotImplementedError,
|
|
18
33
|
"Async execution is explicitly disabled for #{name}. " \
|
|
19
34
|
"Use `async :sidekiq` or `async :active_job` to enable background processing."
|
|
@@ -6,6 +6,10 @@ module Axn
|
|
|
6
6
|
module Sidekiq
|
|
7
7
|
extend ActiveSupport::Concern
|
|
8
8
|
|
|
9
|
+
def self._running_in_background?
|
|
10
|
+
defined?(::Sidekiq) && ::Sidekiq.server?
|
|
11
|
+
end
|
|
12
|
+
|
|
9
13
|
included do
|
|
10
14
|
raise LoadError, "Sidekiq is not available. Please add 'sidekiq' to your Gemfile." unless defined?(::Sidekiq)
|
|
11
15
|
|
|
@@ -14,6 +18,11 @@ module Axn
|
|
|
14
18
|
|
|
15
19
|
include ::Sidekiq::Job
|
|
16
20
|
|
|
21
|
+
# Sidekiq's processor calls .new on the worker class from outside the class hierarchy
|
|
22
|
+
# (see Sidekiq::Processor#dispatch which does `klass.new`).
|
|
23
|
+
# Since Axn::Core makes :new private, we need to restore it for Sidekiq workers.
|
|
24
|
+
public_class_method :new
|
|
25
|
+
|
|
17
26
|
# Apply configuration block if present
|
|
18
27
|
class_eval(&_async_config_block) if _async_config_block
|
|
19
28
|
|
|
@@ -22,48 +31,32 @@ module Axn
|
|
|
22
31
|
end
|
|
23
32
|
|
|
24
33
|
class_methods do
|
|
25
|
-
|
|
26
|
-
job_kwargs = _params_to_global_id(kwargs)
|
|
27
|
-
|
|
28
|
-
if kwargs[:_async].is_a?(Hash)
|
|
29
|
-
options = kwargs.delete(:_async)
|
|
30
|
-
if options[:wait_until]
|
|
31
|
-
return perform_at(options[:wait_until], job_kwargs)
|
|
32
|
-
elsif options[:wait]
|
|
33
|
-
return perform_in(options[:wait], job_kwargs)
|
|
34
|
-
end
|
|
35
|
-
end
|
|
34
|
+
private
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
# Implements adapter-specific enqueueing logic for Sidekiq.
|
|
37
|
+
# Note: Adapters must implement _enqueue_async_job and must NOT override call_async.
|
|
38
|
+
def _enqueue_async_job(kwargs)
|
|
39
|
+
# Extract and normalize _async options (removes _async from kwargs)
|
|
40
|
+
normalized_options = _extract_and_normalize_async_options(kwargs)
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
# Convert kwargs to string keys and handle GlobalID conversion
|
|
43
|
+
job_kwargs = Axn::Util::GlobalIdSerialization.serialize(kwargs)
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
# Process normalized async options if present
|
|
46
|
+
if normalized_options
|
|
47
|
+
if normalized_options["wait_until"]
|
|
48
|
+
return perform_at(normalized_options["wait_until"], job_kwargs)
|
|
49
|
+
elsif normalized_options["wait"]
|
|
50
|
+
return perform_in(normalized_options["wait"], job_kwargs)
|
|
48
51
|
end
|
|
49
52
|
end
|
|
50
|
-
end
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
return {} if params.nil?
|
|
54
|
-
|
|
55
|
-
params.each_with_object({}) do |(key, value), hash|
|
|
56
|
-
if key.end_with?("_as_global_id")
|
|
57
|
-
hash[key.delete_suffix("_as_global_id")] = GlobalID::Locator.locate(value)
|
|
58
|
-
else
|
|
59
|
-
hash[key] = value
|
|
60
|
-
end
|
|
61
|
-
end.symbolize_keys
|
|
54
|
+
perform_async(job_kwargs)
|
|
62
55
|
end
|
|
63
56
|
end
|
|
64
57
|
|
|
65
58
|
def perform(*args)
|
|
66
|
-
context =
|
|
59
|
+
context = Axn::Util::GlobalIdSerialization.deserialize(args.first)
|
|
67
60
|
|
|
68
61
|
# Always use bang version so sidekiq can retry if we failed
|
|
69
62
|
self.class.call!(**context)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Axn
|
|
4
|
+
module Async
|
|
5
|
+
module BatchEnqueue
|
|
6
|
+
# Stores the configuration for a single enqueues_each declaration
|
|
7
|
+
class Config
|
|
8
|
+
attr_reader :field, :from, :via, :filter_block
|
|
9
|
+
|
|
10
|
+
def initialize(field:, from:, via:, filter_block:)
|
|
11
|
+
@field = field
|
|
12
|
+
@from = from
|
|
13
|
+
@via = via
|
|
14
|
+
@filter_block = filter_block
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Resolves the source collection for iteration
|
|
18
|
+
# Can be a lambda, a symbol (method name on target), or inferred from model: true
|
|
19
|
+
def resolve_source(target:)
|
|
20
|
+
return from.call if from.is_a?(Proc)
|
|
21
|
+
return target.send(from) if from.is_a?(Symbol)
|
|
22
|
+
|
|
23
|
+
# Infer from field's model config if 'from' is nil
|
|
24
|
+
field_config = target.internal_field_configs.find { |c| c.field == field }
|
|
25
|
+
model_opts = field_config&.validations&.dig(:model)
|
|
26
|
+
model_class = model_opts[:klass] if model_opts.is_a?(Hash)
|
|
27
|
+
|
|
28
|
+
unless model_class
|
|
29
|
+
raise ArgumentError,
|
|
30
|
+
"enqueues_each :#{field} requires `from:` option or a `model:` declaration " \
|
|
31
|
+
"on `expects :#{field}` to infer the source collection."
|
|
32
|
+
end
|
|
33
|
+
model_class.all
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "axn/async/batch_enqueue/config"
|
|
4
|
+
|
|
5
|
+
module Axn
|
|
6
|
+
module Async
|
|
7
|
+
# BatchEnqueue provides declarative batch enqueueing for Axn actions.
|
|
8
|
+
#
|
|
9
|
+
# Fields with `model:` declarations are automatically inferred for iteration.
|
|
10
|
+
# Use `enqueues_each` to override defaults, add filtering, or iterate non-model fields.
|
|
11
|
+
# All Axn classes have `enqueue_all` defined, which validates configuration and
|
|
12
|
+
# executes iteration asynchronously via EnqueueAllOrchestrator.
|
|
13
|
+
#
|
|
14
|
+
# @example Auto-inference from model: (no enqueues_each needed)
|
|
15
|
+
# class SyncCompany
|
|
16
|
+
# include Axn
|
|
17
|
+
# async :sidekiq
|
|
18
|
+
#
|
|
19
|
+
# expects :company, model: Company # Auto-inferred: Company.all
|
|
20
|
+
#
|
|
21
|
+
# def call
|
|
22
|
+
# # sync logic
|
|
23
|
+
# end
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# SyncCompany.enqueue_all # Automatically iterates Company.all
|
|
27
|
+
#
|
|
28
|
+
# @example With explicit source override
|
|
29
|
+
# enqueues_each :company, from: -> { Company.active }
|
|
30
|
+
#
|
|
31
|
+
# @example With extraction (passes company_id instead of company object)
|
|
32
|
+
# enqueues_each :company_id, from: -> { Company.active }, via: :id
|
|
33
|
+
#
|
|
34
|
+
# @example With filter block
|
|
35
|
+
# enqueues_each :company do |company|
|
|
36
|
+
# company.active? && !company.in_exit?
|
|
37
|
+
# end
|
|
38
|
+
#
|
|
39
|
+
# @example Override on enqueue_all call
|
|
40
|
+
# # Override with enumerable (replaces source)
|
|
41
|
+
# SyncCompany.enqueue_all(company: Company.active.limit(10))
|
|
42
|
+
#
|
|
43
|
+
# # Override with scalar (makes it static, no iteration)
|
|
44
|
+
# SyncCompany.enqueue_all(company: Company.find(123))
|
|
45
|
+
#
|
|
46
|
+
# @example Multi-field cross-product
|
|
47
|
+
# enqueues_each :user, from: -> { User.active }
|
|
48
|
+
# enqueues_each :company, from: -> { Company.active }
|
|
49
|
+
# # Produces user_count × company_count jobs
|
|
50
|
+
module BatchEnqueue
|
|
51
|
+
extend ActiveSupport::Concern
|
|
52
|
+
|
|
53
|
+
included do
|
|
54
|
+
class_attribute :_batch_enqueue_configs, default: []
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# DSL methods for batch enqueueing
|
|
58
|
+
module DSL
|
|
59
|
+
# Batch enqueue jobs for this action.
|
|
60
|
+
#
|
|
61
|
+
# Validates async is configured, validates static args, then executes
|
|
62
|
+
# iteration asynchronously via EnqueueAllOrchestrator.
|
|
63
|
+
#
|
|
64
|
+
# Fields with `model:` declarations are automatically inferred for iteration.
|
|
65
|
+
# You can override iteration by passing enumerables (to replace source) or
|
|
66
|
+
# scalars (to make fields static) as kwargs.
|
|
67
|
+
#
|
|
68
|
+
# @param static_args [Hash] Arguments to pass to every enqueued job.
|
|
69
|
+
# - Scalar values: Treated as static args (passed to all jobs)
|
|
70
|
+
# - Enumerable values: Treated as iteration sources (overrides configured sources)
|
|
71
|
+
# - Exception: Arrays/Sets are static when field expects enumerable type
|
|
72
|
+
# @return [String] Job ID from the async adapter
|
|
73
|
+
# @raise [NotImplementedError] If async is not configured
|
|
74
|
+
# @raise [MissingEnqueuesEachError] If expects exist but no iteration config found
|
|
75
|
+
# @raise [ArgumentError] If required static fields are missing
|
|
76
|
+
def enqueue_all(**static_args)
|
|
77
|
+
EnqueueAllOrchestrator.enqueue_for(self, **static_args)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Declare a field to iterate over for batch enqueueing.
|
|
81
|
+
#
|
|
82
|
+
# Note: Fields with `model:` declarations are automatically inferred, so
|
|
83
|
+
# `enqueues_each` is only needed to override defaults, add filtering, or
|
|
84
|
+
# iterate non-model fields.
|
|
85
|
+
#
|
|
86
|
+
# @param field [Symbol] The field name from expects to iterate over
|
|
87
|
+
# @param from [Proc, Symbol, nil] The source collection.
|
|
88
|
+
# - Proc/lambda: Called to get the collection
|
|
89
|
+
# - Symbol: Method name on the action class
|
|
90
|
+
# - nil: Inferred from field's `model:` declaration (Model.all)
|
|
91
|
+
# @param via [Symbol, nil] Optional attribute to extract from each item (e.g., :id)
|
|
92
|
+
# @param block [Proc, nil] Optional filter block - return truthy to enqueue, falsy to skip
|
|
93
|
+
def enqueues_each(field, from: nil, via: nil, &filter_block)
|
|
94
|
+
self._batch_enqueue_configs += [Config.new(field:, from:, via:, filter_block:)]
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|