232: Routing Walkthrough Part 2
(view original Railscast)
In this episode we’ll continue from last week and carry on looking at the internals of Rails 3’s routing code. At the end of the last episode our application’s routing file looked like this.
/config/routes.rb
Store::Application.routes.draw do match 'products', :to => ProductsController.action("index") match 'products/recent' end
Last time we showed the internals of the match
method and showed what goes on when we call match
in the routes file but there are a number of other methods that we can call and we’ll take a look at some of them in this episode.
If we look in Rails 3.0’s source code we’ll find the routing logic in the actionpack/lib/actiondispatch/routing
directory and we’ll be focussing on the Mapper
class in this directory because, as we discussed last time, the block inside the routes file is scoped to this class. This means that any methods called within the block are called against an instance of Mapper
and we can therefore call any of Mapper
’s methods in our routes file.
The code in the Mapper
class can be a little overwhelming. The code is long, at almost 1,000 lines, and complex but the good news is that this is the longest file related to routing in Rails so if you can grasp what’s going on in this file and understand how it works then you’ll have a pretty good idea as to how routing works in Rails.
In order to get a good overview of the code we’ll collapse it down using the code-folding facility in TextMate. Pressing Command-Option-0
will fold everything. We’ll then expand the root module ActionDispatch
, its submodule Routing
and finally the Mapping
class itself to get an overview of its structure.
The first two items in the Mapper
class are class definitions for Constraints
and Mapping
. We touched on both of these in the last episode but what’s worth noticing here is that the classes are nested under the Mapper
class. This might seem strange if you’re new to Ruby and you could well be wondering why you’d nest classes like this. There’s no magic going on behind the scenes here; the Constraints
class is completely separate from the Mapper
class. The reason this is done is that nesting the classes defines the namespace for the Constraints
and Mapping
classes so that they will appear under the Mapper
namespace. There’s no inheritance or shared behaviour when you nest classes like this in Ruby.
Moving down the class we have two class methods, self.normalize_path
and self.normalize_name
. These are utility methods that are used throughout the class. Below that is a set of modules:
rails/actionpack/lib/action_dispatch/routing/mapper.rb
module Base... module HttpHelpers... module Scoping... module Resources... module Shorthand... include Base include HttpHelpers include Scoping include Resources include Shorthand
These five modules are included into the Mapper
class. The code in them is placed in modules merely as a way to organize the code in the class.
Base
We looked at the first module, Base
, in the last episode. It contains the match
method, the root
method that uses match
, and also a mount
method that provides another way to map a Rack application to a URL.
rails/actionpack/lib/action_dispatch/routing/mapper.rb
module Base def initialize(set) #:nodoc: def root(options = {}) match '/', options.reverse_merge(:as => :root) end def match(path, options=nil)... def mount(app, options = nil)... def default_url_options=(options)... alias_method :default_url_options, :default_url_options= end
HttpHelpers
The next module is HttpHelpers
and this is where the get
, post
, put
and delete
methods are defined. These methods are used to map routes to certain types of requests.
rails/actionpack/lib/action_dispatch/routing/mapper.rb
module HttpHelpers def get(*args, &block) map_method(:get, *args, &block) end def post(*args, &block) map_method(:post, *args, &block) end def put(*args, &block) map_method(:put, *args, &block) end def delete(*args, &block) map_method(:delete, *args, &block) end def redirect(*args, &block)... private def map_method(method, *args, &block) options = args.extract_options! options[:via] = method args.push(options) match(*args, &block) self end end
All of these methods call the private map_method
method. This method sets the :via
option according to the method passed in and then calls match
. You’ll notice in the routing code that a lot of the methods delegate to the match
method, passing in and customizing certain options beforehand. So if we only want a route to respond to a GET request we could do it this way, using the via option.
match 'products/recent', :via => :get
In practice we’d do this by using the shorter get
method, which will create a route with the same option.
get 'products/recent'
The post
, put
and delete
methods work in much the same way as the get method for the other request types. The redirect
method, however, is interesting as it is very different from the others and returns a Rack application.
rails/actionpack/lib/action_dispatch/routing/mapper.rb
def redirect(*args, &block) options = args.last.is_a?(Hash) ? args.pop : {} path = args.shift || block path_proc = path.is_a?(Proc) ? path : proc { |params| path % params } status = options[:status] || 301 lambda do |env| req = Request.new(env) params = [req.symbolized_path_parameters] params << req if path_proc.arity > 1 uri = URI.parse(path_proc.call(*params)) uri.scheme ||= req.scheme uri.host ||= req.host uri.port ||= req.port unless req.standard_port? body = %(<html><body>You are being <a href="#{ERB::Util.h(uri.to_s)}">redirected</a>.</body></html>) headers = { 'Location' => uri.to_s, 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s } [ status, headers, [body] ] end end
The method returns Rack application by returning an array comprised of a status, some headers and a body. The status defaults to 301
which will make the browser perform a simple 301 Moved Permanently
redirect. We can use this redirect
method directly inside our routes file if we want one URL to redirect to another. In our routes file we already have a route that uses the :to
parameter and this parameter takes a Rack application.
match 'products', :to => ProductsController.action("index")
As the redirect
option returns a Rack app we can use it here to redirect to a new URL like this:
match 'products', :to => redirect("/items")
This feature becomes really useful when you’re changing the URLs in your application but still want to support legacy URLs. You can use redirect to redirect
these to their new equivalents.
Shorthand
The next modules listed are Scoping
and Resources
but we’ll come back to those shortly. Instead we’ll take a look at the Shorthand
module. This is an interesting module that redefines the match
method, which was defined back in the Base
module. This match
method supports a different syntax for the options that you can pass to it. The shorthand method is an alternative way to write the :to
option in a route such as the redirect
route we wrote above.
match 'products', :to => redirect('/items')
This is a common thing to do in a routes file and the shorthand method lets us write the route with a simple hash made up of the route and whatever that route should point to. As with the full route syntax we can append parameters to the end of the route.
match 'products' => redirect('/items')
Shorthand
’s match method sets the :to
parameter when it isn’t already set. It then calls super
but as Mapper
doesn’t inherit from another class what does super
call in this case?
rails/actionpack/lib/action_dispatch/routing/mapper.rb
module Shorthand def match(*args) if args.size == 1 && args.last.is_a?(Hash) options = args.pop path, to = options.find { |name, value| name.is_a?(String) } options.merge!(:to => to).delete(path) super(path, options) else super end end end
When we use super
like this Ruby will look for a method with the same name that was defined in an earlier module. The Shorthand
module is defined last in the list of modules that are included in Mapper
so Ruby will look through the earlier modules for a match
method and delegate to that. In this case it will call match
in the Base module.
This technique is used often in the Rails 3 source code. Earlier versions of Rails used alias_method_chain
to override specific behaviour, but now in Rails 3 we can just use super
.
Resources
That’s it for the Shorthand
module; next we’ll take a look at Resources
. As you’d expect this module contains the resources
method and all of its associated methods. We use resources
in our routes file to create RESTful routes.
rails/actionpack/lib/action_dispatch/routing/mapper.rb
def resources(*resources, &block) options = resources.extract_options! if apply_common_behavior_for(:resources, resources, options, &block) return self end resource_scope(Resource.new(resources.pop, options)) do yield if block_given? collection_scope do get :index if parent_resource.actions.include?(:index) post :create if parent_resource.actions.include?(:create) end new_scope do get :new end if parent_resource.actions.include?(:new) member_scope do get :edit if parent_resource.actions.include?(:edit) get :show if parent_resource.actions.include?(:show) put :update if parent_resource.actions.include?(:update) delete :destroy if parent_resource.actions.include?(:destroy) end end self end
This method is fairly complex but if you look at the general structure of it it makes sense. There are a couple of collection methods, get :index
and post :create
; there is a get :new
method and finally get :edit
, get :show
, put :update
and delete :destroy
. You should recognize these as the famous seven RESTful actions and these are created for a controller when you call resources on it in the routes file.
Note the first line in the method’s resource_scope
block. If a block is passed to the method then the method will yield
to that block before it creates the RESTful actions. This gives us the ability to create our own actions in the routes file. For example we could add a new collection route that returns the discounted products.
/config/routes.rb
Store::Application.routes.draw do match 'products', :to => redirect('/items') get 'products/recent' resources :products do collection do: get :discounted end end end
The code inside the block passed to resources
in the routes above will be executed by the yield
call in resource_scope
and the standard RESTful actions will be defined afterwards. We can use similar code in the block above to that in the resources method in the Rails source code to define our custom actions.
Looking at the blocks in the routes file above you might think that the object is changing every time we create a new block but this isn’t the case. We’re still working with the same Mapper
object we worked with in the beginning so calling get
in the innermost blocks is exactly the same as calling it in the outermost. We are dealing with a different scope, though, and we’ll discuss scopes shortly.
If you take another look back at the resources
method from Rails’ source code you’ll see that the code uses a collection_scope
call when it defines the index
and create
actions but inside our routes file we just use collection
. What’s the difference? Well, not much. If we look at the collection
method in the Mapper
class we’ll see that it delegates to collection_scope
.
rails/actionpack/lib/action_dispatch/routing/mapper.rb
def collection unless @scope[:scope_level] == :resources raise ArgumentError, "can't use collection outside resources scope" end collection_scope do yield end end
Let’s take another quick look at our routes file.
/config/routes.rb
Store::Application.routes.draw do match 'products', :to => redirect('/items') get 'products/recent' resources :products do collection do: get :discounted end end end
Both calls to get in the code above call the same method but the one inside the collection
block will assume some additional behaviour according to how it is scoped inside the resources
and collection
blocks.
If we take a look back in the Resources
module we’ll see a familiar-looking method, match
. This redefines the match
method and adds some additional behaviour based on resources.
rails/actionpack/lib/action_dispatch/routing/mapper.rb
def match(*args) options = args.extract_options!.dup options[:anchor] = true unless options.key?(:anchor) if args.length > 1 args.each { |path| match(path, options.dup) } return self end on = options.delete(:on) if VALID_ON_OPTIONS.include?(on) args.push(options) return send(on){ match(*args) } elsif on raise ArgumentError, "Unknown scope #{on.inspect} given to :on" end if @scope[:scope_level] == :resources args.push(options) return nested { match(*args) } elsif @scope[:scope_level] == :resource args.push(options) return member { match(*args) } end action = args.first path = path_for_action(action, options.delete(:path)) if action.to_s =~ /^[\w\/]+$/ options[:action] ||= action unless action.to_s.include?("/") options[:as] = name_for_action(action, options[:as]) else options[:as] = name_for_action(options[:as]) end super(path, options) end
If we look about halfway down the code above you’ll see the line that checks the current scope to see if it is resources
. If it is some different behaviour is added. The logic is fairly complex; all you need to know is that the Resources
module redefines the match
method. Note that at the end it calls super
so that the match
method in Base
is called. Remember that get
calls match
and this is where the additional functionality is located for dealing with get
and other methods that are defined within resources
.
Scoping
We’re now down to the last method in our Mapping
class: Scoping
. Whenever there’s a block inside your routes file there’s a call to Scoping
’s scope
behind the scenes. That means that it will define some additional behaviour for the code inside that block.
Along with the scope method there are a number of other methods, all of which delegate to scope
.
rails/actionpack/lib/action_dispatch/routing/mapper.rb
def initialize(*args) #:nodoc: @scope = {} super end def controller(controller, options={}) options[:controller] = controller scope(options) { yield } end def namespace(path, options = {}) path = path.to_s options = { :path => path, :as => path, :module => path, :shallow_path => path, :shallow_prefix => path }.merge!(options) scope(options) { yield } end def constraints(constraints = {}) scope(:constraints => constraints) { yield } end def defaults(defaults = {}) scope(:defaults => defaults) { yield } end
These methods are all fairly simple and all delegate to a more generic method having first set some options. For example defaults
calls scope
after setting some defaults
options. and likewise constraints
calls scope with some constraints
options. The namespace
method is a little more complex but does essentially the same thing. The module also has an initialize
method which just creates a @scope
instance variable and sets it to be an empty hash. You might be wondering what an initialize
method is doing here as modules can’t be instantiated. This is true, but in this case we’re just overriding a method behaviour. When the Scoping
module is included in the Mapper
class this initialize
method will override the current initialize
method, add the @scope
variable and then call super
.
Finally we have the scope
method itself and this is where all of the work takes place. There’s a lot of complexity in this method but all it is essentially doing is filling up the @scope
variable with some information based on the options that are being passed into the scope. The method merges the options using the a number of private methods in the module. All it does is store up the scope information so that it can be used later on inside whatever match call you have. Essentially it adds additional functionality based on the current scope.
That’s basically how blocks inside the routes file work. If we define routes like this:
/config/routes.rb
Store::Application.routes.draw do controller :products do match #... end end
Whenever we call match
in the controller
block above (and remember it delegates to scope
) the controller
option will automatically be supplied in there.
That’s it for this episode. I hope it’s given you some idea what’s the methods inside the routes file are doing. Even though you have a large number of methods to choose from in the routes file most of them are really simple delegating to either match or scope passing in some additional options.