Creating Resourceful Plugins
It's been some time since we originally proposed the idea of resourceful plugins—a solution to the persistent problem of integrating distinct Rails applications. Over the past year, we've worked on refining the approach (in the context of our two main resourceful projects, Sandstone and Bloget), and in the course of preparing for the tutorial I gave at Railsconf Europe, we've been able to formalize (somewhat) the process of creating resourceful plugins.
Extraction
Like most good, reusable code, the best resourceful plugins are extracted from an existing app, not created ex nihilo. I generally recommend creating a new plugin only after you've built the same functionality into an application at least twice—that way, you've worked through the same problems at least two times, and should be able to create a much better solution than you'd have been able to at the start.
Once you've built the functionality into an application, the extraction process isn't that complicated. You start with the Rails plugin generator (in the examples that follow, I'll use the Bagpipes plugin):
script/generate plugin --with-generator Bagpipes
This command creates the scaffolding for a plugin, including the basics for a new Rails generator (the --with-generator option). It does create some files and folders you may not need, however, so for this example you can delete the following:
/init.rb /install.rb /Rakefile /uninstall.rb /lib/bagpipes.rb /test/*
Moving Code
Once you've got the framework in place, you can start to fill it in by moving code from the application to the plugin. Start by creating controllers/ and models/ folders in lib/bagpipes, and within those, create blank files for each controller and model you'll be extracting. For Bagpipes, that would result in the following:
/lib/bagpipes/controllers/members_controller.rb /lib/bagpipes/controllers/messages_controller.rb /lib/bagpipes/controllers/topics_controller.rb /lib/bagpipes/models/member.rb /lib/bagpipes/models/message.rb /lib/bagpipes/models/topic.rb
Each of these files will contain the code you wrote for the application, but namespaced into a module. The Member module, for instance, ends up looking like this:
module Bagpipes
module Models
module Member
def self.included(base)
base.class_eval do
validates_presence_of :name
validates_presence_of :user
has_many :messages, :dependent => :destroy
belongs_to :user, :polymorphic => true
end
end
end
end
end
The self.included(base) method allows you to call the various ActiveRecord validation and association macros when you include this module into a model; it also appears in the controller modules:
module Bagpipes
module Controllers
module MembersController
def self.included(base)
base.class_eval do
before_filter :require_member_admin
end
end
def index
@members = Member.all
end
def new
@member = Member.new
end
def create
@member = Member.new(params[:member])
if @member.save
flash[:notice] = "The member #{@member.name} has been created"
redirect_to members_path
else
flash[:form_error] = @member
render :action => 'new'
end
end
private
def require_member_admin
unless logged_in? && current_user.member && current_user.member.administrator?
flash[:error] = "You must be a forum administrator to do that"
redirect_to topics_path
end
end
end
end
end
In addition to the self.included(base) method, you can also add instance methods (for controllers, actions) to these modules.
One of the benefits of the extraction method is that it makes this easy to test; since you're pulling code out of an application into a plugin for that same application, all you have to do is this in the original controller:
class MembersController < ApplicationController include Bagpipes::Controllers::MembersController end
If you rerun your tests, then, you should get the same results you did before you moved the code.
Templates
Once you've moved all your existing code into your new plugin, you'll need to set up the templates. At a minimum, you'll want to create folders in generators/bagpipes for your controllers, migrations, models, tests, and views (you might also need folders for helpers and other files, but we'll ignore those for this example). Once those are in place, just copy the files from your application into the new folders—the only change required is that you consolidate multiple migrations into a single file, and remove the timestamp identifier on it:
class ConsolidatedBagpipesMigration < ActiveRecord::Migration
def self.up
create_table :members do |t|
t.string :name
t.boolean :administrator, :default => false
t.datetime :created_at
t.datetime :updated_at
t.integer :user_id, :null => false
t.string :user_type, :null => false
end
create_table :messages do |t|
t.integer :topic_id
t.integer :parent_id
t.string :title
t.text :content
t.datetime :created_at
t.datetime :updated_at
t.integer :member_id
end
add_index :messages, [:topic_id], :name => :index_messages_on_topic_id
add_index :messages, [:parent_id], :name => :index_messages_on_parent_id
create_table :topics do |t|
t.string :title
t.text :description
t.datetime :created_at
t.datetime :updated_at
end
end
def self.down
drop_table :messages
drop_table :members
drop_table :topics
end
end
Infrastructure
The remaining tasks are simple. First, you need to create the generator file itself:
class BagpipesGenerator < Rails::Generator::Base
def manifest
template_dir = File.join(File.dirname(__FILE__), 'templates')
record do |m|
m.class_collisions "Message", "Member", "Topic"
m.class_collisions "TopicsController", "MembersController", "MessagesController"
Dir.chdir(template_dir) do
# handle models, controllers, and views
%w(models controllers views).each do |area|
m.directory(File.join('app', area))
Dir.glob(File.join(area, '**', '*')).each do |file|
m.directory(File.join('app', file)) if File.directory?(file)
m.file(file, File.join('app', file)) if File.file?(file)
end
end
# handle tests
m.directory('test')
Dir.glob(File.join('test', '**', '*')).each do |file|
m.directory(file) if File.directory?(file)
m.file(file, file) if File.file?(file)
end
end
# insert routes
sentinel = 'ActionController::Routing::Routes.draw do |map|'
gsub_file 'config/routes.rb', /(#{Regexp.escape(sentinel)})/mi do |match|
"#{match}\n map.resources :members\n map.resources :topics, :has_many => [:messages]\n map.new_reply 'topics/:topic_id/parent/:parent_id', :controller => 'messages', :action => 'new'\n"
end
# insert member association
print 'What is the name of your user class? [User] '
user_model = STDIN.gets.chomp
user_model = 'User' if user_model == ''
sentinel = "class #{user_model} < ActiveRecord::Base"
gsub_file "app/models/#{user_model.downcase}.rb", /(#{Regexp.escape(sentinel)})/mi do |match|
"#{match}\n has_one :member, :dependent => :destroy, :as => :user\n"
end
# Handle migrations
Dir.glob(File.join(template_dir, 'migrate', '*')).each do |file|
m.migration_template(
File.join('migrate', File.basename(file)),
'db/migrate',
:migration_file_name => File.basename(file, '.rb')
)
end
m.readme '../../../README'
end
end
def gsub_file(relative_destination, regexp, *args, &block)
path = destination_path(relative_destination)
content = File.read(path).gsub(regexp, *args, &block)
File.open(path, 'wb') { |file| file.write(content) }
end
end
This code copies the templates into your application and inserts resourceful routes. It then prompts for the integration point (for Bagpipes, the user class), so that it can insert the appropriate association declaration. Finally, it adds the resource migrations and displays the README.
The final step is the addition of a rake task for the plugin:
namespace :bagpipes do
task :load_rails do
unless Kernel.const_defined?('RAILS_ROOT')
Kernel.const_set('RAILS_ROOT', File.join(File.dirname(__FILE__), '..', '..', '..'))
end
if (File.exists?(RAILS_ROOT) && File.exists?(File.join(RAILS_ROOT, 'app')))
require "#{RAILS_ROOT}/config/boot"
require "#{RAILS_ROOT}/config/environment"
require 'rails_generator'
require 'rails_generator/scripts/generate'
end
end
desc "Installs Bagpipes"
task :install => :load_rails do
Rails::Generator::Scripts::Generate.new.run(['bagpipes'])
end
end
With this in place, users can integrate the plugin by adding it and running rake bagpipes:install, making for a friendlier installation.
Summary
That's it—certainly, there's more you can do (helpers, CSS), but all such tweaks remain within this framework. At the very least, you should fill in the README, USAGE, and other such files. With the work done here, however, you're ready to add this functionality to any application that might need it.

Tyrant is a "meta" Rails application designed to run other Rails applications.
Recent Comments
Tony,
I understand and agree that the back-end shouldn’t output code (html code), and only content. The templates (aka views) should do the trick, but instead of having lot’s of if/else conditionals inside the view, you may just output the following content.
No information available
The template would loop in an array and put all the <li>’s inside the <ul>.
I don’t see anything wrong, nor...