Say, for example, you are tasked with developing a type of taxonomic application that would allow users to navigate a path of increasing levels of specificity (we'll only use 4 of the 7 levels). Easy enough you say; just define a route for each level your app needs to handle.
get "/:kingdom" => "the_controller#classes"
get "/:kingdom/:class/:family" => "the_controller#species"
get "/:kingdom/:class/:family/:species" => "the_controller#details"
get "/:kingdom/:class/:family" => "the_controller#species"
get "/:kingdom/:class/:family/:species" => "the_controller#details"
(Don't whine at me because I'm not using RESTful routes here; this is just a simple example.)
So an url such as /animals/mammals/dogs/wolf would be handled by our defined routes (matches the last one). Ok, so what's the problem? Well what if one of the requirements of the application was to allow users to SKIP one of these levels. For example, what if the user is presented with a list of animal classes along with their associated families, AND they are allowed to select either the class or the family to see an associated list of species.
- Mammals
- Cats
- Dogs
You might be thinking at this stage; well, if :family is optional, let's define it that way: /:kingdom/:class(/:family)/:species But as any decent programmer could tell you, optional parameters only work as expected when placed AT THE END of an arguments list.
So what do you do? Static routes that don't map parameters aren't very useful. You want Rails routing to do it's magic and not have to parse request urls (ewww). So enter one of the uses of routing CONSTRAINTS. What constraints allow us to do here is sort of fudge on the rule about optional parameters. Our final routing solution might look something like this:
get "/:kingdom" => "the_controller#classes"
get "/:kingdom/:class(/:family)" => "the_controller#species", :constraints=>{:family=>Family.all.map(&:name).join('|')}
get "/:kingdom/:class(/:family)/:species" => "the_controller#details", :constraints=>{:family=>Family.all.map(&:name).join('|')}
get "/:kingdom/:class(/:family)" => "the_controller#species", :constraints=>{:family=>Family.all.map(&:name).join('|')}
get "/:kingdom/:class(/:family)/:species" => "the_controller#details", :constraints=>{:family=>Family.all.map(&:name).join('|')}
Let me explain what this accomplishes. First you'll notice that :family has now become optional. But wait, that can't work! Well what the conditional does is define what :family can match against. (All the extra stuff in there is just pulling together a list of Family instance names into a regular expression.) Now if an url segment exists in the location defined by :family AND it matches the regexp we defined, it will be recognized as a family. However, if that segment does NOT match the regexp, routing will say: "Hey, this isn't a family, it must be a species."
Please note: The order in which your routes are defined does matter. For example. if you swapped the placement of the inner-two routes with each other, then any routes with two segments would always go to the species action. Let's see if you can tell me why...
No comments:
Post a Comment