I have two use cases for routing that I would like to explore. First is a common MultiViews pattern by which the file extension is used to determine either the action or the template. The second is the common REST paradigm by which all resources of a given type are handled by the same controller, but the action isn’t determined by the URI, but instead by the HTTP request method used.
To make this more concrete, consider an HTTP GET of the following URI:
http://example.com/blog/10.html
The goal is to parse that URI into
{'controller'=>'blog', 'action'='get', id='10', 'flav'=>'html'}
... where flav
is short for flavour, the name that
blosxom gives to such
things.
I experimented with a number of things to make this work.
For the MultiViews case, one possible solution is to simply have
the routing inhale the last component of the path as :id or even
*path, and have each action pick apart the parameter. But
that wouldn’t exactly be
DRY would
it? Nor would sprinkling each action with
request.get?
queries.
My second solution involved a double dispatch, i.e., having
multiple maps, each of which resolves to the same action, where
that action does some common setup, and then re-dispatches (perhaps
by send params[:subaction]
, or even send
request.method
). The result was fairly
satisfactory.
Still exploring, my third solution involves introducing
mod_rewrite functionality into
ActionController::Routing::RouteSet
. It might
not be the most practical solution long run, but building it
certainly was educational.
Simply by doing a require 'mod_rewrite'
, a rewrite
method is made available to map
, and the
recognize!
method in the
ActionController::Routing::RouteSet
class is
dynamically replaced. Furthermore, each time the
recognize!
method is called, the request in question
is modified so that two instance methods inherited from the base
class are replaced by attribute accessors. The list of
rewrite actions are simply an array of blocks which are executed in
sequence. In the simplest case of a pattern and a replacement
the block is actually a full blown continuation.
All in all, the hardest thing about this solution was ensuring that things are reset properly between calls. CGI is truly only expected to be called once per process, opening the possibility of unit tests ‘bleed through’. Also, apparently rails will call draw multiple times, which would mean that the rewrites would get duplicated. Neither were difficult to address, and neither would be a problem if this functionality were properly folded into rails itself.
In an IM conversation with David Heinemeier Hansson earlier today, I got the impression that he prefers tailored solutions that solve specific needs well as opposed to over-engineered solutions that solve general needs poorly, and I can respect that. However, the fact that I could independently build and integrate such a general purpose addition is a testament to the power of Ruby.
I read this article about an hour ago, and mulled it over a bit, and came up with a couple of thoughts for you, Sam. I’m not a real expert at Rails yet, but if I were to tackle the MultiViews aspect of the problem, the first thing I’d look at is whether filters would do the trick. This would allow you to write the rendering code once.
The other thought I had was to have a look at the render routine. One feature that you can leverage is specifying which layout you want to use. What I’d do here is pass the flav in there as a layout name, and construct templates in whatever format is required by that flav.
It would be the latter option I’d pick, combined with your first attempt to solve the MultiViews challenge. The beautiful thing about all this is that there are so many ways to solve the problem.
Thanks, Robert.
I took a look into this. before_filter
basically does what I need. I have to access params
as controller.params
but other than that, things just work.
Unfortunately, after_filter
is invokes too late to do what I want. What I would ideally want after_filter to do is
render :action => params[:flavour]
Unfortunately, if no other rendering is done by the time the action is complete, Rails will attempt to render a default template by the same name as the action — before invoking after_filter.
Hey, Sam
I was kind of afraid of that (re: the after_filter business). I had another thought since my last post. The render() method is a method inherited from the ApplicationController, and I bet it hasn’t been frozen. That means that you can override the defaults simply by writing your own render() method that tests for, then leverages @params[ :flavour ] if it exists.
default_template_name
method in the controller in question.Hi Sam,
It sounds like the ideas you have for dispatching in Rails based on the HTTP request method are similar to mine. We’ve been discussing RESTifying Rails on the uf-rest mailing list the last week or so and this subject has come up. DHH came up with one approach, and I created a prototype controller that implements it — thought you might be interested: [link]
Hi Sam, I found where my problem is , but I can’t think of a way to solve it. The problem is I need to do some data processing before I know what view should be rendered.
so I have something like:
def default_template_name
“@result_from_controller”
end
I am not sure if this is possible. after_filter is too late, but default_template_name is called too early , before the action code is called.
I couldn’t find your email, what is your email?
If you want to send me email, send it to anything at this website, but in general, I respond better to comments on my weblog. If you prefer email, I’d suggestion rails@lists.rubyonrails.org
With the version of Rails that I used in September, default_template_name was called at the time any render-type method was called. If no render method was called explicitly, one is called on your behalf after the action code completes.