This gem contains the collection of useful extensions to active_interaction gem.
gem 'active_interaction-extras'# app/services/application_interaction.rb
class ApplicationInteraction < ActiveInteraction::Base
  include ActiveInteraction::Extras::All
endThese new filters are added automatically when gem is loaded.
Anything filter accepts as you guest it - anything.
class Service < ActiveInteraction::Base
  anything :model
endclass Service < ActiveInteraction::Base
  uuid :id
endYou can load all filter extensions with:
# config/initializers/active_interaction.rb
require 'active_interaction/extras/filter_extensions'This small extensions allows to accept full hashes without explicit strip option.
class Service < ActiveInteraction::Base
  hash :options_a, strip: false # (Before) Accept all keys
  
  hash :options_b # (After) Accept all keys
  
  hash :options_c do # (Before and After) Accept only specified keys
    string :name
  end
endThis extension allows using object filter with multiple classes.
class Service < ActiveInteraction::Base
  object :user, class: [User, AdminUser]
endclass Service < ActiveInteraction::Base
  include ActiveInteraction::Extras::FilterAlias
  
  hash :params, as: :user_attributes
  def execute
    user_attributes == params # => true
  end
endclass Service < ActiveInteraction::Base
  include ActiveInteraction::Extras::Halt
  def execute
    other_method
    puts('finished') # this won't be called
  end
  def other_method
    errors.add :base, :invalid
    halt! if errors.any?
    # or
    halt_if_errors!
  end
endclass UserForm < ActiveInteraction::Base
  include ActiveInteraction::Extras::ModelFields
  anything :user
  model_fields(:user) do
    string :first_name
    string :last_name
  end
  def execute
    model_fields(:user)                   # => {:first_name=>"Albert", :last_name=>"Balk"}
    any_changed?(:first_name, :last_name) # => true
    given_model_fields(:user)             # => {:first_name=>"Albert"}
    changed_model_fields(:user)           # => {:first_name=>"Albert"}
  end
end
user = OpenStruct.new(first_name: 'Sam', last_name: 'Balk')
UserForm.new(user: user).first_name # => 'Sam'
UserForm.run!(user: user, first_name: 'Albert')class Service < ActiveInteraction::Base
  include ActiveInteraction::Extras::RunCallback
  after_run do
    # LogAttempt.log
  end
  after_successful_run do
    # Email.deliver
  end
  after_failed_run do
    # NotifyAdminEmail.deliver
  end
  def execute
  end
endclass UpdateUserForm < ActiveInteraction::Base
  include ActiveInteraction::Extras::StrongParams
  string :first_name, default: nil, permit: true
  string :last_name, default: nil
  def execute
    first_name # => 'Allowed'
    last_name  # => nil
  end
end
UpdateUserForm.new.to_model.model_name.param_key # => 'update_user_form'
form_params = ActionController::Parameters.new(
  update_user_form: {
    first_name: 'Allowed',
    last_name: 'Not allowed',
  },
)
Service.run(params: form_params)
# OR
form_params = ActionController::Parameters.new(
  first_name: 'Allowed',
  last_name: 'Not allowed',
)
Service.run(form_params: form_params)class UpdateUserForm < ActiveInteraction::Base
  include ActiveInteraction::Extras::Transaction
  run_in_transaction!
  def execute
    Comment.create! # succeeds
    errors.add(:base, :invalid)
  end
end
UpdateUserForm.run
Comment.count # => 0You 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).
class ApplicationInteraction < ActiveInteraction::Base
  include ActiveInteraction::Extras::ActiveJob
  class Job < ActiveJob::Base
    include ActiveInteraction::Extras::ActiveJob::Perform
  end
end
class DoubleService < ApplicationInteraction
  integer :x
  def execute
    x + x
  end
end
DoubleService.delay.run(x: 2) # queues to run in background
DoubleService.delay(queue: 'low_priority', wait: 1.minute).run(x: 2)In ActiveJob mode delay method accepts anything ActiveJob set method does. (wait, wait_until, queue, priority)
You can use sidekiq directly if you need more control. Sidekiq integration comes with default GlobalID support.
class ApplicationInteraction < ActiveInteraction::Base
  include ActiveInteraction::Extras::Sidekiq
  class Job
    include Sidekiq::Worker
    include ActiveInteraction::Extras::Sidekiq::Perform
  end
end
class DoubleService < ApplicationInteraction
  job do
    sidekiq_options retry: 1 # configure sidekiq options
  end
  integer :x
  def execute
    x + x
  end
end
DoubleService.delay.run(x: 2) # queues to run in background
DoubleService.delay(queue: 'low_priority', wait: 1.minute).run(x: 2)In Sidekiq mode delay method accepts anything sidekiq set method does (queue, retry, backtrace, etc). Plus two additional wait and wait_until.
# Advance usage: retry based on given params
class DoubleService < ApplicationInteraction
  job do
    sidekiq_options(retry: ->(job) {
      params = deserialize_active_job_args(job)
      params[:x]
    })
  end
  integer :x
  def execute
    x + x
  end
end# Advance usage: Rescue the job but not service
class DoubleService < ApplicationInteraction
  job do
    def perform(*args)
      super
    rescue StandardError => e
      params = deserialize_active_job_args(args)
      params[:x]
    end
  end
  integer :x
  def execute
    raise
  end
end
DoubleService.run # => RuntimeError
DoubleService.delay.perform_now(x: 2) # => returns 2class SomeService < ActiveInteraction::Base
  integer :x
end
RSpec.describe SomeService do
  include ActiveInteraction::Extras::Rspec
  it 'works' do
    expect_to_execute(SomeService,
      with: [{ x: 1 }]
      return: :asd
    )
    result = SomeService.run! x: 1
    expect(result).to eq :asd
  end
  it 'lists all mocks' do
    # allow_to_run
    # allow_to_execute
    # allow_to_delay_run
    # allow_to_delay_execute
    # expect_to_run / expect_not_to_run / expect_to_not_run
    # expect_to_execute
    # expect_to_delay_run / expect_not_to_run_delayed / expect_to_not_run_delayed
    # expect_to_delay_execute
  end
endAfter checking out the repo, run bin/setup to install dependencies. You can also run bin/console for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.
Bug reports and pull requests are welcome on GitHub at https://github.com/antulik/active_interaction-extras. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
The gem is available as open source under the terms of the MIT License.
- ActiveInteraction::Extras is brought to you by Anton Katunin and was originally built at CarNextDoor.
- Further improvements to this gem brought to you by Anton Katunin once again and the Split Payments team.
Everyone interacting in the ActiveInteraction::Extras project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.