Writing resources_controller: Discovering REST Patterns in Rails

  • 1,150 views
Uploaded on

RailsConf Europe 2008 - Ian White wrote resources_controller in the beginning of 2007 to DRY up RESTful controllers. In its first iteration it simplified controllers by providing CRUD actions, and …

RailsConf Europe 2008 - Ian White wrote resources_controller in the beginning of 2007 to DRY up RESTful controllers. In its first iteration it simplified controllers by providing CRUD actions, and loading enclosing resources.

  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Be the first to comment
No Downloads

Views

Total Views
1,150
On Slideshare
0
From Embeds
0
Number of Embeds
0

Actions

Shares
Downloads
62
Comments
0
Likes
6

Embeds 0

No embeds

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
    No notes for slide

Transcript

  • 1. Writing resources_controller Discovering REST patterns in Rails Ian White ian.w.white@gmail.com @ Argument from Design ardes.com
  • 2. What I’ll cover ardes.com
  • 3. What I’ll cover  Example of RESTful app in vanilla Rails ardes.com
  • 4. What I’ll cover  Example of RESTful app in vanilla Rails  How was that experience? Where did we have to repeat ourselves? ardes.com
  • 5. What I’ll cover  Example of RESTful app in vanilla Rails  How was that experience? Where did we have to repeat ourselves?  Plugins to the rescue! ardes.com
  • 6. What I’ll cover  Example of RESTful app in vanilla Rails  How was that experience? Where did we have to repeat ourselves?  Plugins to the rescue!  resources_controller ardes.com
  • 7. What I’ll cover  Example of RESTful app in vanilla Rails  How was that experience? Where did we have to repeat ourselves?  Plugins to the rescue!  resources_controller  Benefits of abstraction ardes.com
  • 8. What I’ll cover  Example of RESTful app in vanilla Rails  How was that experience? Where did we have to repeat ourselves?  Plugins to the rescue!  resources_controller  Benefits of abstraction  Where to next? ardes.com
  • 9. REST on Rails ardes.com
  • 10. REST on Rails  Lets look at what’s involved in creating a simple RESTful app using Rails ardes.com
  • 11. REST on Rails  Lets look at what’s involved in creating a simple RESTful app using Rails  We’ll then look at adding features ardes.com
  • 12. Video sharing app ardes.com
  • 13. Video sharing app  Users can contribute Videos ardes.com
  • 14. Video sharing app  Users can contribute Videos  Videos can be categorised in many Categories ardes.com
  • 15. Video sharing app class User < ActiveRecord::Base  has_manycan contribute Videos Users :videos end  Videos can be categorised in many Categories class Categorisation < ActiveRecord::Base belongs_to :video belongs_to :category end class Category < ActiveRecord::Base has_many :categorisations has_many :videos, :through => :categorisations end class Video belongs_to :user has_many :categorisations has_many :categories, :through => :categorisations ardes.com end
  • 16. Video sharing app ardes.com
  • 17. Video sharing app  Our API? ardes.com
  • 18. Video sharing app  Our API?  Lets define the URLs ardes.com
  • 19. Video sharing app  Our API?  Lets define the URLs  we want to see all videos: /videos ardes.com
  • 20. Video sharing app  Our API?  Lets define the URLs  we want to see all videos: /videos  we want to see all categories: /categories ardes.com
  • 21. Video sharing app  Our API?  Lets define the URLs  we want to see all videos: /videos  we want to see all categories: /categories  we want to look at videos in the context of one of their categories: /categories/:category_id/videos/:id ardes.com
  • 22. Video sharing app  Our API?  Lets define the URLs  we want to see all videos: /videos  we want to see all categories: /categories  we want to look at videos in the context of one of their categories: /categories/:category_id/videos/:id  WAIT! does this violate some community norm about nesting? ardes.com
  • 23. Video sharing app  Our API?  Lets define the URLs  we want to see all videos: /videos  we want to see all categories: /categories  we want to look at videos in the context of one of their categories: /categories/:category_id/videos/:id  WAIT! does this violate some community norm about nesting?  No - in this case, it’s necessary, because videos can be categorised in many categories, and because we just want to show the video in the context of a category that the user has selected. ardes.com
  • 24. Video sharing app  Our API?  Lets define the URLs  we want to see all videos: /videos  we want to see all categories: /categories  we want to look at videos in the context of one of their categories: /categories/:category_id/videos/:id  WAIT! does this violate some community norm about map.resources :videos nesting?  No - in this case, it’s necessary, because videos can map.resources in:categories and because we be categorised many categories, do |category| category.resources in the context of a category just want to show the video :videos that the user has selected. end ardes.com
  • 25. Video sharing app ardes.com
  • 26. Video sharing app  Because this is a community site, lets not have admin namespaced controllers just yet ardes.com
  • 27. Video sharing app  Because this is a community site, lets not have admin namespaced controllers just yet  We want users to be able submit videos in the context of a category, and have that categorisation automatically applied ardes.com
  • 28. Video sharing app  Because this is a community site, lets not have admin namespaced controllers just yet  We want users to be able submit videos in the context of a category, and have that categorisation automatically applied  What controllers do we need? ardes.com
  • 29. Video sharing app  Because this is a community site, lets not have admin namespaced controllers just yet class  We want users to be able submit videos in the context VideosController < ApplicationController of a category, and have that categorisation # GET /videos, GET /videos.xml automatically applied def index  What= Video.find(:all) @videos controllers do we need? respond_to do |format| format.html # index.html.erb format.xml { render :xml => @videos } end end # And the rest of CRUD ... end ardes.com
  • 30. Video sharing app  Because this is a community site, lets not have admin namespaced controllers just yet class  We want users to be able submit videos in the context VideosController < ApplicationController of a category, and have that categorisation # GET /videos, GET /videos.xml automatically applied def index  What= Video.find(:all) controllers do we need? class CategoriesController < ApplicationControl @videos respond_to /categories, GET /categories.xml # GET do |format| def index format.html # index.html.erb @categories = Category.find(:all) format.xml { render :xml => @videos } end respond_to do |format| end format.html # index.html.erb format.xml { render :xml => @categories } end # And the rest of CRUD ... end end ardes.com # And the rest of CRUD ...
  • 31. class CategoryVideosController < ApplicationC Video sharing app before_filter :load_category # GET /categories/:category_id/videos  Because this is a community site, lets not have admin # GET /categories/:category_id/videos.xml namespaced controllers just yet def index  We want users to be able submit videos in the context class VideosController < ApplicationController of a category, and=have that categorisation @videos @category.videos.find(:all) # GET /videos, GET /videos.xml automatically applied do |format| respond_to def index format.html # index.html.erb  What= Video.find(:all) controllers do we need? class CategoriesController < ApplicationControl @videos # GET /categories,{ GET /categories.xml format.xml respond_to do |format| render :xml => @videos } end def index format.html # index.html.erb end @categories = Category.find(:all) format.xml { render :xml => @videos } end respond_to do |format| # And the rest of CRUD ... end format.html # index.html.erb format.xml { render :xml => @categories } protected end # And the def load_category rest of CRUD ... end end @category = Category.find(params[:categor ardes.com end # And the rest of CRUD ...
  • 32. map.resources :videos class CategoryVideosController < ApplicationC map.resourcesbefore_filter |category| :categories do :load_category Video sharing app category.resources :videos, :controller => 'category_videos' end # GET /categories/:category_id/videos  Because this is a community site, lets not have admin # GET /categories/:category_id/videos.xml namespaced controllers just yet def index  We want users to be able submit videos in the context class VideosController < ApplicationController of a category, and=have that categorisation @videos @category.videos.find(:all) # GET /videos, GET /videos.xml automatically applied do |format| respond_to def index format.html # index.html.erb  What= Video.find(:all) controllers do we need? class CategoriesController < ApplicationControl @videos # GET /categories,{ GET /categories.xml format.xml respond_to do |format| render :xml => @videos } end def index format.html # index.html.erb end @categories = Category.find(:all) format.xml { render :xml => @videos } end respond_to do |format| # And the rest of CRUD ... end format.html # index.html.erb format.xml { render :xml => @categories } protected end # And the def load_category rest of CRUD ... end end @category = Category.find(params[:categor ardes.com end # And the rest of CRUD ...
  • 33. map.resources :videos class CategoryVideosController < ApplicationC map.resourcesbefore_filter |category| :categories do :load_category Video sharing app category.resources :videos, :controller => 'category_videos' end # GET /categories/:category_id/videos  Because this is a community site, lets not have admin # GET /categories/:category_id/videos.xml namespaced controllers just yet def index  We want users to be able submit videos in the context class VideosController < ApplicationController of a category, and=have that categorisation @videos @category.videos.find(:all) # GET /videos, GET /videos.xml automatically applied do |format| respond_to def index format.html # index.html.erb  What= Video.find(:all) controllers do we need? class CategoriesController < ApplicationControl @videos # GET /categories,{ GET /categories.xml format.xml respond_to do |format| render :xml => @videos } end def index format.html # index.html.erb class Video < ActiveRecord::Base attr_accessor end @categories = Category.find(:all) :category_for_new_record # and a create hook to create the:xml => @videos } format.xml { render categorisation end end respond_to do |format| # And the rest of CRUD ... end format.html # index.html.erb class Category < format.xml { render :xml => @categories } ActiveRecord::Base protected has_many :videos, :through => :categorisations do end #def new(*args) load_category And the defrest of CRUD ... end @category = Category.find(params[:categor end # pass proxy_owner through as :category_for_new_record end ardes.com end end end # And the rest of CRUD ...
  • 34. Video sharing app ardes.com
  • 35. Video sharing app  What views do we need? ardes.com
  • 36. Video sharing app  What views do we need?  CRUD stuff ardes.com
  • 37. Video sharing app  What views do we need?  CRUD stuff  What about videos vs category_videos? ardes.com
  • 38. Video sharing app  What views do we need?  CRUD stuff  What about videos vs category_videos?  Can use partials to factor out common stuff - but there’s a cruical difference throughout ardes.com
  • 39. Video sharing app  What views do we need?  CRUD stuff  What about videos vs category_videos?  Can use partials to factor out common stuff - but there’s a cruical difference throughout  In videos we want links and forms to new_video, video ardes.com
  • 40. Video sharing app  What views do we need?  CRUD stuff  What about videos vs category_videos?  Can use partials to factor out common stuff - but there’s a cruical difference throughout  In videos we want links and forms to new_video, video  In category_videos we want links to new_category_video, and category_video. ardes.com
  • 41. Video sharing app  What views do we need?  CRUD stuff  What about videos vs category_videos?  Can use partials to factor out common stuff - but there’s a cruical difference throughout  In videos we want links and forms to new_video, video  In category_videos we want links to new_category_video, and category_video.  We could use polymorphic_url, but we still need to give it different arguments. (just the same as for the controllers) ardes.com
  • 42. Video sharing app views/categories/index.html.erb  What views do we need? views/categories/show.html.erb  CRUD stuff views/categories/edit.html.erb views/categories/new.html.erb  What about videos vs category_videos?  Can use partials to factor out common stuff - but there’s a cruical difference throughout  In videos we want links and forms to new_video, video  In category_videos we want links to new_category_video, and category_video.  We could use polymorphic_url, but we still need to give it different arguments. (just the same as for the controllers) ardes.com
  • 43. Video sharing app views/categories/index.html.erb  What views do we need? views/categories/show.html.erb  CRUD stuff views/categories/edit.html.erb views/categories/new.html.erb  What about videos vs category_videos?  Can use partials to factor out common stuff - but views/videos/index.html.erbthroughout there’s a cruical difference views/videos/show.html.erb forms to new_video,  In videos we want links and views/videos/edit.html.erb video views/videos/new.html.erb want links to  In category_videos we new_category_video, and category_video.  We could use polymorphic_url, but we still need to give it different arguments. (just the same as for the controllers) ardes.com
  • 44. Video sharing app views/categories/index.html.erb  What views do we need? views/categories/show.html.erb  CRUD stuff views/categories/edit.html.erb views/categories/new.html.erb  What about videos vs category_videos?  Can use partials to factor out common stuff - but views/videos/index.html.erbthroughout there’s a cruical difference views/videos/show.html.erb forms to new_video,  In videos we want links and views/videos/edit.html.erb video views/videos/new.html.erb want links to  In category_videos we new_category_video, and category_video. views/category_videos/index.html.erb still need to  We could use polymorphic_url, but we views/category_videos/show.html.erb give it different arguments. (just the same as for the controllers) views/category_videos/edit.html.erb views/category_videos/new.html.erb ardes.com
  • 45. Video sharing app ardes.com
  • 46. Video sharing app  With vanilla Rails, it’s not too bad ardes.com
  • 47. Video sharing app  With vanilla Rails, it’s not too bad  4 models, 3 controllers, 12 views, 3 lines of routes ardes.com
  • 48. Video sharing app  With vanilla Rails, it’s not too bad  4 models, 3 controllers, 12 views, 3 lines of routes  But, it doesn’t feel very DRY, does it? ardes.com
  • 49. Video sharing app  With vanilla Rails, it’s not too bad  4 models, 3 controllers, 12 views, 3 lines of routes  But, it doesn’t feel very DRY, does it?  Let’s see what happens if we want to add some features ardes.com
  • 50. Video sharing app: new feature ardes.com
  • 51. Video sharing app: new feature  We want to be able browse videos in the context of the user that submitted them (similar to the way we can browse by category) ardes.com
  • 52. Video sharing app: new feature  We want to be able browse videos in the context of the user that submitted them (similar to the way we can browse by category)  (Lets assume we’ve added a UsersController and views for browsing users themselves) ardes.com
  • 53. map.resources :videos map.resources :categories do |category| category.resources :videos, :controller => 'category_videos' end Video sharing app: new feature map.resources :users do |user| user.resources :videos, :controller => 'user_videos' end  We want to be able browse videos in the context of the user that submitted them (similar to the way we can browse by category)  (Lets assume we’ve added a UsersController and views for browsing users themselves) ardes.com
  • 54. map.resources :videos map.resources :categories do |category| category.resources :videos, :controller => 'category_videos' end class UserVideosControllerapp: new feature Video sharing < ApplicationController before_filter :load_user map.resources :users do |user| user.resources :videos, :controller => 'user_videos' # GET /users/:user_id/videos end  We want /users/:user_id/videos.xml the context # GET to be able browse videos in of the user that submitted them (similar to the def index way we can browse by category) @videos = @user.videos.find(:all)  (Letsrespond_to do |format| UsersController assume we’ve added a and views for browsing users themselves) format.html # index.html.erb format.xml { render :xml => @videos } end end # And the rest of CRUD ... protected def load_user @user = User.find(params[:user_id]) end ardes.com end
  • 55. map.resources :videos map.resources :categories do |category| category.resources :videos, :controller => 'category_videos' end class UserVideosControllerapp: new feature Video sharing < ApplicationController before_filter :load_user map.resources :users do |user| user.resources :videos, :controller => 'user_videos' # GET /users/:user_id/videos end  We want /users/:user_id/videos.xml the context # GET to be able browse videos in of the user that submitted them (similar to the def index way we can browse by category) @videos = @user.videos.find(:all)  (Letsrespond_to do |format| UsersController assume we’ve added a and views for browsing users themselves) format.html # index.html.erb format.xml { render :xml => @videos } end end # And the rest of CRUD ... views/user_videos/index.html.erb protected views/user_videos/show.html.erb def load_user views/user_videos/edit.html.erb @user = User.find(params[:user_id]) views/user_videos/new.html.erb end ardes.com end
  • 56. Video sharing app: new feature ardes.com
  • 57. Video sharing app: new feature  Another controller, and 4 more views, not bad ardes.com
  • 58. Video sharing app: new feature  Another controller, and 4 more views, not bad  But our videos controllers all look very similar ardes.com
  • 59. Video sharing app: new feature  Another controller, and 4 more views, not bad  But our videos controllers all look very similar  How about a superclass abstraction? ardes.com
  • 60. Video sharing app: new feature  Another controller, and 4 more views, not bad  But our videos controllers all look very similar  How about a superclass abstraction?  Problem: the action methods don’t look very easy to abstract ardes.com
  • 61. # VideosController (simplified) def destroy @video = Video.find(params[:id]) Video sharing app: @video.destroy new feature redirect_to videos_path end  Another controller, and 4 more views, not bad  But our videos controllers all look very similar  How about a superclass abstraction?  Problem: the action methods don’t look very easy to abstract ardes.com
  • 62. # VideosController (simplified) def destroy @video = Video.find(params[:id]) Video sharing app: @video.destroy new feature redirect_to videos_path end  Another controller, and 4 more views, not bad # CategoryVideosController def destroy videos controllers all look very similar But our @video = @category.videos.find(params[:id])  How about a superclass abstraction? @video.destroy action methods don’t look very easy  Problem: the redirect_to category_videos_path(@category) to abstract end ardes.com
  • 63. # VideosController (simplified) def destroy @video = Video.find(params[:id]) Video sharing app: @video.destroy new feature redirect_to videos_path end  Another controller, and 4 more views, not bad # CategoryVideosController def destroy videos controllers all look very similar But our @video = @category.videos.find(params[:id])  How about a superclass abstraction? @video.destroy action methods don’t look very easy  Problem: the redirect_to category_videos_path(@category) to abstract end # UserVideosController def destroy @video = @user.videos.find(params[:id]) @video.destroy redirect_to user_videos_path(@user) end ardes.com
  • 64. Video sharing app: new feature ardes.com
  • 65. Video sharing app: new feature  Users should be able to comment on videos, users, and categories ardes.com
  • 66. Video sharing app: new feature  Users should be able to comment on videos, users, and categories  This looks like a job for :polymorphic ardes.com
  • 67. Video sharing app: new feature class User < ActiveRecord::Base has_many :comments, :as => :commentable  Users should be able to comment on end videos, users, and categories  Video < ActiveRecord::Base class This looks like a job for :polymorphic has_many :comments, :as => :commentable end class Category < ActiveRecord::Base has_many :comments, :as => :commentable end class Comment belongs_to :commentable, :polymorphic => true end ardes.com
  • 68. Video sharing app: new feature ardes.com
  • 69. Video sharing app: new feature  Now, one controller - or three controllers ardes.com
  • 70. Video sharing app: new feature  Now, one controller - or three controllers  If we have one controller we face these problems ardes.com
  • 71. Video sharing app: new feature  Now, one controller - or three controllers  If we have one controller we face these problems - how to load up the comment? ardes.com
  • 72. Video sharing app: new feature  Now, one controller - or three controllers  If we have one controller we face these problems - how to load up the comment? - how to redirect to the commentable? ardes.com
  • 73. Video sharing app: new feature  Now, one controller - or three controllers  If we have one controller we face these problems - how to load up the comment? - how to redirect to the commentable?  So lets try three controllers ardes.com
  • 74. map.resources :videos do |video| video.resources :comments, :controller => 'video_comments' end map.resources :categories do sharing app: new feature Video |category| category.resources :videos, :controller => 'category_videos' category.resources :comments, :controller => 'category_comments' end  Now, one controller - or three controllers map.resources :users do |user|  If we have one controller we face these problems user.resources :videos, :controller => 'user_videos' - how to load up :controller => user.resources :comments, the comment? 'user_comments' end - how to redirect to the commentable?  So lets try three controllers ardes.com
  • 75. map.resources :videos do |video| video.resources :comments, :controller => 'video_comments' end map.resources :categories do sharing app: new feature Video |category| category.resources :videos, :controller => 'category_videos' category.resources :comments, :controller => 'category_comments' end class UserCommentsController < ApplicationController  Now, one controller - or three controllers before_filter :load_user map.resources :users do |user|  def index If we have one controller we face these problems user.resources :videos, :controller => 'user_videos' @comments = @user.comments.find(:all) - how to load up :controller => user.resources :comments, the comment? 'user_comments' respond_to do |format| format.html # index.html.erb end - how to redirect to the commentable? format.xml { render :xml => @comments } end end  So lets try three controllers # And the rest of CRUD ... protected def load_user @user = User.find(params[:user_id]) end end ardes.com
  • 76. map.resources :videos do |video| video.resources :comments, :controller => 'video_comments' end map.resources :categories do sharing app: new feature Video |category| category.resources :videos, :controller => 'category_videos' category.resources :comments, :controller => 'category_comments' end class UserCommentsController < ApplicationController  Now, one controller - or three controllers before_filter :load_user map.resources :users do |user| ApplicationController  def index If we have one controller we face these problems class CategoryCommentsController < user.resources :videos, :controller => 'user_videos' before_filter :load_category @comments = @user.comments.find(:all) - how to load up :controller => user.resources :comments, the comment? 'user_comments' respond_to do |format| def index format.html # index.html.erb end - how |format| end respond_to doto redirect to the commentable? @comments = @category.comments.find(:all) format.xml { render :xml => @comments } end  format.html{try three controllers} So lets #render :xml => @comments format.xml index.html.erb # And end rest of CRUD ... the end protected def # And the rest of CRUD ... load_user @user = User.find(params[:user_id]) protected end end def load_category @category = Category.find(params[:category_id]) end end ardes.com
  • 77. map.resources :videos do |video| video.resources :comments, :controller => 'video_comments' end map.resources :categories do sharing app: new feature Video |category| category.resources :videos, :controller => 'category_videos' category.resources :comments, :controller => 'category_comments' end class UserCommentsController < ApplicationController  Now, one controller - or three controllers before_filter :load_user map.resources :users do |user| ApplicationController  def index If we have one controller we face these problems class CategoryCommentsController < user.resources :videos, :controller => 'user_videos' before_filter :load_category @comments = @user.comments.find(:all) - VideoCommentsController < ApplicationController user.resources :comments, the comment? 'user_comments' class how to load up :controller => respond_to do |format| def index format.html # index.html.erb end before_filter :load_video - how |format| end respond_to doto redirect to the commentable? @comments = @category.comments.find(:all) format.xml { render :xml => @comments } def index format.html # index.html.erb end  format.xml {try@video.comments.find(:all) So lets =render :xmlcontrollers} @comments three => @comments the respond_to do |format| # And end rest of CRUD ... end format.html # index.html.erb protected format.xml { render :xml => @comments } end def # And the rest of CRUD ... load_user end @user = User.find(params[:user_id]) protected end end # And the rest of CRUD ... def load_category @category = Category.find(params[:category_id]) endprotected end def load_video @video = Video.find(params[:video_id]) end end ardes.com
  • 78. map.resources :videos do |video| video.resources :comments, :controller => 'video_comments' end map.resources :categories do sharing app: new feature Video |category| views/user_comments/index.html.erb category.resources :videos, :controller => 'category_videos' views/user_comments/show.html.erb category.resources :comments, :controller => 'category_comments' views/user_comments/edit.html.erb end class UserCommentsController < ApplicationController views/user_comments/new.html.erb  Now, one controller - or three controllers before_filter :load_user map.resources :users do |user| ApplicationController  def index If we have one controller we face these problems class CategoryCommentsController < user.resources :videos, :controller => 'user_videos' before_filter :load_category @comments = @user.comments.find(:all) - VideoCommentsController < ApplicationController user.resources :comments, the comment? 'user_comments' class how to load up :controller => respond_to do |format| def index format.html # index.html.erb end before_filter :load_video - how |format| end respond_to doto redirect to the commentable? @comments = @category.comments.find(:all) format.xml { render :xml => @comments } def index format.html # index.html.erb end  format.xml {try@video.comments.find(:all) So lets =render :xmlcontrollers} @comments three => @comments the respond_to do |format| # And end rest of CRUD ... end format.html # index.html.erb protected format.xml { render :xml => @comments } end def # And the rest of CRUD ... load_user end @user = User.find(params[:user_id]) protected end end # And the rest of CRUD ... def load_category @category = Category.find(params[:category_id]) endprotected end def load_video @video = Video.find(params[:video_id]) end end ardes.com
  • 79. map.resources :videos do |video| video.resources :comments, :controller => 'video_comments' end map.resources :categories do sharing app: new feature Video |category| views/user_comments/index.html.erb category.resources :videos, :controller => 'category_videos' views/user_comments/show.html.erb category.resources :comments, :controller => 'category_comments' views/user_comments/edit.html.erb end class UserCommentsController < ApplicationController views/user_comments/new.html.erb  Now, one controller - or three controllers before_filter :load_user map.resources :users do |user| views/category_comments/index.html.erb  def index If we have one controller we face these problems class CategoryCommentsController < ApplicationController user.resources :videos, :controller => 'user_videos' before_filter :load_category views/category_comments/show.html.erb @comments = @user.comments.find(:all) - VideoCommentsController < ApplicationController user.resources :comments, the comment? 'user_comments' class how to load up :controller => respond_to do |format| def index format.html # index.html.erb views/category_comments/edit.html.erb end before_filter :load_video views/category_comments/new.html.erb - how |format| end respond_to doto redirect to the commentable? @comments = @category.comments.find(:all) format.xml { render :xml => @comments } def index format.html # index.html.erb end  format.xml {try@video.comments.find(:all) So lets =render :xmlcontrollers} @comments three => @comments the respond_to do |format| # And end rest of CRUD ... end format.html # index.html.erb protected format.xml { render :xml => @comments } end def # And the rest of CRUD ... load_user end @user = User.find(params[:user_id]) protected end end # And the rest of CRUD ... def load_category @category = Category.find(params[:category_id]) endprotected end def load_video @video = Video.find(params[:video_id]) end end ardes.com
  • 80. map.resources :videos do |video| video.resources :comments, :controller => 'video_comments' end map.resources :categories do sharing app: new feature Video |category| views/user_comments/index.html.erb category.resources :videos, :controller => 'category_videos' views/user_comments/show.html.erb category.resources :comments, :controller => 'category_comments' views/user_comments/edit.html.erb end class UserCommentsController < ApplicationController views/user_comments/new.html.erb  Now, one controller - or three controllers before_filter :load_user map.resources :users do |user| views/category_comments/index.html.erb  def index If we have one controller we face these problems class CategoryCommentsController < ApplicationController user.resources :videos, :controller => 'user_videos' before_filter :load_category views/category_comments/show.html.erb @comments = @user.comments.find(:all) - VideoCommentsController < ApplicationController user.resources :comments, the comment? 'user_comments' class how to load up :controller => respond_to do |format| def index format.html # index.html.erb views/category_comments/edit.html.erb end before_filter :load_video views/category_comments/new.html.erb - how |format| end respond_to doto redirect to the commentable? @comments = @category.comments.find(:all) format.xml { render :xml => @comments } def index format.html # index.html.erb end  format.xml {try@video.comments.find(:all) So lets =render :xmlcontrollers} @comments three => @comments views/video_comments/index.html.erb the respond_to do |format| # And end rest of CRUD ... views/video_comments/show.html.erb end format.html # index.html.erbviews/video_comments/edit.html.erb protected format.xml { render :xml => views/video_comments/new.html.erb @comments } end def # And the rest of CRUD ... load_user end @user = User.find(params[:user_id]) protected end end # And the rest of CRUD ... def load_category @category = Category.find(params[:category_id]) endprotected end def load_video @video = Video.find(params[:video_id]) end end ardes.com
  • 81. Video sharing app: new feature ardes.com
  • 82. Video sharing app: new feature  Damn, we forgot about commenting on videos within the user and category contexts. (Let’s just pretend that the customer, naively thinks that this is something easy and obvious to do). ardes.com
  • 83. Video sharing app: new feature  Damn, we forgot about commenting on videos within the user and category contexts. (Let’s just pretend that the customer, naively thinks that this is something easy and obvious to do).  Faced with the prospect of creating a bunch more controllers and views, people start becoming religious about Never nesting more than two resources deep ardes.com
  • 84. Video sharing app: new feature  Damn, we forgot about commenting on videos within the user and category contexts. (Let’s just pretend that the customer, naively thinks that this is something easy and obvious to do).  Faced with the prospect of creating a bunch more controllers and views, people start becoming religious about Never nesting more than two resources deep  But, the reasons we might want to do that haven’t gone away, so we need to resort to some other tricks, or just legislate against them ardes.com
  • 85. Video sharing app: rc’d ardes.com
  • 86. Video sharing app: rc’d  For the curious, this is what our app (without comments - will come back to that later) would look like when using resources_controller ardes.com
  • 87. class UsersController < ApplicationController Video sharing app: rc’d resources_controller_for :users end  For the curious, this is what our app (without comments - will come back to that later) would look like when using resources_controller class VideosController < ApplicationController resources_controller_for :videos end class CategoriesController < ApplicationController resources_controller_for :categories end ardes.com
  • 88. Where is the logic? class VideosController < ApplicationController resources_controller_for :videos end ardes.com
  • 89. Where is the logic? class VideosController < ApplicationController resources_controller_for :videos end # the controller resource logic is in routes.rb! map.resources :videos map.resources :categories do |category| category.resources :videos end map.resources :users do |user| user.resources :videos end ardes.com
  • 90. back to Rails w/o rc ardes.com
  • 91. back to Rails w/o rc  Seems like we have to do a lot of almost repeating ourselves ardes.com
  • 92. back to Rails w/o rc  Seems like we have to do a lot of almost repeating ourselves - in actions ardes.com
  • 93. back to Rails w/o rc  Seems like we have to do a lot of almost repeating ourselves - in actions - redirecting ardes.com
  • 94. back to Rails w/o rc  Seems like we have to do a lot of almost repeating ourselves - in actions - redirecting - when loading enclosing resources ardes.com
  • 95. back to Rails w/o rc  Seems like we have to do a lot of almost repeating ourselves - in actions - redirecting - when loading enclosing resources - in views ardes.com
  • 96. back to Rails w/o rc  Seems like we have to do a lot of almost repeating ourselves - in actions - redirecting - when loading enclosing resources - in views  It seems like the reverse of the way it should be (start with fewer classes and views, branch when behaviour requires it) ardes.com
  • 97. Plugins to the rescue! ardes.com
  • 98. Plugins to the rescue!  resources_controller ardes.com
  • 99. Plugins to the rescue!  resources_controller  others - make_resourceful - resource_this - resource_controller - and more... ardes.com
  • 100. Plugins to the rescue!  resources_controller  others - make_resourceful - resource_this - resource_controller - and more...  They all come with a set of actions that cover CRUD, ardes.com
  • 101. Plugins to the rescue!  resources_controller  others - make_resourceful - resource_this - resource_controller - and more...  They all come with a set of actions that cover CRUD,  they all wrap up the pattern of loading enclosing resources in some fashion ardes.com
  • 102. resources_controller: Goals ardes.com
  • 103. resources_controller: Goals NOT scaffolding - but it can be used for that ardes.com
  • 104. resources_controller: Goals NOT scaffolding - but it can be used for that stop repeating all of those actions ardes.com
  • 105. resources_controller: Goals NOT scaffolding - but it can be used for that stop repeating all of those actions loading enclosing resources ardes.com
  • 106. resources_controller: Goals NOT scaffolding - but it can be used for that stop repeating all of those actions loading enclosing resources make referring to related resource URLs easy ardes.com
  • 107. resources_controller: Goals NOT scaffolding - but it can be used for that stop repeating all of those actions loading enclosing resources make referring to related resource URLs easy principle of least surprise ardes.com
  • 108. resources_controller: Goals NOT scaffolding - but it can be used for that stop repeating all of those actions loading enclosing resources make referring to related resource URLs easy principle of least surprise Not a black box - RC internals should be able to be used for solving other problems ardes.com
  • 109. Scaffolding vs abstraction ardes.com
  • 110. Scaffolding vs abstraction  Both are appropriate solutions for different problems ardes.com
  • 111. Scaffolding vs abstraction  Both are appropriate solutions for different problems  In my view, many of the patterns that we’ve seen in RESTful controllers warrant abstraction ardes.com
  • 112. Scaffolding vs abstraction  Both are appropriate solutions for different problems  In my view, many of the patterns that we’ve seen in RESTful controllers warrant abstraction  They enable some nice features in rc ardes.com
  • 113. Scaffolding vs abstraction  Both are appropriate solutions for different problems  In my view, many of the patterns that we’ve seen in RESTful controllers warrant abstraction  They enable some nice features in rc  They enable some nice solutions to REST oriented problems ardes.com
  • 114. Scaffolding vs abstraction  Both are appropriate solutions for different problems  In my view, many of the patterns that we’ve seen in RESTful controllers warrant abstraction  They enable some nice features in rc  They enable some nice solutions to REST oriented problems  Analogy: has_many and belongs_to, are not implemented as sql scaffolds, but they could be. Why is abstraction more appropriate here? ardes.com
  • 115. Scaffolding vs abstraction  Both are appropriate solutions for different problems  In my view, many of the patterns that we’ve seen in RESTful controllers warrant abstraction  They enable some nice features in rc  They enable some nice solutions to REST oriented problems  Analogy: has_many and belongs_to, are not implemented as sql scaffolds, but they could be. Why is abstraction more appropriate here?  The problem is clearly defined ardes.com
  • 116. Scaffolding vs abstraction  Both are appropriate solutions for different problems  In my view, many of the patterns that we’ve seen in RESTful controllers warrant abstraction  They enable some nice features in rc  They enable some nice solutions to REST oriented problems  Analogy: has_many and belongs_to, are not implemented as sql scaffolds, but they could be. Why is abstraction more appropriate here?  The problem is clearly defined  Abstraction enables cool stuff (like association proxies) ardes.com
  • 117. resources_controller: abstraction ardes.com
  • 118. resources_controller: abstraction What’s the problem? ardes.com
  • 119. resources_controller: abstraction What’s the problem? The model name is tightly coupled with controller actions and views ardes.com
  • 120. resources_controller: abstraction What’s the problem? The model name is tightly coupled with controller actions and views so are the named routes ardes.com
  • 121. resources_controller: abstraction What’s the problem? The model name is tightly coupled with controller actions and views so are the named routes and so is the resource service ardes.com
  • 122. class ForumsController < class UsersController < ApplicationController ApplicationController # GET /forums # GET /users def index def index @forums = Forum.find(:all) @users = User.find(:all) end end # GET /forums/1 # GET /users/1 def show def show @forum = Forum.find(params[:id]) @user = User.find(params[:id]) end end # DELETE /forums/1 # DELETE /users/1 def destroy def destroy @forum = Forum.find(params[:id]) @user = User.find(params[:id]) @forum.destroy @user.destroy redirect_to forums_path redirect_to users_path end end
  • 123. class ForumsController < class UsersController < ApplicationController ApplicationController # GET /forums # GET /users def index def index @forums = Forum.find(:all) @users = User.find(:all) end end # GET /forums/1 # GET /users/1 def show def show @forum = Forum.find(params[:id]) @user = User.find(params[:id]) end end # DELETE /forums/1 # DELETE /users/1 def destroy def destroy @forum = Forum.find(params[:id]) @user = User.find(params[:id]) @forum.destroy @user.destroy redirect_to forums_path redirect_to users_path end end
  • 124. class ForumsController < class UsersController < ApplicationController ApplicationController # GET /forums # GET /users def index def index @forums = Forum.find(:all) @users = User.find(:all) end end # GET /forums/1 # GET /users/1 def show def show @forum = Forum.find(params[:id]) @user = User.find(params[:id]) end end # DELETE /forums/1 # DELETE /users/1 def destroy def destroy @forum = Forum.find(params[:id]) @user = User.find(params[:id]) @forum.destroy @user.destroy redirect_to forums_path redirect_to users_path end end
  • 125. class ForumsController < class UsersController < ApplicationController ApplicationController # GET /forums # GET /users def index def index @forums = Forum.find(:all) @users = User.find(:all) end end # GET /forums/1 # GET /users/1 def show def show @forum = Forum.find(params[:id]) @user = User.find(params[:id]) end end # DELETE /forums/1 # DELETE /users/1 def destroy def destroy @forum = Forum.find(params[:id]) @user = User.find(params[:id]) @forum.destroy @user.destroy redirect_to forums_path redirect_to users_path end end
  • 126. de-couple the name: resource ardes.com
  • 127. de-couple the name: resource def destroy self.resource = find_resource resource.destroy # end ardes.com
  • 128. de-couple the name: resource def destroy self.resource = find_resource resource.destroy # end this will, e.g. set @forum to Forums.find(params[:id]) ardes.com
  • 129. de-couple the name: resource def destroy self.resource = find_resource resource.destroy # end this will, e.g. set @forum to Forums.find(params[:id]) we can refer to whatever the current resource is with resource ardes.com
  • 130. de-couple named routes: resource(s)_path ardes.com
  • 131. de-couple named routes: resource(s)_path def destroy self.resource = find_resource resource.destroy redirect_to resources_path end ardes.com
  • 132. de-couple named routes: resource(s)_path def destroy self.resource = find_resource resource.destroy redirect_to resources_path end all named routes are supported, e.g. formatted_resource_tags_url(‘js’) ardes.com
  • 133. de-couple named routes: resource(s)_path def destroy self.resource = find_resource resource.destroy redirect_to resources_path end all named routes are supported, e.g. formatted_resource_tags_url(‘js’) think of this as named routes relativised to the current resource ardes.com
  • 134. de-couple class or association: resource_service ardes.com
  • 135. de-couple class or association: resource_service def find_resource(id = params[:id]) resource_service.find id end ardes.com
  • 136. de-couple class or association: resource_service def find_resource(id = params[:id]) resource_service.find id end resource_service is an ActiveRecord class, or an association proxy ardes.com
  • 137. de-couple class or association: resource_service def find_resource(id = params[:id]) resource_service.find id end resource_service is an ActiveRecord class, or an association proxy (it’s actually a bit more than that - but think of it that way) ardes.com
  • 138. resource_service def find_resources resource_service.find :all end def find_resource(id = params[:id]) resource_service.find id end def new_resource(attributes = params[resource_name]) resource_service.new attributes end ardes.com
  • 139. resource_service def find_resources resource_service.find :all, :order => params[:order] end def find_resource(id = params[:id]) resource_service.find id end def new_resource(attributes = params[resource_name]) resource_service.new attributes end ardes.com
  • 140. load enclosing resources it’s easy, but repetitive. you need to do it, for url/model integrity ardes.com
  • 141. load enclosing resources it’s easy, but repetitive. you need to do it, for url/model integrity the information is already expressed in routes.rb (most of the time) ardes.com
  • 142. load enclosing resources it’s easy, but repetitive. you need to do it, for url/model integrity the information is already expressed in routes.rb (most of the time) resources_controller thinks that one controller for multiple routes is a fine idea ardes.com
  • 143. ardes.com
  • 144. /users/2/forums/3 ardes.com
  • 145. @user = User.find(2) /users/2/forums/3 @forum = @user.forums.find(3) ardes.com
  • 146. @user = User.find(2) /users/2/forums/3 @forum = @user.forums.find(3) /users/2/forums/3/tags ardes.com
  • 147. @user = User.find(2) /users/2/forums/3 @forum = @user.forums.find(3) /users/2/forums/3/tags @user = User.find(2) @forum = @user.forums.find(3) @tags = @forum.tags.find(:all) ardes.com
  • 148. @user = User.find(2) /users/2/forums/3 @forum = @user.forums.find(3) /users/2/forums/3/tags @user = User.find(2) @forum = @user.forums.find(3) @tags = @forum.tags.find(:all) /users/2/image/comments ardes.com
  • 149. @user = User.find(2) /users/2/forums/3 @forum = @user.forums.find(3) /users/2/forums/3/tags @user = User.find(2) @forum = @user.forums.find(3) @tags = @forum.tags.find(:all) /users/2/image/comments @user = User.find(2) @image = @user.image @comments = @image.comments.find(:all) ardes.com
  • 150. resource specification ardes.com
  • 151. resource specification routing does the work of extracting logical reosurces from the url ardes.com
  • 152. resource specification routing does the work of extracting logical reosurces from the url and then THROWS IT AWAY ardes.com
  • 153. resource specification routing does the work of extracting logical reosurces from the url and then THROWS IT AWAY in the mean time: RC re-recognizes the route, and parses it according to the specification ardes.com
  • 154. principle of least surprise ardes.com
  • 155. principle of least surprise class UserForumsController < ApplicationController resources_controller_for :forums, :in => :user def mark_as_boring @forum = @user.forums.find(params[:id]) @forum.boringed_by!(@user) redirect_to user_forums_path(@user) end end ardes.com
  • 156. principle of least surprise class UserForumsController < ApplicationController resources_controller_for :forums, :in => :user def mark_as_boring @forum = @user.forums.find(params[:id]) @forum.boringed_by!(@user) redirect_to user_forums_path(@user) end end <%= @forum.title %> <%= @user.name %> ardes.com
  • 157. principle of least surprise class UserForumsController < ApplicationController resources_controller_for :forums, :in => :user def mark_as_boring @forum = @user.forums.find(params[:id]) @forum.boringed_by!(@user) redirect_to user_forums_path(@user) end end <%= @forum.title %> <%= @user.name %> <%= link_to @forum.name, polymorphic_url [@user, @forum] %> ardes.com
  • 158. Back to our video app  What if we want to add comments on videos, users, categories - and also have them in user_videos and category_videos? ardes.com
  • 159. class UsersController < ApplicationController resources_controller_for :users end Back to our video app class VideosController < ApplicationController  What if we want to add comments on videos, users, resources_controller_for :videos end categories - and also have them in user_videos and category_videos? class CategoriesController < ApplicationController resources_controller_for :categories end class CommentsController < ApplicationController resources_controller_for :comments nested_in :commentable, :polymorphic => true end ardes.com
  • 160. map.resources :videos, :has_many => :comments map.resources :categories ApplicationController class UsersController < do |category| category.resources :videos,:users resources_controller_for :has_many => :comments end end map.resources our video<app Back to :users do |user| class VideosController ApplicationController user.resources want to add :has_many on videos, users,  What if we :videos, comments => :comments resources_controller_for :videos endend categories - and also have them in user_videos and category_videos? class CategoriesController < ApplicationController resources_controller_for :categories end class CommentsController < ApplicationController resources_controller_for :comments nested_in :commentable, :polymorphic => true end ardes.com
  • 161. map.resources :videos, :has_many => :comments map.resources :categories ApplicationController class UsersController < do |category| category.resources :videos,:users resources_controller_for :has_many => :comments end end map.resources our video<app Back to :users do |user| class VideosController ApplicationController user.resources want to add :has_many on videos, users,  What if we :videos, comments => :comments resources_controller_for :videos endend categories - and also have them in user_videos and category_videos? With RC class CategoriesController < ApplicationController resources_controller_for :categories You start with fewer controllers and views end You make more when you have too much behaviour crammed into them class CommentsController < ApplicationController resources_controller_for :comments nested_in :commentable, :polymorphic => true end ardes.com
  • 162. Common patterns, rc’d ardes.com
  • 163. Common patterns, rc’d  account pattern ardes.com
  • 164. Common patterns, rc’d  account pattern  ordering a collection ardes.com
  • 165. Common patterns, rc’d  account pattern  ordering a collection  join models ardes.com
  • 166. account pattern  Account pattern ardes.com
  • 167. account pattern map.resource :account do |account|  Account pattern account.resources :images account.resources :posts end ardes.com
  • 168. account pattern map.resource :account do |account|  Account pattern account.resources :images account.resources :posts end class AccountController < ApplicationController resources_controller_for :account, :class => User, :singleton => true do current_user end end ardes.com
  • 169. account pattern map.resource :account do |account|  Account pattern account.resources :images account.resources :posts end class AccountController < ApplicationController resources_controller_for :account, :class => User, :singleton => true do current_user end end # You don't need to make an AccountImages, or AccountPosts controller! class ApplicationController < ActionController::Base map_enclosing_resource :account, :class => User, :singleton => true do current_user end # or you can put the above in each controller which has :account in its # enclosing resources, or in a mixin end ardes.com
  • 170. ordering a collection ardes.com
  • 171. ordering a collection map.resources :users do |user| users.resources :things, :collection => {:order => :put} end ardes.com
  • 172. ordering a collection map.resources :users do |user| users.resources :things, :collection => {:order => :put} end class Thing < ActiveRecord::Base acts_as_list def self.order_by_ids(ids) transaction do ids.each_index do |i| Thing.update_attribute! :position => i+1 end end end end ardes.com
  • 173. ordering a collection map.resources :users do |user| users.resources :things, :collection => {:order => :put} end def order resource_service.order_by_ids[quot;#{resource_name}_orderquot;] end class Thing < ActiveRecord::Base acts_as_list def self.order_by_ids(ids) transaction do ids.each_index do |i| Thing.update_attribute! :position => i+1 end end end end ardes.com
  • 174. Join model controllers class ModeratorshipsController < Applica... resources_controller_for :moderatorships, :actions => nil def create # ... def destroy # ... end
  • 175. class ModeratorshipsController < Applica... resources_controller_for :moderatorships, :actions => nil def create self.resource = new_resource if resource.save flash[:notice] = “#{resource_name} created” else flash[:error] = ‘oops’ end redirect_to enclosing_resource_path end def destroy # ...
  • 176. class ModeratorshipsController < Applica... resources_controller_for :moderatorships, :actions => nil def create # ... end def destroy self.resource = find_resource resource.destroy flash[:notice] = “#{resource_name} destroyed” redirect_to enclosing_resource_path end
  • 177. test controller plugins garlic do repo 'rails', :url => 'git://github.com/rails/rails' repo 'rspec', :url => 'git://github.com/ianwhite/rspec' repo 'rspec-rails', :url => 'git://github.com/ianwhite/rspec-rails' repo 'resources_controller', :path => '.' target 'edge', :branch => 'master' target '2.0-stable', :branch => 'origin/2-0-stable' target '2.1-stable', :branch => 'origin/2-1-stable' target '2.1.0', :tag => 'v2.1.0' all_targets do prepare do plugin 'resources_controller', :clone => true plugin 'rspec' plugin('rspec-rails') { sh quot;script/generate rspec -fquot; } end run do cd ‘vendor/plugins/resources_controller’ do sh quot;rake spec:rcov:verifyquot; end ardes.com end end
  • 178. RC summary ardes.com
  • 179. RC summary  makes RESTful controllers and views easier ardes.com
  • 180. RC summary  makes RESTful controllers and views easier - Start with fewer controllers/views - make more when required ardes.com
  • 181. RC summary  makes RESTful controllers and views easier - Start with fewer controllers/views - make more when required  sightings ardes.com
  • 182. RC summary  makes RESTful controllers and views easier - Start with fewer controllers/views - make more when required  sightings - rc just got forked by revolutionhealth on github ardes.com
  • 183. RC summary  makes RESTful controllers and views easier - Start with fewer controllers/views - make more when required  sightings - rc just got forked by revolutionhealth on github - Lots of production apps ardes.com
  • 184. RC summary  makes RESTful controllers and views easier - Start with fewer controllers/views - make more when required  sightings - rc just got forked by revolutionhealth on github - Lots of production apps  well speced ardes.com
  • 185. What next? ardes.com
  • 186. What next?  abstract idea of resource, passed by routing to controller on the request (you were invoked with this resource) ardes.com
  • 187. What next?  abstract idea of resource, passed by routing to controller on the request (you were invoked with this resource)  refactoring? ardes.com
  • 188. What next?  abstract idea of resource, passed by routing to controller on the request (you were invoked with this resource)  refactoring?  core-list talk about loading resources in controllers ardes.com
  • 189. Writing resources_controller Discovering REST patterns in Rails Ian White github.com/ianwhite blog.ardes.com/ian Argument from Design ardes.com Sheffield, UK