Rails Dispatch

Rails news delivered fresh

Presented by Engine Yard

In this week’s post, we’ll be looking at the new Routing API in Rails 3. Other than handling all the basic routes efficiently, the new DSL also has some nice advanced features baked in that every developer will sure appreciate.

I have written about basic routing in Rails 3 over on the EngineYard blog before. This time, we’ll go through some of the more advanced examples of using routes. You’ll see how routes hook up with any Rack-compatible framework from a Rails application through routes. We’ll also see how constraints and redirect help you limit routes to a specific regular expression and manipulate them. At the end, we’ll go through some examples showing how flexible the new Routing DSL is.

For a more advanced example of using the Rails router, this week’s screencast walks you through creating a render method that you can use in the router itself to route a URL directly to a template. If you’re not familiar with the new router yet, definitely read the post before watching the screencast!

Rack endpoints

Rack has revolutionized the way Ruby web frameworks are made, taking out all the complexities of talking to a web server by providing a simple interface. We saw Rack support in Rails 2, but with Rails 3, it’s on a whole new level. I am not going to talk about all of the different ways Rails adheres Rack, but one of the areas that Rails has managed to squeeze Rack support in is Routes. With the new Routes DSL, every endpoint is essentially a Rack application. So:

match "/foo", :to => proc {|env| [200, {}, ["Hello world"]] }

Since a proc responds to call, that makes it a valid Rack application. This way, we bypass the Rails stack and gain some significant performance boost.

What this means is that we can substitute any Rack-compatible framework there and it works as expected. Here’s a basic Sinatra application hooked into Rails. Sinatra should be included in the Gemfile for this example to work.

class SinatraApp < Sinatra::Base
  get '/'
    "Hello World!"
  end

  get '/:name'
    "Hello #{name}"
  end
end

match '/hello' => SinatraApp

The root route for the Sinatra application becomes ‘/hello’. This way, the Sinatra application behaves and works independently as it should. You can substitute the code above with a Ramaze, Camping or Merb application there as well.

Redirect

An application grows and with growth comes changes to the application’s API and URL endpoints. The redirect method, among its wide applicable use, is a suitable method to use on such circumstances. This creates an HTTP 301 response and redirects according to the route given. Here’s one way to use redirect:

match "/api/v1/post/:id", :to => redirect("/api/article/%{id}")

Any calls such as /api/v1/post/3 will be redirected to /api/article/3. This a simple way of deprecating old API and redirecting it to the new one, for example. You can redirect to a full blown URL as well:

match '/questions', :to => redirect("http://www.stackoverflow.com/")

But that’s not all, redirect also allows you to manipulate parameters on-the-fly. To replicate such functionality in earlier versions of Rails, a controller and an action would have been required.

match '/group/:name', :to => redirect {|params| "/group/#{params[:name].pluralize}" }

This pluralizes the name parameter, so passing /group/person would redirect it to /group/people, using the built-in pluralize method in Rails. The possibilities are endless here as you can use any Ruby method to manipulate the parameters however you want.

Constraint

There are situations when a URL needs to be limited to a criteria to ensure or differentiate what the user wants. Constraints allow you to limit the routes based on Regular Expressions, for that matter:

match "/posts/show/:id", :to => "posts#index", :constraints => {:id => /\d+/}

This example limits the :id parameter to only allow numbers to pass through, otherwise a RoutingError is shown. And with the flexibility of regular expressions, we can constraint any route according to our needs.

That’s not what constraints can be used for only though, we can also restrict a route to only work if the user’s IP matches our expression:

constraints(:ip => /127.0.0.1/) do
  match  '/questions', :to => redirect("http://www.stackoverflow.com/")
end

The best part is that constraints are highly flexible as well. If any class responds to matches? method, it is a valid constraint. Here’s the same example as above:

class IpRestrictor
  def self.matches?(request)
    request.ip =~ /127.0.0.1/
  end
end

constraints IpRestrictor do
  match  '/questions', :to => redirect("http://www.stackoverflow.com/")
end

Scope

We’ve used namespace as an option to specify that the controllers reside in a directory. The namespace is now available as a method that takes a block:

namespace :admin do
  resources :posts
end

The above code is actually a shorthand to the following snippet:

scope :path => :admin, :module => :admin do
  resources :posts
end

This means that we’d like to access the posts resources from /admin/posts and PostsController is inside the app/controllers/admin directory. There are several other options when defining scopes, like :name_prefix allows you to prepend a string to all the generated helpers.

scope 'admin', :name_prefix => "admin" do
  resources :posts
end
GET    /admin/posts(.:format)    
    admin_posts POST   /admin/posts(.:format)    
 new_admin_post GET    /admin/posts/new(.:format)
                GET    /admin/posts/:id(.:format)
                PUT    /admin/posts/:id(.:format)
     admin_post DELETE /admin/posts/:id(.:format)
edit_admin_post GET    /admin/posts/:id/edit(.:format)

Since the :path option is widely used, the first argument without any key is assumed to be the path of the scope, as we’ve used in this example.

Router’s Flexibility

The new router in Rails 3 is highly flexible. This is evident by the fact that even though Rails hasn’t released for mass use yet, there are examples showing how Routes can be taken advantage of to create specific DSLs inside Rails. Astaire is one such example. It allows Sinatra-like DSL inside a Rails controller:

class RockingController < ApplicationController
  get "/goodbye" do
    render :text => "goodbye"
  end
end

With that set, you don’t need to specify anything inside config/routes.rb since Astaire defines them itself. Your Routes file can practically stay empty when using Astaire. While this might not be the solution for everyday use, it shows a very good possibility for certain situations.

Conclusion

This brief rundown shows why some of these features added in Routes are well-deserved for the ever-demanding world of creating web applications. As you saw, Rack integration, constraints, redirect and the Route API itself is flexible enough to accommodate even the complex requirement of an application. For a more complete overview on the changes to Routes in Rails 3 and its API, be sure to check out my post on the Engine Yard blog or the update Rails 3 guide on the router.