ActiveInteraction::Extras

Gem Version CI build

This gem contains the collection of useful extensions to active_interaction gem.

Installation

gem 'active_interaction-extras'

Basic Usage

# app/services/application_interaction.rb
class ApplicationInteraction < ActiveInteraction::Base
  include ActiveInteraction::Extras::All
end

Filters

These new filters are added automatically when gem is loaded.

Anything

Anything filter accepts as you guest it - anything.

class Service < ActiveInteraction::Base
  anything :model
end

UUID

class Service < ActiveInteraction::Base
  uuid :id
end

Filter Extensions

You can load all filter extensions with:

# config/initializers/active_interaction.rb
require 'active_interaction/extras/filter_extensions'

Hash: auto strip

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
end

Object: multiple classes

This extension allows using object filter with multiple classes.

class Service < ActiveInteraction::Base
  object :user, class: [User, AdminUser]
end

Extensions

Filter Alias

class Service < ActiveInteraction::Base
  include ActiveInteraction::Extras::FilterAlias

  hash :params, as: :user_attributes

  def execute
    user_attributes == params # => true
  end
end

Halt

class 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
end

ModelFields

class 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')

RunCallback

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
end

StrongParams

class 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)

Transaction

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 # => 0

Jobs

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).

ActiveJob

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)

Sidekiq

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 2

Rspec

class 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
end

Development

After 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.

Contributing

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.

License

The gem is available as open source under the terms of the MIT License.

Credits

Code of Conduct

Everyone interacting in the ActiveInteraction::Extras project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.