August 6, 2006

Inside Rails Engines

Filed under: engines, rails, tutorial — Casper Fabricius @ 1:36 pm

Rails Engines are small subsets of an application that can be dropped into any of your Rails applications and handle common parts of the application from scratch. The title might be a bit misleading, since the technical side of this article is really a beginner’s introduction to Rails Engines spiced up with a few tips and tricks. I’m not diving into the inner workings of engines - the “inside” part of the title is more aimed against the fact that I’ve - out of pure curiosity and as a preparation for this article - asked the author of the engine plugin and the first and still most popular engines; James Adam, a few questions about his thoughts behind the engines and which direction he wants them to go in.

What is an engine?
There are two aspects of Rails Engines: There’s the engine plugin, a standard Rails plugin that you need to install as any other plugin. This plugin enables you install the individual actual engines that you want to use - the engines are also seen as standard plugins from Rails’ point of view, but they won’t work without the engine plugin.

An engine works by allowing a large part of the standard Rails application structure (that is; the /app directory in your projects) to exist as a part of the engine. Where a normal plugin only compromises Ruby code (often core extensions and helper classes), an engine are capable of having controllers, views and models that behaves like they are situated in the “real” /app directory.

This allows you to reuse a lot of traditional support functionality such as authentication, authorization and user management, a wiki, a shopping basket, a blog, a CMS and so on. Several engine implementations of some of this functionality already exist, most of which can be found through Rails-Engines.org, and while the wonders of CSS can get you a long way in getting these engines to fit into the design of the rest of your app, you will almost certainly want to change some things. Enter Rails Engine’s biggest advantage: The ability to override individual views and controller actions on a pr. need basis.

Let’s say you want to add some text and images to the login view of the Login engine, because this action effectively will function as your frontpage. All have to do is to copy the view from the /app/views directory of the engine into the corresponding directory in your own views directory - and through the magic of the engine plugin, this view now overrides the view that came with the engine. Often you’ll also need to modify the behavior of various actions supplied by the engine - you do this simply by creating a controller with the same name as the controller in the engine with action you want to override in your own /app/controllers directory, and define the actions in the controller. The engine plugin is able to combine the controllers of the engine with the ones in your app, so you can both override actions already defined by the engine, and you can add actions to a controller defined by the engine.

When should you use an engine?
While it is indeed very convenient to be able to drop all this functionality into your app without writing any code your self, you should be aware that this ability is not the primary goal with the original engine plugin, and that many people sees a lot of potential problems in constructing an application from one or more engines. James Adam explained it like this to me in an email:

Rails and Ruby make developing applications very easy, so there’s very little reason why a competent developer couldn’t write every part of the application that they need, and not rely on any code from outside. Working in this way has significant benefits, because it puts you in a position where you intimately understand exactly how your application works. There are no mysterious black boxes.

So when should you use an engine? James’ original intention with Rails Engines, and the way he continues to use them himself, is by abstracting commonly used application “chunks” so they can be reused in his company’s applications:

Where I work, we are working on a varying collection of fundamentally similar applications, all at the same time. They don’t have widely different requirements in terms of their basic architecture (authentication, importing Excel data, reporting back, loading and storing data and so on…), so it makes sense that we only maintain a single version of each of these shared aspects. “Enterprise DRY”, if you like. This is why engines exists; we wrote the engines plugin to make it possible for use to reuse as much of this shared code as made sense in our situation, in a manner that was easy to maintain where generators aren’t, but easy to override when the situation required it.

As for myself, I gladly use the Login, User, Riki and Substruct engines in several applications, and while I cannot claim that it haven’t taken an effort to integrate the engines into the application, I’d still say that it has saved me a considerable amount of time - especially when I’m reusing an engine that I already know well. I guess the ultimate DRY-proof solution would be to abstract my own versions of these engines - probably not to share with others, but use in my own projects working just the way I want them.

Implementing the Login and User engines in your application
Now to the more tutorial-ish aspects of this article. In this section, I’ll explain how to implement two of the most commonly used engines in your application. I expect you to have a running Rails application with a functional database connection. I also suggest that you setup a Subversion repository so you can add the plugins as externals, but it is not strictly necessary, just DRY’er.

  1. Install the Engine plugin

    1. Setup an SVN external attribute to point vendor/plugins/engines to http://svn.rails-engines.org/engines/tags/rel_1.1.3/ (or just download the files to that directory).
    2. The engine plugin is automatically loaded by Rails, and nothing visible happens until you install some engines. However, if you are running engines on Edge Rails, you should add this to the very top of config/environment.rb:
      # environment.rb
      module Engines
        CONFIG = {:edge => true}
      end
      
  2. Install the Login and User engines
    1. Setup SVN external attributes to point vendor/plugins/login_engine to http://svn.rails-engines.org/login_engine/tags/rel_1.0.2/ and vendor/plugins/user_engine to http://svn.rails-engines.org/user_engine/tags/rel_1.0.1/.
    2. Create two new migrations; login_engine_schema and user_engine_schema, and copy the contents of the migration(s) in the db/migrations of each engine into each of the new migrations. This way, you can still create the entire database structure with the rails migrate, without the need for running specialized engine tasks.
    3. Add the required configuration and startup code to config/environment.rb. The configuration is well documented in the read me files, so I won’t dive into that here, but instead provide a sample of my setup:
      # environment.rb
      
      #####################################################
      # Engine setup
      #####################################################
      
      module LoginEngine
        config :salt, "not_my_real_salt"
        #...
      end
      
      module UserEngine
        config :admin_login, "admin"
        #...
      end
      
      Engines.start :login, :user
      
      UserEngine.check_system_roles
      Permission.sync rescue false # Ignore any errors here
      
    4. Add code to application.rb in order to add user authorization to all actions, and to make the user-logic available to helpers and models:
      # application.rb
      
      require 'login_engine'
      require 'user_engine'
      
      class ApplicationController < ActionController::Base
      
        # Login and User Engines setup
        include LoginEngine
        include UserEngine
        helper :user
        model :user
        before_filter :authorize_action # unless RAILS_ENV == 'test'
      
      end
      

      (Be sure to also include Login- and UserEngine in application_helper.rb to make support available to the views.)

  3. Override views, actions and models as needed
    • A view is overridden by creating a view with the same name in your own app-structure - not by modifying the view directly in the engine. For example; if you want to override the template for the list action of the user controller, simply create the file list.rhtml in /app/views/user/, and this file will be used.
    • An action is overridden by creating the controller the action belongs to, and defining a function with the same name. For example; if you want to override the list action of the UserController to provide additional data to your list view, you should create the user_controller.rb file in /app/controllers/ and define the action:
      # user_controller.rb
      
      def list
        # ...
      end
      
    • Overriding a model is a bit more complicated, since the engine plugin has no clever way of supporting this due to various reasons. However, in the Login- and User engines, it is possible to extend the User-model because it is implemented as a namespaced module, which can be included in your own model-file. So if you want to associate a user with, say, a log entry, you can create the file /app/models/user.rb with this code:
      # user.rb
      
      class User < ActiveRecord::Base
        include LoginEngine::AuthenticatedUser
        include UserEngine::AuthorizedUser
      
        has_many :log_entries
      
      end
      

Tips and tricks
Many people complain that it is hard to test your application with authentication and authorization enabled. The easy solution is to add unless RAILS_ENV == 'test' to the before_filter :authorize_action line in application.rb. The better solution is to actually have a loaded user during the tests, since this is what the application generally expects. One way to solve this is to put this in your test case:

# /test/fixtures/companies_controller.rb

  def setup
    @controller = CompaniesController.new
    @request    = ActionController::TestRequest.new
    @response   = ActionController::TestResponse.new
    @request.session[:user] = User.find(1)
  end

Another issue the synchronization of the permissions data for the User engines authorization logic. I’m not entirely happy with the way this work, since the Permission.sync done on system startup tends to fail, which is why I’ve had to add rescue false to ignore any errors.

Some final thoughts
Should the standard Rails plugins go in the direction of engines? I asked James Adam this very question, and here is what he answered:

Yes and no. Some things that I feel should be easier in plugins:

  • Managing stylesheets/javscripts/etc in plugins. The engines plugin makes this trivial, and I’ll be working on adapting the implementation of this feature so it seems natural in any plugin.
  • Sharing views in plugins. While you can do that with a plugin (overriding a controller’s template_root), that seems like a bit of a kludge. I’d like to see a ‘load_path’ for views.
  • Controlling whether plugins are loaded or not. This is handy for development, and as a side effect, you get control over the plugin load order.

What shouldn’t be a part of the default plugin system:

  • Overriding aspects of controllers, helpers and views automagically. This behavior is significantly different from that way that Rails works in a normal situation. Because of this, users should explicitly ‘ask’ (by installing/creating engines-based plugins) for this behavior, so they know what to expect. This is really the significant addition that engines add beyond what might seem natural for a plugin anyway.

Thank you for Rails Engines, James, and keep up the good work!

Additional resources