(← Part I, for those who missed it.)
Wow! I didn’t realize plugins would be so popular. It’s tempting to name my next post “The Complete Guide to What Geoff Ate For Dinner: Part VII.”
I did have the misfortune of writing a popular article during a spell of mayhem at Dreamhost. Several people blogged about the difficulty of keeping Typo running smoothly at the end of last week. So, I did what I should have done a while ago and switched this blog to a speedy VPS server at Rimuhosting. Shared hosts are still a great place for sites with a small codebase or for sites that can use page caching (where an entire HTML file is written to disk). I still have a few sites at Dreamhost.
As it turns out, I’ll be switching again to a dedicated host in a few weeks. I’ll be sharing it with one or two other Rails developers. So you can expect “The Complete Guide to Shared, Virtual, and Dedicated Hosting in Eighteen Parts” sometime this summer.
Today I’ll show you how to package a helper method as a plugin, which will show the basics of making any kind of code-based plugin. I’ll be using a plugin I wrote today based on code from Jeremy Voorhis and Typo. It’s a helper that fills in some missing header tag functionality, such as printing a doctype declaration at the top of your HTML pages or rendering a meta_tag for your site’s description and keywords.
The final project is the meta_tags plugin.
Here are some of the methods it defines:
xhtml_doctype :strict html_tag :lang => 'zh' meta_tag 'keywords', 'plugins, rails, tutorials'
The basics steps are:
init.rb that includes your module into the appropriate classThere are several ways to write code that enhances Rails, but the cleanest way to do it is with a mixin. A mixin is a simple module that defines a few methods. The difference is that a mixin module is basically worthless on its own until it is combined with an existing class. Because it is designed to be combined with an existing class, it can use any of the variables or methods of the class it will be a part of.
When you design a plugin, you have to decide how it will be used:
include SomeSpecialModule to the relevant classes. Your plugin will work this way if you leave init.rb blank.include the module in your plugin’s init.rb.Some classes you might want to automatically enhance are:
ActionView::Base for helpers. Example: calendar_helperActiveRecord::Base for models. Example: The newer acts_as_taggableTest::Unit::TestCase for adding your own assertions.ActionController::Base for controller methods (but not full actions). Rarely done, but possible. account_location could be used this way (but is implemented differently).meta_tag methodThere are several methods in the meta_tags plugin, but the meta_tag is one of the simplest. Here it is:
# Goes in lib/meta_tag_helper.rb
module MetaTagHelper
# Renders a meta tag for use in the HEAD section of an html document.
def meta_tag(name, value)
tag :meta, :name => name, :content => value unless value.blank?
end
end
Here are a few things to note about this method:
tag helper from Rails. You don’t see the tag method defined in this module because we will mixin to ActionView::Base.Topfunky::MetaTagHelper. The Java convention is to use your domain name in reverse, but Ruby doesn’t have a convention like that. It’s up to you to choose something unique.init.rbI want this method to be automatically available to all my views as if it were built-in. For that, I add one line to init.rb:
# Contents of init.rb ActionView::Base.send :include, MetaTagHelper
This tells ActionView to call itself with the include method, and to pass my MetaTagHelper module as the argument. All the methods in MetaTagHelper are now part of ActionView and can be called from any view template. That was easy!
In a normal Ruby library I would have to require 'meta_tag_helper', but Rails does this automatically. In fact, I could drop a full model file into the lib folder and it would be picked up by Rails without any extra code. My Mint plugin has several models that work this way.
A surprisingly large number of plugins have no tests at all. Part of the reason might be that writing a plugin test is a little bit harder than writing a normal unit test. Since a module is worthless on its own, you must add the rest of the functionality for any Rails methods you call in your new helper. If your method doesn’t use any built-in methods, your job is easier.
There are two ways to run the tests for your plugin. First, you can use the Rake task created by the plugin generator. Navigate to your plugin’s directory and type rake. Your tests will be run.
The second way is to test all your plugins from the root of your Rails app. Type rake test:plugins and all tests will be run. NOTE This uses a global test task. Changes to the Rakefile in your plugin’s directory will not affect the behavior of the test:plugins task.
Of course, you need to write some tests first!
A simple method that doesn’t call any built-in Rails methods can be tested like this:
require 'test/unit'
# Add your module file here
require File.dirname(__FILE__) + '/../lib/meta_tag_helper'
class MetaTagTest < Test::Unit::TestCase
# Explicitly include the module
include MetaTagHelper
def test_end_html_tag
assert_equal "</html>", end_html_tag
end
end
The important thing is that you require your helper file and that you include your module.
You may remember that I used the built-in tag helper in my own helper. The steps are the same, but are slightly more complicated since you need to require the relevant file from Rails and also include the relevant helper.
Here’s how I do it:
# Rubygems is where Rails is located
require 'rubygems'
require 'test/unit'
require File.dirname(__FILE__) + '/../lib/meta_tag_helper'
# Here's the helper file we need
require 'action_view/helpers/tag_helper'
class MetaTagTest < Test::Unit::TestCase
# This is the helper with the 'tag' method
include ActionView::Helpers::TagHelper
include MetaTagHelper
def test_meta_tag
output = meta_tag('keywords', 'cat, mouse, squirrel')
assert_equal "<meta content=\"cat, mouse, squirrel\" name=\"keywords\" />", output
end
end
That’s a little bit of extra work, but it works!
Someone asked how to test models that are part of a plugin. The full answer is quite complicated, but here’s are a few options to explore:
Generators and other assorted fun facts.

Good work! On the subject of mocks. Am I correct in thinking that creating a mocks folder etc inside your plugin/tests/ as in a main app does not work?
Look forward to the Generators guide.
You’re right…the Rakefile in your plugin directory could look for that, but it doesn’t. In the meantime, you could add it to the load path (in the Rakefile, with
t.libs) and require it in your plugin test.The plugin system is well designed, but has a few rough edges like that which I hope to submit a patch for.
This is truly great. I’ve written a few plugins and had to guess at a lot of this. Thanks for clearing this up! I know quite a few people who will find this useful.
If you needed a class shared and automatically included in a bunch of different apps (in the same server), would you do it as a plugin? Even if you want to be able to make changes to the class and have it automatically propagate?
@Luis:
There are two solutions for that:
./script/plugin install -x my_plugin. Everytime you update a rails project, it will check for the newest version of the plugin. However, this requires you to update all the apps on the server in order for them to get the benefit.Also, see Scott Barron’s excellent acts_as_state_machine for how to setup Sqlite as a test database for ActiveRecord-powered plugins.
What is the proper way for the plugin to add migrations? suppose a plugin needs to add a column to an existing table or a new table? Should the init script add migrations to the migrations directory? What about later versions of the plugin and how the interact with previous migrations?
Hi,
Where is part III? :-)
Although the helper works fine, the test code pukes, failing to successfully include ActionView::Helpers::TagHelper. Here’s the full error message:
$ rake test:plugins --trace (in /Data/Rails/custom_plugin_exercise) ** Invoke test:plugins (first_time) ** Invoke environment (first_time) ** Execute environment ** Execute test:plugins /usr/local/bin/ruby -Ilib:test "/usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake/rake_test_loader.rb" "vendor/plugins/transmogrifier/test/transmogrifier_test.rb" /usr/local/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_view/helpers/tag_helper.rb:11: uninitialized constant ActionView::Helpers::TagHelper::Set (NameError) from /usr/local/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:32:in `gem_original_require' from /usr/local/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:32:in `require' from ./vendor/plugins/transmogrifier/test/transmogrifier_test.rb:7 from /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake/rake_test_loader.rb:5:in `load' from /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake/rake_test_loader.rb:5 from /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake/rake_test_loader.rb:5:in `each' from /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake/rake_test_loader.rb:5 rake aborted! Command failed with status (1): [/usr/local/bin/ruby -Ilib:test "/usr/local...] /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:899:in `sh' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:906:in `call' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:906:in `sh' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:985:in `sh' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:920:in `ruby' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:985:in `ruby' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake/testtask.rb:117:in `define' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:1003:in `verbose' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake/testtask.rb:102:in `define' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:546:in `call' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:546:in `execute' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:541:in `each' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:541:in `execute' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:508:in `invoke_with_call_chain' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:501:in `synchronize' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:501:in `invoke_with_call_chain' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:494:in `invoke' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:1931:in `invoke_task' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:1909:in `top_level' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:1909:in `each' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:1909:in `top_level' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:1948:in `standard_exception_handling' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:1903:in `top_level' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:1881:in `run' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:1948:in `standard_exception_handling' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:1878:in `run' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/bin/rake:31 /usr/local/bin/rake:19:in `load' /usr/local/bin/rake:19Hi webmaster!
Hi webmaster!
When we use rails default plugin generator (”./script/generate plugin my_plugin”), it creats related files under “vendor” directory.We can write our plugin code in the files there. but how can we make this plugin available for other applications or how can we install it from there. Does it always need an svn to intall a plugin?