Engine Yard Blog RSS Feed

In this final segment, I'll talk about how Rails 3 takes advantage of the Rack standard, leveraging it in ways that make sharing code between Ruby web applications easier. There are a ton of good blog posts and presentations on Rack itself, so I won't go into too much detail beyond a brief summary.

What is Rack?

Rack defines a standard for interaction between web servers and Ruby web applications. It's based on the CGI standard, but stripped of CGI's global characteristics (such as its use of environment variables and standard output).

Servers send requests to Rack applications by calling their #call method with an environment Hash containing information as a number of standardized keys. For instance, it provides the requested path in the PATH_INFO variable. Applications send responses back to the server as an Array of three elements: the status code, a Hash of the headers, and the body. Applications may return any object that responds to #each as the body, and each iteration must return a String object.

Here are a couple of simple Rack applications:


# A Proc responds to #call
proc do |env|
  [200, {"Content-Type" => "text/html"}, ["Hello world"]]
end

# Other objects that respond to #call work too
class MyApp
  def self.call(env)
    [
      200,
      {"Content-Type" => "text/html"},
      ["Hello world"]
    ]
  end
end

Rails 2.3

In Rails 2.3, Rails became a fully valid Rack application, leveraging Rack in a number of places:

  • ActionController::Dispatcher.new is the top-level Rack application
  • The parameter parser is implemented as middleware
  • The router is a Rack application that dispatches to controllers
  • Each controller is a Rack application
This was a modest step toward leveraging Rack, and made Rails applications work anywhere Rack worked. However, the internals of some of these pieces were messy, and an overhaul would be needed to take full advantage of the Rack ecosystem and the power of Rack's architecture.

Enter Rails 3: The Application

First, rather than having a global Dispatcher object pointing at a global router, Rails 3 introduces the idea of the Rails Application. The Application, an object holding the router and other configuration, is itself a Rack application.

The following is a valid config.ru.


# Set up load paths as desired

require "action_controller/railtie"

class FooController < ActionController::Base
  def bar
    self.response_body = "HELLO"
  end
end

class MyApp < Rails::Application
   config.session_store :disabled

   routes.draw do
     match "/foo" => "foo#bar"
  end
end

run MyApp

As you can see, a Rails application is a valid Rack application. The above config.ru file will run with rackup, Passenger, Glassfish, or any other Rack server.

ActionDispatch and Rack Middleware

Rails 3 introduces a new framework called ActionDispatch, a library extending the features of the Rack library for Rails' use. This library can also be used standalone in any Rack application.

The new ActionDispatch::Request and ActionDispatch::Response objects now inherit from Rack::Request and Rack::Response. Integration tests now use Rack::Test and can now be used to test any Rack application.

Rails now includes a number of new general purpose middlewares:

  • A middleware to run preparation callbacks, which Rails runs once in production mode, or once per request in development mode
  • A middleware to set up and write cookies
  • A middleware to clean up flash notices
  • A middleware to handle HEAD requests
  • A middleware to handle IP spoof checking
  • A middleware to serve static assets
  • Middlewares to handle rescuing exceptions down the stack
  • Middlewares for various session stores
Rails also uses a number of middlewares that ship with the Rack library:
  • A middleware to synchronize non-threadsafe requests
  • A middleware to measure and set the runtime of requests
  • A middleware to implement Send-File semantics in various web servers
  • A middleware to handle PUT and DELETE requests coming in via POST requests
The middleware stack object, available as config.middleware, supports reordering these middlewares, or inserting new middleware relative to default ones.

One nice thing about implementing the functionality as middleware is that it forces them to be simple, standalone objects, and understanding, patching or even replacing them becomes much easier than trying to modify a monolithic controller or request object.

The Router

The Rails 3 router was written from the ground up by Josh Peek as the Rack::Mount project, which provides a general-purpose Rack router. Rack::Mount recognizes URLs and dispatches them to any Rack application. As a result, the router now natively supports matching any URL to any arbitrary Rack application--even a Sintra application.


Rails.application.routes.draw do
  match "/blog" => Rack::Blog # Someone should write it!
end

The router is itself a Rack application, so it's possible to mount a router inside the main router. Carl Lerche's Astaire project uses this to provide the Sinatra routing DSL inside a Rails controller. The following is a valid config.ru:


$:.unshift File.expand_path('../../lib', __FILE__)

require 'rubygems'
require 'astaire'

class OmgController < ActionController::Base
  include Astaire::DSL
  append_view_paths File.expand_path('../views', __FILE__)

  get "/goodbye" do
    render :text => "goodbye"
  end

  get "/hello" do
    render "hello"
  end
end

run OmgController

Astaire uses ActionDispatch::Routing::RouteSet internally, a Rails wrapper around Rack::Mount.

Actions

Because the router dispatches directly to Rack applications, each controller action is its own mini-application. To get a valid Rack application for a controller's action, use the ArticlesController.action(:index) syntax.

The Rails router uses the syntax match "/articles" => "articles#index" to match a URL to a controller/action pair. That syntax is identical to match "/articles" => ArticlesController.action(:index).

Because actions are their own Rack applications, you can add middleware between the router and controller actions:


class ArticlesController < ApplicationController
  use MyMiddleware
end

This will insert the middleware between the router and the dispatching of an action. The following is a valid config.ru:



class ArticlesController < ActionController::Base
  append_view_path "/path/to/views"

  def index
    render
  end

  # implicit actions, such as "articles/show" would work here
end

run ArticlesController.action(:index)

This endpoint would behave exactly like a normal action, including callbacks and rendering. In fact, it's roughly identical to what happens internally inside the router.

You can also call into an action using this syntax:


env = Rack::MockRequest.env_for("/articles")
status, headers, body = ArticlesController.action(:index).call(env)

# Create a response object, if you want one
response = ActionDispatch::Response.new(status, headers, body)

You could also use Rack::Test:



class TestArticles < Test::Unit::TestCase
  include Rack::Test::Methods

  def app
    ArticlesController.action(:index)
  end

  def test_get
    get "/articles"
    assert_equal "Hello world", last_response.body
  end
end

# This would include the application's middleware,
# which you probably want as it includes sessions
# and cookies
class TestApplication < Test::Unit::TestCase
  include Rack::Test::Methods

  def app
    MyApplication
  end

  def test_get
    get "/articles"
    assert_equal "Hello world", last_response.body
  end
end

You would probably use Rails integration tests, which wrap Rack::Test with some additional conveniences, in practice. It's nice to know that there's very little magic going on, and that all of the individual pieces of Rails are just standard Rack.

Conclusion

While Rails 2 had started down the path of supporting Rack, Rails 3 took a few dozen steps forward, deeply integrating the framework with the Rack standard. In addition to improving the general interaction with servers, Rails 3 takes much more advantage of the Rack library, exposes its own functionality as reusable Rack components, and wires up the stack using the Rack architecture. This means that Rails itself integrates well with tools that work with Rack applications, and can also incorporate other applications or middleware following the Rack standard.

Stay tuned for more Rails 3 content to come—the series may be over, but there's plenty left to talk about.


Tagged:

comments powered by Disqus