At first it seemed alright. A little business logic in a controller or model wasn't going to hurt anything. Then one day you wake up and you're surrounded by fat models and unwieldy controllers. Curled up and crying in the corner, you can't help but wonder how it came to this.
Take back control. Slim down models and wrangle monstrous controller methods with ActiveInteraction.
Read more on the project page or check out the full documentation on RubyDoc.info.
This project uses semantic versioning.
Add it to your Gemfile:
gem 'active_interaction', '~> 1.0'
And then execute:
$ bundle
Or install it yourself with:
$ gem install active_interaction
ActiveInteraction::Base lets you create interaction models. These
models ensure that certain inputs are provided and that those
inputs are in the format you want them in. If the inputs are valid
it will call execute
, store the return value of that method in
result
, and return an instance of your ActiveInteraction::Base
subclass. Let's look at a simple example:
# Define an interaction that signs up a user.
class UserSignup < ActiveInteraction::Base
# required
string :email, :name
# optional
boolean :newsletter_subscribe, default: nil
# ActiveRecord validations
validates :email, format: EMAIL_REGEX
# The execute method is called only if the inputs validate. It
# does your business action. The return value will be stored in
# `result`.
def execute
user = User.create!(email: email, name: name)
if newsletter_subscribe
NewsletterSubscriptions.create(email: email, user_id: user.id)
end
UserMailer.async(:deliver_welcome, user.id)
user
end
end
# In a controller action (for instance), you can run it:
def new
@signup = UserSignup.new
end
def create
@signup = UserSignup.run(params[:user])
# Then check to see if it worked:
if @signup.valid?
redirect_to welcome_path(user_id: signup.result.id)
else
render action: :new
end
end
You may have noticed that ActiveInteraction::Base quacks like
ActiveRecord::Base. It can use validations from your Rails application
and check option validity with valid?
. Any errors are added to
errors
which works exactly like an ActiveRecord model. Additionally,
everything within the execute
method is run in a transaction if
ActiveRecord is available.
There are two way to call an interaction. Given UserSignup, you can do this:
outcome = UserSignup.run(params)
if outcome.valid?
# Do something with outcome.result...
else
# Do something with outcome.errors...
end
Or, you can do this:
result = UserSignup.run!(params)
# Either returns the result of execute,
# or raises ActiveInteraction::InvalidInteractionError
Interactions only accept a Hash for run
and run!
.
# A user comments on an article
class CreateComment < ActiveInteraction::Base
model :article, :user
string :comment
validates :comment, length: { maximum: 500 }
def execute; ...; end
end
def somewhere
outcome = CreateComment.run(
comment: params[:comment],
article: Article.find(params[:article_id]),
user: current_user
)
end
-
Subclass ActiveInteraction::Base
class YourInteraction < ActiveInteraction::Base # ... end
-
Define your attributes:
string :name, :state integer :age boolean :is_special model :account array :tags, default: nil do string end hash :prefs, default: nil do boolean :smoking boolean :view end date :arrives_on, default: Date.today date :departs_on, default: Date.tomorrow
-
Use any additional validations you need:
validates :name, length: { maximum: 10 } validates :state, inclusion: { in: %w(AL AK AR ... WY) } validate :arrives_before_departs private def arrive_before_departs if departs_on <= arrives_on errors.add(:departs_on, 'must come after the arrival time') end end
-
Define your execute method. It can return whatever you like:
def execute record = do_thing(...) # ... record end
Check out the documentation for a full list of methods.
You can run interactions from within other interactions by calling compose
.
If the interaction is successful, it'll return the result (just like if you had
called it with run!
). If something went wrong, execution will halt
immediately and the errors will be moved onto the caller.
class AddThree < ActiveInteraction::Base
integer :x
def execute
compose(Add, x: x, y: 3)
end
end
AddThree.run!(x: 5)
# => 8
To bring in filters from another interaction, use import_filters
. Combined
with inputs
, delegating to another interaction is a piece of cake.
class AddAndDouble < ActiveInteraction::Base
import_filters Add
def execute
compose(Add, inputs) * 2
end
end
ActiveInteraction is i18n-aware out of the box! All you have to do
is add translations to your project. In Rails, they typically go
into config/locales
. So, for example, let's say that (for whatever
reason) you want to print out everything backwards. Simply add
translations for ActiveInteraction to your hsilgne
locale:
# config/locales/hsilgne.yml
hsilgne:
active_interaction:
types:
array: yarra
boolean: naeloob
date: etad
date_time: emit etad
file: elif
float: taolf
hash: hsah
integer: regetni
model: ledom
string: gnirts
time: emit
errors:
messages:
invalid: dilavni si
invalid_nested: '%{type} dilav a ton si'
missing: deriuqer si
Then set your locale and run an interaction like normal:
I18n.locale = :hsilgne
class Interaction < ActiveInteraction::Base
boolean :a
def execute; end
end
p Interaction.run.errors.messages
# => {:a=>["deriuqer si"]}
ActiveInteraction is brought to you by @AaronLasseigne and @tfausak from @orgsync. We were inspired by the fantastic work done in Mutations.