6.5. match

If we use /home/applepie in addition to the URL /home/ping to access the same action in the same controller, we need to change the file config/routes.rb as follows:
Shop::Application.routes.draw do
  get "home/index"

  get "home/ping"

  get "home/pong"

  root :to => "home#index"

  match "home/applepie" => "home#ping"
end
We then have the following routes available:
$ rake routes
   home_index GET /home/index(.:format)    home#index
    home_ping GET /home/ping(.:format)     home#ping
    home_pong GET /home/pong(.:format)     home#pong
         root     /                        home#index
home_applepie     /home/applepie(.:format) home#ping
$
http://0.0.0.0:3000/home/applepie
If you want to give the route /home/applepie a different name, you can do this via the setting :as => "name" in the config/routes.rb:
Shop::Application.routes.draw do
  get "home/index"

  get "home/ping"

  get "home/pong"

  root :to => "home#index"

  match "home/ping-ping" => "home#ping", :as => "applepie"
end
You now have the following route names available in the system:
MacBook:shop xyz$ rake routes
home_index GET /home/index(.:format)    home#index
 home_ping GET /home/ping(.:format)     home#ping
 home_pong GET /home/pong(.:format)     home#pong
      root     /                        home#index
  applepie     /home/ping-ping(.:format) home#ping
MacBook:shop xyz$
As you can see, a URL can contain a minus symbol, but the route name cannot (though an underscore is fine).

Important

The routes in the file config/routes.rb are always processed from top to bottom. The first hit from the top wins!

Parameters

Match can not just assign fixed routes but also pass parameters. A typical example would be date specifications. To demonstrate this, let's create a mini blog application:
$ rails new blog
  [...]
$ cd blog
$ rails generate scaffold Post subject content published_at:date
  [...]
$ rake db:migrate
  [...]
$
As example data in the db/seeds.rb we take:
Post.create(subject: 'A test', published_at: '01.10.2011')
Post.create(subject: 'Another test', published_at: '01.10.2011')
Post.create(subject: 'And yet one more test', published_at: '02.10.2011')
Post.create(subject: 'Last test', published_at: '01.11.2011')
Post.create(subject: 'Very final test', published_at: '01.11.2012')
With rake db:seed we populate the database with this data:
$ rake db:seed
$
If we now start the Rails server with rails server and go to the page http://0.0.0.0:3000/posts in the browser, we will see this:
Index view of all posts
For this kind of blog it would of course be very useful if you could render all entries for the year 2010 with the URL http://0.0.0.0:3000/2010/ and all entries for October 1st 2010 with http://0.0.0.0:3000/2010/10/01. We can do this by using optional parameters in the match entry. Please enter the following configuration in the config/routes.rb:
Blog::Application.routes.draw do
  resources :posts

  match "/:year(/:month(/:day))" => "posts#index"
end
The round brackets represent optional parameters. In this case, you have to specify the year, but not necessarily the month or day. rake routes shows the new route at the last line:
$ rake routes
    posts GET    /posts(.:format)                 posts#index
          POST   /posts(.:format)                 posts#create
 new_post GET    /posts/new(.:format)             posts#new
edit_post GET    /posts/:id/edit(.:format)        posts#edit
     post GET    /posts/:id(.:format)             posts#show
          PUT    /posts/:id(.:format)             posts#update
          DELETE /posts/:id(.:format)             posts#destroy
                 /:year(/:month(/:day))(.:format) posts#index
$ 
If we do not change anything else, we still get the same result when calling http://0.0.0.0:3000/2011/ and http://0.0.0.0:3000/2011/10/01 as we did with http://0.0.0.0:3000/posts. This is logical. But have a look at the output of rails server:
Started GET "/2011" for 127.0.0.1 at 2012-11-21 17:10:50 +0100
Connecting to database specified by database.yml
Processing by PostsController#index as HTML
  Parameters: {"year"=>"2011"}
  Post Load (0.1ms)  SELECT "posts".* FROM "posts" 
  Rendered posts/index.html.erb within layouts/application (10.9ms)
Completed 200 OK in 122ms (Views: 82.5ms | ActiveRecord: 2.0ms)
The route has been recognised and an element "year" => "2011" has been assigned to the hash params (written misleadingly as Parameters in the output). Going to the URL http://0.0.0.0:3000/2010/12/24 results in the following output, as expected:
Started GET "/2010/12/24" for 127.0.0.1 at 2012-11-21 17:13:55 +0100
Connecting to database specified by database.yml
Processing by PostsController#index as HTML
  Parameters: {"year"=>"2010", "month"=>"12", "day"=>"24"}
  Post Load (0.1ms)  SELECT "posts".* FROM "posts" 
  Rendered posts/index.html.erb within layouts/application (9.6ms)
Completed 200 OK in 93ms (Views: 62.5ms | ActiveRecord: 1.9ms)
In case of the URL http://0.0.0.0:3000/2010/12/24, the following values have been saved in the hash params: "year"=>"2010", "month"=>"12", "day"=>"24".
In the controller, we can access params[] to access the values defined in the URL. We simply need to adapt the index method in app/controllers/posts_controller.rb to output the posts entered for the corresponding date, month or year:
def index
  if params[:day]
    # Request for a specific day.
    #
    @posts = Post.where(published_at: Date.parse("#{params[:day]}.#{params[:month]}.#{params[:year]}"))
  elsif params[:month]
    # Request for a specific month.
    #
    @posts = Post.where(published_at: ( Date.parse("01.#{params[:month]}.#{params[:year]}") .. Date.parse("01.#{params[:month]}.#{params[:year]}").end_of_month ))
  elsif params[:year]
    # Request for a specific year.
    #
    @posts = Post.where(published_at: ( Date.parse("01.01.#{params[:year]}") .. Date.parse("31.12.#{params[:year]}") ))
  else
    # Request for all posts.
    #
    @posts = Post.all
  end

  respond_to do |format|
    format.html # index.html.erb
    format.json { render json: @posts }
  end
end
If we now go to http://0.0.0.0:3000/2011/10/01 , we can see all posts of October 1st 2011.
Index view for 1st Oct 2011

Constraints

In the section called “Parameters” I showed you how you can read out parameters from the URL and pass them to the controller. Unfortunately, the entry defined there in the config/routes.rb
match "/:year(/:month(/:day))" => "posts#index"
has one important disadvantage: it does not verify the individual elements. For example, the URL http://0.0.0.0:3000/just/an/example will be matched just the same and then of course results in an error:
Error message
In the log output in log/development.log we can see the following entry:
Started GET "/just/an/example" for 127.0.0.1 at 2012-11-21 17:26:23 +0100
Processing by PostsController#index as HTML
  Parameters: {"year"=>"just", "month"=>"an", "day"=>"example"}
Completed 500 Internal Server Error in 3ms
Obviously, Date.parse( "example.an.just") cannot work. A date is made up of numbers, not letters.
Constraints can define the content of the URL more precisely via regular expressions. In the case of our blog, the config/routes.rb with contraints would look like this:
Blog::Application.routes.draw do
  resources :posts

  match "/:year(/:month(/:day))" => "posts#index", :constraints => { :year => /\d{4}/, :month => /\d{2}/, :day => /\d{2}/ }
end

Warning

Please note that you cannot use regex anchors such as "^" in regular expressions in a constraint.
If we go to the URL again with this configuration, Rails gives us an error message "No route matches":
Routing Error

Advanced Constraints

With the route match "/:year(/:month(/:day))" => "posts#index", :constraints => { :year => /\d{4}/, :month => /\d{2}/, :day => /\d{2}/ } we have checked the URL for a date in terms of syntax, but a user could of course still call the URL http://0.0.0.0:3000/2011/02/31. As there is no 31st of February, this route should logically not exist. So we need way of checking a specified, syntactically complete date to see whether it is really a correct date in accordance with the calendar.
To do this, we need to define a special class, with which objects with the method matches?() are defined. Within matches?() we can carry out the desired custom validation and then reply with true or false. Please create a file lib/valid_date_contraint.rb with the following content:
class ValidDateConstraint
  def matches?(request)
    begin
      Date.parse("#{request.params[:day]}.#{request.params[:month]}.#{request.params[:year]}")
      true
    rescue
      false
    end
  end
end
We now need to load this class. Please create the file config/initializers/load_extensions.rb with this content:
require 'valid_date_constraint'
We now split the date route in the config/routes.rb:
Blog::Application.routes.draw do
  resources :posts

  match "/:year/:month/:day" => "posts#index", :constraints => { :year => /\d{4}/, :month => /\d{2}/, :day => /\d{2}/ }, :constraints => ValidDateConstraint.new

  match "/:year(/:month)" => "posts#index", :constraints => { :year => /\d{4}/, :month => /\d{2}/ }
end
The first match reacts to all URLs with three parameters that correspond to the specified constraints in terms on syntax. Additionally, the date is verified. This route will only be used if the date itself is valid. So it is not called for the URL http://0.0.0.0:3000/2011/02/31. But the URL http://0.0.0.0:3000/2011/10/01 would work fine.
Obviously we need to take care of the year and month rules too. But that is a bit easier:
Blog::Application.routes.draw do
  resources :posts

  match "/:year/:month/:day" => "posts#index", :constraints => { :year => /\d{4}/, :month => /\d{2}/, :day => /\d{2}/ }, :constraints => ValidDateConstraint.new

  match "/:year(/:month)" => "posts#index", :constraints => { :year => /\d{4}/, :month => /0[0-9]/ }
  match "/:year(/:month)" => "posts#index", :constraints => { :year => /\d{4}/, :month => /1[0-2]/ }
end

Note

By now I expect a smile on your face. Aren't these possibilities awesome!? ;-)

Redirects

With a match, you can also redirect to another page. If you want to redirect the nonsense URL http://0.0.0.0:3000/2010/02/31 to /2010/02, it works like this:
match "/:year/02/31" => redirect("/%{year}/02")
So you could also redirect a single-digit month to a double-digit month (including a leading 0):
match "/:year/:month/:day" => redirect("/%{year}/0%{month}/%{day}"), :constraints => { :year => /\d{4}/, :month => /\d{1}/, :day => /\d{2}/ }
The same of course applies to a one-digit day. If we take all combinations into account, our config/routes.rb then looks like this:
Blog::Application.routes.draw do
  resources :posts

  match "/:year/:month/:day" => redirect("/%{year}/0%{month}/%{day}" ), :constraints => { :year => /\d{4}/, :month => /\d{1}/, :day => /\d{2}/ }
  match "/:year/:month/:day" => redirect("/%{year}/0%{month}/0%{day}"), :constraints => { :year => /\d{4}/, :month => /\d{1}/, :day => /\d{1}/ }
  match "/:year/:month/:day" => redirect("/%{year}/%{month}/0%{day}" ), :constraints => { :year => /\d{4}/, :month => /\d{2}/, :day => /\d{1}/ }

  match "/:year/:month" => redirect("/%{year}/0%{month}"), :constraints => { :year => /\d{4}/, :month => /\d{1}/ }

  match "/:year(/:month(/:day))" => "posts#index", :constraints => { :year => /\d{4}/, :month => /\d{2}/, :day => /\d{2}/ }
end
With this set of redirect rules, we can ensure that a user of the page can also enter single-digit days and months and still ends up in the right place, or is redirected to the correct format.

Note

Redirects in the config/routes.rb are by default http redirects with the code 301 ("Moved Permanently"). So even search engines will profit from this.

Updates about this book will be published on my Twitter feed.