A Guide To Starting Your Own Rails Engine Gem

Advertisement

Since Rails 3 was released, developers have been writing Rails engines in a new clean style that can be packaged as RubyGems. A Rails engine is a prepackaged application that is able to be run or mounted within another Rails application. An engine can have its own models, views, controllers, generators and publicly served static files.


(Image: maicos)

Now, unless you like writing a lot of code, this is great news, because it means you can write an engine once and use it over and over again. Let’s say you build a lot of websites for small businesses. A common requirement for such websites is a page listing all of the employees at a company and some basic information about them. This is a great candidate for a Rails engine gem because the functionality will change very little and can be abstracted to a common set of requirements.

In this post, we’ll walk through the process of creating an engine gem that you would use to create a database-backed team page displaying a list of employees.

Enginex

Jose Valim, a core Rails contributor, has created a tool named Enginex, which scaffolds Rails 3-compatible engine gems. This tool protects you from many of the gotchas that engine gem developers face. It provides basic set-up, including a test Rails application, which you’ll need to get started.

To begin, run the following from the command line in your standard projects directory:

gem install enginex
enginex team_page

With this, you will end up with a project in the team_page directory containing the standard Enginex scaffolding.

Set-Up

To set up our gem, we’ll modify a few files. First, our team_page.gemspec and our Gemfile need a little love.

# CURRENT FILE :: team_page.gemspec
require File.expand_path("../lib/team_page/version", __FILE__)

# Provide a simple gemspec so that you can easily use your
# Enginex project in your Rails apps through Git.
Gem::Specification.new do |s|F
  s.name                      = "team_page"
  s.version                   = TeamPage::VERSION
  s.platform                  = Gem::Platform::RUBY
  s.authors                   = [ "Your Name" ]
  s.email                     = [ "your@email.com" ]
  s.homepage                  = "http://yourwebsite.com"
  s.description               = "A simple Rails 3 engine gem that adds a team page to any Rails 3 application."
  s.summary                   = "team_page-#{s.version}"

  s.rubyforge_project         = "team_page"
  s.required_rubygems_version = "> 1.3.6"

  s.add_dependency "activesupport" , "~> 3.0.7"
  s.add_dependency "rails"         , "~> 3.0.7"

  s.files = `git ls-files`.split("n")
  s.executables = `git ls-files`.split("n").map{|f| f =~ /^bin/(.*)/ ? $1 : nil}.compact
  s.require_path = 'lib'
end
# CURRENT FILE :: Gemfile
source "http://rubygems.org"
# Specify any dependencies in the gemspec
gemspec

This sets up our gemspec to automatically use files we have committed with Git, and any executables we may add in the future, and to use the VERSION constant that we’ll specify in our gem.

Also, by calling gemspec in our Gemfile, running bundle install will load dependencies from our team_page.gemspec, which is the convention.

Next, to turn our gem into a Rails engine, let’s add or modify three files. First, our top-level team page file:

# CURRENT FILE :: lib/team_page.rb
# Requires
require "active_support/dependencies"

module TeamPage

  # Our host application root path
  # We set this when the engine is initialized
  mattr_accessor :app_root

  # Yield self on setup for nice config blocks
  def self.setup
    yield self
  end

end

# Require our engine
require "team_page/engine"

To use the mattr functions, our ActiveSupport dependencies are required. We also set up a nice way to configure our gem with the self.setup method. The engine is required at the end of our file so that we can be sure that any dependencies are specified first.

Secondly, our version file:

# CURRENT FILE :: lib/team_page/version.rb
module TeamPage
  VERSION = "0.0.1"
end

Lastly, our engine file:

# CURRENT FILE :: lib/team_page/engine.rb
module TeamPage

  class Engine < Rails::Engine

    initialize "team_page.load_app_instance_data" do |app|
      TeamPage.setup do |config|
        config.app_root = app.root
      end
    end

    initialize "team_page.load_static_assets" do |app|
      app.middleware.use ::ActionDispatch::Static, "#{root}/public"
    end

  end

end

This defines two Rails initialize blocks that clue us into the root directory of our host Rails application, as well as serve up any files in the root public directory of our gem.

Data Model

To add a model in our gem, we first need to specify a migration and a generator class to copy it over to the host Rails application. Although this process will become much more transparent in Rails 3.1, we now need to build some generator classes. A great resource for this can be found over at Nepal on Rails.

First, let’s add our generators class:

# CURRENT FILE :: lib/generators/team_page/team_page_generator.rb
# Requires
require 'rails/generators'
require 'rails/generators/migration'

class TeamPageGenerator < 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.new.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_team_members_table.rb'
  end
end

Adding this will allow developers to run rails g team_page from within their Rails application and to create the necessary migration file to power our team page.

Next, we’ll put together a sample migration:

# CURRENT FILE :: lib/generators/team_page/templates/migration.rb
class CreateTeamMembers < ActiveRecord::Migration
  def self.up
    create_table :team_members do |t|
      t.string :name
      t.string :twitter_url
      t.string :bio
      t.string :image_url
      t.timestamps
    end
  end

  def self.down
    drop_table :team_members
  end
end

Finally, we can create a sample model namespaced to our gem.

# CURRENT FILE :: app/models/team_page/team_member.rb
module TeamPage
  class TeamMember < ActiveRecord::Base
    attr_accessible :name , :twitter_url , :bio , :image_url
  end
end

What we’ve done so far is walked through the steps for bootstrapping a Rails 3 engine gem. It has been configured as an engine, given its own migration generator, and supplied with an ActiveRecord model.

Now, let’s set up our gem with a route, controller and view that any host Rails application can use.

The Route

Rails engines gems, when set up properly, automatically load the config and app directories of our project. This is handy because it enables us to set up our code with exactly the same structure as a full Rails application.

So, to set up our route, create a routes.rb file in the config directory of our project. To have it match on the team route, let’s do the following:

# CURRENT FILE :: config/routes.rb
Rails.application.routes.draw do
  get "team" => "team_page/team#index" , :as => :team_page
end

A bit of pain can be avoided by examining what we’ve done here. First, we’re going to match the /team route from any requests to our host Rails app.

Secondly, we’ve told Rails to send requests to the index route of the namespaced controller that we’re going to create in our engine. Namespacing our controller is best practice because it isolates our engine code from any application that it’s included in.

Lastly, our route is named so that we can use link helpers elsewhere in our application.

The Controller

Our controllers will live in the app directory, just as we’re used to. One caveat is that we’ll want to place them in a team_page directory, just as we did with our model. It simply needs to load all of our team members to be displayed on the page.

# CURRENT FILE :: app/controllers/team_page/team_controller.rb
module TeamPage
  class TeamController < ::ApplicationController
    def index
      @team_members = TeamMember.all
    end
  end
end

As you can see, we’ve subclassed our top-level ::ApplicationController, which lives in the host Rails application.

The View

To finish off, we need a view to render. By default, it will use the main application layout from our host Rails application, since we didn’t specify a different layout in the controller.

Just as we did with our model and controller, we’ll nest our view in a team_page directory. Because we want to minimize external dependencies, we’ll write our views in ERB instead of something like HAML.

<!-- CURRENT FILE :: app/views/team_page/index.html.erb -->
<ul class="team-member-list">
  <% @team_members.each do |team_member| %>
  <li class="team-member">
    <span class="team-member-name">
      <%= link_to @team_member.name , @team_member.twitter_url %>
    </span>
    <%= @team_member.bio %>
    <%= image_tag @team_member.image_url , :class => "team-member-image" %>
  </li>
  <% end %>
</ul>

Getting Started With Tests

Obviously, we haven’t yet written any unit or integration tests here to cover the gem that we created. Completing this exercise will improve your understanding of Rails 3 engine gems. The enginex tool we used automatically creates a test directory for you with a basic Rails application.

Let’s start by making sure our test_helper.rb file is up to snuff.

# CURRENT FILE :: test/test_helper.rb
# Configure Rails Environment
ENV["RAILS_ENV"] = "test"
ENV["RAILS_ROOT"] = File.expand_path("../dummy",  __FILE__)

require File.expand_path("../dummy/config/environment.rb",  __FILE__)
require "rails/test_help"

ActionMailer::Base.delivery_method = :test
ActionMailer::Base.perform_deliveries = true
ActionMailer::Base.default_url_options[:host] = "test.com"

Rails.backtrace_cleaner.remove_silencers!

# Run any available migration
ActiveRecord::Migrator.migrate File.expand_path("../dummy/db/migrate/", __FILE__)

# Load support files
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }

One thing to notice, which is unusual for a testing set-up helper, is that we aren’t requiring our local gem code anywhere in the test helper. Because we’re using Bundler, our gem is actually required in the dummy Rails app via our Gemfile at test/dummy/config/application.rb. Other than that, we’re setting up our Rails environment, booting the application and running any available migrations. Here’s a glance at a sample unit test.

# CURRENT FILE :: test/team_page_test.rb
require 'test_helper'

class TeamPageTest < ActiveSupport::TestCase

  test "truth" do
    assert_kind_of Module, TeamPage
  end

  test 'setup block yields self' do
    TeamPage.setup do |config|
      assert_equal TeamPage, config
    end
  end

end

To continue playing around with how engine testing and integration in a Rails app work, head to the test/dummy/ Rails app and boot the server or play in the console. Everything should work in there as well as it would in any other Rails application.

Resources And Tips

Here are a few helpers to make sure you’re on the right track. The following represents what you would expect the directory structure to look like for your engine gem after following the code examples in this article.

## DIRECTORY STRUCTURE
#

- team_page/
  - app/
    - controllers/
      - team_page/
        + team_controller.rb
    - models/
      - team_page/
        + team_member.rb
    - views/
      - team_page/
        + index.html.erb
  - config/
    + routes.rb
  - lib/
    - team_page.rb
    - generators/
      - team_page/
        + team_page_generator.rb
        - templates/
          + migration.rb
    - team_page/
      + engine.rb
      + version.rb
  - test/
    + team_page_test.rb
    + test_helper.rb
  + team_page.gemspec
  + Gemfile
  + Gemfile.lock

Here’s a simple script to load some team members into your database.

## DATA SEED
#
# => Method 1
# Copy the code into your application's db/seeds.rb file.
#
# => Method 2
# If you would like to run this code in the engine that you are
# developing, place it in the seeds file inside of the dummy app
# contained in your integration tests.
#
# With either of the above methods, be sure to run the following from
# your command line…
#
#     rake db:seed
#

5.times do |i|

  TeamPage::TeamMember.create!( {
    :name        => "Team Member #{i}",
    :twitter_url => "http://twitter.com/team_member_#{i}",
    :bio         => "A really fancy team member!",
    :image_url   => "http://bit.ly/muSWki"
  } )

end

Some External Resources

Conclusion

The goal of this post was to demonstrate how straightforward it is to create a Rails engine packaged as a Ruby gem.

To recap, we’ve boostrapped our gem, added a model and migration generator, set up a route to hit our controller, created a sample view, and wrote some example tests. This process can be used for any engine gem you feel you need, and it can speed up development time and improve the reliability and maintainability of your code bases. The possibilities are endless. What will you build?

(al)

↑ Back to top

Ryan enjoys conceptualizing ideal implementations of products and then helping to make them happen. A backend dev, Ryan loves developing custom APIs almost as much as he loves writing tests. Other than that, he can usually be found daydreaming about powder days in the mountains or at a park futzing around with a frisbee, soccer ball, or book.

  1. 1

    Enginex is included in rails 3.1. There’s no need to use it as a gem anymore on new applications !

    6
  2. 2

    The code above… is there a demo view of this that I can look at to see how it displays?

    0
  3. 3

    This is exactly what I’ve been after! Could you post the full source code as its easier to compare where things should be. Cheers.

    1
  4. 4

    What is “Rails” and what does it do?

    0
  5. 5

    Man, why did they have to name it Enginex? It’s just going to get confused with nginx all the time.

    2
  6. 6

    Great tutorial Ryan!
    I’m pretty new to RoR myself and I’ve been going through tutorials and books like crrazy.
    I just wish the RoR community had some more active blogs with constant high quality tutorials / content like this one.

    RoR just needs one active blog with daily RoR articles and you’ll see how it was spread like a pandemic virus :D

    <3 RoR

    0
  7. 7

    Forwarded on rubyflow.
    Thx for this excellent post =)

    0
  8. 8

    How do you integrate the engine in the rails project? Do you have to restart the server when you change some code in the engine?

    0
  9. 9

    I noticed that the link to http://nepalonrails.com/blog/2010/10/rails-engine-generators-migrations-series-part-III was added in the Data Model section. The link is dead for now. But you can see the posts on http://nepalonrails.heroku.com

    1
  10. 10

    Jonathan Lozinski

    July 1, 2011 2:06 am

    Note, that in the engine.rb it should read:

    initializer not initailize, other wise you get very badness. The r being really quite important for that..

    3
  11. 11

    Brian Cardarella

    July 1, 2011 6:31 am

    I haven’t given your example code a try but doesn’t the path of the view need to be: app/views/team_page/team so it coincides with the controller name?

    0
  12. 12

    Enginex is a part of rails 3.1 now.

    rails plugin new my_plugin

    0
  13. 13

    This is exactly what I’ve been after! Could you post the full source code as its easier to compare where things should be. Cheers.
    plasterers in st albans

    0
  14. 14

    Christian Frugard

    November 8, 2011 9:26 am

    Shouldnt the below sections read “initializeR”, with an “R”, as documented at

    http://edgeapi.rubyonrails.org/classes/Rails/Engine.html

    # CURRENT FILE :: lib/team_page/engine.rb
    module TeamPage

    class Engine < Rails::Engine

    initialize "team_page.load_app_instance_data" do |app|
    TeamPage.setup do |config|
    config.app_root = app.root
    end
    end

    initialize "team_page.load_static_assets" do |app|
    app.middleware.use ::ActionDispatch::Static, "#{root}/public"
    end

    end

    end

    1
  15. 15

    Now that rails 3.1 and even 3.2 are out, would you have other resources to point us to for the smoothest way to gemify an engine?
    I have a running rails app and I want some of it’s resources available to me as a gem (say it’s user model) and so far this was the most helpful guide as to how to achieve that!

    0
  16. 16

    This tutorial is old and has a few problems, make sure to follow http://railscasts.com/episodes/277-mountable-engines and remember to use the –mountable flag or you wont get the app folder

    1
  17. 17

    I’ve submitted a pull request to fix the initialize -> initializer issue.

    0
  18. 18

    Hi Ryan,

    Thanks for the great tutorial, just what I wanted … :)

    0
  19. 19

    Simple and very usefull…keep it up Ryan.

    0
  20. 20

    Excellent point Damien – see http://edgeguides.rubyonrails.org/plugins.html for details!

    1
  21. 21

    Hey Alan – check out the official site :: http://rubyonrails.org/

    0
  22. 22

    Agreed except now, with Rails 3.1 coming, it will simply be a more appropriately named scaffold generator. See http://edgeguides.rubyonrails.org/plugins.html for details.

    0
  23. 23

    Joe, there’s actually no styling applied to the HTML sample above, so it would look very plain by default. The point that I was trying to make was that it is very easy to apply your own styles to any views provided by an engine gem.

    In general I’d say that it’s best practice to apply minimal or no styling to views that will be exposed publicly from inside an engine gem as the end-application developer will probably want to customize those.

    0
  24. 24

    Hi Gareth – I’ll get to this as soon as I’m able and respond here with a github repo containing the sample code. Thanks for the suggestion!

    0
  25. 25

    Thanks for the reminder – here’s a link to the source repo…

    https://github.com/cookrn/team_page

    This includes some errors I noticed in the inline code in the post above. There also seems to be an error in getting the tests running. I’ll push the fix to the repo as I have a chance to debug.

    0
  26. 26

    Try Railscasts :)

    0
  27. 27

    Hey JP – between books, blog posts, Railscasts/Asciicasts and the source code of your favorite libraries (including Rails), I’d say there’s plenty to read.

    Thanks for the compliment!

    0
  28. 28

    Railscasts is only once a week :( I need new material on a daily basis from novice to intermediate to advanced :|

    0
  29. 29

    Any luck on posting the source code? Also, the link to Nepal on Rails doesn’t sem to work. Cheers!

    1
  30. 30

    There’s plenty on there to keep you going so that you won’t be a novice after you’ve been through them all ;) Also try Zombies for Rails

    0
  31. 31

    Thanks for doing that fdutey – totally slipped my mind…

    0
  32. 32

    Great questions Cody!

    To utilize the gem in a Rails 3 project you’re working on, you’ll need to require it in your Rails project’s Gemfile. Here’s a Gist that explains how you might do this: https://gist.github.com/20316c11d7e43d63df10 . The link is in the Gist as well, but here’s the manual on writing Gemfiles: http://gembundler.com/man/gemfile.5.html .

    You will always have to restart the Rails server if you make changes to the engine.

    0
  33. 33

    jonathan is correct. this change is neccessary.

    2
  34. 34

    Yeah, initialize should be initializer…took me a while to figure that out, someone should update the post.

    1
  35. 35

    I ran into this error too. Thanks!

    0
  36. 36

    good question, i’d like to start creating gems/engines myself, but i’m a bit unsure where’s the best place to start at the time being.

    0

Leave a Comment

Yay! You've decided to leave a comment. That's fantastic! Please keep in mind that comments are moderated and rel="nofollow" is in use. So, please do not use a spammy keyword or a domain as your name, or else it will be deleted. Let's have a personal and meaningful conversation instead. Thanks for dropping by!

↑ Back to top