User-centric Routing in Rails 3
Have you ever noticed that the GitHub homepage is different once you log in? I’m not talking about little changes here and there. It’s a completely different page. I have no idea how GitHub does this but I’ll venture a guess and demonstrate how to achieve the same effect.
There are several common ways to pull this off but many miss the mark.
In the View
Switching content based on the current user is easy enough in the view. After all,
current\_user is available in the view so why not?
<% if current_user %> Welcome back, <%= current_user.name %>! <% else %> <%= link_to "Learn about us!", about_path %> <% end %>
But this falls apart when more user-specific information is needed in the view. In the case of GitHub’s user homepage, a lot of extra information (repositories, activity, etc.) is included. This information should be loaded up in the controller. So maybe that’s where the content switch belongs.
In the Controller
With the content switch in the controller, an action might look like this:
def home if current_user @feed_entries = current_user.feed_entries render :private_home else render :public_home end end
This too feels just a little bit… off. The responsibility of a controller action is to load a dataset from the models and to render a template from the views. So if, depending on whether a user is logged in, an action loads up one of two datasets and renders one of two templates, shouldn’t that action be split into two actions? The question, then, is where do we switch between two actions?
In the Routes!
If you think about it, we do this all the time with REST. The same resource route can map to multiple actions depending the request’s HTTP method. A homepage could be mapped to two actions with:
root :to => "users#show", :via => :get root :to => "users#create", :via => :post
But how can the routes access whether a user is logged in?
Rails has had routing constraints for some time but Rails 3 introduced request-based and advanced constraints. An advanced constraint is a custom object with a
matches? method that receives the request as an argument.
In this case, I’m storing a unique user token in the cookies when a user is logged in. So in
lib/logged\_in\_constraint.rb, I can write my constraint class to check for that token:
class LoggedInConstraint < Struct.new(:value) def matches?(request) request.cookies.key?("user_token") == value end end
I also need to require the new constraint in
require File.expand_path("../../lib/logged_in_constraint", __FILE__)
The Fun Part
Now I can write my two new, GitHub-style homepage routes:
root :to => "static#home", :constraints => LoggedInConstraint.new(false) root :to => "users#show", :constraints => LoggedInConstraint.new(true)
And that’s it! Give it a shot.
This is just one application of the new Rails 3 request-based constraints and there is no shortage of other fun and interesting applications:
root :to => "static#welcome_to_holland", :constraints => GeolocationConstraint.new("Holland, Michigan") root :to => "static#visit_holland"