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/
to "/bar/
. 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 thatApplicationController
is just another controller name, andapplication.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.).