William Notowidagdo Kiranatama Staff
Knowledge


What is a Rails Engine?

Engine are miniature Rails applications that you embed into your main application. You can share an Engine accross different applications. Since Rails 3.0, every Rails application is nothing more than an Engine, allowing you to share it very easily

Sentinel

In this post, we are going to show you how to write an Engine. Since the Engine will provides authentication functionality so I named it, Sentinel. Sentinel is a simple Rails application that provides user model with sign up and log in. Most of the code here is taken directly from the nifty:authentication code which is part of the nifty-generators gem.

Let's create the basic Engine files and directories, here is my structure

├── Gemfile
├── Gemfile.lock
├── lib
│   └── sentinel.rb
├── MIT-LICENSE
├── README.rdoc
└── sentinel.gemspec

Open up the gemspec file and add the following

Gem::Specification.new do |s|
  s.name = "sentinel"
  s.summary = "Simple authentication Engine."
  s.description = "Sentinel is an Engine that provides user model with sign up and log in."
  s.files = Dir["{app,lib,config}/**/*"] + ["MIT-LICENSE", "Gemfile", "README.rdoc"]
  s.version = "0.0.1"
end

for now the sentinel.rb should look like

module Sentinel
end

and the Gemfile

source "http://rubygems.org"
gem "rails", "3.0.7"

Start the Engine

Modify lib/sentinel.rb so it look like

module Sentinel
  class Engine < Rails::Engine; end
end

By this point you already have a working Engine. You can try to use it by adding the following to your application's Gemfile

gem 'sentinel', :path => '/path/to/sentinel'

The Controllers

Let's prepare the controllers. In your Engine root, create directory structure like this

  ├── app
  │   ├── controllers
  │   │   └── sentinel

then in sentinel put sessions_controller.rb and users_controller.rb.

Modify the sessions_controller.rb, put the SessionsController class inside module Sentinel and add unloadable in SessionsController. This must be included in your engine controllers. Unloadable marks your class for reloading inbetween requests.

module Sentinel
  class SessionsController < ApplicationController

    unloadable
    ...
end

Then do the same for the UsersController class.

The Models

We don't modify user.rb, just put it in app/models of your Engine root directory.

The Views

Copy layouts, users and sessions directories to app/views/sentinel so the structure should look like

├── app
│   ├── controllers
│   │   └── sentinel
│   │       ├── sessions_controller.rb
│   │       └── users_controller.rb
│   ├── models
│   │   └── user.rb
│   └── views
│       └── sentinel
│           ├── layouts
│           │   └── application.html.erb
│           ├── sessions
│           │   └── new.html.erb
│           └── users
│               ├── edit.html.erb
│               ├── _form.html.erb
│               └── new.html.erb

The Routes

Create file routes.rb in config directory and add the following

Rails.application.routes.draw do |map|
  match 'user/edit' => 'sentinel/users#edit', :as => :edit_current_user
  match 'signup' => 'sentinel/users#new', :as => :signup
  match 'logout' => 'sentinel/sessions#destroy', :as => :logout
  match 'login' => 'sentinel/sessions#new', :as => :login

  resources :sessions, :controller => 'sentinel/sessions', :only => [:new, :create, :destroy]
  resources :users, :controller => 'sentinel/users', :only => [:new, :create]
end

Database migration

We need to create users table. Let's create a migration file for that purpose. The file will be placed in lib/generators/sentinel/templates/migration.rb with the following content

class CreateUsers < ActiveRecord::Migration
  def self.up
    create_table :users do |t|
      t.string :username
      t.string :email
      t.string :password_hash
      t.string :password_salt
      t.timestamps
    end
  end

  def self.down
    drop_table :users
  end
end

Now, let's define the generator by subclassing Rails::Generators::Base in lib/generators/sentinel/sentinel_generator.rb

require 'rails/generators'
require 'rails/generators/migration'

class SentinelGenerator < Rails::Generators::Base
  include Rails::Generators::Migration

  def self.source_root
    @source_root ||= File.join(File.dirname(__FILE__), 'templates')
  end

  def self.next_migration_number(dirname)
    if ActiveRecord::Base.timestamped_migrations
      Time.now.utc.strftime("%Y%m%d%H%M%S")
    else
      "%.3d" % (current_migration_number(dirname) + 1)
    end
  end

  def create_migration_file
    migration_template 'migration.rb', 'db/migrate/create_users.rb'
  end
end

Controller authentication

The ControllerAuthentication module is needed to be included in your application controller which makes several methods available to all controllers and views. Copy controller_authentication.rb to lib directory and modify it by adding put the original ControllerAuthentication module inside Sentinel module

module Sentinel
  module ControllerAuthentication
  ...
Next, modify the lib/sentinel.rb so it look like
module Sentinel
  class Engine < Rails::Engine; end
  require 'controller_authentication'
end

Using the Engine

Let's say you already have a Rails application with a simple CRUD functionality, i.e. product items management. Now you want to add log in, log out and sign up to your application. User needs to be authenticated before she can adding or updating product items. Sentinel will be used for that purpose.

Add sentinel into your Gemfile

gem 'sentinel', :path => '/path/to/sentinel'

then run bundle install.

Run the migration generator followed by the database migration from your application

william@walden:~/Public/sentinel-app$ rails generate sentinel
      create  db/migrate/20110513082547_create_users.rb
william@walden:~/Public/sentinel-app$ rake db:migrate
(in /home/william/Public/sentinel-app)
==  CreateUsers: migrating ====================================================
-- create_table(:users)
  -> 0.0014s
==  CreateUsers: migrated (0.0015s) ===========================================

Next, on the top of your application_controller.rb add the following

include Sentinel::ControllerAuthentication

I assume that the CRUD functionality is on the products_controller.rb, so you need to define before_filter in there

before_filter :login_required

Now, start your application and visit the products path. If everything goes well, you should see the Log in form.

Summary

I showed you how to build a simple Rails Engine. You see that the Engine is just a typical Rails application. You can easily convert a whole application or some functionality into an Engine so you can have more reusable component that you can share accross different application.

The Engine used as example in this post is available at https://gitorious.org/rails-app-examples/sentinel

Here are some resources to learn more about Rails Engine

  1. http://www.themodestrubyist.com/2010/03/05/rails-3-plugins---part-2---writing-an-engine/
  2. http://nicksda.apotomo.de/2010/10/testing-your-rails-3-engine-sitting-in-a-gem/
  3. http://thechangelog.com/post/2336985491/railsadmin-rails-3-engine-to-view-your-data
  4. http://olympiad.posterous.com/how-to-building-a-rails-3-engine-and-set-up-t
  5. https://github.com/krschacht/rails3engine_demo
  6. http://www.arailsdemo.com/posts/43