Yehuda Katz is a member of the Ruby on Rails core team, and lead developer of the Merb project. He is a member of the jQuery Core Team, and a core contributor to DataMapper. He contributes to many open source projects, like Rubinius and Johnson, and works on some he created himself, like Thor.
Generic Actions in Rails 3
December 20th, 2009
So Django has an interesting feature called “generic views”, which essentially allow you to to render a template with generic code. In Rails, the same feature would be called “generic actions” (just a terminology difference).
This was possible, but somewhat difficult in Rails 2.x, but it’s a breeze in Rails 3.
Let’s take a look at a simple generic view in Django, the “redirect_to” view:
urlpatterns = patterns('django.views.generic.simple', ('^foo/(?P<id>\d+)/$', 'redirect_to', {'url': '/bar/%(id)s/'}), )
This essentially redirects "/foo/<id>" to "/bar/<id>s/". In Rails 2.3, a way to achieve equivalent behavior was to create a generic controller that handled this:
class GenericController < ApplicationController def redirect redirect_to(params[:url] % params, params[:options]) end end
And then you could use this in your router:
map.connect "/foo/:id", :controller => "generic", :action => "redirect", :url => "/bar/%{id}s"
This uses the new Ruby 1.9 interpolation syntax (“%{first} %{last}” % {:foo => “hello”, :bar => “sir”} == “hello sir”) that has been backported to Ruby 1.8 via ActiveSupport.
Better With Rails 3
However, this is a bit clumsy, and requires us to have a special controller to handle this (relatively simple) case. It also saddles us with the conceptual overhead of a controller in the router itself.
Here’s how you do the same thing in Rails 3:
match "/foo/:id", :to => redirect("/bar/%{id}s")
This is built-into Rails 3′s router, but the way it works is actually pretty cool. The Rails 3 router is conceptually decoupled from Rails itself, and the :to key points at a Rack endpoint. For instance, the following would be a valid route in Rails 3:
match "/foo", :to => proc {|env| [200, {}, ["Hello world"]] }
The redirect method simply returns a rack endpoint that knows how to handle the redirection:
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 = Rack::Request.new(env) params = path_proc.call(env["action_dispatch.request.path_parameters"]) url = req.scheme + '://' + req.host + params [status, {'Location' => url, 'Content-Type' => 'text/html'}, ['Moved Permanently']] end end
There’s a few things going on here, but the important part is the last few lines, where the redirect method returns a valid Rack endpoint. If you look closely at the code, you can see that the following would be valid as well:
match "/api/v1/:api", :to => redirect {|params| "/api/v2/#{params[:api].pluralize}" } # and match "/api/v1/:api", :to => redirect(:status => 302) {|params| "/api/v2/#{params[:api].pluralize}" }
Another Generic Action
Another nice generic action that Django provides is allowing you to render a template directly without needing an explicit action. It looks like this:
urlpatterns = patterns('django.views.generic.simple', (r'^foo/$', 'direct_to_template', {'template': 'foo_index.html'}), (r'^foo/(?P<id>\d+)/$', 'direct_to_template', {'template': 'foo_detail.html'}), )
This provides a special mechanism for rendering a template directly from the Django router. Again, this could be implemented by creating a special controller in Rails 2 and used as follows:
class GenericController < ApplicationController def direct_to_template render(params[:options]) end end # Router map.connect "/foo", :controller => "generic", :action => "direct_to_template", :options => {:template => "foo_detail"}
A Prettier API
A nicer way to do this would be something like this:
match "/foo", :to => render("foo")
For the sake of clarity, let’s say that directly rendered templates will come out of app/views/direct unless otherwise specified. Also, let’s say that the render method should work identically to the render method used in Rails controllers themselves, so that render :template => "foo", :status => 201, :content_type => Mime::JSON et al will work as expected.
In order to make this work, we’ll use ActionController::Metal, which exposes a Rack-compatible object with access to all of the powers of a full ActionController::Base object.
class RenderDirectly < ActionController::Metal include ActionController::Rendering include ActionController::Layouts append_view_path Rails.root.join("app", "views", "direct") append_view_path Rails.root.join("app", "views") layout "application" def index render *env["generic_views.render_args"] end end module GenericActions module Render def render(*args) app = RenderDirectly.action(:index) lambda do |env| env["generic_views.render_args"] = args app.call(env) end end end end
The trick here is that we’re subclassing ActionController::Metal and pulling in just Rendering and Layouts, which gives you full access to the normal rendering API without any of the other overhead of normal controllers. We add both the direct directory and the normal view directory to the view path, which means that any templates you place inside app/views/direct will take be used first, but it’ll fall back to the normal view directory for layouts or partials. We also specify that the layout is application, which is not the default in Rails 3 in this case since our metal controller does not inherit from ApplicationController.
Note for the Curious
In all normal application cases, Rails will look up the inheritance chain for a named layout matching the controller name. This means that the Rails 2 behavior, which allows you to provide a layout named after the controller, still works exactly the same as before, and that
ApplicationControlleris just another controller name, andapplication.html.erbis its default layout.
And then, the actual use in your application:
Rails.application.routes do extend GenericActions match "/foo", :to => render("foo_index") # match "/foo" => render("foo_index") is a valid shortcut for the simple case match "/foo/:id", :constraints => {:id => /\d+/}, :to => render("foo_detail") end
Of course, because we’re using a real controller shell, you’ll be able to use any other options available on the render (like :status, :content_type, :location, :action, :layout, etc.).

Brian Cardarella, Posted December 21, 2009, 12:48 am
It looks like DHH has already outdated your blogpost.
match “/route”, :to => “controller#action”
is now
match “/route” => “controller#action”
http://github.com/rails/rails/commit/3ff9e9ee147b682cb13aed4c057e750228892f42
Kieran P, Posted December 21, 2009, 12:48 am
Awesome write up.
As you mentioned breifly in a comment in the last code sample, you can use a shorter version, which DHH pushed recently.
match “/foo/:id”, :to => redirect(“/bar/%{id}s”)
can thus be
match “/foo/:id” => redirect(“/bar/%{id}s”)
I think this format would be neat also, to make things more clear:
redirect “/foo/:id” => “/bar/%{id}s”
Brian Cardarella, Posted December 21, 2009, 12:49 am
…and I spoke to soon. Didn’t notice that comment in your last code block. Whoops. :)
Stefan Nuxoll, Posted December 21, 2009, 3:02 am
The neat thing about the shorthand syntax is you can use the ruby 1.9 hash syntax for it too
match “/foo/:id”: redirect(“/bar/%{id}s”)
Although I think
match “/foo/:id” to: redirect(“/bar/%{id}s”)
looks better, almost like smalltalk
wycats, Posted December 21, 2009, 3:09 am
Unfortunately, {“foo”: bar} is invalid in Ruby 1.9. Also, you’d have do
match "/foo/:id", to: redirect("/bar/%{id}s")(comma required)Chris, Posted December 21, 2009, 3:12 am
I really like the new routing syntax.
Have there been any benchmarks comparing the speed of the Rails 2 and Rails 3 routers? I was really impressed by the Merb router’s performance, and was hoping that some of that power would be in Rails 3.
Thomas Ritz, Posted December 21, 2009, 3:13 am
In Python, the “s” is not part of the output, but the conversion type, in this case a string.
>>> ‘/bar/%(id)s/’ % {‘id’: 123}
‘/bar/123/’
See http://docs.python.org/library/stdtypes.html
Chapter 6.6.2. String Formatting Operations
@codecuisine, Posted December 21, 2009, 3:40 am
BTW, will there be another pre-version of Rails 3? I’m thinking about when to start new projects based on 3.0
wycats, Posted December 21, 2009, 3:42 am
Whoops, my mistake. The Ruby equivalent would be
"/bar/%{id}" % {id: 123}. Type specifiers are allowed via"/bar/%.d" % {id: 123}.Botanicus, Posted December 21, 2009, 3:59 am
Well done Yehuda, this is pretty good. For guys who don’t wait for Rails 3 (even if the release will be soon then it will take ages before plugins will be rewritten for Rails 3 etc, see situation in Ruby 1.9), there are generic views for ages in Rango. And since Rango is more lightweight and is trying to give you tools for easier dealing with Rack, I would say it’s easier to write generic view in Rango.
OK, talk is cheap, so I can show you http://is.gd/5vU2c & http://is.gd/5vU2e for rendering, defered routes and redirect. As you can see you don’t have to create a class for rendering, just use render mixin and that’s it.
If you are interested in, come to #rango at freenode and let’s talk about it!
wycats, Posted December 21, 2009, 4:13 am
@botanicus there are very simple ways to get simple rendering working with Rails 3, but I wanted to expose the identical API that a user would expect from a Rails controller.
Also, you had to make a module, which doesn’t seem like much of an improvement over making a class. Are you worried about object allocation?
wycats, Posted December 21, 2009, 4:17 am
@botanicus another question: it seems that your generic view for templates creates a new instance of the Template object (and therefore results in a brand new eval) for each use. Am I missing something?
DAddYE, Posted December 21, 2009, 5:05 am
wycats, but are you sure about:
This uses the new Ruby 1.9 interpolation syntax (“%{first} %{last}” % {:foo => “hello”, :bar => “sir”} == “hello sir”) that has been backported to Ruby 1.8 via ActiveSupport.
I did find it in 2.3.5 version of AS.
Thanks!
wycats, Posted December 21, 2009, 5:09 am
You’re correct. active_support/core_ext/string/interpolation exists on master (3.0.pre) but not on 2-3-stable.
Botanicus, Posted December 21, 2009, 5:23 am
@wycats I know that this could be pretty easy, but it’s not the point – I just want to show Rango approach. BTW I don’t have to create a module I’m just doing it because I think it’s better, but it would work with def render() end; get(“/”).to(method(:render)) as well. And the point about this was more about it that you have to create a method AND a controller when I’m creating just the method (which will be probably in the module but it doesn’t matter really). So I’m not worried about object allocation (Rango is pretty efficient anyway), but about consistency.
Yes, I’m creating a new instance of template object every time at the moment. I’m going to implement some caching soon (end of December/beginning of January). I’m not sure which approach I choose but I’ll compile the template into a method as well as Merb does, but it won’t be for a controller, since a controller is really just optional part of Rango, so the caching itself will be very likely part of the Template class.
ippa, Posted December 21, 2009, 6:19 am
Great stuff Yehuda, I always enjoy reading your tech filled blog posts about rails 3.
We’re all happy you came in making something neat and pretty out of the previous code smell ;)
John Callahan, Posted December 21, 2009, 9:26 am
Merb FTW !!!! ;-)
Kyle Fox, Posted December 21, 2009, 12:10 pm
Very handy. The two cases described here are two of the more trivial generic view which Django provides, but it’s a start.
Any plans to implement the more interesting generics, like date-based or CRUD views?
http://docs.djangoproject.com/en/1.1/ref/generic-views/#date-based-generic-views
http://docs.djangoproject.com/en/1.1/ref/generic-views/#create-update-delete-generic-views
Conrad Taylor, Posted December 24, 2009, 1:49 am
Yehuda, great overview of Rails 3.0 generic actions and I look forward to playing with them.
Rizwan Reza, Posted December 25, 2009, 3:12 pm
This is nice. The redirect method in Routes really contribute a lot to adding much more flexibility to Rails.
I’ve also posted a write-up on the changes of routes from Rails 2 to Rails 3. Check it out here: http://rizwanreza.com/2009/12/20/revamped-routes-in-rails-3.
Giles Bowkett, Posted December 30, 2009, 2:01 am
Awesome.
Allen, Posted January 25, 2010, 4:19 am
Okay, we see the generic action in Rails 2 at the same time, that’s nice!
Justin Marney, Posted January 27, 2010, 8:03 pm
I’ve been digging around in a Rails3 app trying some of this out. I ran into something interesting/strange. I noticed you did: “include ActionController::Layouts” in the metal end point. I can’t actually figure out where the ActionController module gets Layouts. In addition, if I go into the console in a Rails3 app and enter: ActionController::Layouts it returns “Layouts” and if I do it again (2x in a row) I get an error stating that it can’t find ActionController::Layouts. Very weird. I will continue to investigate, but is something you could shed some light on?
Steve, Posted July 14, 2010, 7:41 am
I just get a no method error for render?!
Jesse Clark, Posted November 27, 2010, 2:12 pm
@Justin the Layouts module ended up in AbstractController not ActionController.
I tried to implement this but got stuck on the javascripts_dir and stylesheets_dir not being set when the page the page was rendering. I included AbstractController::AssetPaths in RenderDirectly and tried setting these variables via:
javascripts_dir = Rails.root.join(“public”, “javascripts”)
stylesheets_dir = Rails.root.join(“public”, “stylesheets”)
but they still weren’t set when javascript_link_tag was called.
At this point I gave up because it started to feel like a too much effort when using the older method of having a PagesController and catch all routing is pretty dead simple:
controller :pages do
get ‘:action’, :to => ‘pages#show’
end
def show
# TODO: return a 404 if the page doesn’t exist
render params[:action]
end
Here is a gist of my attempt to enable render() directly in routes.rb if anyone else wants to play with it:
https://gist.github.com/718175