Rails and Merb Merge: Plugin API (Part 3 of 6)

By Yehuda Katz | January 11th, 2010 at 10:01AM

I started off this series on the Rails/Merb merge (aka Rails 3) talking about modularity, and then performance. Next up, plugins!

When we announced the Rails/Merb merge,  we promised to bring a more stable plugin API to Rails 3.  Merb had strong opinions about an explicitly exposed plugin API, so we hoped to end the complications of alias_method_chain in favor of exposed APIs which plugin authors could rely on across versions.

Dogfooding

By far the most important element in developing the plugin APIs for Rails 3 was to eliminate the “secret” APIs used in Rails itself. Instead, we wanted to build Rails on top of the same core components that we would ask plugin authors to build on. The focal point of this effort (because we already had experience doing this with Merb) was ActionPack, and specifically ActionController.

ActionController presented us with another excellent opportunity: because ActionMailer reused a lot of the controller functionality, we had a built-in opportunity to create components that could be used in both ActionController and ActionMailer. In a sense, ActionController and ActionMailer would become plugins of the core system we were creating.

We also took a second important conceptual step. In Rails 2.3, the idea of “metal” was that you could skip the entire ActionController chain and write raw Rack handlers where needed for performance. In Rails 3, we are exposing an incremental series of components that, in sum, make up ActionController::Base. These components include rendering, layouts, conditional get, respond_to, streaming, and others.

In Rails 3, ActionController::Metal is the simplest possible controller, with no additional components added in. ActionController::Base is simply a subclass of ActionController::Metal with all components included. Again, we are dog-fooding our componentized APIs, not simply giving you a watered down version of what we use internally.

Now, let’s dig into a few specific examples of new APIs.

ActionController::Renderers

One of the most common reasons people extend controllers is to expose additional renderers. For instance, you may want to render a PDF as well as HTML. The Rails 3 API that we expose to plugin authors for this purpose is identical to the one that we use internally.  (As you probably know, in addition to being able to render templates, you can render :json, :xml, or :js in normal Rails controllers.)

If you take a look at the source for ActionController::Renderers, here’s how we do it:

add :json do |json, options|
  json = ActiveSupport::JSON.encode(json) unless json.respond_to?(:to_str)
  json = "#{options[:callback]}(#{json})" unless options[:callback].blank?
  self.content_type ||= Mime::JSON
  self.response_body  = json
end

This method is in ActionController::Renderers, so it’s like saying ActionController::Renderers.add(:json) do |json, options|. You can add additional renderers easily in Rails 3 plugins, and you don”t have to figure out how to hook into Rails at the appropriate time. Like other parts of the public API, we’re committing to keeping this API unchanged across multiple versions of Rails. If and when when the API DOES change, the change will be flagged for you first, via the normal Rails API deprecation process.

ActionController::Metal

In Rails 3, there’s a supported way to use a simple, stripped down controller, and opt into the functionality you want. The overhead of the completely stripped down controller is a mere 8 microseconds (not much more than the overhead for a pure Rack app), compared with 100 or so microseconds of overhead for the full ActionController::Base (as I said last time, this itself is significantly less than Rails 2.3).

Here’s an example of using ActionController::Metal:

class SimpleController < ActionController::Metal
  abstract!
end
 
class HelloController < SimpleController
  def index
    self.response_body = "Hello World"
  end
end

A few things to note here. First of all, you don’t need a superclass, but you can build one to re-use in multiple places. The abstract! declaration tells Rails 3 not to include public methods on the superclass as controller actions. It’s not strictly needed here (because there are no public methods), but it’s a good habit for custom ActionController::Metal subclasses. Second of all, because this is a controller, you can route to it through the normal Rails router. From the perspective of the rest of Rails, this is a perfectly normal controller.

Finally, the self.response_body usage is the simple, low-level API used in ActionController itself (for instance, during template rendering), but it’s now 100% first-class and available for your use. You can also set status, content_type and headers directly, regardless of whether you use the Request and Response helpers or not.

If you want to add rendering in, you can do something like this:

class SimpleController < ActionController::Metal
  abstract!
  include ActionController::Rendering
  append_view_path "#{Rails.root}/app/views"
end
 
class HelloController  "hello"
  end
end

You've now added in a new module, which provides rendering support. This keeps things simple by not including layout support, because you don't need it in this case. With this capability, you can add the features that you want to add, leaving out what you don't. This means that as a metal grows, you don't need to completely rewrite it as a "normal" controller. Instead, you can pull in the specific features you need. This process is seamless, and again, you can route to it as a normal controller throughout.

If you look at the source for ActionController::Base, you can see that we aren't doing anything particularly special anymore:

module ActionController
  class Base < Metal
    abstract!
 
    include AbstractController::Callbacks
    include AbstractController::Layouts
 
    include ActionController::Helpers
    helper :all # By default, all helpers should be included
 
    include ActionController::HideActions
    include ActionController::UrlFor
    include ActionController::Redirecting
    include ActionController::Rendering
    include ActionController::Renderers::All
    include ActionController::ConditionalGet
    include ActionController::RackDelegation
    include ActionController::Logger
    include ActionController::Configuration
    ...

Here, ActionController::Base inherits from Metal, and then proceeds to pull in all of the functionality not exposed as separate modules. Although Base pulls them all in, you don't have to worry about dependencies between these modules; handling dependencies is built into the way these modules work.

View Contexts

As one last example, the way that ActionController communicates with ActionView itself has become a fully defined, exposed API. From the perspective of ActionController, a view implements three APIs:

  • Context.for_controller(controller)
  • Context#render_partial(options)
  • Context#render_template(template, layout, options)

By implementing this API, you can drop in any object in place of ActionView. How would you use this? Well to date, I can think of two examples: rspec_rails is going to use this functionality to implement isolation tests, and you can use this to implement a Merb shim in which the controller and view are the same object (an implementation using an earlier version of the API).

In addition to the usefulness of exposing this functionality via a stable Public API, reducing the boundary between ActionController and ActionView significantly improved the architecture, and exposed areas of brittleness that benefited quite a bit from the definition.

Conclusion

The APIs provided here, while stable at this point, are not final. If you feel that they (or any other part of Rails 3) aren't adequate for a plugin you're building, please let us know by commenting.

This post is just a taste of the new ways you can extend Rails 3. In the weeks ahead, expect a lot more documentation and discussion about the exposed plugin hooks.

Share this post:
  • email
  • Digg
  • del.icio.us
  • Reddit
  • Slashdot
  • StumbleUpon
  • Technorati
  • Twitter
  • Google Bookmarks
  • Facebook
  • LinkedIn
Popularity: 10% |
Rate this post: 1 Star2 Stars3 Stars4 Stars5 Stars
Loading ... Loading ...

This website uses IntenseDebate comments, but they are not currently loaded because either your browser doesn't support JavaScript, or they didn't load fast enough.

16 Responses to “Rails and Merb Merge: Plugin API (Part 3 of 6)”

  1. DrErnie DrErnie says:

    Please tell me you're missing a "do" or something; that syntax scares me….

    class HelloController "hello"

    end

    end

  2. The ActionController refactoring looks great. The ability to build up controllers from Metal will allow for much more optimization.

    As for plugin API requests, it would be nice to have something similar to add_slice in Merb to hook into the router.

  3. mrkris mrkris says:

    Interesting, but your code formatting is a bit broken.

  4. Patrick Patrick says:

    Great series. I really hope that you will plan to touch on Mountable Apps as part of this. I haven't seen much info on that subject lately.

  5. Andru Andru says:

    Awesome series of posts – really great to see how well you guys are integreating the Merb attitude of modularity and api transparency with the convention over configuration Rails is so well known for. Looking forward to the next posts almost as much as the big 3.0 itsself :)

  6. Andru Andru says:

    I'm still seing the 3rd code block as broken by the way:

    class HelloController "hello" end end

  7. k776 k776 says:
    • How do plugins/gems hook into the router nicely in Rails 3?
    • How do plugins/gems run initializers in Rails 3?
    • How do plugins/gems add Rack middleware nicely in Rails 3?
    • Sam Granieri Sam Granieri says:

      Look at the source code in edge railties/lib/rails/[application,bootstrap,initializable,railtie,plugin].rb and then look at the railtie.rb files in active record, action pack, action view, etc…

      • k776 k776 says:

        I was actually hoping for more indepth topics from Yehuda about those questions, to explain the API that is in Rails 3, not where to find it.

        • wycats wycats says:

          Ha. I think I probably would have pointed you to those same places. Rails 3 exposes a Railtie class that any plugin can subclass and have access to initialization, middleware, configuration, etc. We're still working on the right workflow for routing stuff (because there are multiple "correct" solutions), and we'll have stuff for you soon.

  8. I believe you meant to say milliseconds, not microseconds. I highly doubt you have timing accuracy to the microsecond level let alone have code with that low a time. :)

    • Jon Mischo Jon Mischo says:

      No, I'm pretty sure he meant microseconds. Did you read the previous posts?

      If ActionController's overhead was 100ms, requests would be ridiculously slow and it would be very hard to scale Rails, since AC would limit you to a maximum of 10 requests per second, assuming your code did absolutely nothing.

    • wycats wycats says:

      Then you believe incorrectly ;)

      The benchmarks are located at http://github.com/wycats/rails-simple-benches and can be run by checking the repo out into the actionpack dir in Rails. It includes the full cost of a Rack request, without the network overhead (important, but not a cost that Rails is responsible for).

    • wycats wycats says:

      Then you believe incorrectly ;)

      The benchmarks are located at http://github.com/wycats/rails-simple-benches and can be run by checking the repo out into the actionpack dir in Rails. It includes the full cost of a Rack request, without the network overhead (important, but not a cost that Rails is responsible for).

Leave a Reply