A DRY and RESTful Blog

August 18, 2006

One of the main ideas behind writing my own blog application, was to explore the implementation of the Simply Restful plugin in Edge Rails.

In practice Simply Restful adds some relatively benign features to Rails, to support some less benign changes in how we structure our applications. Lots have been said about this since David Hannson’s keynote presentation at RailsConf 2006, but let us have a look at what the changes mean for a blog such as this one.

Routing Resources

The main feature of Simply Restful is the new way of specifying routes. Here is the line from routes.rb, that does almost all of the routing needed for Biilmann Blog:

  1. map.resources :users, :sessions, :pages, :search, :menu_categories, :menu_entries

This single line of code defines a whole lot of routes. Let us take the pages controller as an example. Because of the inclusion in the list of resources, the following requests will now be routed to it:

GET /pages => PagesController.index
GET /pages/:id => PagesController.show
GET /pages/new => PagesController.new
GET /pages /:id;edit => PagesController.edit
POST /pages=> PagesController.create
PUT /pages/:id => PagesController.update
DELETE /pages/:id => PagesController.destroy

The idea behind this is that all controllers should present a uniform interface that responds, in a logical way, to the basic CRUD operations, expressed through the HTTP methods: POST, GET, PUT and DELETE.

Restful Users

Biilmann Blog uses the popular acts_as_authenticated plugin for user authentication, but the users controller generated by the plugin does not present the uniform interface that we strive to make all our restful controllers adhere to. The controller comes with the following methods:

  • index
  • login
  • signup
  • logout

Index is fine and signup can just be renamed to create, but login and logout does not correspond to any CRUD operation on the user model. After all we are not creating or destroying users just because they log in and out. What we are destroying and creating is authenticated sessions. So in Biilmann blog the user controller is split into UsersController and SessionsController - each relying only on CRUD operations.

Associated Comments

An important feature of any half-decent blog is comments, and of course this one has them. The comments always belongs to a blogpost, so just calling CommentsController.index would hardly make any sense. We need to supply the controller with a blogpost id.

Simply Restful makes this easy. The following lines set up the routing for blogposts and comments:

  1. map.resources :blogposts do |posts|
  2. posts.resources :comments
  3. end

Now blogposts gets the routes we already saw in the pages controller example, while comments gets some slightly different ones:

GET /blogposts/:blogpost_id/comments => CommentsController.index
GET /blogposts/:blogpost_id/comments/:id => CommentsController.show
GET /blogposts/:blogpost_id/comments/new => CommentsController.new
GET /blogposts/:blogpost_id/comments/:id;edit => CommentsController.edit
PUT /blogposts/:blogpost_id/comments/:id => CommentsController.update
POST /blogposts/:blogpost_id/comments => CommentsController.create
DELETE /blogposts/:blogpost_id/comments/:id => CommentsController.destroy

To view a blogpost with id 5, you would go to the url /blogposts/5. To create a comment for this blogpost you would send a HTTP POST request to /blogposts/5/comments.

What About the Views?

Making Rails apps the Simply Restful way requires us to change the way we construct our dynamic URLs a little. With the new routings we get some very handy named routes for free, so we better use them.

To create a link to the index action on our blogposts controller this is all we have to do:

  1. <%= link_to blogposts_url %>

In the form we use when posting to the blog, the URL is the same:

  1. <%= start_form_tag blogposts_url %>

The only difference is that the form will send a POST request, the link a GET request. Linking to the post itself can be done like this:

  1. <%= link_to blogpost_url(:id => 5) %>

Notice the singularization of the controller name here. A delete link would use the same URL, but with an addition:

  1. <%= link_to blogpost_url(:id => 5), :method => 'delete' %>

By specifying the method we tell the controller what to do with the specified post. Update works the same way.

What about the comments? How do we construct the /blogposts/5/comments URL?

In much the same way. Lets look at the form for a new comment. It could look like this:

  1. <%= start_form_tag comments_url(:blogpost_id => 5) %>

This form will send a POST request to /blogposts/5/comments.

To get a link to the edit action for a comment we can do this:

  1. <%= link_to edit_comment_url(:blogpost_id => 5, :id => 2) %>

This creates a link to /blogposts/5/comments/2;edit Refactoring the CRUD Controllers

Earlier we had a look at the routes for the pages controller. Lets have a look at the first implementation of the create and destroy actions in PagesController.

  1. def create
  2. @page = Page.new(params[:page])
  3. if params[:preview] > " "
  4. render :action => 'new'
  5. return
  6. elsif @page.save
  7. flash[:notice] = "New page created.'"
  8. redirect_to :action => 'show'
  9. return
  10. end
  11. render :action => 'new'
  12. end
  13. def destroy
  14. @page = Page.find(params[:id])
  15. if Page.destroy(params[:id])
  16. flash[:notice] = "The page was deleted"
  17. else
  18. flash[:notice] = "Could not delete the page"
  19. end
  20. redirect_to :action => index
  21. end

This is very basic and works just fine, but lets have a look at the same two actions from the blogposts:

  1. def create
  2. @blogpost = Blogpost.new(params[:blogpost])
  3. @blogpost.user = current_user
  4. if params[:preview] > ""
  5. render :action => 'new'
  6. return
  7. elsif @blogpost.save
  8. flash[:notice] = "New entry posted"
  9. redirect_to :action => 'show', :id => @blogpost.id
  10. return
  11. end
  12. render :action => 'new'
  13. end
  14. def destroy
  15. if Blogpost.destroy(params[:id])
  16. flash[:notice] = "The entry was deleted"
  17. redirect_to :action => 'index'
  18. return
  19. else
  20. flash[:notice] = "The blogpost could not be deleted"
  21. redirect_to :action => 'index'
  22. return
  23. end
  24. end

One of the basic principles of coding that we should always strive towards, is the DRY one: Don’t Repeat Yourself! It doesn’t take more than a quick glance at these four methods to note that the code is soaking wet. And it only gets worse if we look through all of the application’s controllers. They all implement some of the methods:

  • index
  • show
  • new
  • create
  • edit
  • update
  • destroy

And that’s it - there’s no other methods in any of the controllers, and apart from those in the sessions controller, the implementations of these methods all look very alike. This is a prime target for refactoring.

Mixing in a Module

Ruby offers us an obvious route to refactor a bunch of classes with similar methods: mixin a module. This is exactly what we are going to do. Here is the full CrudBase module I created for this blog:

  1. module CrudBase
  2. # CRUD Mixin - include in a controller to get the following actions
  3. # index, show, new, create, edit, update, destroy.
  4. # The module is has a bunch of hook methods to overwrite for a more specific behaviour
  5. # after creation, updating or deletion.
  6. # The module will try to infer the name of the instance variable set for use in the view,
  7. # the parameters recieved from the forms and the model to operate on, from the name
  8. # of the controller.
  9. def create
  10. set_instance_var model.new(params[obj_name])
  11. create_associations
  12. if params[:preview] && params[:preview] > ""
  13. preview_new
  14. return
  15. elsif instance_var.save
  16. created
  17. return
  18. end
  19. create_failed
  20. end
  21. def update
  22. set_instance_var model.find(params[:id])
  23. instance_var.attributes = params[obj_name]
  24. if params[:preview] && params[:preview]> ""
  25. preview_edit
  26. return
  27. elsif instance_var.save
  28. updated
  29. return
  30. end
  31. edit_failed
  32. end
  33. def destroy
  34. set_instance_var model.find(params[:id])
  35. if model.destroy(params[:id])
  36. destroyed
  37. return
  38. else
  39. destroy_failed
  40. return
  41. end
  42. end
  43. def show
  44. set_instance_var model.find(params[:id])
  45. end
  46. def new;
  47. set_instance_var model.new
  48. end
  49. def edit
  50. set_instance_var model.find(params[:id])
  51. end
  52. def index
  53. instance_variable_set("@#{list_name}", model.find(:all, :conditions => "#{association_conditions}"))
  54. end
  55. protected
  56. # these methods try to infer the name of the instance variables and models
  57. def model
  58. obj_name.camelize.constantize
  59. end
  60. def set_instance_var(value)
  61. instance_variable_set("@#{obj_name}", value)
  62. end
  63. def instance_var
  64. instance_variable_get("@#{obj_name}")
  65. end
  66. def list_name
  67. controller_name
  68. end
  69. def obj_name
  70. list_name.singularize
  71. end
  72. # Owerwrite in controller if more associations needs to be handled here
  73. # This method should also be handled by the controller if you have
  74. # protected attributes besides from the user_id that you dont want
  75. # the user to be able to set through the post params
  76. def create_associations
  77. model.reflect_on_all_associations(:belongs_to).each do |association|
  78. # if the object belongs to a user, set it to the current_user
  79. if association.class_name == current_user.class.to_s
  80. instance_var[association.primary_key_name] = current_user.id
  81. else
  82. instance_var[association.primary_key_name] = params[association.primary_key_name]
  83. end
  84. end
  85. end
  86. # Created the conditions for find. If an id of a belongs_to associations is specified in
  87. # the params the find will be limited to objects belonging to that id
  88. def association_conditions
  89. conditions = ""
  90. model.reflect_on_all_associations(:belongs_to).each do |association|
  91. if params[association.primary_key_name].to_i
  92. conditions << " AND " if conditions > ""
  93. conditions << "#{association.primary_key_name} = #{params[association.primary_key_name].to_i}"
  94. end
  95. end
  96. conditions = "1" if conditions == ""
  97. conditions
  98. end
  99. # the following methods is called after success or failure of the CRUD methods
  100. # overwrite in the model for different behaviour
  101. def created
  102. flash[:notice] = "New #{obj_name} added"
  103. redirect_to :action => 'show', :id => instance_var.id
  104. end
  105. def updated
  106. flash[:notice] = "#{obj_name.capitalize} updated"
  107. redirect_to :action => 'show', :id => instance_var.id
  108. end
  109. def destroyed
  110. flash[:notice] = "#{obj_name.capitalize} deleted"
  111. redirect_to :action => 'index'
  112. end
  113. def create_failed
  114. render :action => 'new'
  115. end
  116. def edit_failed
  117. render :action => 'edit'
  118. end
  119. def destroy_failed
  120. flash[:notice] = "The #{obj_name} could not be deleted"
  121. render :action => 'show', :id => instance_var.id
  122. end
  123. def preview_new
  124. render :action => 'new'
  125. end
  126. def preview_edit
  127. render :action => 'edit'
  128. end
  129. end

A Sign of Things to Come?

In a long post about Opinionated Controllers, Christian Anderson calls for something like the ActiveResource component that David Hannson proposed in his keynote, but on the end that is providing the resource.

He wants to be able to do something like:

  1. controls :memberships

To get a CRUD controller for memberships.

What I have done here is hardly as elegant or generalized - it is really just a refactoring of the specific code in this blog app - but I like to think that it is at least a proof of the concept: showing that the creation of a convention based component for CRUD controllers is probably not too tough a nut to crack. Lets hope we get to see something like it in a future version of Rails.


Posted by Mathias Biilmann. Category: Ruby. Tags: ruby, rails, blogging.

Similar Posts


Comments

Leave a comment