Chapter 19. Extending Rails with Plugins

 

Once again, when we come to the creation of things by people, the form this unfolding takes, always, is step by step to please yourself. We cannot perform the unfolding process without knowing how to please ourselves.

 
 --Christopher Alexander

Even though the standard Ruby on Rails APIs are very useful, sooner or later you’ll find yourself wishing for a particular feature not in Rails core or that a bit of standard Rails behavior were different. That’s where plugins come into play, and this book has already described many useful plugins that you will use on a day-to-day basis to write your Rails applications.

What about plugins as a way to accomplish reuse with our own code? Would learning how to write plugins help us write more modular applications and better understand how Rails itself is implemented? Absolutely!

This chapter covers the basic topics of managing plugins in your project, including the use of a tool that some consider indispensable for the task: Piston. We’ll also supply you with enough information to get you started writing your own Rails plugins.

Managing Plugins

Rails 1.0 introduced a plugin system that lets developers easily add new functionality into the framework. An official mechanism makes it feasible to extract some of the novel, useful features you’ve come up with in your individual applications and share those extracted solutions with other developers, as a single self-contained unit that is easy to both maintain and share.

Plugins aren’t only useful for sharing new features: As Rails matures, more and more focus is being placed on the use of plugins to test alterations to the Rails framework itself. Almost any significant new piece of functionality or patch can be implemented as a plugin and road-tested easily by a number of developers before it is considered for inclusion in the core framework. Whether you find a bug in Rails and figure out how to fix it or you come up with a significant feature enhancement, you will want to put your code in a plugin for easy distribution and testing.

Of course, changing significant core behavior of the framework demands a solid understanding of how Rails works internally and is beyond the scope of this book. However, some of the techniques demonstrated will help you understand the way that Rails itself is implemented, which we trust will help you start patching core behavior the day that you need to do so.

Reusing Code

Our jobs as programmers require us to be abstract problem solvers. We solve problems that range from searching databases to updating online to-do lists to managing user authentication. The product of our labor is a collection of solutions, usually in the form of an application, to a particular set of problems that we’ve been asked to solve.

However, I doubt that many of us would still be programmers if we had to solve exactly the same problems repeatedly, day after day. Instead, we are always looking for ways to reapply existing solutions to the problems we encounter. Your code represents the abstract solution to a problem, and so you are often striving to either reuse this abstraction (albeit in slightly different contexts), or refine your solution so that it can be reused. Through reuse, you can save time, money, and effort, and give yourself the opportunity to focus on the interesting and novel aspects of the particular problem you’re currently trying to solve. After all, it’s coming up with interesting and novel solutions to problems that makes us really succeessful.

The Plugin Script

Using the script/plugin command is often the simplest and easiest way to install plugins. It should be run from the root directory of the application you are developing.

Before getting into gory details, here is an example of script/plugin in action:

$ cd /Users/obie/time_and_expenses
$ script/plugin install acts_as_taggable
+./acts_as_taggable/init.rb
+./acts_as_taggable/lib/README
+./acts_as_taggable/lib/acts_as_taggable.rb
+./acts_as_taggable/lib/tag.rb
+./acts_as_taggable/lib/tagging.rb
+./acts_as_taggable/test/acts_as_taggable_test.rb

Checking the vendor/plugins directory after running this script, you can see that a new directory has appeared named acts_as_taggable.

Where did these files come from? How did script/plugin know where to go to download acts_as_taggable? To understand what’s really going on under the hood here, let’s examine the plugin script’s commands a bit more closely.

In the following sections, we cover each command in depth:

script/plugin list

Finding a list of all the available plugins is simple, using the list command:

$ script/plugin list
account_location
http://dev.rubyonrails.com/svn/rails/plugins/account_location/
acts_as_taggable
http://dev.rubyonrails.com/svn/rails/plugins/acts_as_taggable/
browser_filters
http://dev.rubyonrails.com/svn/rails/plugins/browser_filters/
continuous_builder
http://dev.rubyonrails.com/svn/rails/plugins/continuous_builder/
deadlock_retry
http://dev.rubyonrails.com/svn/rails/plugins/deadlock_retry/
exception_notification
http://dev.rubyonrails.com/svn/rails/plugins/exception_notification/
localization
http://dev.rubyonrails.com/svn/rails/plugins/localization/
...

This command returns a list of available plugins, along with the URL where that plugin can be found. If you take a closer look at the list of URLs, it should be clear that groups of plugins are often located under the same base URL: http://dev.rubyonrails.com/svn/rails/plugins, for instance. This URL is called a source, and the list command uses a collection of these when searching for plugins.

For example, when running script/plugin install acts_as_taggable earlier, the command checked each source in turn for one that contains a directory of the name specified—in this case, acts_as_taggable. The script found one under the source URL http://dev.rubyonrails.com/svn/rails/plugins, and downloaded that directory to your local machine, giving you a copy of the acts_as_taggable plugin.

script/plugin sources

You can examine the list of all the plugin sources Rails will currently search when looking for plugins by using the sources command:

$ script/plugin sources
http://dev.rubyonrails.com/svn/rails/plugins/
http://svn.techno-weenie.net/projects/plugins/
http://svn.protocool.com/rails/plugins/
http://svn.rails-engines.org/plugins/
http://lesscode.org/svn/rtomayko/rails/plugins/
...

Note that you may see more or fewer URLs than this; don’t worry, that’s perfectly normal. This list is stored in a file on your local machine, and can be examined directly by opening it in any text editor. On Mac OS X and Linux, the file is located at: ~/.rails-plugin-sources

script/plugin source [url [url2 [...]]]

It’s possible to add a new plugin source manually, using the source command:

$ script/plugin source http://www.our-server.com/plugins/
Added 1 repositories.

The URL for the source includes everything up to but not including the name of your plugin itself. You can verify this by running script/plugin sources afterward; the added URL should be there at the end of the list.

When this command fails, the URL specified has probably already been added as a source. In fact, that’s one of its only failure modes, which conveniently brings us to the unsource command.

script/plugin unsource [url[url2 [...]]]

Imagine that you’ve added a plugin source with the following command:

$ script/plugin source http:///www.our-server.com/plugins/
 Added 1 repositories.

The triple slash (///) between http and www means that this URL isn’t going to work properly, so you need to remove this source and add a corrected version. The source command’s destructive twin, unsource, removes URLs from the list of active plugin sources:

$ script/plugin unsource http:///www.our-server.com/plugins/
Removed 1 repositories.

You can ensure that the source has been removed by using script/plugin sources again. For both the source and unsource commands, multiple URLs can be given and each will be added (or removed) from the source list.

script/plugin discover [url]

The discover command checks via the Internet for any new plugin, and lets you add new sources for plugins to your collection. These sources are actually found by scraping the “Plugins” page on the Rails wiki[1] for the string “plugin” on any HTTP or Subversion URL. As you can see, each of the URLs returned matches this pattern:

$ script/plugin discover
Add http://opensvn.csie.org/rails_file_column/plugins/? [Y/n] y
Add http://svn.protocool.com/rails/plugins/? [Y/n] y
Add svn://rubyforge.org//var/svn/laszlo-plugin/rails/plugins/? [Y/n] y
Add http://svn.hasmanythrough.com/public/plugins/? [Y/n] y
Add http://lesscode.org/svn/rtomayko/rails/plugins/? [Y/n] y
...

You can supply your own plugin source page for script/plugin discover to scrape. Supplying the URL as an argument causes the discover command to use your page, rather than the Rails wiki, when it attempts to discover new plugin sources:

$ script/plugin discover http://internaldev.railsco.com/railsplugins

This can be especially effective if you maintain a list of sources you find useful and wish to share them with all of the developers on your team, for instance.

script/plugin install [plugin]

We’ve already seen this command in action, but install still has some tricks up its sleeve that will prove very useful. When using the install command, typically you supply it with a single argument, specifying the name of the plugin to download and install, for example:

$ script/plugin install simply_restful

As seen earlier, this command relies on the plugin being available from the list of sources you’ve manually added or discovered. On many occasions, you will bypass the source list and install a plugin directly from a known URL, by supplying it as an argument to the command:

$ script/plugin install
http://www.pheonix.org/plugins/acts_as_macgyver
+./vendor/plugins/acts_as_macgyver/init.rb
+./vendor/plugins/acts_as_macgyver/lib/mac_gyver/chemistry.rb
+./vendor/plugins/acts_as_macgyver/lib/mac_gyver/swiss_army_knife.rb
+./vendor/plugins/acts_as_macgyver/assets/toothpick.jpg
+./vendor/plugins/acts_as_macgyver/assets/busted_up_bike_frame.html
+./vendor/plugins/acts_as_macgyver/assets/fire_extinguisher.css

By specifying the direct URL explicitly, you can install the plugin without searching the list of sources for a match. Perhaps most usefully, avoiding a search through the list of sources can save a lot of time.

This isn’t the end of the install command’s talents, but those more advanced features are discussed later in the section “Subversion and script/plugin.”

script/plugin remove [plugin]

Quite appropriately, this command performs the opposite of install: It removes the plugin from vendor/plugins:[2]

$script/plugin -v remove acts_as_taggable
Removing 'vendor/plugins/acts_as_taggable'

A quick inspection of your vendor/plugins directory shows that the acts_as_taggable folder has indeed been removed completely.

Running the remove command will also run the plugin’s uninstall.rb script, if it has one.

script/plugin update [plugin]

Intuitively you might expect that running a command like $ script/plugin update acts_as_taggable will update your version of acts_as_taggable to the latest release, should any update exist, but that isn’t quite the case, unless you have used one of the Subversion installation methods covered in the following section.

If you have installed your plugin using the simple, standard methods described so far, you can update the plugin in place by using the install command with the force flag:

$ script/plugin -f install my_plugin

The inclusion of the -f flag will force the plugin to be removed and then reinstalled.

Subversion and script/plugin

As mentioned earlier, most of the plugin sources you encounter will actually be Subversion repositories. Why is this useful for plugin users? Most importantly, because you don’t have to be a developer contributing to a repository to receive updates from it; you can maintain a copy of a plugin that can be easily (or even automatically) updated when the plugin author adds new features, fixes bugs, and generally updates the central plugin code.

Before you can use Subversion, you need to ensure that it has been installed on your local systems. The Subversion project can be found at http://subversion.tigris.org, where they maintain a number of binary distributions of the Subversion tools. If you’re running on Linux or Mac OS X, chances are that you already have it installed, but Windows users will almost certainly need to use one of the prebuilt installers available on the Subversion web site.

Checking Out a Plugin

When you run the install command without any options, it will produce a direct copy of the plugin files and place them in a folder under vendor/plugins. You will have to check the plugin’s files into your own Subversion repository, and there will not be any direct link between the plugin’s files and where they came from, except your memory and any documentation the author may have supplied, which can get somewhat problematic when you want to update this plugin with bug fixes or new features.

A possibly better option is to use Subversion to check out a copy of the code to your application and keep additional information that can be used to determine the current version of the plugin and where the plugin came from. This information can also be used to automatically update the plugin to the latest version from the repository.

To install a plugin by checking it out via Subversion, add the -o flag when running the install command:

$ script/plugin install -o white_list
A  t_and_e/vendor/plugins/white_list/test
A  t_and_e/vendor/plugins/white_list/test/white_list_test.rb
A  t_and_e/vendor/plugins/white_list/Rakefile
A  t_and_e/vendor/plugins/white_list/init.rb
A  t_and_e/vendor/plugins/white_list/lib
A  t_and_e/vendor/plugins/white_list/lib/white_list_helper.rb
A  t_and_e/vendor/plugins/white_list/README
Checked out revision 2517.

In the example, the white_list plugin is now checked out to my working directory, beneath the plugins folder, but it isn’t linked in any way to my project or my own source control.

script/plugin update

When you’re using Subversion to download your plugins, the update command becomes useful. When you run the update command against a plugin installed with the -o flag, the plugin script instructs Subversion (via the svn command) to connect to that plugin’s Subversion repository and download any changes, updating your copy to the latest version. As with the install -o command, you can use the -r parameter to specify a specific revision to update to.

SVN Externals

While using Subversion with the install -o command is somewhat useful, it may cause you some grief when you try deploying your application. Remember that other than the existence of that plugin’s files in your local working directory, it isn’t linked to your project in any way. Therefore, you will need to install each of the plugins manually on the target server all over again when you attempt to deploy. Not good.

What we really need is some way of stating, as part of our application, that version X of plugin Y is needed wherever the application is expected to run. One way to achieve that outcome is to use a somewhat advanced feature of Subversion named externals.

When you set svn:externals properties on source-controlled folders of your application, you are effectively telling Subversion, “Whenever you check out or update this code, also check out or update this plugin from this other repository.”

The plugin install script takes an -x parameter that tells it to do just that.

$ script/plugin install -x continuous_builder
A  t_and_e/vendor/plugins/continuous_builder/tasks
A  t_and_e/vendor/plugins/continuous_builder/tasks/test_build.rake
A  t_and_e/vendor/plugins/continuous_builder/lib
A  t_and_e/vendor/plugins/continuous_builder/lib/marshmallow.rb
A  t_and_e/vendor/plugins/continuous_builder/lib/builder.rb
A  t_and_e/vendor/plugins/builder/README.txt
Checked out revision 5651.

Running svn propget svn:externals allows you to see the properties that have been set for a given source-controlled directory. We’ll run it on the vendor/plugins directory of our application:

$ svn propget svn:externals vendor/plugins/ continuous_builder
  http://dev.rubyonrails.com/svn/rails/plugins/continuous_builder

Because we installed the continuous builder plugin using the -x option, whenever you check out your application from the repository (including on a production server), that plugin will automatically be checked out also. However, it’s still not an ideal solution, because the version that will be checked out is the latest HEAD revision of the plugin, not necessarily one that we’ve proven works correctly with our application.

Locking Down a Specific Version

As with the install -o and update commands, you can specify a specific revision to link via svn:externals by using the -r flag with a version number. When the -r flag is used, the specified plugin version will be used even when the plugin author releases a new version.

If you think about the chaos that could ensue from dependencies of your application being updated to new, potentially unstable versions without your explicit knowledge, you’ll understand why it’s a good practice to lock down specific revisions of your plugins. However, there’s an even simpler way to manage plugin dependencies.

Using Piston

The free open-source utility Piston (http://piston.rubyforge.org/) makes managing the versions of libraries in your project’s vendor folder (Rails, Gems, and Plugins) much less time-consuming and error-prone than working directly with Subversion.

Piston imports copies of dependent libraries into your own repository instead of linking to them via svn:externals properties. However, Piston also keeps metadata having to do with the source and revision number of the dependency as Subversion properties associated with the imported content. Piston’s hybrid solution works out quite well in practice.

For example, since the plugin code becomes part of your source code repository, you can make changes to it as needed. (Local changes are not possible when using svn:externals.) When the day comes that you want to update the plugin to a newer version, in order to pick up bug fixes or new features, Piston will automatically merge your compatible local changes with the updated versions.

Installation

Piston is distributed as a RubyGem. Installation is as simple as typing gem install piston:

$ sudo gem install —include-dependencies piston
Need to update 13 gems from http://gems.rubyforge.org
.............
complete
Successfully installed piston-1.2.1

After installation, a new executable named piston will be available on your command line, with the following commands:

$ piston
Available commands are:
  convert      Converts existing svn:externals into Piston managed
folders
  help         Returns detailed help on a specific command
  import       Prepares a folder for merge tracking
  lock         Lock one or more folders to their current revision
  status       Determines the current status of each pistoned
directory
  unlock       Undoes the changes enabled by lock
  update       Updates all or specified folders to the latest revision

Importing a Vendor Library

The import command tells Piston to add a vendor library to your project. For example, let’s use Piston to make our sample project run EdgeRails, meaning that Rails is executed out of the vendor/rails folder instead of wherever it is installed as a RubyGem:

$ piston import http://dev.rubyonrails.org/svn/rails/trunk
vendor/rails
Exported r5731 from 'http://dev.rubyonrails.org/svn/rails/trunk' to
'vendor/rails'

Piston does not commit anything to Subversion on its own. To make Piston changes permanent, you need to check in the changes yourself.

$ svn commit -m "Importing local copy of Rails"

Also, don’t forget that unlike Rails’ own plugin script, Piston takes a second argument specifying the target directory to install the library into (and if you leave the parameter off, it will default to the current directory).

For example, here’s how you would install Rick Olsen’s excellent white_list plugin, from the projects directory:

$ piston import
  http://svn.techno-
weenie.net/projects/plugins/white_list/vendor/plugins/white_list
Exported r2562 from
'http://svn.techno-weenie.net/projects/plugins/white_list' to
'vendor/plugins/white_list'

Converting Existing Vendor Libraries

If you’ve already been using svn:externals to link plugins into the source code of your project, the first thing you should do is to convert those over to Piston, by invoking the piston convert command from your project directory:

$ piston convert
Importing 'http://macromates.com/svn/Bundles/trunk/Bundles/
Rails.tmbundle/Support/plugins/footnotes' to vendor/plugins/footnotes
(-r 6038)
Exported r6038 from 'http://macromates.com/svn/Bundles/trunk/Bundles/
Rails.tmbundle/Support/plugins/footnotes' to 'vendor/plugins/footnotes'

Importing 'http://dev.rubyonrails.com/svn/rails/plugins/
continuous_builder' to vendor/plugins/continuous_builder (-r 5280)
Exported r5280 from 'http://dev.rubyonrails.com/svn/rails/plugins/
continuous_builder' to 'vendor/plugins/continuous_builder'

Done converting existing svn:externals to Piston

Again, remember that it’s necessary to check in the resulting changes to your project files after running Piston.

Updating

When you want to get the latest changes from a remote repository for a library installed with Piston, use the update command:

$ piston update vendor/plugins/white_list/
Processing 'vendor/plugins/white_list/'...
  Fetching remote repository's latest revision and UUID
  Restoring remote repository to known state at r2562
  Updating remote repository to r2384
  Processing adds/deletes
  Removing temporary files / folders
  Updating Piston properties
  Updated to r2384 (0 changes)

Locking and Unlocking Revisions

You can prevent a local Piston-managed folder from updating by using the piston lock command. And once a folder is locked, you can unlock it by using the piston_unlock command. Locking functionality is provided as an extra precaution available to teams of Rails developers. If you know that updating a plugin will break the application, you can lock it and other developers will get an error if they try to update without unlocking.

Piston Properties

If we use svn proplist to examine the properties for vendor/plugins/continuous_builder, we’ll see that Piston stores its own properties for each plugin folder rather than on the plugins folder itself:

$ svn proplist —verbose vendor/plugins/continuous_builder/

Properties on 'vendor/plugins/continuous_builder':

  piston:root :
http://dev.rubyonrails.com/svn/rails/plugins/continuous_builder

  piston:local-revision : 105
  piston:uuid : 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  piston:remote-revision : 5280

Writing Your Own Plugins

At some point in your Rails career, you might find that you want to share common code among similar projects that you’re involved with. Or if you’ve come up with something particularly innovative, you might wonder if it would make sense to share it with the rest of the world.

Rails makes it easy to become a plugin author. It even includes a plugin generator script that sets up the basic directory structure and files that you need to get started:

$ script/generate plugin my_plugin
      create  vendor/plugins/my_plugin/lib
      create  vendor/plugins/my_plugin/tasks
      create  vendor/plugins/my_plugin/test
      create  vendor/plugins/my_plugin/README
      create  vendor/plugins/my_plugin/MIT-LICENSE
      create  vendor/plugins/my_plugin/Rakefile
      create  vendor/plugins/my_plugin/init.rb
      create  vendor/plugins/my_plugin/install.rb
      create  vendor/plugins/my_plugin/uninstall.rb
      create  vendor/plugins/my_plugin/lib/my_plugin.rb
      create  vendor/plugins/my_plugin/tasks/my_plugin_tasks.rake
      create  vendor/plugins/my_plugin/test/my_plugin_test.rb

The generator gives you the entire set of possible plugin directories and starter files, even including a /tasks folder for your plugin’s custom rake tasks. The install.rb and uninstall.rb are optional one-time setup and teardown scripts that can do anything you want them to do. You don’t have to use everything that’s created by the plugin generator.

The two defining aspects of a plugin are the presence of the init.rb file and of a directory in the plugin called lib. If neither of these exists, Rails will not recognize that subdirectory of vendor/plugins as a plugin. In fact, many popular plugins consist only of an init.rb script and some files in lib.

The init.rb Hook

If you pop open the boilerplate init.rb file that Rails generated for you, you’ll read a simple instruction.

# insert hook code here

Hook code means code that hooks into the Rails initialization routines. To see a quick example of hook code in action, just go ahead and generate a plugin in one of your projects and add the following line to its init.rb:

puts "Current Rails version: #{Rails::VERSION::STRING}"

Congratulations, you’ve written your first simple plugin. Run the Rails console and see what I mean:

$ script/console
Loading development environment.
Current Rails version: 1.2.3
>>

Code that’s added to init.rb is run at startup. (That’s any sort of Rails startup, including server, console, and script/runner.) Most plugins have their require statements in init.rb.

A few special variables are available to your code in init.rb having to do with the plugin itself:

  • nameThe name of your plugin ('my_plugin' in our simple example).

  • directorThe directory in which the plugin exists, which is useful in case you need to read or write nonstandard files in your plugin’s directory.

  • loaded_pluginsA Set containing all the names of plugins that have already been loaded, including the current one being initialized.

  • configThe configuration object created in environment.rb. (See Chapter 1, “Rails Environments and Configuration,” as well as the online API docs for Rails::Configuration to learn more about what’s available via config.)

Our simple example is just that, simple. Most of the time you want a plugin to provide new functionality to the rest of your application or modify the Rails libraries in more interesting ways than printing out a version number on startup.

The lib Directory

The lib directory of your plugin is added to Ruby’s load path before init.rb is run. That means that you can require your code without needing to jump through hoops specifying the load path:

require File.dirname(__FILE__) + '/lib/my_plugin' # unnecessary

Assuming your lib directory contains a file named my_plugin.rb, your init.rb just needs to read:

require 'my_plugin'

Simple. You can bundle any class or Ruby code in a plugin’s lib folder and then load it in init.rb (or allow other developers to optionally load it in environment.rb) using Ruby’s require statement. This is the simplest way to share Ruby code among multiple Rails applications.

It’s typical for plugins to alter or enhance the behavior or existing Ruby classes. As a simple example, Listing 19.1 is the source of a plugin that gives ActiveRecord classes a cursorlike iterator. (Please note that a smarter implementation of this technique might incorporate transactions, error-handling, and batching. See http://weblog.jamisbuck.org/2007/4/6/faking-cursors-in-activerecord for more on the subject.)

Example 19.1. Adding Each to ActiveRecord Classes

# in file vendor/plugins/my_plugin/my_plugin.rb

class ActiveRecord::Base

  def self.each
    ids = connection.select_values("select id from #{table_name}")
    ids.each do |id|
      yield find(id)
    end
    ids.size
  end

end

In addition to opening existing classes to add or modify behavior, there are at least three other ways used by plugins to extend Rails functionality:

  • Mixins, which describes inclusion of modules into existing classes

  • Dynamic extension through Ruby’s callbacks and hooks such as method_missing, const_missing, and included

  • Dynamic extension using runtime evaluation with methods such as eval, class_eval, and instance_eval

Extending Rails Classes

The way that we re-open the ActiveRecord::Base class in Listing 19.1 and simply add a method to it is simple, but most plugins follow a pattern used internally in Rails and split their methods into two modules, one each for class and instance methods. We’ll go ahead and add a useful to_param instance method to all our ActiveRecord objects too[3].

Let’s rework my_plugin so that it follows that style. First, after requiring 'my_plugin' in init.rb, we’ll send an include message to the ActiveRecord class itself:

ActiveRecord::Base.send(:include, MyPlugin)

There’s also another way of accomplishing the same result, which you might encounter when browsing through the source code of popular plugins[4]:

ActiveRecord::Base.class_eval do
  include MyPlugin
end

Now we need to write a MyPlugin module to house the class and instance variables with which we will extend ActiveRecord::Base. See Listing 19.2.

Example 19.2. Extensions to ActiveRecord::Base

module MyPlugin
  def self.included(base)
    base.extend(ClassMethods)
    base.send(:include, InstanceMethods)
  end

  module ClassMethods
    def each
      ids = connection.select_values("select id from #{table_name}")
      ids.each do |id|
        yield find(id)
      end
      ids.size
    end
  end

  module InstanceMethods
    def to_param
      has_name? ? "#{id}-#{name.gsub(/[^a-z0-9]+/i, '-')}" : super
    end

    private

      def has_name?
        respond_to?(:name) and not new_record?
      end

  end
end

You can use similar techniques to extend controllers and views.[5] For instance, if you want to add custom helper methods available in all your view templates, you can extend ActionView like this:

ActionView::Base.send(:include, MyPlugin::MySpecialHelper)

Now that we’ve covered the fundamentals of writing Rails plugins (init.rb and the contents of the lib directory), we can take a look at the other files that are created by the plugin generator script.

The README and MIT-LICENSE File

The first thing that developers do when they encounter a new plugin is to take a look in the README file. It’s tempting to ignore this file, but at the very least, you should add a simple description of the what the plugin does, for future reference. The README file is also read and processed by Ruby’s RDoc tool, when you generate documentation for your plugin using the doc:: Rake tasks. It’s worth learning some fundamentals of RDoc formatting if you want the information that you put in the README file to look polished and inviting later.

Rails is open-sourced under the extremely liberal and open MIT license, as are most of the popular plugins available. In his keynote address to Railsconf 2007, David announced that the plugin generator will auto-generate an MIT license for the file, to help to solve the problem of plugins being distributed without an open-source license. Of course, you can still change the license to whatever you want, but the MIT license is definitely considered the Rails way.

The install.rb and uninstall.rb Files

This pair of files is placed in the root of the plugin directory along with init.rb and README. Just as the init.rb file can be used to perform a set of actions each time the server starts, these files can be used to ensure that prerequisites of your plugin are in place when the plugin is installed using the script/plugin install command and that your plugin cleans up after itself when it is uninstalled using script/plugin remove.

Installation

For example, you might develop a plugin that generates intermediate data stored as temporary files in an application. For this plugin to work, it might require a temporary directory to exist before the data can be generated by the plugin—the perfect opportunity to use install.rb. See Listing 19.3.

Example 19.3. Creating a Temporary Directory During Plugin Installation

require 'fileutils'
FileUtils.mkdir_p File.join(RAILS_ROOT, 'tmp', 'my_plugin_data')

By adding these lines to your plugin’s install.rb file, the directory tmp/my_plugin_data will be created in any Rails application in which the plugin is installed. This fire-once action can be used for any number of purposes, including but not limited to the following:

  • Copying asset files (HTML, CSS, and so on) into the public directory

  • Checking for the existence of dependencies (for example, RMagick)

  • Installing other requisite plugins (see Listing 19.4)

Example 19.4. Installing a Prerequisite Plugin

# Install the engines plugin unless it is already present
unless File.exist?(File.dirname(__FILE__) + "/../engines")
  Commands::Plugin.parse!(['install',
    'http://svn.rails-engines.org/plugins/engines'])
end

Listing 19.4 demonstrates how with creativity and a little digging through the Rails source code, you can find and reuse functionality such as the parse! directive of Commands::Plugin.

Removal

As mentioned, the script/plugin remove command checks for the presence of a file called uninstall.rb when removing a plugin. If this file is present, it will be evaluated just prior to the plugin files actually being deleted. Typically, this is useful for reversing any actions performed when the plugin was installed. This can be handy for removing any directories or specific data files that your plugin might have created when installed, or while the application was running.

Common Sense Reminder

What might not be so obvious about this scheme is that it isn’t foolproof. Users of plugins often skip the installation routines without meaning to do so. Because plugins are almost always distributed via Subversion, it is trivial to add a plugin to your project with a simple checkout:

$ svn co http://plugins.com/svn/whoops vendor/plugins/whoops # no install

Or perhaps even more common is to add a plugin to your project by copying it over from another Rails project using the filesystem. I know I’ve done this many times. Same situation applies to plugin removal—a developer that doesn’t know any better might uninstall a plugin from his project simply by deleting its folder from the vendor/plugins directory, in which case the uninstall.rb script would never run.

If as a plugin writer you are concerned about making sure that your install and/or uninstall scripts are actually executed, it’s probably worthwile to stress the point in your announcements to the community and within the plugin documentation itself, such as the README file.

Custom Rake Tasks

It is often useful to include Rake tasks in plugins. For example, if your plugin stores files in a temporary directory (such as /tmp), you can include a helpful task for clearing out those temporary files without having to dig around in the plugin code to find out where the files are stored. Rake tasks such as this should be defined in a .rake file in your plugin’s tasks folder (see Listing 19.5).

Example 19.5. A Plugin’s Cleanup Rake Task

# vendor/plugins/my_plugin/tasks/my_plugin.rake

namespace :my_plugin do

  desc 'Clear out the temporary files'
  task :cleanup => :environment do
    Dir[File.join(RAILS_ROOT, 'tmp', 'my_plugin_data')].each do |f|
      FileUtils.rm(f)
    end
  end

end

Rake tasks added via plugins are listed alongside their standard Rails brothers and sister when you run rake -T to list all the tasks in a project. (In the following snippet, I limited Rake’s output by passing a string argument to use for matching task names):

$ rake -T my_plugin
rake my_plugin:cleanup   # Clear out the temporary files

The Plugin’s Rakefile

Generated plugins get their own little Rakefile, which can be used from within the plugin’s directory to run its tests and generate its RDoc documentation (see Listing 19.6).

Example 19.6. A Generated Plugin Rakefile

require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'

desc 'Default: run unit tests.'
task :default => :test

desc 'Test the my_plugin plugin.'
Rake::TestTask.new(:test) do |t|
  t.libs << 'lib'
  t.pattern = 'test/**/*_test.rb'
  t.verbose = true
end

desc 'Generate documentation for the my_plugin plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
  rdoc.rdoc_dir = 'rdoc'
  rdoc.title    = 'MyPlugin'
  rdoc.options << '--line-numbers' << '--inline-source'
  rdoc.rdoc_files.include('README')
  rdoc.rdoc_files.include('lib/**/*.rb')
end

While we’re on the subject, I’ll also mention that Rails has its own default rake tasks related to plugins, and they’re fairly self-explanatory:

$ rake -T plugin

rake doc:clobber_plugins        # Remove plugin documentation
rake doc:plugins                # Generate docs for installed plugins
rake test:plugins               # Run the plugin tests in
                                  vendor/plugins/*/**/test
                                  (or specify with PLUGIN=name)

Before closing this section, let’s make the distinction between a plugin’s Rakefile and any .rake files in the tasks folder clear:

  • Use Rakefile for tasks that operate on the plugin’s source files, such as special testing or documentation. These must be run from the plugin’s directory.

  • Use tasks/*.rake for tasks that are part of the development or deployment of the application itself in which the plugin is installed. These will be shown in the output of rake -T, the list of all Rake tasks for this application.

Testing Plugins

Last, but not least, after you’ve written your plugin, it’s essential that you provide tests that verify its behavior. Writing tests for plugins is for the most part identical to any testing in Rails or Ruby and for the most part the methods used to test both are the same. However, because plugins cannot often predict the exact environment in which they are run, they require extra precautions to ensure that the test behavior of your plugin code is isolated from the rest of the application.

There is a subtle distinction between running plugin tests using the global test:plugins rake task and via the plugin’s own Rakefile. Although the former can test all installed plugins at the same time, the internal Rakefile can and should be exploited to add any specific tasks your plugin requires to be tested properly.

Techniques used in testing plugins properly include bootstrapping a separate database for testing plugins in complete isolation. This is particularly useful when a plugin augments ActiveRecord with additional functionality, because you need to test the new methods in a controlled environment, minimizing the interaction with other plugins and the application’s own test data.

As you can imagine, testing of plugins is a lengthy topic that is primarily of interest to plugin authors. Unfortunately, I must leave further analysis of the subject out of this book for reasons of practicality and overall length.

Conclusion

You have now learned about all the basic aspects of Rails plugins. You learned how to install them, including use of the Piston tool to help you manage plugin versions. You also learned the fundamentals of writing your own plugins—probably enough to get you started.

To cover everything related to Rails plugins would require its own book and would go beyond the needs of most Rails developers. To that end, we did not cover testing plugins or the more advanced techniques employed by plugin developers. We also did not discuss topics related to the life of a plugin beyond its initial development.

For in-depth learning about extending Rails with plugins, I strongly recommend the Addison-Wesley publication Rails Plugins by James Adam, who is considered the world’s top expert on the subject.

References

1.

http://wiki.rubyonrails.org/rails/pages/Plugins

2.

The -v flag turns on verbose mode, and is only present in the example because the remove command does not normally give any feedback; without -v, it would be difficult to demonstrate that anything had actually happened.

3.

See http://www.jroller.com/obie/entry/seo_optimization_of_urls_in for an explanation of how smart use of the to_param method can help your search engine optimization efforts on public-facing websites.

4.

Jay Fields has a good blog post about the motivations behind using the various types of code extension at http://blog.jayfields.com/2007/01/class-reopening-hints.html.

5.

Alex Young’s http://alexyoung.org/articles/show/40/a_taxonomy_of_rails_plugins covers a variety of different kinds of Rails plugins, including a useful explanation of how to handle passed-in options for runtime-configuration.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.142.194.230