Skip to content

Commit

Permalink
add interactor design experiment
Browse files Browse the repository at this point in the history
  • Loading branch information
garybernhardt committed Jan 11, 2012
1 parent 1b01d11 commit b4731d6
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 0 deletions.
43 changes: 43 additions & 0 deletions design_experiment/README
@@ -0,0 +1,43 @@
This is a thought experiment for how Raptor apps might be designed. The module
hierarchy is:

Routes
Injectables
.current_user
.post_params
Interactors
CreatePost
PostSaved
ValidationFailure
Models
User
AnonymousUser
Post
Records
User
Post

Responsibility breakdown:
- Routes are the same Raptor routes as ever. Then can route out to different
actions/redirects based on the interactor's response.
- Injectables provide objects for Raptor's implicit DI. For example, the
`post_params` injectable does what it sounds like, so that
Interactors::CreatePost can take a `post_params` argument without worrying
about where it came from. These are class methods directly on the
Injectables module.
- Interactors contain the application's business logic. They return (or
raise) response models that are defined directly in the interactor's
class (or maybe from a shared location if appropriate).
- Models wrap database records to provide simple mutations and data
wrappers. For example, Post#publish updates the published flag and saves.
These don't necessarily map to records one-to-one. For example, the
AnonymousUser model exists to avoid nil current_user, and doesn't map to
anything in the database.
- Records are straight-up database records. No methods; just field
definitions.

This hierarchy doesn't correspond to the file layout. The files are grouped by
topic. For example, posts.rb contains both Interactors::CreatePost and the
post_params injectable. The modules in the system will be reopened many times
as code is loaded. This gives us lasagna files instead of ravioli files.

28 changes: 28 additions & 0 deletions design_experiment/models.rb
@@ -0,0 +1,28 @@
module Models
class User
extend Raptor::Model
delegate [:email, :posts] => :@record

def anonymous?; false; end
end

class AnonymousUser
def anonymous?; true; end
end

class Post
extend Raptor::Model
delegate [:title, :body] => :@record

def publish
@record.update_attributes(:published => true)
@record.save!
end

def save_as_draft
@record.update_attributes(:published => false)
@record.save!
end
end
end

30 changes: 30 additions & 0 deletions design_experiment/posts.rb
@@ -0,0 +1,30 @@
require "raptor/shorty"

module Injectables
def self.post_params(params)
params.fetch(:post)
end
end

module Interactors
class CreatePost
class PostSaved < Struct.new(:current_user, :post); end
class ValidationFailure < RuntimeError
takes :current_user, :post
end

def self.create(current_user, post_params)
post = Models::Post.new(post_params)
raise ValidationFailure.new(current_user, post) unless post.valid?

if current_user.admin?
post.publish
else
post.save_as_draft
end

PostSaved.new(current_user, post)
end
end
end

46 changes: 46 additions & 0 deletions design_experiment/posts_spec.rb
@@ -0,0 +1,46 @@
require_relative "posts"

module Models; class Post; end; end

describe Interactors::CreatePost do
Post = Models::Post
CreatePost = Interactors::CreatePost
PostSaved = Interactors::CreatePost::PostSaved
ValidationFailure = Interactors::CreatePost::ValidationFailure

let(:post_params) { stub(:post_params) }

context "when the post is valid" do
let(:post) { stub(:post, :valid? => true) }
before { Post.stub(:new).with(post_params) { post } }

it "publishes the post if the user is an admin" do
user = stub(:user, :admin? => true)
post.should_receive(:publish)
CreatePost.create(user, post_params)
end

it "saves the post as a draft if the user isn't an admin" do
user = stub(:user, :admin? => false)
post.should_receive(:save_as_draft)
CreatePost.create(user, post_params)
end

it "returns a post saved response" do
user = stub(:user, :admin? => false)
post.stub(:save_as_draft)
response = CreatePost.create(user, post_params)
response.should be_a PostSaved
end
end

it "raises a validation failure when the post is invalid" do
user = stub(:user)
post = stub(:post, :valid? => false)
Post.stub(:new).with(post_params) { post }
expect do
CreatePost.create(user, post_params)
end.to raise_error(ValidationFailure)
end
end

13 changes: 13 additions & 0 deletions design_experiment/records.rb
@@ -0,0 +1,13 @@
module Records
class User < SomeORM::Record
value :email
list :posts => "Post"
end

class Post < SomeORM::Record
value :title
value :body
reference :author => "User"
end
end

9 changes: 9 additions & 0 deletions design_experiment/routes.rb
@@ -0,0 +1,9 @@
Routes = Raptor.routes do
path "posts" do
# We could also scope all route targets inside Interactors, allowing this
# to say :to => "CreatePost.create".
create :to => "Interactors::CreatePost.create",
:ValidationFailure => render(:new)
end
end

12 changes: 12 additions & 0 deletions design_experiment/users.rb
@@ -0,0 +1,12 @@
module Injectables
def current_user(session)
begin
id = session.fetch(:user_id)
rescue KeyError
AnonymousUser.new
else
Models::User.find(id)
end
end
end

0 comments on commit b4731d6

Please sign in to comment.