active_interaction-extras 1.0.4 → 2.0.0

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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +4 -3
  3. data/.gitignore +1 -0
  4. data/CHANGELOG.md +24 -0
  5. data/README.md +325 -0
  6. data/active_interaction-extras.gemspec +8 -5
  7. data/lib/active_interaction/extras/active_job.rb +36 -5
  8. data/lib/active_interaction/extras/add_to_batch.rb +13 -0
  9. data/lib/active_interaction/extras/after_initialize.rb +20 -0
  10. data/lib/active_interaction/extras/all.rb +16 -1
  11. data/lib/active_interaction/extras/all_runner.rb +91 -0
  12. data/lib/active_interaction/extras/append_inputs.rb +60 -0
  13. data/lib/active_interaction/extras/current.rb +23 -0
  14. data/lib/active_interaction/extras/file_blobs.rb +80 -0
  15. data/lib/active_interaction/extras/filter_alias.rb +28 -2
  16. data/lib/active_interaction/extras/filter_extensions/object_classes.rb +2 -0
  17. data/lib/active_interaction/extras/filter_extensions.rb +8 -0
  18. data/lib/active_interaction/extras/filters/anything_filter.rb +9 -2
  19. data/lib/active_interaction/extras/filters/ar_relation_filter.rb +22 -0
  20. data/lib/active_interaction/extras/filters/uuid_filter.rb +7 -2
  21. data/lib/active_interaction/extras/form_for.rb +33 -0
  22. data/lib/active_interaction/extras/good_job.rb +45 -0
  23. data/lib/active_interaction/extras/include_errors.rb +10 -0
  24. data/lib/active_interaction/extras/initialize_with.rb +16 -0
  25. data/lib/active_interaction/extras/instance_runnable.rb +35 -0
  26. data/lib/active_interaction/extras/model_fields.rb +75 -37
  27. data/lib/active_interaction/extras/model_names.rb +20 -0
  28. data/lib/active_interaction/extras/named_callbacks.rb +54 -0
  29. data/lib/active_interaction/extras/nested_attributes.rb +126 -0
  30. data/lib/active_interaction/extras/strong_params.rb +50 -10
  31. data/lib/active_interaction/extras/transaction.rb +27 -5
  32. data/lib/active_interaction/extras/version.rb +1 -1
  33. data/lib/active_interaction/extras.rb +10 -23
  34. metadata +49 -17
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e7c9ab1b398885ce07d041afd0698600f69ac5813434822945c5e1247e81e9a5
4
- data.tar.gz: b8e73b105796596890f5518823c430c02e402c5d5aa4be353617b7bddae65182
3
+ metadata.gz: 0475c2d5f3660b0b545a444835c4cbcac60c5379fb352f43a427e9e3f12a5496
4
+ data.tar.gz: 4a70441a2ee4f40a01ab49bbcf2473007affbf5049db891c8d69a51462321338
5
5
  SHA512:
6
- metadata.gz: f983de2973a1aae555f1576ec2eca88de323a714e8a0e778b2be96a15b29486cc0dfe0f03219ccb800b3354421c9dc138fd50324f992e7b415bd20b634360b54
7
- data.tar.gz: 64b07f5287072bf90156987a0ec823b73341dc021e2b224c98f40dacbfa80240bd71b83d141323168a33e125fedb73d7547d7a919e0b3e4b7d603cd37743dc49
6
+ metadata.gz: be224738bce179d169ef765122691eecf957a333ce5798094f3d0d31b431fafef253759bc64a1b23f09224e59f576cb0abdb4512b619de5ed326d67281b1d740
7
+ data.tar.gz: f947b78e59b9f047b6ac8fe913cfa9ed90280d4136cf03b9a6731689609e03b03badd94d6d2b30c191d77068a30d75b19756191ace663b665107f9fa415e368d
@@ -19,17 +19,18 @@ jobs:
19
19
  runs-on: ubuntu-latest
20
20
  strategy:
21
21
  matrix:
22
- ruby-version: ['2.6', '2.7', '3.0']
22
+ ruby-version: ['3.2', '3.3', '3.4', '4.0']
23
23
 
24
24
  steps:
25
- - uses: actions/checkout@v2
25
+ - uses: actions/checkout@v4
26
26
  - name: Set up Ruby
27
27
  # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
28
28
  # change this to (see https://github.com/ruby/setup-ruby#versioning):
29
29
  # uses: ruby/setup-ruby@v1
30
- uses: ruby/setup-ruby@473e4d8fe5dd94ee328fdfca9f8c9c7afc9dae5e
30
+ uses: ruby/setup-ruby@v1
31
31
  with:
32
32
  ruby-version: ${{ matrix.ruby-version }}
33
33
  bundler-cache: true # runs 'bundle install' and caches installed gems automatically
34
+ cache-version: 3
34
35
  - name: Run tests
35
36
  run: bundle exec rake
data/.gitignore CHANGED
@@ -7,3 +7,4 @@
7
7
  /spec/reports/
8
8
  /tmp/
9
9
  Gemfile.lock
10
+ .ruby-version
data/CHANGELOG.md CHANGED
@@ -4,6 +4,30 @@ This project adheres to [Semantic Versioning](http://semver.org/).
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.0.0] - 2026-10-13
8
+
9
+ - Add the `ar_relation` filter for `ActiveRecord::Relation` inputs without forcing relation loading when checking defaults
10
+ - Extend the `uuid` filter to accept UUID versions 5 and 7.
11
+ - Add Rails-style `NestedAttributes` for single and collection inputs, including `allow_destroy` and `reject_if`. Nested attributes now require explicit `permit:` rules.
12
+ - Extend `StrongParams` to support object filters and custom permit lists for scalar, hash, object, array, and nested-attribute inputs.
13
+ - Add `FormFor` to delegate form identity and routing to a model input, and `ModelNames` to derive route keys for namespaced forms.
14
+ - Add an `AfterInitialize` callback hook plus `InitializeWith` and `AppendInputs` for post-initialization defaults that do not overwrite explicitly supplied inputs; allow `initialize_with` blocks to return `nil` and ensure defaults run after model-field initialization.
15
+ - Add `InstanceRunnable` with one-shot instance execution and `save`/`save!` methods for form-style usage.
16
+ - Add `IncludeErrors` to merge model validation errors into interaction errors and halt when the model is invalid.
17
+ - Extend `ModelFields` with `assign_model_fields`, `assign_model_changes`, `save_model!`, and `save_model_changes!`; improve change detection for new records and plain objects, and preserve custom filter blocks.
18
+ - Add `NamedCallbacks` for before, after, and around filter, validation, and execution callbacks.
19
+ - Extend transaction helpers to allow disabling transactions and add `force_explicit_transactions!` and `list_missing_transaction_definition` for enforcing and auditing transaction choices.
20
+ - Add ActiveJob inline execution and the `Current` context for the running job; avoid redundant job dispatch during GoodJob batch completion.
21
+ - Add GoodJob batch and concurrency support, including current batch/event context, and `AddToBatch` for joining the current batch.
22
+ - Add `AllRunner` to run an interaction for every record in its object scope, synchronously or through GoodJob bulk enqueueing.
23
+ - Add `FileBlobs` helpers for working with ActiveStorage blobs, attachments, and signed IDs in form objects.
24
+ - Refactor extensions for Zeitwerk autoloading; require Ruby 3.0+, ActiveModel and ActiveSupport 7.1+, and Zeitwerk 2+. Update CI to cover Ruby 3.2 through 4.0.
25
+
26
+ ## [1.1.0] - 2023-03-08
27
+
28
+ - Add support for active_interaction v5
29
+ - Fix `model_fields` helper
30
+
7
31
  ## [1.0.4] - 2022-06-08
8
32
 
9
33
  - Improve halt's robustness (https://github.com/antulik/active_interaction-extras/pull/13)
data/README.md CHANGED
@@ -9,9 +9,11 @@ This gem contains the collection of useful extensions to [active_interaction](ht
9
9
  - [Filters](#filters)
10
10
  - [Anything](#anything)
11
11
  - [UUID](#uuid)
12
+ - [ActiveRecord Relation](#activerecord-relation)
12
13
  - [Filter Extensions](#filter-extensions)
13
14
  - [Hash: auto strip](#hash-auto-strip)
14
15
  - [Object: multiple classes](#object-multiple-classes)
16
+ - [Timezone support](#timezone-support)
15
17
  - [Extensions](#extensions)
16
18
  - [Filter alias](#filter-alias)
17
19
  - [Halt](#halt)
@@ -19,9 +21,22 @@ This gem contains the collection of useful extensions to [active_interaction](ht
19
21
  - [RunCallback](#runcallback)
20
22
  - [StrongParams](#strongparams)
21
23
  - [Transaction](#transaction)
24
+ - [NamedCallbacks](#namedcallbacks)
25
+ - [ModelNames](#modelnames)
26
+ - [FormFor](#formfor)
27
+ - [NestedAttributes](#nestedattributes)
28
+ - [InitializeWith](#initializewith)
29
+ - [AppendInputs](#appendinputs)
30
+ - [IncludeErrors](#includeerrors)
31
+ - [InstanceRunnable](#instancerunnable)
32
+ - [Current](#current)
33
+ - [AddToBatch](#addtobatch)
34
+ - [AllRunner](#allrunner)
35
+ - [FileBlobs](#fileblobs)
22
36
  - [Jobs](#jobs)
23
37
  - [ActiveJob](#activejob)
24
38
  - [Sidekiq](#sidekiq)
39
+ - [GoodJob](#goodjob)
25
40
  - [RSpec](#rspec)
26
41
 
27
42
  ## Installation
@@ -61,6 +76,20 @@ class Service < ActiveInteraction::Base
61
76
  end
62
77
  ```
63
78
 
79
+ ### ActiveRecord Relation
80
+
81
+ ```ruby
82
+ class Service < ActiveInteraction::Base
83
+ ar_relation :users
84
+
85
+ def execute
86
+ users.where(active: true)
87
+ end
88
+ end
89
+
90
+ Service.run!(users: User.all) # accepts any ActiveRecord::Relation
91
+ ```
92
+
64
93
  ## Filter Extensions
65
94
 
66
95
  You can load all filter extensions with:
@@ -96,6 +125,27 @@ class Service < ActiveInteraction::Base
96
125
  end
97
126
  ```
98
127
 
128
+ ### Timezone support
129
+
130
+ Parses time strings in the configured timezone. Requires a separate require:
131
+
132
+ ```ruby
133
+ require 'active_interaction/extras/filter_extensions/timezone_support'
134
+
135
+ class Service < ActiveInteraction::Base
136
+ time :epoch
137
+
138
+ def execute
139
+ epoch.iso8601
140
+ end
141
+ end
142
+
143
+ Time.use_zone('Bangkok') do
144
+ Service.run!(epoch: '2000-12-11 10:30')
145
+ # => '2000-12-11T10:30:00+07:00'
146
+ end
147
+ ```
148
+
99
149
 
100
150
  ## Extensions
101
151
 
@@ -237,6 +287,248 @@ UpdateUserForm.run
237
287
  Comment.count # => 0
238
288
  ```
239
289
 
290
+ ### NamedCallbacks
291
+
292
+ Provides friendly method names for ActiveInteraction's internal callbacks.
293
+
294
+ ```ruby
295
+ class Service < ActiveInteraction::Base
296
+ include ActiveInteraction::Extras::NamedCallbacks
297
+
298
+ before_filter :normalize_inputs
299
+ after_validate :log_validation
300
+ before_execute :setup_context
301
+ after_execute :send_notification
302
+
303
+ def execute
304
+ end
305
+ end
306
+ ```
307
+
308
+ ### ModelNames
309
+
310
+ Note: This module is currently NOT included in `ActiveInteraction::Extras::All`.
311
+ Sets up automatic ActiveModel naming for forms based on their class name for URL helpers.
312
+
313
+ ```ruby
314
+ class ApplicationForm < ActiveInteraction::Base
315
+ include ActiveInteraction::Extras::ModelNames
316
+ end
317
+
318
+ class User::SignUpForm < ApplicationForm
319
+ # route_key is auto-set to :user_sign_up_forms
320
+ end
321
+
322
+ class User::UpdateForm < ApplicationForm
323
+ singular_resource_route_key! # route_key becomes :user_update_form
324
+ end
325
+ ```
326
+
327
+ ### FormFor
328
+
329
+ Makes form objects work with Rails URL helpers by delegating routing to a model field.
330
+
331
+ ```ruby
332
+ class User::Form < ActiveInteraction::Base
333
+ include ActiveInteraction::Extras::FormFor
334
+
335
+ object :user
336
+ form_for :user # delegates routing to user
337
+
338
+ # url_for(form) => "/users/1"
339
+ end
340
+ ```
341
+
342
+ ### NestedAttributes
343
+
344
+ Rails-style `accepts_nested_attributes_for` for interactions.
345
+
346
+ ```ruby
347
+ class Form < ActiveInteraction::Base
348
+ include ActiveInteraction::Extras::NestedAttributes
349
+
350
+ array :items
351
+
352
+ accepts_nested_attributes_for :items,
353
+ permit: [:name, :quantity]
354
+
355
+ def execute
356
+ # items_attributes automatically mapped to items
357
+ end
358
+ end
359
+ ```
360
+
361
+ ### InitializeWith
362
+
363
+ Lazily sets input values after initialization, only for inputs not explicitly given.
364
+
365
+ ```ruby
366
+ class Service < ActiveInteraction::Base
367
+ include ActiveInteraction::Extras::InitializeWith
368
+
369
+ anything :current_user
370
+
371
+ initialize_with do
372
+ { current_user: User.current }
373
+ end
374
+
375
+ def execute
376
+ end
377
+ end
378
+
379
+ Service.run! # current_user => User.current
380
+ Service.run!(current_user: other_user) # current_user => other_user
381
+ ```
382
+
383
+ ### AppendInputs
384
+
385
+ Set input defaults after initialization without overwriting explicitly given inputs.
386
+
387
+ ```ruby
388
+ class Service < ActiveInteraction::Base
389
+ include ActiveInteraction::Extras::AppendInputs
390
+
391
+ string :tenant_name
392
+
393
+ default_inputs do
394
+ { tenant_name: Current.tenant.name }
395
+ end
396
+
397
+ # Or single default:
398
+ default_input :tenant_name do
399
+ Current.tenant.name
400
+ end
401
+ end
402
+ ```
403
+
404
+ ### IncludeErrors
405
+
406
+ Merges a model's errors into the interaction's errors and halts if invalid.
407
+
408
+ ```ruby
409
+ class Service < ActiveInteraction::Base
410
+ include ActiveInteraction::Extras::IncludeErrors
411
+
412
+ def execute
413
+ model = User.new(params)
414
+ model.save
415
+ include_errors!(model) # merges model errors and halts if invalid
416
+ end
417
+ end
418
+ ```
419
+
420
+ ### InstanceRunnable
421
+
422
+ Allows running interactions from an already-instantiated object. Useful for controller-style usage with `save`/`save!` aliases.
423
+
424
+ ```ruby
425
+ class Form < ActiveInteraction::Base
426
+ include ActiveInteraction::Extras::InstanceRunnable
427
+
428
+ string :name
429
+
430
+ def execute
431
+ end
432
+ end
433
+
434
+ # Controller-style:
435
+ def create
436
+ if form.save
437
+ redirect_to record_path
438
+ else
439
+ render :new
440
+ end
441
+ end
442
+
443
+ def form
444
+ @form ||= Form.new(params:)
445
+ end
446
+ ```
447
+
448
+ ### Current
449
+
450
+ Provides `ActiveSupport::CurrentAttributes` context accessible via `current`.
451
+
452
+ ```ruby
453
+ class Service < ActiveInteraction::Base
454
+ include ActiveInteraction::Extras::Current
455
+
456
+ def execute
457
+ current.job # the current job (if running in background)
458
+ current.batch # the current GoodJob batch (if applicable)
459
+ end
460
+ end
461
+ ```
462
+
463
+ ### AddToBatch
464
+
465
+ Conditionally runs a block within a GoodJob batch if currently in a batch context.
466
+
467
+ ```ruby
468
+ class Service < ActiveInteraction::Base
469
+ include ActiveInteraction::Extras::AddToBatch
470
+
471
+ def execute
472
+ add_to_batch do
473
+ SomeOtherService.delay.run(some_param: value)
474
+ end
475
+ end
476
+ end
477
+ ```
478
+
479
+ ### AllRunner
480
+
481
+ Runs the service for every record in the default scope of the object filter.
482
+
483
+ ```ruby
484
+ class NotifyUsers < ActiveInteraction::Base
485
+ include ActiveInteraction::Extras::AllRunner
486
+
487
+ object :user
488
+
489
+ def execute
490
+ # process each user
491
+ end
492
+ end
493
+
494
+ # Run synchronously for all users:
495
+ NotifyUsers.all.run!
496
+
497
+ # Run async via GoodJob batches:
498
+ NotifyUsers.all.delay.run!
499
+
500
+ # With custom scope:
501
+ NotifyUsers.all(user: User.active).delay(wait: 5.minutes).run!
502
+ ```
503
+
504
+ ### FileBlobs
505
+
506
+ Helper for working with ActiveStorage attachments in form objects.
507
+
508
+ ```ruby
509
+ class Doc::Form < ActiveInteraction::Base
510
+ include ActiveInteraction::Extras::FileBlobs
511
+
512
+ object :doc
513
+
514
+ model_fields(:doc) do
515
+ array :files
516
+ end
517
+
518
+ def execute
519
+ save_model!(:doc)
520
+ end
521
+ end
522
+ ```
523
+
524
+ ```slim
525
+ / _form.html.slim
526
+ = form.blobs_for(:files).each do |blob|
527
+ = f.hidden_field :files, value: blob.signed_id
528
+
529
+ = f.file_field :files, multiple: true, direct_upload: true
530
+ ```
531
+
240
532
  ## Jobs
241
533
 
242
534
  You no longer need to create a separate Job class for the each interaction. This Job extension automatically converts interactions to background jobs. By convention each interaction will have a nested `Job` class which will be inherited from the parent interaction `Job` class (e.g. `ApplicationInteraction::Job`).
@@ -339,6 +631,39 @@ DoubleService.run # => RuntimeError
339
631
  DoubleService.delay.perform_now(x: 2) # => returns 2
340
632
  ```
341
633
 
634
+ ### GoodJob
635
+
636
+ Extends the ActiveJob integration with GoodJob batch support and concurrency keys.
637
+
638
+ ```ruby
639
+ class ApplicationInteraction < ActiveInteraction::Base
640
+ include ActiveInteraction::Extras::GoodJob
641
+
642
+ class Job < ActiveJob::Base
643
+ include GoodJob::ActiveJobExtensions::Concurrency
644
+ include ActiveInteraction::Extras::GoodJob::Perform
645
+ end
646
+ end
647
+
648
+ class ProcessUser < ApplicationInteraction
649
+ object :user
650
+ def execute
651
+ end
652
+ end
653
+
654
+ class ProcessUserFinished < ApplicationInteraction
655
+ def execute
656
+ end
657
+ end
658
+
659
+ # Create and enqueue a GoodJob batch:
660
+ batch = ProcessUserFinished.batch_job
661
+ batch.add do
662
+ ProcessUser.delay.run!(user: User.last)
663
+ end
664
+ batch.enqueue
665
+ ```
666
+
342
667
  ## Rspec
343
668
 
344
669
  ```ruby
@@ -17,6 +17,8 @@ Gem::Specification.new do |spec|
17
17
  "changelog_uri" => "https://github.com/antulik/active_interaction-extras/blob/master/CHANGELOG.md",
18
18
  }
19
19
 
20
+ spec.required_ruby_version = '>= 3.0'
21
+
20
22
  spec.files = `git ls-files -z`.split("\x0").reject do |f|
21
23
  f.match(%r{^(test|spec|features)/})
22
24
  end
@@ -24,15 +26,16 @@ Gem::Specification.new do |spec|
24
26
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
25
27
  spec.require_paths = ["lib"]
26
28
 
27
- spec.add_dependency "active_interaction", ">= 4.0.2"
28
- spec.add_dependency "activemodel", ">= 6.0"
29
- spec.add_dependency "activesupport", ">= 6.0"
29
+ spec.add_dependency "active_interaction", ">= 5.0", "< 6.0"
30
+ spec.add_dependency "activemodel", ">= 7.1"
31
+ spec.add_dependency "activesupport", ">= 7.1"
32
+ spec.add_dependency "zeitwerk", ">= 2"
30
33
 
31
- spec.add_development_dependency "bundler", "~> 2.2"
34
+ spec.add_development_dependency "bundler", ">= 2.2"
32
35
  spec.add_development_dependency "rake", ">= 12.3.3"
33
36
  spec.add_development_dependency "rspec", "~> 3.7"
34
37
  spec.add_development_dependency "pry"
35
- spec.add_development_dependency "sqlite3"
38
+ spec.add_development_dependency "sqlite3", ">= 2"
36
39
  spec.add_development_dependency "activerecord"
37
40
  spec.add_development_dependency "activejob"
38
41
  spec.add_development_dependency "actionpack"
@@ -2,34 +2,65 @@ require 'active_job'
2
2
 
3
3
  module ActiveInteraction::Extras::ActiveJob
4
4
  extend ActiveSupport::Concern
5
-
6
5
  include ActiveInteraction::Extras::Jobs::Core
7
6
 
8
7
  class_methods do
9
8
  def configured_job_class
10
9
  ConfiguredJob
11
10
  end
11
+
12
+ def perform_now(&block)
13
+ job_class.perform_now(&block)
14
+ end
12
15
  end
13
16
 
14
17
  module Perform
15
18
  extend ActiveSupport::Concern
16
19
 
17
20
  def perform(*args)
21
+ ActiveInteraction::Extras::Current::CurrentContext.set(job: self) do
22
+ parent_class.run!(*args)
23
+ end
24
+ end
25
+
26
+ def parent_class
18
27
  if self.class.respond_to?(:module_parent)
19
- self.class.module_parent.run!(*args)
28
+ self.class.module_parent
20
29
  else
21
- self.class.parent.run!(*args)
30
+ self.class.parent
31
+ end
32
+ end
33
+
34
+ class_methods do
35
+ def perform_now
36
+ Thread.current[name] ||= []
37
+ Thread.current[name].push true
38
+ yield
39
+ ensure
40
+ if Thread.current[name].size == 1
41
+ Thread.current[name] = nil
42
+ else
43
+ Thread.current[name].pop
44
+ end
45
+ end
46
+
47
+ def perform_now?
48
+ Thread.current[name]
22
49
  end
23
50
  end
24
51
  end
25
52
 
26
53
  class ConfiguredJob < ::ActiveJob::ConfiguredJob
27
54
  def run(*args)
28
- perform_later(*args)
55
+ run!(*args)
29
56
  end
30
57
 
31
58
  def run!(*args)
32
- perform_later(*args)
59
+ if @job_class.perform_now?
60
+ perform_now(*args)
61
+ else
62
+ perform_later(*args)
63
+ end
33
64
  end
34
65
  end
35
66
  end
@@ -0,0 +1,13 @@
1
+ module ActiveInteraction::Extras::AddToBatch
2
+ extend ActiveSupport::Concern
3
+
4
+ include ActiveInteraction::Extras::Current
5
+
6
+ def add_to_batch(&block)
7
+ if current.batch
8
+ current.batch.add(&block)
9
+ else
10
+ block.call
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,20 @@
1
+ module ActiveInteraction::Extras::AfterInitialize
2
+ extend ActiveSupport::Concern
3
+
4
+ include ActiveSupport::Callbacks
5
+
6
+ included do
7
+ define_callbacks :initialize
8
+ end
9
+
10
+ class_methods do
11
+ def after_initialize(...)
12
+ set_callback(:initialize, :after, ...)
13
+ end
14
+ end
15
+
16
+ def initialize(...)
17
+ super
18
+ run_callbacks :initialize, :after
19
+ end
20
+ end
@@ -2,11 +2,26 @@ module ActiveInteraction::Extras::All
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  # order dependant, include first so around callback includes other modules
5
+ include ActiveInteraction::Extras::NamedCallbacks
6
+ include ActiveInteraction::Extras::AfterInitialize
5
7
  include ActiveInteraction::Extras::Transaction
6
8
 
7
9
  include ActiveInteraction::Extras::FilterAlias
8
10
  include ActiveInteraction::Extras::Halt
9
11
  include ActiveInteraction::Extras::ModelFields
10
- include ActiveInteraction::Extras::RunCallback
12
+ include ActiveInteraction::Extras::NestedAttributes
13
+
11
14
  include ActiveInteraction::Extras::StrongParams
15
+ include ActiveInteraction::Extras::Current
16
+
17
+ include ActiveInteraction::Extras::AddToBatch
18
+ include ActiveInteraction::Extras::FormFor
19
+ include ActiveInteraction::Extras::InstanceRunnable
20
+ include ActiveInteraction::Extras::InitializeWith
21
+ include ActiveInteraction::Extras::IncludeErrors
22
+ include ActiveInteraction::Extras::AllRunner
23
+ include ActiveInteraction::Extras::AppendInputs
24
+ include ActiveInteraction::Extras::FileBlobs
25
+
26
+ include ActiveInteraction::Extras::RunCallback
12
27
  end
@@ -0,0 +1,91 @@
1
+ module ActiveInteraction::Extras::AllRunner
2
+ extend ActiveSupport::Concern
3
+
4
+ class AllExecution
5
+ attr_reader :klass, :params
6
+
7
+ def initialize(klass:, params:)
8
+ @klass = klass
9
+ @params = params
10
+ end
11
+
12
+ def run(opts = {})
13
+ execute(opts) { klass.run(_1) }
14
+ end
15
+
16
+ def run!(opts = {})
17
+ execute(opts) { klass.run!(_1) }
18
+ end
19
+
20
+ def delay(**delay_opts)
21
+ DelayedExecution.new(klass:, params:, delay_opts:)
22
+ end
23
+
24
+ private
25
+
26
+ def noop
27
+ yield
28
+ end
29
+
30
+ def default_params
31
+ object_filters = klass.filters
32
+ .reject { _2.default? }
33
+ .select { _2.is_a?(ActiveInteraction::ObjectFilter) }
34
+ raise "only one object input allowed" if object_filters.size > 1
35
+ name, _ = object_filters.first
36
+
37
+ {
38
+ name => name.to_s.classify.constantize.all,
39
+ }
40
+ end
41
+
42
+ def execute(opts, around: method(:noop))
43
+ params = self.params.dup.reverse_merge(default_params)
44
+
45
+ name, scope = params.find { _2.is_a?(ActiveRecord::Relation) }
46
+ params.delete(name)
47
+
48
+ scope.find_in_batches do |batch|
49
+ around.call do
50
+ batch.each do |record|
51
+ yield(opts.merge(name => record, **params))
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ class DelayedExecution < AllExecution
59
+ attr_reader :delay_opts
60
+ def initialize(delay_opts: {}, **kw)
61
+ super(**kw)
62
+ @delay_opts = delay_opts
63
+ end
64
+
65
+ def run(opts = {})
66
+ require 'good_job'
67
+ around = ::GoodJob::Bulk.method(:enqueue)
68
+ execute(opts, around:) { klass.delay(delay_opts).run(_1) }
69
+ end
70
+
71
+ def run!(opts = {})
72
+ run(opts)
73
+ end
74
+ end
75
+
76
+ class_methods do
77
+ # class Service < ActiveInteraction::Base
78
+ # object :user
79
+ # def execute; end
80
+ # end
81
+ #
82
+ # @example
83
+ # Service.all.run!
84
+ # Service.all.run
85
+ # Service.all.delay.run!
86
+ # Service.all(user: User.all).delay(wait: 5.minutes).run!
87
+ def all(params = {})
88
+ AllExecution.new(klass: self, params:)
89
+ end
90
+ end
91
+ end