Make your Rails interactors clean, powerful, and less error-prone!
InteractorSupport
extends the Interactor pattern to make your business logic more concise, expressive, and robust.
- Automatic Validations β Validate inputs before execution
- Data Transformations β Trim, downcase, and sanitize with ease
- Transactional Execution β Keep data safe with rollback support
- Conditional Skipping β Skip execution based on logic
- Auto Record Lookup & Updates β Reduce boilerplate code
- Request Objects β Lean on ActiveModel for structured, validated inputs
- Custom RuboCop Cops β Enforce best practices
Add to your Gemfile:
bundle add interactor_support
Or install manually:
gem install interactor_support
Without InteractorSupport
class UpdateTodoTitle
include Interactor
def call
todo = Todo.find_by(id: context.todo_id)
return context.fail!(error: "Todo not found") if todo.nil?
todo.update!(title: context.title.strip, completed: context.completed)
context.todo = todo
end
end
Problems:
β Repetitive boilerplate
β Manual failure handling
β No automatic transformations
With InteractorSupport, your interactor is now elegant and expressive:
class UpdateTodoTitle
include Interactor
include InteractorSupport
requires :todo_id, :title
transform :title, with: :strip
find_by :todo, query: { id: :todo_id }, required: true
update :todo, attributes: { title: :title }
def call
context.message = "Todo updated!"
end
end
π What changed?
β
Self documenting validation using requires
β
Trimmed the title with transform
β
Automatic record lookup with find_by
β
Automatic update with update
class CompleteTodo
include Interactor
include InteractorSupport
transaction
requires :todo_id
find_by :todo, query: { id: :todo_id }, required: true
update :todo, attributes: {
completed: true,
completed_at: -> { Time.current }
}
end
β
Wraps the interactor in an active record transaction
β
Self documenting validation using requires
β
Automatic record lookup with find_by
β
Automatic update with update
using static values, and a context aware lambda.
class CompleteTodo
include Interactor
include InteractorSupport
transaction
requires :todo
skip if: -> { todo.completed? }
update :todo, attributes: { completed: true, completed_at: -> { Time.current } }
end
Instead of raw hashes, Request Objects provide validation, transformation, and structure.
- Works just like an ActiveRecord model
- Supports validations out of the box
- Automatically transforms & sanitizes data
class TodoRequest
include InteractorSupport::RequestObject
attribute :title, transform: :strip
attribute :email, transform: [:strip, :downcase]
attribute :completed, type: Boolean, default: false
validates :title, presence: true
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
end
Provides the update
method to automatically update records based on context.
# example incantations
update :todo, attributes: { title: :title } # -> context.todo.update!(title: context.title)
update :todo, attributes: { title: :title }, context_key: :updated_todo # -> context.updated_todo = context.todo.update!(title: context.title)
update :todo, attributes: { request: { title: :title } } # -> context.todo.update!(title: context.request.title)
update :todo, attributes: { request: [:title, :completed] } # -> context.todo.update!(title: context.request.title, completed: context.request.completed)
update :todo, attributes: :request # -> context.todo.update!(context.request)
update :todo, attributes: [:title, :completed] # -> context.todo.update!(title: context.title, completed: context.completed)
update :todo, attributes: { title: :title, completed: true, completed_at: -> { Time.zone.now } } # -> context.todo.update!(title: context.title, completed: true, completed_at: Time.zone.now)
Provides find_by
and find_where
to automatically locate records.
# example incantations
# Genre.find_by(id: context.genre_id)
find_by :genre, context_key: :current_genre
# lambdas are executed within the interactor's context, can be anything needed to compute at runtime
# Genre.find_by(name: context.name, created_at: context.some_context_value )
find_by :genre, query: { name: :name, created_at: -> { some_context_value } }
find_by :genre, query: { name: :name, created_at: -> { 7.days.ago...1.day.ago } }
# be careful here, this is not advisable.
find_by :genre, query: { name: :name, created_at: 7.days.ago...1.day.ago }
# Genre.find_by(name: context.name)
find_by :genre, query: { name: :name }
# context.current_genre = Genre.find_by(id: context.genre_id)
find_by :genre, context_key: :current_genre
# Genre.find_by(id: context.genre_id), fails the context if the result is nil
find_by :genre, required: true
# find_where
# Post.where(user_id: context.user_id)
find_where :post, where: { user_id: :user_id }
# Same as above, but will fail the context if the results are empty
find_where :post, where: { user_id: :user_id }, required: true
# lambdas are executed within the interactor's context
# Post.where(user_id: context.user_id, created_at: context.some_context_value )
find_where :post, where: { user_id: :user_id, created_at: -> { some_context_value } }
# Post.where(user_id: context.user_id).where.not(active: false)
find_where :post, where: { user_id: :user_id }, where_not: { active: false }
# Post.active.where(user_id: context.user_id)
find_where :post, where: { user_id: :user_id }, scope: :active
# context.user_posts = Post.where(user_id: context.user_id)
find_where :post, where: { user_id: :user_id }, context_key: :user_posts
Provides transform
to sanitize and normalize inputs.
Allows an interactor to skip execution if a condition is met.
Provides automatic input validation before execution.
Provides structured, validated request objects based on ActiveModel.
Enable them in .rubocop.yml
:
require:
- interactor_support/rubocop
Cop/RequireRequiredForInteractorSupport:
Enabled: true
Cop/UnusedIncludedModules:
Enabled: true
Cop/UsedUnincludedModules:
Enabled: true
Cop/RequireRequiredForInteractorSupport
registers an offense when not invoking required
when including InteractorSupport.
Since required both acts as documentation as to what context attributes are required for your Interactor or Organizer, and an attr_reader
for context values.
Cop/UnusedIncludedModules
a highly aggressive cop intended to promote lean interactors. It registers an offense when including modules that are unused in the interactor. eg: include InteractorSupport
includes all of the concerns, and validations. However, Request Objects are ignored.
Cop/UsedUnincludedModules
registers an offense when including Interactor, and invoking methods in InteractorSupport, but not including the correct module for it. This can give false positives if using another module that implements the same method name
as an InteractorSupport method, and intentionally not including InteractorSupport.
Pull requests are welcome on GitHub.
Released under the MIT License.