The Pavlov gem provides a Command/Query/Interactor framework.
Interactors make up the API for your application's backend. In a Rails application, this means that your controllers would call out to interactors and handle rendering, flashes, redirections etc. The interactors perform business logic like authorization, input validation. Your interactors would also handle things like after_create
callbacks to send an e-mail on signup. They only decide what to do, and call queries and commands to perform the actual work.
Queries and commands are used to manipulate your data store. This has several advantages:
- You can have queries that return objects that don't map directly to a specific database table.
- You can replace your database from SQL-based to MongoDB, Redis or even a webservice without having to touch your business logic.
For discussion about this gem, join #pavlov
on irc.freenode.net
. For convenience you can talk in #pavlov using webchat.freenode.net/.
Warning
This software is no longer maintained and depends on vulnerable packages; it remains here as an historical artifact.
All versions < 0.2 are to be considered alpha. We're working towards a stable version 0.2, following the readme as defined here. For now, unfortunately we don't support all features described here yet.
Currently unsupported functionality, which is already described below:
-
Context: For now use alpha_compatibility, and pass in
pavlov_options
as arguments. -
Interactions: Right now the
interactor
helper calls the interactor immediately, and doesn't return an interaction object.
Installation
Add this line to your application's Gemfile:
gem 'pavlov'
Then generate some initial files with:
rails generate pavlov:install
Usage
class Commands::CreateBlogPost
include Pavlov::Command
attribute :id, String
attribute :title, String
attribute :body, String
attribute :published, Boolean
private
def validate
errors.add(:id, "can't contain spaces") if id.include?(" ")
end
def execute
$redis.hmset("blog_post:#{id}", title: title, body: body, published: published)
$redis.sadd("blog_post_list", id)
end
end
class Queries::AvailableId
include Pavlov::Query
private
def execute
generate_uuid
end
def generate_uuid
SecureRandom.uuid
end
end
class Interactors::CreateBlogPost
include Pavlov::Interactor
attribute :title, String
attribute :body, String
attribute :published, Boolean, default: true
private
def authorized?
context.current_user.is_admin?
end
def validate
errors.add(:body, "NO SHOUTING!!!!") if body.matches?(/\W[A-Z]{2,}\W/)
end
def execute
command :create_blog_post, id: available_id,
title: title,
body: body,
published: published
Struct.new(:title, :body).new(title, body)
end
def available_id
query :available_id
end
end
class PostsController < ApplicationController
include Pavlov::Helpers
respond_to :json
def create
interactor :create_blog_post, params[:post] do |interaction|
if interaction.valid?
respond_with interaction.call
else
respond_with { errors: interaction.errors }
end
end
rescue AuthorizationError
flash[:error] = "Hacker, begone!"
redirect_to root_path
end
end
Attributes
Attributes work mostly like Virtus does. Attributes are always required unless they have a default value.
Validations
Authorization
Interactors must define a method authorized?
that determines if the interaction is allowed. If this method returns a truthy value, Pavlov will allow the interaction to be executed. This check is performed when interaction.call
is executed.
To help you determine whether operations are allowed, you can set up a global interaction context, which you can then access from your interactors:
class Interactors::CreateBlogPost
include Pavlov::Interactor
def authorized?
context.current_user.is_admin?
end
end
If the interaction is not authorized, a Pavlov::AuthorizationError
exception will be thrown. In normal execution you wouldn't expect this to ever occur, so might be reasonable to set up a global catch for this exception that redirects users to your homepage:
class ApplicationController
rescue_from Pavlov::AuthorizationError, with: :possible_hack_attempt
private
def possible_hack_attempt
logger.warn 'This might have been a hacker'
redirect_to root_path
end
end
Context
You probably have certain aspects of your application that you always, or at least very often, want to pass into the interactors, so that they can check authorization, either in terms of blocking unauthorized executions, or automatically scoping queries so that e.g. users will only see data belonging to their account.
class ApplicationController < ActionController::Base
include Pavlov::Helpers
before_filter :set_pavlov_context
private
def set_pavlov_context
context.add(:current_user, current_user)
end
end
In your tests, you could write:
describe CreateBlogPost do
include Pavlov::Helpers
let(:user) { mock("User", is_admin?: true) }
before { context.add(:current_user, user) }
it 'should create posts' do
interactor(:create_blog_post, title: 'Foo', body: 'Bar').call
# test for the creation
end
end
Is it any good?
Yes.
Related
If Pavlov happens not to be to your taste, you might look at these other libraries:
- Mutations provides service objects
- LightService provides service objects with shared context
- Interactor Like light-service, but also adds rollbacks
- Imperator provides command objects
- Wisper provides callbacks
Contributing
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Run bundle, before starting development.
- Implement your feature/bugfix and corresponding tests.
- Make sure your tests run against the latest stable mri.
- Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request