Yehuda Katz is a member of the Ember.js, Ruby on Rails and jQuery Core Teams; he spends his daytime hours at the startup he founded, Tilde Inc.. Yehuda is co-author of best-selling jQuery in Action and Rails 3 in Action. He spends most of his time hacking on open source—his main projects, like Thor, Handlebars and Janus—or traveling the world doing evangelism work. He can be found on Twitter as @wycats and on Github.

Generic Actions in Rails 3

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 ApplicationController is just another controller name, and application.html.erb is 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.).

25 Responses to “Generic Actions in Rails 3”

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

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”

…and I spoke to soon. Didn’t notice that comment in your last code block. Whoops. :)

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

Unfortunately, {“foo”: bar} is invalid in Ruby 1.9. Also, you’d have do match "/foo/:id", to: redirect("/bar/%{id}s") (comma required)

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.

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

BTW, will there be another pre-version of Rails 3? I’m thinking about when to start new projects based on 3.0

Whoops, my mistake. The Ruby equivalent would be "/bar/%{id}" % {id: 123}. Type specifiers are allowed via "/bar/%.d" % {id: 123}.

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!

@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?

@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?

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!

You’re correct. active_support/core_ext/string/interpolation exists on master (3.0.pre) but not on 2-3-stable.

@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.

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 ;)

Merb FTW !!!! ;-)

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

Yehuda, great overview of Rails 3.0 generic actions and I look forward to playing with them.

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.

Awesome.

Okay, we see the generic action in Rails 2 at the same time, that’s nice!

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?

I just get a no method error for render?!

@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

Leave a Reply

Archives

Categories

Meta