Permalink
Browse files

add interactor design experiment

  • Loading branch information...
1 parent 1b01d11 commit b4731d6e0bd18925fa3d35a8bc4058cd04ab860e @garybernhardt committed Jan 11, 2012
@@ -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.
+
@@ -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
+
@@ -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
+
@@ -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
+
@@ -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
+
@@ -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
+
@@ -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.