Beginners Tutorial: Routing in Rails 2.0 (with REST) – Part 3 of n

Part 4

Part 3

At the end of part 2, routes.rb looked like this:

ActionController::Routing::Routes.draw do |map|
  map.connect 'music', :controller => "albums", :action => "index"
  map.connect '', :controller => "albums", :action => "index"
  map.connect 'music/show/:id', :controller => "albums", :action => "show"
  map.connect 'music/delete/:id', :controller => "albums", :action => "destroy"
  map.connect 'music/edit/:id', :controller => "albums", :action => "edit"
  map.connect 'music/update/:id', :controller => "albums", :action => "update"
  map.connect 'music/new', :controller => "albums", :action => "new"
  map.connect 'music/create', :controller => "albums", :action => "create"
end

To simplify things, let’s remove the routing rule for the empty URL. Remove line 3.

We can no longer browse to http://localhost:3000/, but we can still browse to http://localhost:3000/music. Besides that, the site should continue to work as before.
Looking at the routes.rb file we see 7 rules defined.

ActionController::Routing::Routes.draw do |map|
  map.connect 'music', :controller => "albums", :action => "index"
  map.connect 'music/show/:id', :controller => "albums", :action => "show"
  map.connect 'music/delete/:id', :controller => "albums", :action => "destroy"
  map.connect 'music/edit/:id', :controller => "albums", :action => "edit"
  map.connect 'music/update/:id', :controller => "albums", :action => "update"
  map.connect 'music/new', :controller => "albums", :action => "new"
  map.connect 'music/create', :controller => "albums", :action => "create"
end

We have 7 rules and only one database table. Does this mean we need to add 7 more rules for every table we add? No, there are ways to reduce the amount of explicitly defined rules.

When Rails receives a URL it tries to find a matching routing rule. It does this by comparing the URL to the pattern string in the rule.
The pattern string for map.connect ‘music/create’, :controller => “albums”, :action => “create” Is: music/create

The pattern string for map.connect ‘music/edit/:id’, :controller => “albums”, :action => “edit”
Is: music/edit/:id

The :id is a Ruby symbol and is referred to as a receptor. If you are new to Ruby you may not be too familiar with symbols. I once came across a short and sweet description of a symbol “A symbol is the name badge, not the person”. I may have read that in here.

A receptor is used as a wildcard. It matches anything that it is compared against. The wildcard receptor gets passed into the controller action within the params hash e.g.

def edit
    @album = Album.find(params[:id])
end

You can change :id to almost anything as long as you remember to change it in the controller and in the views where the URL is generated e.g. when using link_to. I recommend you use :id as that is the convention.

Now we will focus on the process Rails follows when it receives a request.
If Rails receives this URL: http://localhost:3000/music/edit/1

Rails will get the first rule defined in routes.rb and look if the URL matches the pattern string. The first rule in our case is:
map.connect ‘music’, :controller => “albums”, :action => “index”

Rails performs the test to see if there is a match:
URL: music/edit/1
Rule: music

This test fails, no match because the rules pattern string does not contain edit or a receptor (:id).

Rails tests the URL against the pattern string defined in the second rule:
URL: music/edit/1
Rule: music/show/:id

Again, this is not a match. Rails will then test the next rule. This process continues until Rails finds a match. In our case, this will be on this rule:
map.connect ‘music/edit/:id’, :controller => “albums”, :action => “edit”

The test:
URL: music/edit/1
Rule: music/edit/:id

The match is made! Rails will then follow the rule and invoke the edit action on the albums controller. Passing through the :id (as the key) with the value of 1 into the edit action. This is done through the params hash.

Take note of these two points:
1. If the pattern string does not specify a receptor and the URL contains a value to be used, then the route will not match.
We can see this by using this URL: http://localhost:3000/music/new/42

2. If the pattern string does contain a receptor and the URL does not supply a value for it, the route will still match – although an error may occur.
We can see this by using this URL: http://localhost:3000/music/edit/

When using that URL, the match is still made and the edit action is still invoked. Rails will simply pass through nil to the action. This will result in an error as the find method can not find an album with no ID.

We could add a receptor to the ‘new’ pattern string e.g. ‘music/new/:id’
Now you can invoke the new action with http://localhost:3000/music/new/42 – if you really wanted to.

The albums_controller.new action does not use the :id from the params hash. Therefore you can safely omit the receptor value and call it by using http://localhost:3000/music/new. As we can see, this request works even when Rails used nil as the value of :id.

Putting things back into perspective, the goal here is to reduce the amount of explicit routing rules. Let’s have a look at some of the pattern strings and resulting actions invoked:

‘music/new‘ results in the new action being invoked
‘music/create‘ results in the create action being invoked
‘music/edit/:id’ results in the edit action being invoked
Etc…

We see that the URL contains the name of the action. We could write a more general rule if we could extract the action name from the URL. Rails provides us with an easy way of doing this. We can use the ‘Magic Receptors’.

Experiment 3.1 Applying a ‘Magic’ Receptor

Rails has two ‘magic’ receptors, these are :controller and :action. These are really just receptors, however they are special. These receptors form a part of the Rails magic.

Here is the current new route:
map.connect ‘music/show/:id’, :controller => “albums”, :action => “show”

We could rewrite this with the ‘magic’ receptor – :action
map.connect ‘music/:action/:id’, :controller => “albums”, :action => “show”

As previously stated, a receptor is a wildcard. Therefore :action will match anything that it is compared against. There we can get this match occurring.

URL: music/show/1
Rule: music/:action/:id

This results with a new entry into the params table, with a key of :action and a value of “show”. Now Rails can use this entry to determine the controller action to call. Now ‘show’ rule can be reduced to:
map.connect ‘music/:action/:id’, :controller => “albums”

The delete rule can be modified from
map.connect ‘music/delete/:id’, :controller => “albums”, :action => “destroy”
to
map.connect ‘music/:action/:id’, :controller => “albums”

Which is exactly the same as the show!

We can also change edit and update to:
map.connect ‘music/:action/:id’, :controller => “albums”

There is no need to repeat the same rules. Therefore we can shrink our routes.rb to:

ActionController::Routing::Routes.draw do |map|
  map.connect 'music', :controller => "albums", :action => "index"
  map.connect 'music/:action/:id', :controller => "albums"
  map.connect 'music/new', :controller => "albums", :action => "new"
  map.connect 'music/create', :controller => "albums", :action => "create"  end

Before you test this, you need to modify edit.html.erb. The modification is in the form_for line. The edit.html.erb should look like this:

<h1>Editing album</h1>
<%= error_messages_for :album %>

<% form_for :album, @album, :url => { :controller => "albums", :action => "update", :id => @album} do |f| %>

 <b>Title</b>
 <%= f.text_field :title %>

 <b>Review</b>
 <%= f.text_area :review %>

 <%= f.submit "Update" %>

 <% end %>

<%= link_to 'Show', :controller => "albums", :action => "show", :id => @album.id %> |
 <%= link_to 'Back', :controller => "albums", :action => "index" %>

Your site should be fully operational at this point.

New and Create appear to be different as they do not require the album ID.
Therefore we might decide to change the new rule from
map.connect ‘music/new’, :controller => “albums”, :action => “new”
to
map.connect ‘music/:action’, :controller => “albums”

And the same for the create rule. This will allow us to end up with a routing file like this:

ActionController::Routing::Routes.draw do |map|
 map.connect 'music', :controller => "albums", :action => "index"
 map.connect 'music/:action/:id', :controller => "albums"
 map.connect 'music/:action', :controller => "albums"
end

Let’s remove the fist rule, so that we are left with:

ActionController::Routing::Routes.draw do |map|
 map.connect 'music/:action/:id', :controller => "albums"
 map.connect 'music/:action', :controller => "albums"
end

If you test your site, you should find that it still works! If we use a url of http://localhost:3000/music we see the index? But we did not specify an action?

That is true, there was no action specified. And when no action is specified, Rails falls back to a default action – this default action is index.

Now remove the second rule so that your routes.rb looks like this:

ActionController::Routing::Routes.draw do |map|
 map.connect 'music/:action/:id', :controller => "albums"
end

And the site still works…

As stated above:
If the pattern string does contain a receptor and the URL does not supply a value for it, the route will still match – although an error may occur. Rails uses nil if the URL does not supply a value for the receptor.

The only times we don’t supply a value for id is in the create and new actions. These actions do not use the id parameter. Therefore the site still works without a hitch.

Experiment 3.2 Applying the other ‘Magic’ Receptor

As I said before, Rails has two ‘magic’ receptors (:controller and :action). We have already used :action and now we are going to use :controller.

In part 2 we moved from using URLs like

http://localhost:3000/albums

to

http://localhost:3000/music

One of the reasons I did this was to show you that the routes are independent of the projects folder and file structure. Now we are going to move back towards a Rails convention. Our controller is named albums therefore (according to convention) our URL should be of the form http://localhost:3000/albums.

To achieve this, we could modify our routes.rb to look like this:

ActionController::Routing::Routes.draw do |map|
 map.connect 'albums/:action/:id', :controller => "albums"
end

Now you should be able to access the site with: http://localhost:3000/albums
The site should work, but now using albums in the URL.

If we used the :controller receptor, we would have a more generic rule, one that would aslo work with other controllers.

Make your routes.rb look like this:

ActionController::Routing::Routes.draw do |map|
 map.connect ':controller/:action/:id'
end

Now we have a very generic routing rule. When we add a new controller, this rule may be perfect for it. We only need to add rules if we require them, this rule can be used as the default rule. In fact, this is known as the default route. And now you know…

The End of Part 3

And so ends this long awaited third edition. I hope this series continues to help those of you who are new to Rails.

The only topics left in the series are Named Routes and then RESTfull routing.

Your feed back is appreciated…

- Thanks, Daryn

Part 4

About these ads

38 thoughts on “Beginners Tutorial: Routing in Rails 2.0 (with REST) – Part 3 of n

  1. Pingback: Beginners Tutorial: Routing in Rails 2.0 (with REST) - Part 2 of n « YAB

  2. Pingback: Routing in Rails 2.0 | Life of a College Entrepreneur

  3. I just stumbled up on the first 3 parts of this series, and it has cleared up a lot of the basics for me. Thanks!

    As I try to figure out this routing stuff, I have noticed a lot of statements such as
    map.resources :products, :member => { :short => :get, :toggle => :post }, :collection => { :sold => :get }

    When I do a “rake routes” with these, they randomly map GET, POST, ANY, etc.

    What, exactly is the grammar/syntax for writing these routing rules? Everywhere I look, there are lots of examples, and magic functions such as map.resources, map.home, map.namespace, etc, but no general grammar that shows how to write routes to get the routing table as described in “rake routes”.

    If you need fodder for part 4 of n, this is it.

    Thanks, Jay

  4. Hello Jay,

    Firstly there is a lot of magic going on. It is probably more important to understand how to use the magic, then to understand the magic.

    The good news is that there is not random mapping going on. That is RESTful routing, I would say it is the opposite of random mapping. Too many ‘spaghetti’ custom routes lead to random mapping.

    You are right, your questions are fodder for the next articles.

    I recommend you that you go through Part 1 and 2 (and 3 again if required) of this series. I will do my best to whip up part 4 and 5 as soon as possible. I don’t want to commit to a deadline, after all my ‘real’ work comes first.

    Don’t despair, there is light at the end of the tunnel…

  5. I look forward to the topics on Named Routes and RESTful routing. This is the part I’m struggling with now.

    Thank you for taking the time to prepare and present this material.

  6. When you get to RESTful routing I would like to see an example of how to add a SaveAs function. This would allow the user to derive a new record in a table from an existing record. So it would start as an edit function but end as a create action instead of an update action

  7. Hi Tom, I think that ‘save as’ functionality would make for an interesting discussion. I will certainly cover that when I move onto RESTful routing.

    – Thanks

  8. When I remove:

    map.connect ‘music’, :controller => “albums”, :action => “index”

    I get:

    No route matches “/music” with {:method=>:get}

    I like the tutorial so far – I am hanging out for RESTful routing!

    Thanks,

    Phil.

  9. Hi Philip…

    I think you may have removed the wrong route. You should have removed
    map.connect ”, :controller => “albums”, :action => “index”

    and not
    map.connect ‘music’, :controller => “albums”, :action => “index”

    Does this fix the problem?

    Glad you have enjoyed the series this far…

  10. Daryn,

    When we have:

    ActionController::Routing::Routes.draw do |map|
    map.connect ‘music’, :controller => “albums”, :action => “index”
    map.connect ‘music/:action/:id’, :controller => “albums”
    map.connect ‘music/:action’, :controller => “albums”
    end

    You say:
    “Let’s remove the fist rule, so that we are left with:

    ActionController::Routing::Routes.draw do |map|
    map.connect ‘music/:action/:id’, :controller => “albums”
    map.connect ‘music/:action’, :controller => “albums”
    end

    ??

  11. Hey Philip, sorry I see you were talking about a different part of the tutorial.

    I tried it again on my side, and it worked fine.

    I copied the routes.rb file from the tutorial:
    ActionController::Routing::Routes.draw do |map|
    map.connect ‘music/:action/:id’, :controller => “albums”
    map.connect ‘music/:action’, :controller => “albums”
    end

    Then used the following URLs:

    http://localhost:3000/music/index

    http://localhost:3000/music

    Both worked fine. Did http://localhost:3000/music/index work on your side?

    I wonder if it is a Rails version problem, what version are you using?
    Has anyone else had this problem?

    -Daryn

  12. Philip another question, did you follow part 1 and 2?

    Or did you jump in here and start at part 3?

  13. Pingback: Beginners Tutorial: Routing in Rails 2.0 (with REST) - Part 4 of n « YAB

  14. In case anyone was wondering, Philips problem has been solved. There was a typo in his routes.rb. The post itself seems to be error free…

  15. Small typo – “And Know you Know” should be “And now you know”

    Thanks for tutorial :)

  16. Diabolo, you know you are right… I have fixed it know now.

    Thanks :)

  17. Pingback: This Week in Ruby (May 12, 2008) | Zen and the Art of Programming

  18. Pingback: 3 n

  19. If you create a new map.connect route like this for example:

    map.connect ‘music’, :controller => “albums”, :action => “index”

    How do you actually get Rails to build urls that begin with music/ like with a link_to?

    I know how to do this with named routes.

    Thanks

  20. Actually nevermind. The link_to is working properly. I had another route that was blocking the map.connect ‘music’…..

  21. Awesome! Tutorial. Thats what I was looking for. Unfortunately there are not many tutorials for Rails 2. Thanks for sharing your knowledge man.

    Hope to see more from you soon!.

  22. Pingback: rails 2 0 tutorials

  23. This series is FANTASTIC. You’re talented at making seemingly cryptic topics simple to understand. Bookmarked.

    Now write a book!

  24. Excellent – loved the way you took us through the logic to get to the default route step by step and why different supplied url patterns can still work. This has really helped me.

  25. Hey Daryn,

    As far as I’m concerned, you performed a great service to the Rails community. I’ve been trying to get a grip on RESTful routing without much success. Part 3 was great! That’s my starting point for really for “owning” the new routing concept. Thank for your generous and informative series.

  26. Pingback: My site.

  27. man u r simply amazing!!!!
    God bless u!!
    i was stuck on this routing stuff and although
    i read some book on the matter(written by “important” authors -lol-) , none of them explain it
    clearly like u did!!!

  28. Good tutorial. I am new to rails routing and I don’t understand in example 3.1, when you use the magic routing, why you need to modify edit.html.erb to add :id explicitly. Why if you keep your own update routing rule you don’t need to set :id explicitly? Thank you very much for your answer.

  29. thanks for your articles, it helps me a lot to understand the functioning of the routes!!!!!!

  30. Thank you very much….great content and nicely done! Very easy to understand it.
    When i first read about rails and just try to do an example everything was so confused so i got to remember, to learn it by memory the controller instructions…..now verything has come to sense.
    Thanks Daryn.

  31. The tutorial is really awesome man. I was having sooo much problem in understanding how this routes basically works, but this tutorial really makes it clear whats happens inside rails routes. Thanks a lot .

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s