Addison-Wesley Professional Ruby Series Rails Plugins: Extending Rails Beyond the Core

Next Page

Book Cover
Addison-Wesley Professional Ruby Series Rails Plugins: Extending Rails Beyond the Core
By James Adam
...............................................
Publisher: Addison Wesley Professional
Pub Date: September 08, 2006
Print ISBN-10: 0-321-48351-0
Print ISBN-13: 978-0-321-48351-5
Pages: 123

Table of Contents
Overview

One of the most powerful aspects of the Rails framework is the ability to extend and override it within your own applications. This is achieved through the Rails plugin mechanism, which provides an avenue for Rails developers to add powerful new features to their applications and share this functionality with the worldwide community.

This short cut introduces Rails plugins and considers each aspect of their behavior and development. You'll learn what plugins are, how they work, and why they're useful. Discover how to find and install plugins using the provided script, then explore the world of plugin development, including common plugin idioms, testing, and version control. Finally, learn how to share your own plugins.

What This Short Cut Covers 4

Section 1: Introduction 5

Section 2: Installing Plugins 14

Section 3: Saluton, Mondo! (init rb) 33

Section 4: Sharing Code (lib) 39

Section 5: The Rest (README, install rb, Rakefile, tasks, and More) 51

Section 6: Plugin Development 62

Section 7: Sharing Classes and Code 83

Section 8: Testing Plugins 100

Section 9: Sharing Your Plugin 113

Section 10: Conclusions 121

About the Author 123

Next Page


Table of Contents

Previous Page
Next Page

Book Cover
Addison-Wesley Professional Ruby Series Rails Plugins: Extending Rails Beyond the Core
By James Adam
...............................................
Publisher: Addison Wesley Professional
Pub Date: September 08, 2006
Print ISBN-10: 0-321-48351-0
Print ISBN-13: 978-0-321-48351-5
Pages: 123

Table of Contents

Copyright
What This Short Cut Covers
Section 1. Introduction
 | Ruby on Rails
 | Oh Plugin, Where Art Thou?
 | Overview of This Short Cut
 | Ruby and Rails Versions
 | A Short Essay about Reusable Code
Section 2. Installing Plugins
 | Installing Plugins with script/plugin
 | Plugins from Subversion
 | script/plugin update, Redux
 | Summary
Section 3. Saluton, Mondo! (init.rb)
 | The Plugin Generator
 | init.rb
 | Summary
Section 4. Sharing Code (lib)
 | The lib Directory
 | The CopyrightHelper Plugin
 | Rails, Modules, and Auto-Loading Code
 | Using the Copyright Plugin in an Application
 | Including Code Automatically
 | Adding to ApplicationHelper
 | Summary
Section 5. The Rest (README, install.rb, Rakefile, tasks, and More)
 | READMECapitalized for a Good Reason
 | install.rb and uninstall.rb
 | uninstall.rb (Edge Rails Only)
 | about.yml (Edge Rails Only)
 | Rake and Plugins
 | Rakefile versus tasks/What's the Difference?
 | Summary
Section 6. Plugin Development
 | Prelude: Mixing a Module's Methods into a Class
 | Staring into the Heart of Ruby
 | Extracting Behavior into a Plugin
 | Writing an acts_as Plugin
 | Summary
Section 7. Sharing Classes and Code
 | Using Plugins to Share Code
 | Models and Helpers
 | Controllers
 | Organizing Your Shared Classes
 | Sharing Files That Aren't Code
 | Using Engine Plugins
 | Generators in Plugins
 | Creating Generators
 | Running Plugin Generators
 | Sharing Generators
 | Upgrading Generated Code
 | Summary
Section 8. Testing Plugins
 | Writing Plugin Tests
 | Testing Plugins Against an Isolated Database
 | Summary
Section 9. Sharing Your Plugin
 | To Share, or Not to Share
 | Hosting
 | Releasing Your Plugin
 | Supporting the Plugin
Section 10. Conclusions
 | Ruby, Ruby, Ruby
About the Author

Previous Page


Copyright

Many of the designations used by manufacturers and sellers to distinguish their products are claimed as trademarks. Where those designations appear in this work, and the publisher was aware of a trademark claim, the designations have been printed with initial capital letters or in all capitals.

The author and publisher have taken care in the preparation of this work, but make no expressed or implied warranty of any kind and assume no responsibility for errors or omissions. No liability is assumed for incidental or consequential damages in connection with or arising out of the use of the information or programs contained herein.

Visit us on the Web: www.awprofessional.com

Copyright © 2007 Pearson Education, Inc.

All rights reserved. Printed in the United States of America. This publication is protected by copyright, and permission must be obtained from the publisher prior to any prohibited reproduction, storage in a retrieval system, or transmission in any form or by any means, electronic, mechanical, photocopying, recording, or likewise. For information regarding permissions, write to:

Pearson Education, Inc.
Rights and Contracts Department
75 Arlington St., Suite 300
Boston, MA 02116
Fax: (617) 848-7047

This product is offered as an Adobe Reader™ PDF file and does not include digital rights management (DRM) software. While you can copy this material to your computer, you are not allowed to share this file with others.

First release, September 2006

Dedication

Thanks to Sean for giving me a job where I could use Ruby every day; to Tom, Jon, and Murray for their support from nine to five; and to my family for covering the other hours.

JA


What This Short Cut Covers

One of the most powerful aspects of the Rails framework is the ability to extend and override it within your own applications. This is achieved through the Rails plugin mechanism, which provides an avenue for Rails developers to add powerful new features to their applications and share this functionality with the worldwide community.

This short cut introduces Rails plugins and considers each aspect of their behavior and development. You'll learn what plugins are, how they work, and why they're useful. Discover how to find and install plugins using the provided script, then explore the world of plugin development, including common plugin idioms, testing, and version control. Finally, learn how to share your own plugins.


Section 1. Introduction

This short cut about Railsand more specifically, about Rails pluginsaims to provide a comprehensive overview of what Rails plugins are, what they can be useful for (and they can be extremely useful!), and, most importantly, how to start developing plugins yourself.

Ruby on Rails | 6
Oh Plugin, Where Art Thou? | 7
Overview of This Short Cut | 8
Ruby and Rails Versions | 9
A Short Essay about Reusable Code | 10

Plugins exist primarily to help you reuse your code, but learning how to write them can help you write more modular applications and understand how Rails itself is implemented. Before starting to examine plugins in detail, it will serve you well to look at them in the wider context of their immediate environment, framework, and language of implementation.


Ruby on Rails

Rails is probably the hottest piece of open-source Web development software on the planet at the moment. Perhaps it's just a case of the right framework at the right time, or maybe it's the mix of frustration, inspiration, and a dynamic language that seized the imagination of developers. The dynamic nature of Ruby plays a large part of what makes Rails such a pleasure to work with.

With Ruby, you can add code to running applications, and alter existing classesor even individual instancesto suit your needs in any given situation. The Ruby language itself provides much of the flexibility that you need to "plug in" new and novel functionality into existing applications.

Ruby Will Give You Infinite Power and Unlimited Riches

By this, of course, I mean programming power and riches in the form of satisfaction. When you write plugins, you're programming in Ruby, so it pays dividends to become familiar with the language itself. This short cut will discuss some of the more advanced Ruby programming techniques in later sections, but I strongly encourage you to discover Ruby in its own rightit's a beautiful language that can make programming a joy rather than a chore.


Oh Plugin, Where Art Thou?

If you wanted to reuse code among applications in the time before Rails 1.0, you had to employ a number of…well, interesting techniques to achieve this. Times were hard indeed.

  • Writing generators was time-consuming and labor-intensive, and they were even more tiresome to maintain across several deployments.

  • Copying and pasting files between projects left developers feeling like charlatans with no respect for their craft.

  • Symbolic linking was a solution so lacking in portability that it seemed like utter folly from the very start.

Thankfully, Rails 1.0 introduced a plugin system that let developers easily add new functionality into the Rails framework. By including an official mechanism, it is now possible to extract some of the novel, useful features in your individual applications and share those abstracted solutions confidently 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.

Changing the core behavior of the framework demands a solid understanding of how Rails works internally. For this reason, it falls beyond the scope of this short cut. However, some of the techniques demonstrated will help you understand the way that Rails itself is implemented, and in turn help you start patching core behavior.


Overview of This Short Cut

This short cut covers plugin usage and development, and demonstrates how you can take advantage of the plugin features that Rails provides, along with the language features that Ruby contributes, to share new and novel features among your Rails applications.

You've just read (or skimmed, which is fine) the first part of Section 1, in which I've tried to set the scene for things to come. After this overview, you'll find some notes about Ruby and Rails versions, and a few words about reusable code.

Section 2 examines in detail the script/plugin suite of commands and how they can help you find, install, update, and remove plugins. It also touches briefly on some version control issues, and how you can use Subversion to great effect when sharing plugins.

Section 3 introduces plugin development and describes one of the most important files in a plugin, the init.rb file. This file is responsible for bootstrapping your plugin code into action each time the server starts.

Section 4 discusses the second key element of a plugin, the lib directory. Almost all of your Ruby code, classes, and modules should be placed in this folder.

In Section 5, the rest of the common files comprising a Rails plugin are covered, including documentation, Rake tasks, and special files that are run only when your plugin is installed or removed.

Section 6 introduces some more advanced plugin development techniques, including adding methods from modules into Ruby classes and enhancing Rails with acts_as-style features.

Section 7 goes on to show how you can share many different types of files with plugins and demonstrates useful ways to share models, controllers, and views in simple plugins. Sharing public asset files such as JavaScripts and stylesheets are discussed, and more advanced methods using engines and generators are introduced.

In Section 8, we'll quickly cover some of the particular issues to keep in mind when testing your plugins, notably the different mechanisms available for running your tests and testing a plugin against its own database.

Section 9 highlights some issues regarding sharing your code and outlines the common requirements and steps to unleash your plugin on the world at large.

Section 10 concludes with a brief look back at everything covered and has a few quick words of encouragement as you start developing plugins of your own.


Ruby and Rails Versions

At the time of writing, the latest version of Rails available is 1.1.4, which is available via Subversion from http://dev.rubyonrails.org/svn/rails/tags/rel_1-1-4/. This version of Rails requires Ruby 1.8.4 (and is known to have problems on earlier versions). While I've made every effort to provide examples that will work with any version of Ruby or Rails, if you are developing the examples in the following sections against versions other than these, you may find the behavior of the code examples differs from that indicated here.

I've tried to cover not only the features of Rails' plugin system that are available in official releases, but also features that are upcoming in future versions of the framework. The current development version of Rails, usually called Edge Rails, is currently available via Subversion from http://dev.rubyonrails.org/svn/rails/trunk. Instructions for working with Edge Rails can also be found at http://dev.rubyonrails.org.

Please remember that Edge Rails is a version of the framework that is being actively developed and modified on a daily basis, and that it is not suitable for running production systems.

Information about Edge Rails discussed in this short cut is only to stay current with the very latest features. Furthermore, these features are not guaranteed to make it into official releases of Rails. I do not endorse using Edge Rails for anything other than experimentation, and of course to supply patches and feedback to the Rails Core team.


A Short Essay about Reusable Code

Here are a few of my thoughts about when reusing code is appropriate. You may already have opinions about this issue, in which case it's perfectly fine to skip to the next section. If you've never really thought about reuse, however, hopefully some of the points here will prompt you to come to your own conclusions. For those who are impatient, the gist is exactly thatonly you can decide what appropriate reuse is in your own particular development situation.

Programmers Are Abstract Problem Solvers

It's our job as programmers to solve problems that range from searching databases to updating online to-do lists to managing user authentication. The product of your labor is a collection of solutions, usually in the form of an application, to a particular set of problems that you'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.

To Reuse, or Not to Reuse; That Is the Question

This is not to say that you should simply become the producers and pluggers-together of opaque Lego-esque bricks of functionality. No small part of the skill involved in being a successful problem-solver is recognizing when it makes sense to reuse, and when it is more prudent to tailor a solution specific to the problem in this context.

A classic example is user authentication: ensuring that only a particular set of users can access some part of your application. Let's see if you can decide whether it's worth your time developing a reusable version of user authentication.

In Search of the Reuse Equation

First, presume that you already have one application with a working authentication mechanism running in it. Your existing solution may require a certain amount of work to adapt it for use both in its original setting and in the new application you're developing; let's call this amount X. This represents the total amount of time and effort you must expend to generalize your login mechanism so that it works appropriately in both settings. The amount of effort required to develop a completely new login system for your application is quantified by Y.

If X > Y, then it clearly makes no sense to reuse the existing implementation. Even if X < Y, if (X + C) > Y, where C is the amount of effort it takes to configure the now-reusable subsystem, then it still doesn't make sense to abstract the authentication solution into a reusable component. Let's add three more factors since we're on a roll, so that you can present the final, soon-to-be-famous Reuse Equation:

(X + C + K) > (Y + (M x (A-1)))

K is the amount of effort required to customize the general authentication mechanism in each application you want to use it, should such customization be required; M is the amount of time and work it would take to maintain (fix bugs and update) a solution; and A is the number of applications a generalized solution will be deployed within. Simple.

And That Is All You Need to Know. Isn't It?

Still with me? I'm sorry about that, and please forget about the Reuse Equation; unfortunately, it's never quite that simple. The only real way to tell what's going to make sense for you to abstract into a reusable solution is through experience and understanding your environment (that is, the kinds of problems you're going to have to solve in the foreseeable future). If you're going to be producing a particular kind of chart in many different applications, then you should seriously consider developing the chart mechanism in a way that it can be reused; on the other hand, if it's only going to be used for one project, then developing a generic system will be a waste of your time.

However, there is at least one general rule that we can apply:

It's incredibly hard to produce an abstracted solution if you don't truly, madly, deeply understand what the problem is you're trying to solve. Far too often, developers begin by thinking about what a particular system might need to do in any conceivable situation. That road leads to bloated and overly complex software.

Instead, take a lesson from the development of Rails itself. Rails is an extracted Web framework, derived from a real applicationBasecamp. Instead of producing a foundation based on expectation and presumption about what a software framework should provide, extracting the common functionality left only the features that were actually needed in the application.

The very same applies to reuse in your own development. Rather than spending time trying to guess every possible behavior your software should have, go ahead and build it now, doing only what you actually need it to do at this very moment. Nine times out of ten, you'll realize that most of the ideas that you had before you started were unnecessary and just distractions. Anything that really is missing will become obvious as development progresses, and you can alter your solution accordingly, safe in the knowledge that a feature exists only because it's genuinely useful.

Oh, and here's another rule of thumb:

In other words, smaller extractions tend to be more adaptable, and therefore reusable, in more contexts than larger ones. The simpler the idea you want to reuse (e.g., "I want these things to behave like they were a list" or "I want to be able to store multiple versions of this object"), the easier it will be to reuse.

This is not to say that reusing larger parts of an application is not useful in certain situations; the engines plugin, discussed in Section 7, was developed to assist in such cases. In general, however, the more your abstracted solution does, the fewer situations it will be useful in without some degree of adaptation.


Section 2. Installing Plugins

Okay, now it's time to get your hands dirty. Let's spend a few minutes looking at how to install plugins from the Internet into your Rails application. There are two basic ways of installing plugins: with or without Subversion. This section describes both of these methods.

Installing Plugins with script/plugin | 15
Plugins from Subversion | 24
script/plugin update, Redux | 29
Summary | 32


Installing Plugins with script/plugin

While there are certainly other means, using the script/plugin command is often the simplest and easiest way to find and install plugins. This script should be run from the root directory of the application you are developing. Before the intricate details, here is an example of script/plugin in action:

$ cd /Users/james/rails_app
$ 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 a new directory has appeared called acts_as_taggable. Congratulationsyou've just installed your first plugin!

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 a bit more closely. This example demonstrates the use of the install command with script/plugin. Running script/plugin without any arguments presents a list of all the available commands.

  • list

  • sources

  • source

  • unsource

  • discover

  • install

  • remove

  • info (Edge Rails only)

  • update

Each of these commands is covered in this section.

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 specifiedin 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    # i.e. /Users/<your_username>/.rails-plugin-sources

On Microsoft Windows, the file can normally be found here:

C:\Documents and Settings\<Your Username>\.rails-plugin-sources

If you open this file, you can easily view and edit the exact list of URLs that the sources command reported. However, it's not a good idea to edit this file directly unless you know what you're doing. Instead, you can achieve the same level of control using some of the other commands the plugin script provides.

Finding the .rails-plugin-sources File

The exact location of the .rails-plugin-sources file depends on the value of the HOME or USERPROFILE environment variables. You can find out the value in any normal IRB or script/console session. For example:

>> ENV[ 'HOME']
=> "/Users/james"

The value displayed should be the directory that the plugin script stores its information in.

It's important to remember that because of the preceding dot in the .rails-plugin-sources filename, this file is hidden by default. You may need to configure your file browser to show hidden files to see it.

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

It's possible to add a new plugin source manually, using the script/plugin 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. If this command fails, this URL has probably already been added as a source.

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/

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]

It would get boring very quickly if you had to add every new plugin source manually each time you find out about a new plugin. The plugin script has one more trick up its sleeve in the form of the discover command:

$ 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
<...>

The discover command checks via the Internet for any new plugin, and lets you add this source to your collection. These sources are actually found by scraping the "Plugins" page on the Rails wiki (http://wiki.rubyonrails.org/rails/pages/Plugins) for the string "plugin" on any HTTP or Subversion URL. As you can see, each of the URLs returned matches this pattern.

Plugin Repositories

While the Rails wiki is easy to edit and add information to, it is also brittle; in theory there's nothing to stop a malicious user from wiping all of the information from the page, temporarily disrupting the effectiveness of the discover command. In the future, a proper plugin repository may be established, rather than relying on the collaborative editing of a wiki page. Two candidates for such a repository already exist:

  • http://www.agilewebdevelopment.com/plugins

  • http://plugins.radrails.org/

As plugins become more important for Rails developers, it seems certain that some unified repository will be created, and work is already underway to realize that goal.

Tip: Plugin URLs

Only the discover command requires the source URL to include the phrase "plugins" in it. When adding a plugin repository directly via the source command, the URL doesn't need to include "plugins" anywhere; since you are explicitly asking Rails to add this source, Rails will accept it without asking any questions.

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://www.our-server.com/plugins_list.html

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. There may be occasions when you wish to bypass the source list and install a plugin directly from a known URL. Once you have the plugin's URL, it can be supplied as an argument to the command like this:

$ 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/knowledge_of_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. And it doesn't need to be said that when you need to act like MacGyver, every second counts.

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:[1]

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

$ 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.

Note: The remove Command and Rails 1.1.4

The plugin command in Rails 1.1.4 will complain if your project is not in SVN when you do this. Don't worrythe plugin will have been removed.

Note: Edge Rails

When using the latest Edge Rails, running the remove command will also run the plugin's uninstall.rb script, if it has one (see Section 5).

script/plugin info [plugin]

The info command is currently in Edge Rails only, but we'll cover it for sake of completeness.. You can see how this works by running it against a recently updated plugin:

$ script/plugin info acts_as_attachment
author:
    name: technoweenie
    homepage: http://techno-weenie.net
summary: File upload handling plugin.
homepage: http://technoweenie.stikipad.com
plugin: http://svn.techno-weenie.net/projects/plugins/acts_as_attachment
license: MIT

Exactly where this information comes from will be discussed in Section 5 (Hint: it's a YAML file), but at least for the moment this gives us a handy way of getting information about plugins without having to go to either the wiki or the plugin source itself.

Because this feature is currently only present in Edge Rails, don't be surprised if many plugins don't report anything when you run the info command. Once the feature enters the mainstream, authors will almost certainly start adding this information.

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. Unfortunately this is not quite the case, unless you have used one of the Subversion installation methods covered below.

Until then, if you have installed your plugin using the simple, standard methods described so far:

$ script/plugin install my_plugin

then 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.


Plugins from Subversion

The beginning of this section mentioned that there are two ways to install plugins into your application. The first, using the built-in script/plugin command, is great for quickly finding and downloading plugins from the Internet. What may not be apparent is that most of these plugins are actually stored in public Subversion repositories.

Subversion for the Uninitiated

Subversion is a version control system. For any reader who hasn't had much exposure to version control, it is, in a nutshell, a developer's very best friend. Version control systems allow individuals and teams of developers to gain control over a body of source code as it grows and changes throughout its development. Every change to each file is stored and can be recalled, inspected, and even reverted with complete confidence.

Not only do version control systems give you a huge safety net for reverting changes, but they also allow multiple developers to work on a single repository by ensuring that conflicting changes are always resolved before they are committed to the central code store. No more inadvertently overwriting months of someone else's work can only be a good thing. Changes that other developers have made can be easily synchronized with your own working copy of the code by updating your local copy with changes from the server. In most cases, the changes are automatically merged into your copy, so you can continue working without breaking pace.

A proper discussion of version control is beyond the scope of this short cut, so if you aren't familiar with at least one version control system, I encourage you to learn about one of these systems (such as the free SVN RedBook at http://svnbook.red-bean.com). With that in mind, Subversion is certainly not a bad place to start. If it doesn't save your career at least once in your life, I solemnly swear that I will eat any hat of your choosing.

Note: Other Version Control Systems Are Available

It's only fair to point out that there are other version control systems available, such as SVK, darcs, and the classic CVS. However, the plugin script currently only deals with the svn command, and Rails itself is developed using Subversion.

Exploring the different features each versioning system provides can be useful, but if you are unsure which specific system to adopt, Subversion makes a safe bet.

Subversion and script/plugin

As mentioned earlier, most of the plugin sources you encounter will actually be Subversion repositories (a repository is simply a subversion file system that performs the job of storing versioned documents). 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. Pretty neat, huh?

Installing Subversion

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.

You can check for the presence of Subversion by trying to run the svn command in a terminal or at a command prompt:

$ svn --version
svn, version 1.3.0 (r17949)
<...>

Don't worry if your version number doesn't exactly match thisif in doubt, try to ensure you have the latest version available for your operating system.

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. After that, there is no direct link between those files and where they came from, except your memory and any documentation the author may have supplied. This can be somewhat problematic when you want to update this plugin after the author has fixed some bugs or added new features.

Instead, you can use Subversion to check out a copy of the code to your application. When we do this, the plugin is stored with additional information that Subversion can use 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 using Subversion, add the -o flag when running the install command:

$ script/plugin install -o mint
A    /Users/james/rails_app/vendor/plugins/mint/Rakefile
A    /Users/james/rails_app/vendor/plugins/mint/init.rb
A    /Users/james/rails_app/vendor/plugins/mint/tasks
<...>
A    /Users/james/rails_app/vendor/plugins/mint/about.yml
A    /Users/james/rails_app/vendor/plugins/mint/README
Checked out revision 92.

Caution: Using Subversion Install Methods in Practice

Rails 1.1.4 won't let you use the o flag, or indeed any of the Subversion-enabled plugin installation methods, unless your project itself is stored in a Subversion repository. This behavior is curious and almost certainly broken, because it is very unlikely that you would want to check out rather than export a plugin into an existing working copy, since this application will not be deployable without manually copying it to the production server.

Instead, here are my recommendations for using the Subversion flags when installing plugins.

If your project is already a Subversion working copy, do one of the following.

  • Use x (discussed later in this section) to allow easy updating to plugins to the latest version using SVN externals

  • Install without any options, and add the plugin files to your project directly so they will be deployed with the rest of your code

If your project is not stored in Subversion:

  • Install without any Subversion options, to avoid having problems when you add this project to a version control system later on

As you can tell from the final line of output, the install o command has downloaded version 92 of the plugin. This is the latest version, but it is possible to download specific versions by adding the r flag:

$ script/plugin install -o -r 70 mint
<...>
A    /Users/james/rails_app/vendor/plugins/mint/lib/mint_cache.rb
A    /Users/james/rails_app/vendor/plugins/mint/MIT-LICENSE
A    /Users/james/rails_app/vendor/plugins/mint/README
Checked out revision 70.


script/plugin update, Redux

So far so goodyou have a copy of the plugin in your vendor/plugins directory, just as before. So what has been gained? In the previous discussion of the update command, it seemed somewhat less than useful, given that you had to revert to

$ script/plugin install -f <plugin_name>

to get a fresh copy of the plugin code.

Thankfully, when 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:

$ script/plugin update mint
Updating plugin: mint

$ svn info vendor/plugins/mint/
Path: vendor/plugins/mint
URL: http://topfunky.net/svn/plugins/mint
Repository Root: http://topfunky.net/svn/plugins
Repository UUID: 798a5b91-ee08-0410-b43b-a734d69ea54e
Revision: 92
<...>

Using the svn info command (which is a part of Subversion itself), you can check that the plugin has indeed been updated to the latest revision (92 in this case). As with the install -o command, you can use the -r parameter to specify a specific revision to update to.

Automatic Updates: SVN Externals

While using Subversion with the install o command is already useful, when it comes to deploying your application, you will need to install each of the plugins manually on the target server all over again. What we really need is some way of stating, as part of our application, version X of plugin Y is needed wherever the application is expected to run.

Caution: Subversion Externals and Foreign Dependencies

There are two important factors you must keep in mind when attempting to use Subversion externals to link plugins into your project. First, your project must be stored in a Subversion repository; without this, Subversion cannot store the externals property information.

Second, by linking a plugin to your application via Subversion externals, you are introducing a dependency between your application and the remote repository. Should that remote repository become unavailable, you may not be able to deploy your application without manually installing your plugins.

If you are using externals to pull in plugin code from your own personal or company repositories, this is not likely to be an issue, and using the Subversion externals feature is an excellent idea. However, if you are not in control of the remote repository, think carefully about introducing this dependency.

The key to achieving this is a somewhat advanced feature of the Subversion system called externals. By setting an externals property without 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." That will become clearer in a second, but first let's try using it:

$ script/plugin install -x continuous_builder
A  /rails_app/vendor/plugins/continuous_builder/tasks
A  /rails_app/vendor/plugins/continuous_builder/tasks/test_build.rake
A  /rails_app/vendor/plugins/continuous_builder/lib
A  /rails_app/vendor/plugins/continuous_builder/lib/continuous_builder.rb
A  /rails_app/vendor/plugins/continuous_builder/README.txt
Checked out revision 4448.

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

Running the svn propget svn:externals command lets you interrogate the externals properties for the vendor/plugins directory of the application. You can see here that whenever you check out your application, a collection of code from http://dev.rubyonrails.com/svn/rails/plugins/continuous_builder will also be checked out automatically into a subdirectory called continuous_builder. This information will be stored as part of the application's repository, and used whenever (or wherever) you deploy the application.

As with the install o and update commands, you can specify a specific revision with the r flag to link a specific revision of a plugin to your application. When this flag is used, the specified plugin version will be used even when the plugin author releases a new version.

It's not possible to discuss Subversion in any further detail in this short cut without dedicating a significant amount of space to its use. I again urge you to investigate the benefits of version control when developing even the most trivial of applications, and I can heartily recommend Subversion for most styles of development.


Summary

We've now given the plugin script a thorough examination and can use it to install, remove, update, and maintain your plugins.

By using Subversion with your project, you can easily and automatically deploy and update plugins, or lock your plugins to a specific version that the rest of your application expects. In the next section, we're going to take the next step: learning how to develop plugins from scratch.


Section 3. Saluton, Mondo! (init.rb)

In this section, you'll start developing plugins, getting acquainted with the facilities that Rails provides for extending and overriding its behavior. From this point forward, we will be dealing with the implementation of new and novel features and occasionally using sophisticated techniques. If your Ruby needs a bit of polishing, doing so now will greatly help you understand the implementations and techniques in the following sections.

Ready? Goodlet's start writing plugins.

The Plugin Generator | 34
init.rb | 35
Summary | 38


The Plugin Generator

While it is certainly possible to create a plugin from scratch by hand, often the easiest way to start is with the built-in plugin generator. Create a fresh Rails application somewhere, and run the plugin generator as follows:

$ script/generate plugin HelloWorld
      create  vendor/plugins/hello_world/lib
      create  vendor/plugins/hello_world/tasks
      create  vendor/plugins/hello_world/test
      create  vendor/plugins/hello_world/README
      create  vendor/plugins/hello_world/Rakefile
      create  vendor/plugins/hello_world/init.rb
      create  vendor/plugins/hello_world/install.rb
      create  vendor/plugins/hello_world/lib/hello_world.rb
      create  vendor/plugins/hello_world/tasks/hello_world_tasks.rake
      create  vendor/plugins/hello_world/test/hello_world_test.rb

As you can see in Figure 3.1, the plugin generator has created a collection of files under the new directory hello_world, in the vendor/plugins directory of your application. Over the course of this section, we'll get to each of these in turn.

Figure 3.1. Basic plugin file layout

Previous Page
Next Page


init.rb

For the moment however, let's focus on only one: init.rb. Open init.rb in your preferred text editor and edit it to look like Listing 3.1.

Listing 3.1. "Hello, World"

vendor/plugins/hello_world/init.rb

# Include hook code here
puts 'Saluton, Mondo!'

Saluton, Mondo is Esperanto for Hello, World (in case you're not fluent). Once you've saved the file, go back to your shell and run the script/console command. You should see the following output:

$ script/console
Loading development environment.
Saluton, Mondo!
>>

Excellentyour application is that much closer to displaying HAL-9000-style intelligence already. It is now clear that Rails evaluates a plugin's init.rb method when the framework started up. With this in mind, you can probably do something more useful just with this single file. Try changing init.rb to match Listing 3.2.

Listing 3.2. Printing the current Rails version

vendor/plugins/hello_world/init.rb

# Include hook code here
puts "Current Rails version: #{Rails::VERSION::STRING}"

Once you've done this, quit and restart the console:

>> exit
$ script/console
Loading development environment
Current Rails version: 1.1.4
>>

Now you'll know which version of Rails you're working with whenever you start the console in any application that includes this plugin. Table 3.1 outlines a number of special variables that you can access in init.rb.

Table 3.1. Special Methods Available in init.rb

Method

Description

Example

name

The name of the current plugin (i.e., the name of the directory in /vendor/plugins).

"hello_world"

directory

The directory in which this plugin exists.

"script/../config/../vendor/\

plugins/hello_world/lib"

loaded_plugins

A Set containing the names of all plugins al ready loaded, including this one.

#<Set: {"hello_world"}>

config

The Configuration object created in environment.rb.

See the Rails::Configuration class in the Rails source code for more information

While this plugin certainly isn't the most revolutionary piece of programming, it's sufficient to demonstrate that every time Rails loads, the init.rb files for each plugin are loaded. We can actually be more specific than this: every time the Rails environment is loaded, your plugin's init.rb files will be evaluated, if they exist. This includes when your application starts in a server, or even when you are running code directly via script/runner:

$ script/runner "puts 'Rails is ready.'"
Current Rails version: 1.1.4
Rails is ready.

In general, if you need your plugin to do something every time Rails loads, then the code that performs this action is placed in the init.rb file. We're going to use this file frequently, as you'll see later.


Summary

This section introduced plugin creation and highlighted the importance of the init.rb file. If your plugin needs to perform some setup behavior each time the application server starts, that functionality will ultimately be initiated from code in this file.


Section 4. Sharing Code (lib)

It's great being able to display information when your application starts, but typically you'll want to enhance Rails applications in more interesting and useful ways than that. Most commonly, you'll want to make one or more methods or classes available to the rest of your application.

The lib Directory | 40
The CopyrightHelper Plugin | 42
Rails, Modules, and Auto-Loading Code | 44
Using the Copyright Plugin in an Application | 45
Including Code Automatically | 48
Adding to ApplicationHelper | 48
Summary | 50


The lib Directory

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, then Rails will not recognize the subdirectory of vendor/plugins as a plugin. This section discusses the lib directory.

In the previous section, we saw that the init.rb file is evaluated each time the Rails environment is loaded. However, before Rails even evaluates init.rb, it adds the lib directory to Ruby's load path. You can verify this by opening a console session and searching the load path for an entry that matches the plugin's lib directory:

>> $LOAD_PATH.select { |p| p =~ /hello_world\/lib$/ }
=> ["script/../config/../config/../vendor/plugins/hello_word/lib"]

Adding this directory to the load path means that Ruby can now load source files from this directory using the require statement.

A Simple Example

Typical application development often included creating little classes that encapsulate some small, useful behavior. For example, it might often be useful to be able to create deep copies of objects. A deep copy is the duplication of an object and all its attributes, so that no change in the copied object will change any attribute in the original.

The following is an example of why Object#dup (which performs shallow copying) can be problematic:

>> a = [1]; b = [a]
=> [[1]]
>> c = b.dup # perform a shallow copy
=> [[1]]
>> b[0][0] = :changed # now change something in b
=> [[:changed]]
>> c
=> [[:changed]] # c has also been changed!

While this example is fairly trivial, it's not hard to imagine situations where shallow copying could cause unexpected results. To avoid this, you can add a new method to the Object class that lets you perform deep copying (see Listing 4.1).

Listing 4.1. Adding deep_copy to the Object class

vendor/plugins/misc_stuff/lib/deep_copy.rb
class Object
      def deep_copy
        Marshal::load(Marshal.dump(self))
      end
end

Since you just created a file called deep_copy.rb in the lib directory, you can now use it in your application by require-ing it:

>> require 'deep_copy'
=> true
>> [1].deep_copy   # to prove deep_copy is available
=> [1]

Adding require 'deep_copy' to the plugin's init.rb file will ensure that the deep copy method is available to every object in your Rails application.

The Code Payload

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


The CopyrightHelper Plugin

Imagine you're developing several applications that require a similar copyright line at the bottom of every page:

     © 2006, 43indicators LLC. Click here for Terms of Service.

Rather than type this into each application, you can save some effort and write a Ruby method to produce this string automatically. Listing 4.2 shows the beginnings of a simple implementation of this.

Listing 4.2. A copyright message method

def copyright_notice(company_name)
 "&copy; #{Date.today.year}, #{company_name}."
end

How do we make this method available in the views in our Rails application? First, create a new plugin:

$ script/generate plugin CopyrightHelper
      create vendor/plugins/copyright_helper/lib
      <...>

In the new plugin's lib directory, copy the code from Listing 4.3 into a file called copyright_helper.rb. This is a slightly expanded version of the copyright method from Listing 4.2, which can link to an optional "terms of service" document. You can use this method in your views to automatically create a copyright notice.

By placing the method into a module, it can be easily included in other classes. You may come across modules being used in this way referred to as mixins because of their use of "mixing in" new methods to existing classes.

Listing 4.3. The copyright helper

vendor/plugins/copyright_helper/lib/copyright_helper.rb
module CopyrightHelper
      # Displays a copyright notice including the current year and
      # company name. A link to a terms and conditions document.
      def copyright_notice(company_name, terms=nil)
        msg = "&copy; #{Date.today.year}, #{company_name}."
        if terms
          tos_link = link_to('here', terms)
          msg << "Click #{tos_link} for Terms of Service"
        end
        msg
      end
end


Rails, Modules, and Auto-Loading Code

Normally in Ruby, when you want to include code from another file in your application, you have to include a require statement, like the example shown earlier. However, Rails enhances Ruby's default behavior by establishing a simple convention that enables Rails to automatically load your code in most cases.

It's easier to understand this convention once you've seen it in action. Quit your console session and start it again, to be sure that the application is in a state where you haven't explicitly loaded the copyright module:

>> exit
$ script/console
Loading development environment
>> CopyrightHelper.class
Module

Notice that you didn't use any require statement, and yet once you enter CopyrightHelper.class, Rails seems to have loaded the module automatically!

This is how it works: If Rails encounters a class or module in your code that is not already defined, Rails will use the following convention to guess which files it should require in order to load that module or class.

  • If the class or module is not nested, insert an underscore between the constant's names and require a file of this name. For example:

    CopyrightHelper becomes require "copyright_helper"

    KittTurboBoost becomes require "kitt_turbo_boost"

  • If the class or module is nested, insert an underscore between each of the containing modules and require a file in the corresponding set of subdirectories. For example:

    MacGyver::SwissArmyKnife becomes require "mac_gyver/swiss_army_knife"

    Some::ReallyRatherDeeply::NestedClass becomes require

    "some/really_rather_deeply/nested_class"

If not already loaded, Rails would expect to find the code for YourCompany::Project::ClassName in a file called class_name.rb, in a directory called project, itself in the directory your_company, which can be found somewhere in Ruby's load path (e.g., in a plugin's lib directory).

In the previous example, because the plugin's lib directory is now in Ruby's load path, Rails can automatically perform the require "copyright_helper", and the file in the plugin will be loaded whenever you make a reference to that module.


Using the Copyright Plugin in an Application

Now you have a method that outputs the desired copyright message, you will want to put it to good use in all your application views. This example creates a simple controller and a view to display the copyright notice in:

$ script/generate controller Home index
exists app/controllers/
exists app/helpers/
create app/views/home
exists test/functional/
create app/controllers/home_controller.rb
create test/functional/home_controller_test.rb
create app/helpers/home_helper.rb
create app/views/home/index.rhtml

Edit the index.rhtml file so it matches Listing 4.4. In this view, we're simply calling the copyright_notice method defined in the plugin, indicating that the copyright notice is to be shown at the bottom of the view. However, if you start the server and go to http://localhost:3000/home/, you are not greeted by the copyright notice but by an error message instead (see Figure 4.1).

Listing 4.4. Copyright in a view; first attempt

app/views/home/index.rhtml
<h1>Home#index</h1>

<%= copyright_notice('43indicators LLC') %>

Figure 4.1. The copyright_notice method cannot be found

[View full size image]

To make the method available in Rails' views, the controller needs to know about the CopyrightHelper module. While the corresponding helper module for each controller is automatically loaded (so the HomeHelper module is loaded when the home controller is accessed), any additional helpers must be explicitly declared. You can do this quickly by inserting the line

helper :copyright

in the HomeController class definition (app/controllers/home_controller.rb). Add this line, reload the page in your browser, and beholdit works (see Figure 4.2).

Figure 4.2. The working copyright helper

[View full size image]


Including Code Automatically

At this point you have a fully working plugin. However, at the moment you have to remember to add the helper :copyright line to each and every controller whose views need to display the copyright notice. Wouldn't it be great if you didn't have to remember to do that all the time?


Adding to ApplicationHelper

Since CopyrightHelper is a module, it can be included in other classes and modules, thereby making its methods available anywhere that the enclosing class or modules methods are. Because the ApplicationController is the superclass of all controllers, and the ApplicationHelper module is automatically loaded whenever ApplicationController is, any methods mixed into the ApplicationHelper module will be available in any view in your application.

In other words, another way to make your copyright_notice method available to all views is to mix the CopyrightHelper module into the ApplicationHelper module. Edit ApplicationHelper (app/helpers/application_helper.rb), adding

include CopyrightHelper

to the module definition. Reloading the page again in the browser shows that the copyright_notice method is still running perfectly.

Fully Automatic Module Inclusion

Mixing your module into ApplicationHelper is a perfectly valid ways of injecting your plugin's helper methods into a Rails application. However, it still requires you (or the eventual users of your plugin) to manually edit their application in order to use the plugin. Wouldn't it be better if they could install the plugin and have the method immediately accessible without having to add any code to their application?

To do this, remove any straggling helper :copyright or include CopyrightHelper statements that might be in your project. Next, open up the plugin's init.rb file (I told you we'd be using this file again), and add the single line from Listing 4.5 to the bottom.

Listing 4.5. Automatically including modules in Rails base classes

vendor/plugins/copyright_helper/init.rb
# Automatically include your helper into ActionView so it will be
    # available for use in all views

    # The following is a one-line equivalent to
    #
    #   module ActionView
    #     class Base
    #       include CopyrightHelper
    #     end
    #   end
    #
    # but because include is a private method, it must be called using send
ActionView::Base.send :include, CopyrightHelper

What distinguishes this technique from the previous two is that you can ensure that the helper is included without having to ask users to change code in their applications. Mixing the CopyrightHelper module into the application helpers is performed by the init.rb file; all of the behavior required to make the copyright helper work is encapsulated in the plugin itself. Including the CopyrightHelper module in the ActionView::Base class, rather than ApplicationHelper, also ensures that the method will be available wherever the ActionView framework is used, and avoids any problems when ApplicationHelper is reloaded in development mode.

Restart your server so that the init.rb file is evaluated (remember, this only happens when the Rails environment is first loaded), and go to the test page (http://localhost:3000/home/ if you are following this example). You should see the copyright notice displayed perfectly, as in Figure 4.2. Since the instruction occurs in init.rb, users of your plugin are not required to add anything to their applications in order to use the new functionality.

This method of adding functionality to existing Rails classes is one of the key tools for a plugin developer. A quick survey of the init.rb files in currently available plugins will show similar lines for injecting functionality into ActiveRecord, TestUnit, and many other Ruby libraries.


Summary

You've now seen the second key facet of a pluginthe lib directory in your plugin. This directory is added to Ruby's load path (which can be interrogated from the console via the $LOAD_PATH global variable), allowing Rails to find the classes, modules, and methods that your plugin carries as its payload.

This section also covered Rails' automatic loading conventions, which allows it to automatically require source code files based on the names of classes or modules. Following these conventions whenever possible while developing for Rails helps to avoid needing to remember to require each file a plugin contains.

Finally, you've learned how to include simple modules in Rails' base classes using the init.rb file, so you can include your new behavior into Rails without needing to modify your application.


Section 5. The Rest (README, install.rb, Rakefile, tasks, and More)

The two previous sections discussed the two key elements that define a plugin: the init.rb file and the lib directory. This section explores the other files that are created automatically by the plugin generator, with the exception of the test folder, which is covered in more detail in Section 8.

READMECapitalized for a Good Reason | 52
install.rb and uninstall.rb | 53
uninstall.rb (Edge Rails Only) | 55
about.yml (Edge Rails Only) | 55
Rake and Plugins | 56
Rakefile versus tasks/What's the Difference? | 60
Summary | 60


READMECapitalized for a Good Reason

If there is one thing every developer should do more of, it's writing documentation. The plugin generator subtly reminds us of this by automatically placing a README file in the plugin's root directory.

While it is often tempting to simply ignore this file, adding even the most cursory information will pay dividends. How many times have you come back to an old project and needed to scour through the code to figure out how it works and how to use it? Think of the README file as a note to your future self.

The README also serves as the first port of call when other users are browsing your documentation, looking to learn how to put your plugin to work in their applications. It is the basis of the front page created by the doc:* Rake tasks.

Since this file can be read and processed by the rdoc tool, it's worth formatting it using RDoc's conventions, so the HTML documentation that rdoc spits out looks polished and inviting. Listing 5.1 outlines the most basic contents for a README file, but remember that the more information that you include, the better.

Listing 5.1. The start of a typical README file

vendor/plugins/copyright_helper/README
= Copyright Helper
    This plugin provides a simple method to display copyright
    information in Rails views.

    == Usage
    Just include the following line in a view or layout wherever
    you want the copyright notice to appear:

      <%= copyright_notice('your company name') %>

    You can also supply the URL (or options hash for +url_for+)
    that points to a "Terms and Conditions" document, like so:

      <%= copyright_notice('your company name', '/terms.html') %>

    or

      <%= copyright_notice('your company name',
                              :controller => 'info',
                              :action => 'terms') %>
    == Credits
    T.J. Hacker, 2006. All rights and most lefts reserved, yo.

    == License
This code is available under the MIT license.


install.rb and uninstall.rb

The final file created by the plugin generator is install.rb, which is placed in the root of the plugin 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, the install.rb file can be used to ensure that some prerequisites of your plugin are in place when the plugin is installed using the script/plugin install command.

For example, you may develop a plugin that generates intermediate data stored as temporary files in an application. For this plugin to work, it may require a temporary directory to exist before the data can be generated by the pluginthe perfect opportunity to use install.rb (see Listing 5.2).

Listing 5.2. 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, etc.) into the public directory

  • Checking for the existence of dependencies (e.g., RMagick)

  • Installing other requisite plugins (see Listing 5.3)

Listing 5.3. Installing a prerequisite plugin (taken from the login engine)

# 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


uninstall.rb (Edge Rails Only)

The Edge Rails 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 may have created when installed, or while the application was running (see Listing 5.4).

Listing 5.4. Removing a temporary directory (and all its contents) on plugin removal

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


about.yml (Edge Rails Only)

Another upcoming feature in Edge Rails is the info command (introduced in Section 2), which gives users summary information about plugins before they are installed. This information will be loaded from a small, YAML-formatted file named about.yml in the root of your plugin. A short example is shown in Listing 5.5.

Listing 5.5. A basic about.yml file

author:
        name: T.J. Hacker
        homepage: http://www.our-server.com
    summary: Possibly the greatest plugin ever written, nay, conceived.
    homepage: http://www.our-server.com/docs/super_plugin
    plugin: http://www.our-server.com/svn/rails/plugins/super_plugin
license: MIT

The script/plugin info command simply outputs the contents of this file, so you are free to include any information. However, it's probably a good idea to keep it concise, clearly stating the function of your plugin and giving essential contact information only.

Currently this file is only used by the info command, but the structured description of your plugin will be more valuable when a more sophisticated plugin distribution mechanism becomes available.


Rake and Plugins

Rakeoriginally a replacement for the ubiquitous make command but written entirely in Rubyhas become an essential tool when working with Rails applications. From migrating database schemas to generating documentation to running testsRake dutifully handles each of these tasks. It's a veritable workhorse.

To examine the range of Rake tasks that Rails itself provides, open a shell in your application and run rake T at the prompt:

$ rake -T
rake db:fixtures:load  # Load fixtures into the current environment's \
                         database.   Load specific fixtures using \
                         FIXTURES=x, y
rake db:migrate        # Migrate the database through scripts in db/migrate. \
                         Target specific version with VERSION=x
rake db:test:clone     # Recreate the test database from the current \
                         environment's database schema
rake doc:app           # Build the app HTML Files
rake doc:plugins       # Generate documentation for all installed plugins
<...etc...>

You've probably used some of these Rake tasks already. These default tasks are provided as part of the Rails framework, but you can add your own application-specific tasks from plugins quite easily, using the tasks folder.

tasks/*.rake

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. Listing 5.6 outlines a sample Rake task.

Listing 5.6. A sample plugin 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

Once you've added this task, it can be run from any application that includes this plugin, just as with the built-in tasks that Rails provides. You can even see this new task listed in the output of rake T:

$ cd /Users/james/Code/my_app
$ rake -T
<... regular Rails tasks ...>
rake log:clear           # Truncates all *.log files in log/ to zero bytes
rake my_plugin:cleanup   # Clear out the temporary files
rake rails:freeze:edge   # Lock to latest Edge Rails or a specific...
<... etc ...>

The use of namespaces helps gather related tasks together and avoids collisions with rake tasks from other sources (such as Rails itself). For more information about Rake and Rakefile syntax, please refer to the documentation on the Rake Web site (http://docs.rubyrake.org).

The Plugin Rakefile

Table 5.1 outlines the three built-in Rake tasks Rails provides for use with plugins to generate documentation and run any packaged tests in plugins (testing plugins is the subject of Section 8). These application-wide tasks (with the exception of the test:plugins task) operate on the whole set of plugins in your application.

Table 5.1. Rake Tasks for Plugins

Task

Description

Arguments

doc:plugins

Generates documentation for all plugins.
Documentation will be stored in doc/plugins.

None

doc:clobber_plugins

Removes all generated plugin documentation from doc/plugins.

None

test:plugins

Runs plugin tests (all files in each plugin's test directory whose name ends in _test.rb).

PLUGIN=name (only runs the tests for a single plugin)

If you want to generate the documentation for a single plugin, you can run the rdoc task in that plugin's directory:

$ cd vendor/plugins/copyright_helper
$ rake rdoc
rm -r rdoc
                              README:
                 copyright_helper.rb: m.
Generating HTML...

Files:   2
Classes: 0
Modules: 1
Methods: 1
Elapsed: 0.077s

The rake rdoc task generates documentation for the copyright_helper plugin (including the README file, of course), and places the resulting HTML files in vendor/plugins/copyright_helper/rdoc (rather than doc/plugins, as the global doc:plugins task would).

Similarly, if you want to run the tests for a single plugin only, you can run rake test from the plugin directory, rather than supplying a PLUGIN=<name> argument to the application's test:plugins task. Section 8 looks at the different testing mechanisms for testing plugins in greater detail.

The implementation of both the rdoc and test tasks is found in the Rakefile in your generated plugin. If you want to customize either of these tasks at some point (perhaps supplying a different template or including different files when generating documentation), you should edit that file as you would any other Rakefile.


Rakefile versus tasks/What's the Difference?

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 directory.

  • Use tasks/*.rake for tasks for as part of the development or deployment of the application itself. These will be shown in the output of rake T the list of all Rake tasks for this application.


Summary

This section covered the remainder of the files produced by the plugin generator. Each of these files can play a useful part in a plugin's lifecycle, and help users of the plugin understand and make best use of its features.

The install.rb file is evaluated once, after a plugin is downloaded using the script/plugin install command. Because this file is only evaluated once, it is best used for a single-shot action that only needs to be performed once during an application's lifetime. You've also seen how an upcoming feature of Rails plugins can perform actions when a plugin is removed via the contents of the uninstall.rb file.

The importance of the README file was also discussed. This file forms the first point of contact between the developer and users of the plugin (including your future self), and should certainly not be ignored. By formatting its contents as RDoc, your generated documentation will look clearer and be easier to use. You learned how to provide information for the script/plugin info command via the about.yml file (in Edge Rails only at present).

The role of Rake with regards to plugins was also explored. Plugins carry two different types of files that Rake can use:

  • A Rakefile for generating custom documentation and specialized testing

  • Any number of .rake files in the tasks directory, which should contain tasks that complement this plugin's role in the running application

You have now learned about all of the basic aspects of Rails plugins. The following sections share some more advanced techniques employed by plugin developers and consider the life of a plugin beyond its initial development.


Section 6. Plugin Development

You now know the basic ingredients that make up a plugin, so it's time to consider some of the more advanced ways to share functionality between Rails applications. This section discusses a number of common idioms and programming techniques that can help you write plugins.

Prelude: Mixing a Module's Methods into a Class | 63
Staring into the Heart of Ruby | 67
Extracting Behavior into a Plugin | 67
Writing an acts_as Plugin | 73
Summary | 82

It is worth noting (again) at the outset that the following techniques take advantage of some of the rich language features that Ruby provides. While you are going to learn the basic principles behind each technique, an in-depth understanding would require a book in its own right. Again, I encourage you to spend time learning Ruby as a programming language independent of Rails, and in particular how Ruby classes and objects relate.


Prelude: Mixing a Module's Methods into a Class

Before considering some of the more advanced techniques you can use to abstract code into plugins, you need to understand one common idiom that will prove useful on a regular basis: how to mix in methods from a module so that they become available as class methods. But first, Listing 6.1 shows how not to do this.

Listing 6.1. How not to mix class methods from a module into a class

module Nickname
      # an 'instance method' definition
      def nickname
        self.class.nickify(@name)
      end

      # a 'class method' definition
      def self.nickify
        suffices = ['o', 'ster', 'miester']
        name + suffices[rand(sufficies.length)]
      end
    end

    class Dude
      include Nickname
      def initialize(name)
        @name = name
      end
end

Often, newcomers to Ruby expect the code shown in Listing 6.1 to add a class method to any class that includes the Nickname module. In this example, the Nickname module is given two methods.

  • nickname, an instance method that will be available to any objects whose class includes the Nickname module

  • self.nickify, a class method that you want to be added to any class that includes the Nickname module

However, as can be seen from the following error message, this hasn't happened. The class method defined in the Nickname module has not become a class method of the Dude class:

>> d = Dude.new('James')
=> #<Dude:0x54a3c @name="James">
>> d.nickname
NoMethodError: undefined method 'nickify' for Dude:Class
     from (irb):3 in 'nickname'

So, let's figure out what is going on here. Listing 6.1 created two methods in the Nickname module. The first methodnicknameappears as you would expect in the class which includes the module. The second method was defined as

def self.nickify
  # the method contents, which aren't actually important
end

which typically denotes the creation of a class method when writing Ruby. The key is that in the definition of the Nickname module, self refers to Nickname module, and so this method is being defined as a class method of the Nickname module. You can test this by running:

>> Nickname.nickify('Tom')
=> "Tomster"

Next, we need to understand what happens when you add the line

include Nickname

in a class definition. When Ruby encounters this, it adds the constants, methods, and variables of the Nickname module to the surrounding class, so they will be available when instances are created. However, since the nickify method is defined as a class method rather than a regular method, it is not mixed in with the including class.

This explains why it's not present in the Dude class. Listing 6.1 isn't broken; it's just not asking Ruby to do quite what we want.

How to Successfully Mix in Class Methods

As you'll see in the following sections, adding methods to a class is going to be a very useful skill when developing plugins. Listing 6.2 shows one way of successfully achieving this. If you examine the sources of other plugins, you will encounter this pattern repeatedly.

Listing 6.2. Mixing in class methods successfully

module Nickname
      # Module.included is called whenever a module is included
      # in another class
      def self.included(base_class)
        base_class.extend(ClassMethods)
      end

      # a module to hold class method definitions
      module ClassMethods
        def nickify(name)
          # ... the same as the self.nickify method from Listing 6.1
        end
      end

      # ... the regular module methods from Listing 6.1...
    end

    class Dude
      include Nickname
    end

    # Let's try again to invoke the class method from Dude
    Dude.nickify('Murray')
=> "Murraymiester"

So, how does the version in Listing 6.2 manage to achieve what the code in Listing 6.1 did not? The secret is in the self.included method at the top of the revised Nickname module. Whenever a module is included in a class (or another module), Ruby will call the module's included class method, passing the included class as a parameter. In Listing 6.2, Nickname.included is passed the class Dude as the argument base_class.

In the included method, we call base_class.extend with another module (a submodule of Nickname called ClassMethods, in this case) as the argument. The extend method is an instance method of the Object class (remember, in Ruby everything, including the Class and Module classes, are subclasses of Object). Calling this method will mix in a module in that object instance, adding any methods and constants from the given module.

The result of the call to base_class.extend(ClassMethods) is that all of the instance methods in the Nickname::ClassMethods module have been added to the Class instance Dude. In this example, this adds the nickify method to the Dude class, which is what we were trying to achieve in the first place.


Staring into the Heart of Ruby

It is undoubtedly possible to write a whole book just about the interactions and relationships between Ruby classes, modules, and objects; what is touched on here really is only the briefest summary. However, for the moment the pattern outlined in Listing 6.2 will serve you well when developing plugins.


Extracting Behavior into a Plugin

As discussed in Section 1, the core idea behind plugins is reuse. When you find yourself producing the same code time and time again across numerous projects, it's time to extract this repeated code into a plugin. In Listing 6.3, two controllers are shown (probably from entirely different applications), each implementing some behavior to store a user's trail through your Web site.

Listing 6.3. A collection of controllers, all featuring similar breadcrumb behavior

class SomeController < ApplicationController
      before_filter :save_history
      def save_history
        session[:history] ||= []
        session[:history] << url_for(params)
      end
      # ... other controller actions ...
    end

    class AnotherController < ApplicationController
      before_filter :store_breadcrumbs, :except => [:save, :destroy]
      def store_breadcrumbs
        session[:crumbs].push(params).uniq!
      end
      # ... other controller actions ...
end

While the method names are different, it's clear that the fundamental behavior is very similar. Rather than continue reimplementing this particular wheel, it makes more sense to extract the breadcrumb behavior and share it among your applications using a plugin.

Listing 6.4 shows the final implementation aimed for here: from duplicated code, you now have a concise way of adding the breadcrumb behavior to any controller in any project. Essentially, you've gone from implementing the behavior to simply specifying it in your class definition.

Listing 6.4. Using the extracted breadcrumbs plugin

class PhatController < ApplicationController
      store_breadcrumbs :except => [:save, :destroy, :edit]
      # ... the rest of the normal controller actions ...
end

Specifying Behavior with Ruby Code

When developing with Rails, you are already specifying behavior to great effect. Consider this typical code snippet, which features validation and relationships:

class SuperModel < ActiveRecord::Base
  has_many :campaigns
  belongs_to :agency

  validates_presence_of :rockstar_boyfriend, :if => career_in_decline
end
my_girlfriend = SuperModel.new
my_girlfriend.campaigns << Campaign.find_by_name('Derelict!')

This class doesn't provide any method definition that will return an object when you call my_girlfriend.agency, and certainly nothing that might access a database when you call my_girlfriend.campaigns. In fact, there aren't any methods in this entire class definition. Despite this, the agency and campaigns methods are present and callable from instances of the SuperModel class, because Rails lets you specify the behavior of this class via has_many, belongs_to, and validates_presence_of. These "macros" are actually class methods made available by ActiveRecord, and invoking these class methods adds the appropriate methods to this particular class definition.

Listing 6.4 demonstrates the macro-esque method that we want to use for the breadcrumb behavior. Now let's actually bring the store_breadcrumbs method to life.

Creating the breadcrumbs Plugin

This plugin is only going to have an init.rb file and a single module in the lib directory, but it's a good idea to use the plugin generator anyway; having the tests folder around will nag your conscience to write a comprehensive suite of unit tests for this new plugin. Running:

$ script/generate plugin breadcrumbs

will produce (amongst the collection of plugin files) the file lib/breadcrumbs.rb, which will contain the extracted functionality from your original controllers. Without much extra effort, you can take the method the before_filters were calling and add it to the Breadcrumbs module (see Listing 6.5).

Listing 6.5. Extracting the before_filter into a new module

vendor/plugins/breadcrumbs/lib/breadcrumbs.rb
module Breadcrumbs
      private
      # store the current URL in the session if the request was HTTP GET
      # this should be called as a before_filter in a controller
      def drop_breadcrumb
        (session[:crumbs] ||= []) << url_for(params) if request.get?
        session[:crumbs].uniq!
      end
end

Making the method private will ensure that it can't be called accidentally as a controller action from the dispatcher. You can also use init.rb to ensure that this module is mixed in to all your controllers, as illustrated in Listing 6.6.

Listing 6.6. Automatically including a module in all controllers

vendor/plugins/breadcrumbs/init.rb
ActionController::Base.send :include, Breadcrumbs

All that remains is to implement the store_breadcrumbs method for use in the application controllers.

Creating the stores_breadcrumbs Class Method

The goal, as outlined in Listing 6.4, is to be able to add the breadcrumb-storing behavior to any controller with the single line store_breadcrumbs in the controller class definition. To achieve this, you need to define the store_breadcrumbs method and make it available as a class method to all application controllers.

Since you are already mixing in the Breadcrumbs module, and you know how to add methods to a class when including a module, you can reuse the techniques shown in Listing 6.2 to create the new store_breadcrumbs macro-like class method.

Listing 6.7 shows a revised Breadcrumbs module, with the new class method in place. When the Breadcrumbs module is included in ActionController::Base (by init.rb, see Listing 6.6), the store_breadcrumbs method is mixed into the base class of all Rails controllers. This makes the method available from the definitions of all its subclasses, including ApplicationController and any others you might define.

Listing 6.7. Adding the store_breadcrumbs class method

vendor/plugins/breadcrumbs/lib/breadcrumbs.rb
module Breadcrumbs
      def self.included(base_class)
        base_class.extend(ClassMethods)
      end
      module ClassMethods
        # attaches a before filter, which will drop breadcrumbs for each
        # request made on this controller
        def store_breadcrumbs(filter_options={})
          before_filter :drop_breadcrumb, filter_options
        end
      end

      private
      # store the current URL in the session if the request was HTTP GET
      # this should be called as a before_filter in a controller
      def drop_breadcrumb
        (session[:crumbs] ||= []) << url_for(params) if request.get?
        session[:crumbs].uniq!
      end
end

In the example of this plugin in use (seen in Listing 6.4), when the store_breadcrumbs method is encountered in the class definition, its implementation in the ClassMethods module is run. This results in a before_filter being added to the controller, which will call the drop_breadcrumb method before each request.

To summarize, this example presents a simple mechanism you can use to abstract common method definitions and class features into a plugin. It also shows how to make those features available to classes by including a single class method "macro" in the class definition.


Writing an acts_as Plugin

Another common example of feature abstraction can be found in the acts_as family of plugins. This family extends the built-in set of acts_as directives (acts_as_tree, acts_as_list, and acts_as_nested_set) with new behaviors, such as

  • acts_as_paranoid, which prevents objects from really being destroyed

  • acts_as_taggable, which lets you tag object instances

These plugins add meaningful behavior to ActiveRecord objects in a similar way to Rails' own association and validation methods: via a single line in your class definitions.

Writing your own acts_as plugin provides a good opportunity to cement some of the behavior abstraction techniques described earlier in this section. Next, you're going to create a plugin that can enhance ActiveRecord models with a very simple feature. Each time the object is saved, a checksum of the model's attributes is stored in the model itself. You can use this to check that nothing outside of your Rails application has tainted the database (yes, this is paranoid, but it makes for a good example).

Before starting to implement this plugin, it is again worth thinking about how you want to use this plugin once it's finished. Listing 6.8 shows how your finished plugin will be used in a running Rails application.

Listing 6.8. The finished simple checksum plugin in use

class Thing < ActiveRecord::Base
      has_many :other_things
      acts_as_checksummed #our new behavior-specifying method

      # the rest of the model code...
end

Implementing acts_as_checksummed

Okay, it's time to starting writing code. Once again you can get a head start by using the plugin generator to produce the default set of plugin files:

$ script/generate plugin ActsAsChecksummed
create vendor/plugins/acts_as_checksummed/lib
create vendor/plugins/acts_as_checksummed/test
create vendor/plugins/acts_as_checksummed/Rakefile
create vendor/plugins/acts_as_checksummed/init.rb
<...etc...>

From our experience implementing the breadcrumbs plugin earlier, you already have a basic outline of the code that must be used to implement the acts_as_checksummed method:

  • You must add an acts_as_checksummed method to ActiveRecord::Base, so it can be used in any subclasses.

  • A method to calculate the check must be added to the ActiveRecord subclass.

  • Some method should create a before_save callback which triggers the checksum calculation before the record is saved back to the database.

We'll consider each of these problems separately, before putting them all together to build the final acts_as_checksummed plugin.

Writing the acts_as_checksummed method should pose no problem, since we've implemented this type of method before. In the breadcrumbs example, we created a new module called ClassMethods, and implemented the macro-style method in that. The included class method of Module itself was then used to extend the target class and mix in the class method. Listing 6.9 shows the implementation of the acts_as_checksummed method.

Listing 6.9. The acts_as_checksummed method

vendor/plugins/acts_as_checksummed/lib/acts_as_checksummed.rb
module ActsAsChecksummed
      self.included(base_class)
        base_class.extend(ClassMethods)
      end

      module ClassMethods
        def acts_as_checksummed
          # method contents coming soon...
        end
      end
end

By adding a single line to the plugin's init.rb file, you can provide the acts_as_checksummed method to all ActiveRecord models (see Listing 6.10).

Listing 6.10. Adding acts_as_checksummed to ActiveRecord

vendor/plugins/acts_as_checksummed/init.rb
ActiveRecord::Base.send :include, ActsAsChecksummed

Implementing the checksum Method

Next, you must actually implement the meat of this plugincalculating and storing checksums for ActiveRecord model objects. To do this, it is simplest to use the MD5 library that comes with Ruby. This library provides a simple method for calculating the MD5 hash from a given string. To get this string, you can simply take all the attributes of a model object and convert them to YAML. Listing 6.11 shows this implementation.

Listing 6.11. Calculating the checksum from a model object

def calculate_checksum(obj)
      # Don't consider the checksum attribute, since it will naturally change
      # when you save the object.
      # Ignore the ID attribute since it won't exist for new model instances.
      attributes = obj.attributes.delete_if { |key| %(checksum id).include(key) }
      MD5.md5(attributes.to_yaml).to_s
end

Adding the Callback

The final piece of the acts_as_checksummed plugin is the method that will be called each time the record is saved (see Listing 6.12). The method simply calculated the current checksum of that instance, and then writes the value to the checksum attribute, ready to be saved to the database.

Listing 6.12. The checksum callback

def store_checksum
     write_attribute('checksum', calculate_checksum(self))
end

Listing 6.12 calls the checksum calculation method on self, which will be the current model instance once all these methods are collected together into a coherent plugin. Let's do that now.

Putting the Pieces Together

The final acts_as_checksummed plugin is shown in Listing 6.13. The ActsAsChecksummed module now contains two submodulesClassMethods and Behavior. As usual, the ActsAsChecksummed: :ClassMethods module contains methods that will be added to the ActiveRecord::Base class itself, enabling users to call acts_as_checksummed in their model definitions.

Listing 6.13. The final ActsAsChecksummed module

vendor/plugins/acts_as_checksummed/lib/acts_as_checksummed.rb
require 'md5'
    require 'yaml'
    module ActsAsChecksummed
      def self.included(base)
        base.extend(ClassMethods)
      end

      # Methods to be added to classes which contain acts_as_checksummed
      module Behavior
        def self.included(base)
          # Extend the Behaviors::ClassMethods into this
          # ActiveRecord subclass
          base.extend(ClassMethods)
        end

        def valid_checksum?
          read_attribute('checksum')
        end

        private
        def store_checksum
          write_attribute('checksum',
          true # return true to avoid halting the filter chain
        end

        # Methods to be added to the checksummed class, rather than
        # the instance
        module ClassMethods
          def calculate_checksum(obj)
            attrs = obj.attributes.delete_if do |k, v|
              %(checksum id).include?(k)
            end
            MD5.md5(attrs.to_yaml).to_s
          end
        end
      end

      # Methods to be added to ActiveRecord::Base itself
      module ClassMethods
        def acts_as_checksummed
          # Anything in here has the same effect as if it was being run
          # in the class definition itself. So this is the perfect place
          # to add filters, include other modules, and so on.
          include ActsAsChecksummed::Behavior
          before_save :store_checksum
        end
      end
end

The Behavior Module

The ActsAsChecksummed::Behavior module contains everything to be added to the models which do use the acts_as_checksummed plugineverything that describes the behavior of an acts_as_checksummed model.

When the acts_as_checksummed method is called from a model's class definition, the ActsAsChecksummed:: Behavior module is included in that class. As is normal, the methods in this module are mixed into the model's class.

  • The store_checksum method, which is called by a before_save filter to generate the checksum

  • The valid_checksum? method, to let you check individual objects to see if they still match their checksum

Adding Class Methods to the Model Class

The Behavior module also contains its own ClassMethods submodule (ActsAsChecksummed::Behavior::ClassMethods), and the self.included method to mix it into a target class. Let's spend a moment considering what is happening. Since the Behavior module is being included in an ActiveRecord::Base subclass, these methods are being added as class methods to that subclass.

In a nutshell, you're adding some class methods only to the model classes that wish to use the acts_as_checksummed behavior.

Why Bother Adding Class Methods to a Subclass Only?

You're right to ask this question, especially since we already have a mechanism for adding class methods to ActiveRecord::Base itself, which would then be available to all subclasses via inheritance. Why do we need another ClassMethods module?

If you were to implement the calculate_checksum method in the ActsAsChecksumed::ClassMethods module, the method would be added to ActiveRecord::Base and be available to all its subclasses. By adding more methods to ActiveRecord::Base, you are polluting the class with methods that some subclasses may never need. It's cleaner and safer to mix methods only into classes that have explicitly specified that they wish this behavior.[2]

[2] Looking back at the breadcrumbs plugin, you may choose to reimplement it using a Behavior module too; in its current form, all controller subclasses will have a private drop_breadcrumb method, even if they don't require the breadcrumb behavior.

In the acts_as_checksummed example, you added the class method for calculation instance checksums to only those subclasses that have called acts_as_checksummed in their class definitions. This class method is then available to the instance methods, which can use it to calculate the checksum of their instance attributes.

acts_as, Redux

The techniques introduced in this section can be found in most of the plugins available. The names of the submodules used here try to give accurate descriptions of their respective purposes, but in your own plugins the modules can be given any name.

This also doesn't represent the only way to create plugins that extends Rails with new syntax. Thanks to Ruby's rich object-oriented model, there are many ways to add your functionality to existing classes. Another common technique you may encounter when examining plugin sources is the use of class_eval or instance_eval. Any good Ruby reference can guide you through the use of these advanced and powerful methods.

Learning from other plugin developers is a great way to pick up new ideas for implementing your plugins; examining the code of existing plugins, and the source code of Rails itself, is an excellent way to learn about different techniques and to get new ideas for use in your own plugins and Ruby development as a whole.


Summary

This section covered one of the most important techniques used in plugin development. You can now abstract the implementation of class features into plugins, and these features in other classes, by calling simple, expressive macro-esque methods in their class definitions.

Much of the power and simplicity in Rails is implemented using these very techniques. Being able to perform your own extractions will let you work faster, avoid repetition, and help you to become a happier developer, all in one.


Section 7. Sharing Classes and Code

While plugins are often used to abstract and extract some functionality from an application that can then be put to use in the future, often we want to reuse complete classes between applications. There are certainly occasions when I find myself writing the same basic controller or model again, and again, and again...

Using Plugins to Share Code | 84
Models and Helpers | 84
Controllers | 84
Organizing Your Shared Classes | 85
Sharing Files That Aren't Code | 86
Using Engine Plugins | 89
Generators in Plugins | 93
Creating Generators | 95
Running Plugin Generators | 96
Sharing Generators | 97
Upgrading Generated Code | 97
Summary | 98


Using Plugins to Share Code

The ability to share code among many applications is a powerful tool when you are developing many similar applications. Wouldn't it be great to just be able to drop common classes into new applications using a plugin?


Models and Helpers

Sharing models within plugins is as simple as can bejust put the model class file in the plugin's lib directory. Thanks to Rails' auto-loading features (see Section 4), your model class will be loaded as if it were placed in app/models in the main application.

The same applies to any helper modules that you want to share between applicationsthese can be placed directly in the lib directory, and Rails will load them quite happily without you doing any additional work.


Controllers

Adding controller classes to plugins is almost as trivial as the process for models and helpers. By placing them in the lib directory (or in subdirectories if the controllers are in modules), Rails will load them seamlessly, as you have grown accustomed to expecting.

However, ActionController's routing mechanism requires an extra hint in order to properly rewrite URLs according to the routes defined in the application. To point the routing system in the direction of your plugin controllers, it is enough to add

require 'some_controller' # ...for example

to the plugin's init.rb file. You will need to require each controller individually; by explicitly requiring the file, we are ensuring that Rails' routing code will work properly with this controller.[3]

[3] The need to explicitly require controllers was introduced in Rails 1.1.6, as the side effect of making the routing subsystem more secure.


Organizing Your Shared Classes

When sharing a number of models, helpers, and controllers, the lib directory may end up looking rather crowded and messy. Naturally, there is a simple and quick technique for organizing shared Rails classes and keeping your lib directory tidy, based on an idea by Ezra Zygmuntowicz.

While the lib directory of your plugins is the only directory automatically added to the load path, there's nothing to stop you from explicitly adding other paths from a plugin. With this in mind, you can create a new directory at the root of your plugin called app, and within it subdirectories called controllers, helpers, and models. You do this by adding the following lines to init.rb:

%w(controllers helpers models).each do |code_dir|
  $LOAD_PATH << File.join(directory, 'app', code_dir)
end
require 'first_controller'
require 'another_controller' # and so on for any other controllers.

Once again, we must explicitly require the controllers ro ensure that Rails' routing works as expected. This simple mirroring of a normal /app directory provides an intuitive way of organizing any shared Rails classes without getting lost in a mess of Ruby source files in lib. Sharing these vertical slices (containing elements from each of the horizontal levels of your applicationmodel, controller, and view) can be very powerful; this idea is extended further by the engines plugin, discussed later in this section.


Sharing Files That Aren't Code

We've seen that plugins can include most of the types of Ruby code files that can be found in Rails applications, including:

  • models

  • helpers

  • controllers

  • modules (for mixing in functionality to any of the above)

However, it is often useful to share files that aren't Ruby source code between applications. Examples of this include stylesheets, shared images (such as the ubiquitous animated spinner image, which indicates that some fantastic Ajax action is underway), JavaScript files, and possibly most importantly, Rails views and partials.

Views and Partials

It is actually quite simple to provide a set of views in a plugin, because Rails lets you control the directory an individual controller will expect to contain its views. If your plugin contains a ListController class in the apps/controllers subdirectory scheme described earlier, you can set this controller to use views from the plugin by adding the following line in the ListController class:

class ListController
  self.template_root = File.join(File.dirname(__FILE__), '..', 'views')

  # ... etc ...
end

With this set, you can create an app/views/list subdirectory in the plugin; views and partials requested by the ListController will be loaded from this directory.

Caution: Sharing Views: All or Nothing

This view-sharing technique has a limitation that may not be immediately obvious. Because we have set the template root of this controller, it will then expect all views and partials to be available from this source. If your plugin controller attempts to load a partial or template from the main application, a "Template is missing" message will be displayed.

Public Assets

When a Rails application starts, usually the only files that can be served by the Web server are those in the public directory; all other files are placed out of bounds. This presents a challenge if you want to include files in your plugins that should be available directly from the Web server, such as JavaScripts or stylesheet assets.

Copying via install.rb

A trivial technique for making any plugin file available to the Web server is to simply copy these files into the application's own public directory. You can add the code to copy these files in the install.rb file, so the files will be copied as soon as the plugin is installed. The example in Listing 7.1 shows how to achieve this. This code snippet assumes that your files will be stored in a directory in the plugin called assets, and in subdirectories therein mirroring the corresponding subdirectories in the main application's public folder.

Listing 7.1. Copying public assets from a plugin into a Rails application

# install.rb
    require 'fileutils'
    %w(stylesheets javascripts images).each do |type|
      dir = File.join(File.dirname(__FILE__), 'assets', type)
      FileUtils.cp_r(dir, File.join(RAILS_ROOT, 'public')) if File.exist?(dir)
end

Copying via init.rb

An alternative to this approach is to have the files copied each time the server starts. This is easily achieved by simply moving the code from the install.rb file to the plugin's init.rb file (in this file you can also take advantage of the directory method; see Section 3). Copying the files each time the Rails environment loads can be dangerous, however; if the user has modified the file the plugin copied, these changes can easily be lost as the plugin file is recopied the next time the application starts.

Using Symbolic Links

You may be tempted to simply create symbolic links from the main public directory into the assets (or equivalent) folder of your plugin. This technique can be easy for users comfortable dealing with the file system at this level, but it also raises a number of issues.

  • Creating any symbolic links outside of the public directory may inadvertently expose areas of your server's file system to the Web if the link is not created correctly. In a worst-case scenario, you could allow malicious users access to sensitive information, including passwords and server configuration details.

  • Creating symbolic links on Microsoft Windows servers is technically possible (if the file system is NTFS), but it is certainly not trivial and requires special tools. Using this mechanism may therefore alienate potential users who must develop or deploy on this platform.

Other Mechanisms

The engines plugin (discussed next) offers an alternative mechanism that will mirror a plugin's public assets (and any other files) into a controlled and separate subdirectory of the public folder, providing convenient helper methods to use these files. The plugin also incorporates Duane Johnson's bundled_resource plugin, which has special mechanisms for linking collections of Web assets (such as the required JavaScript and stylesheet code for a date picker) easily into layouts and views.


Using Engine Plugins

The previous techniques described can go some way toward letting you share classes between your Rails applications, but using engines is particularly effective when developing multiple applications within a team or as part of a company. This desire to reuse greater functionality between applications has also given rise to this slightly different breed of plugins.

An engine is basically the same as a plugin; it contains lib, tasks, and test directories along with a README file. Engines will also commonly contain the app directory structure described in the previous section. This section covers some of the notable differences between engines and plugins.

Developing with Engines

To use engines in your application, you must first install the engines plugin. This plugin enhances the Rails framework and allows controllers, models, and views (and almost any other kind of file you could see in a Rails application) to be automatically loaded from engines.

Installing the engines plugin itself is the same as installing any other plugin (described in Section 2). Running the script/plugin install command will download a copy of the engines plugin into your application:

$ script/plugin install engines

The init_engine.rb File

Rather than the init.rb file, which is in almost every plugin, engines are created with an init_engine.rb file. Its purpose is exactly the same as its counterpart in a normal plugin. It is named differently so that the engines plugin itself can control which engines are loaded, and in which order. Engines are loaded by calling the Engines.start method in your application's environment.rb file:

Engines.start :reporting, :job_queue, :data_processor

Code Mixing

Another useful feature that the engines plugin has is the ability to selectively override small parts of an engine in the application. If the layout of a particular view or partial doesn't suit the current application, a replacement can be created in the application that overrides the view or partial contained in the engine.

Similarly for controllers and helpers, individual methods can be overridden in the application, keeping any custom implementation distinct and separate from the original engine. Separating these application-specific customizations from the underlying implementation makes it much easier to upgrade engines without having to check every file for any changes made since the engine was originally installed.

Sharing Public Assets

Engines also provide support for sharing stylesheets, JavaScripts, and other static assets, automatically making these files available to the Web server. Any files and directories stored under the public directory in an engine are automatically mirrored into discrete subfolders in the application's public folder.

To use these files in layouts and views, the engines plugin provides several helper methods to load stylesheets, JavaScripts, and images easily. Listing 7.2 shows some of these helpers in use in an application layout.

Listing 7.2. Using engine helpers in a simple application layout

<html>
      <head>
        <%= engine_stylesheet 'reporting', 'query' %>
        <%= engine_javascript 'reporting' %>
      </head>
      <body>
<!  and so on... >

With Great Power Comes Great Responsibility

Because so much functionality can be placed inside an engine, it is often tempting to try and graft entire parts of existing applications together. While such things may be technically possible, they are almost certainly inadvisable. Unless a full-featured plugin or engine has been designed to "play well" in unpredictable environments or applications, it can be impossible to mediate conflicts between the application and the plugin/engine.

For instance, if both the application and plugin/engine contain a model with the same name, it might not be clear which one will actually be used (in this case if it is an engine, the application model will be used. If it is a plugin using the technique described in the section "Organizing Your Shared Classes," the plugin model will be used).

Appropriate Reuse

The key to successfully developing plugins or engines that contain a significant amount of functionality beyond framework-level abstractions is to ensure that your desire to reuse them is truly appropriate. A small team that must produce numerous similar applications quickly will soon become tired of having to re-implement essentially identical authentication schemes for every application they are asked to build.

Developing a single common subsystem and then sharing it among applications whose requirements are essentially identical is clearly advantageous, not only because it avoids repeating yourself and wasting time, but also because it lets you use Subversion externals (see Section 2) to greatly simplify maintenance of these shared subsystems. The ability to roll out bug fixes with minimal effort across your entire portfolio of applications is extremely powerful. Teams reusing code in this manner find it incredibly useful.

Eventually, it all comes down to understanding the environment in which you are developing. Your unique understanding of the particular kind of applications you will be producing will help you make informed decisions about which aspects of your applications to reuse and which to tailor individually.

EnginesFurther Information

All of the techniques for developing plugins presented here can be applied to engine development, but space limits the discussion of the specifics of building engines. If you'd like to know more about this type of plugin, a Web site and active community exist; please see the Rails Engines Web site (http://www.rails-engines.org) for more information.


Generators in Plugins

A generator is a collection of template files that can be processed and output into a Rails application to produce controllers, views, models, or any other kind of file that might be needed. Almost every Rails developer will need a generator at some pointto create scaffolds, models, controllers, or indeed plugins themselves, as you have done already several times.

Generators were the primary mechanism for sharing code before plugins were available in Rails, and they have to some extent been superceded by the newer, more flexible plugin system. However, generators can still prove useful when you need to create files in the application that users should then edit or modify to suit their applications.

Generators can be distributed as part of a plugin by including a generators directory in the root of the plugin (see Figure 7.1). Any number of generators can be placed in this directory. For instance, you may wish to add a "terms" generator to your CopyrightHelper plugin (see Section 4) that allows users to create a boilerplate "Terms and Conditions" document, along with a privacy statement, in their public directory.

Figure 7.1. Including a generator in a plugin

Previous Page
Next Page


Creating Generators

Listing 7.3 shows a very simple generator, which simply reads two template files from the templates subdirectory, processes them with ERB (just like normal Rails' views), and then writes them into the application's public folder as normal HTML files. The generator makes a number of parameters available for use in these templates. This simple example defines the single argument passed as the company name. You can get the value of this parameter by using the name method in your templates (see Listing 7.4).

Listing 7.3. The terms generator

vendor/plugins/copyright_helper/generators/terms/terms_generator.rb
class TermsGenerator < Rails::Generator::NamedBase
      def manifest
        record do |m|
          # Process the template files and place them in the public folder
          m.template 'terms.rhtml', File.join('public', 'terms.html')
          m.template 'privacy.rhtml', File.join('public', 'privacy.html')
        end
      end
end

Listing 7.4. The terms file template

[View full width]
vendor/plugins/copyright_helper/generators/terms/templates/terms.rhtml
<html>
           <head>
             <title><%= name %> &raquo; Terms and Conditions</title>
             <! add your application stylesheets here! >
           </head>
           <body>
             <h1><%= name %></h1>
             <h2>Terms and Conditions of Use</h2>
             <p>We, <%= name %>, take no responsibility if this Web application makes you
  bald, calls
         you names, phones your boss and insults him, leaves nasty notes around your house
   undermining
         your self-confidence, or deletes everything on your mother's computer. You need
   to call her
         more often, anyway.</p>
           </body>
</html>


Running Plugin Generators

Any generators distributed in plugins can be run using the script/generate command. This command will find all available generators, not only in the Rails framework, but also in any plugins that are installed in the current application:

$ script/generate terms "Shamazon, Ltd."
     create    public/terms.html
     create    public/privacy.html


Sharing Generators

Plugins can be an excellent mechanism for simply sharing just generators, without including any other significant content. It is often more convenient to package a generator in an otherwise empty plugin than it is to create a Gem or distribute one as a compressed file package. For example, the acts_as_authenticated plugin distributes almost all of its logic as a generator in the plugin; the only other features are a README file, and an install.rb file that prints the contents of the README, providing the user with a set of post-install instructions. The engines plugin also includes a generator for creating plugins ready-made with the particular features distinguishing engines from normal plugins.


Upgrading Generated Code

One potential disadvantage of sharing functionality using generated code is that once code has been generated and placed in the user's application, it can be very difficult to upgrade this code. For example, imagine you have been using a generator to share an authentication (login) system among several applications. The generator has dutifully been creating controllers, models, and so on in each application, and in some instances you have modified these files to suit an application's particular needs.

However, if you discover a bug in the authentication implementation the generator produces, there is no simple way to automatically share any fixes for this bug with all deployed applications.

  • You could develop a patch and manually apply it to each application.

  • You could update the plugin, recreating the generator containing a patched version. Each application could then quite easily update their version of the plugin (possibly using Subversion externals; see Section 2), but you must then rerun the generator and ensure that it doesn't overwrite any of the customizations you have made.

Essentially, by generating code into the destination application, we lose any separation of that functionality from the existing application-specific controllers. This is an issue that may not prove significant unless you are required to maintain a number of applications simultaneously, but nevertheless it is important to bear in mind when considering the use of generators.


Summary

In this section you have seen that Ruby's highly dynamic nature, and specifically the ability to add new methods to existing classes at runtime, makes Rails' plugin mechanism extremely powerful. You can add new directives to ActiveRecord (or almost any other aspect of Rails), which lets you and other developers easily add sophisticated features to classes using only a small amount of expressive code.

An understanding of Ruby's object model is crucial when trying to extend Rails in this way, and I once again highly recommend that you spend some time exploring the alternative techniques by downloading and examining the work of other plugin developers.

In addition to adding new functionality, plugins can also be used to directly share models, controllers, and any other Ruby class simply by including these files in the lib directory. Techniques for organizing these classes into a more application-like structure have also been explored. While plugins don't directly support sharing views and other public asset files, there are a number of techniques that you can employ to allow Rails to use and share these. If more sophisticated sharing of MVC (model-view-controller) slices is required, then the engines plugin can be used to provide a more powerful mechanism for reusing code.

Finally, you've seen that plugins can also contain generators, a convenient way to introduce larger sets of files into a user's application. Once a generator has run, the generated files are placed in the application alongside any other controllers, giving users the opportunity to tailor them to their particular needs. The disadvantage to this approach is that you may lose the ability to easily and automatically share patched or updated versions of the plugin's code among your applications.


Section 8. Testing Plugins

Once 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. This section discusses some special issues that must be taken into consideration to test plugins effectively.

Writing Plugin Tests | 101
Testing Plugins Against an Isolated Database | 105
Summary | 112


Writing Plugin Tests

The plugin generator creates a test folder in your plugins (which we have happily ignored up to this point). However, the importance of testing your code cannot be overstated. Test-driven development not only can improve the reliability and maintainability of your code, but it can actually help you develop faster.

The test:plugins Task

Rails provides a built-in mechanism for running plugin tests via the test:plugins Rake task. This task runs through the tests for every plugin installed in the current application. Specifically, it runs tests from all the files with names ending in _test.rb, in each test subdirectory of each plugin.

Suppose that you've taken the Nickname module from Listing 6.1 and packaged it as a plugin. To test that the plugin works as expected, create a file in the test directory of this plugin with contents similar to those in Listing 8.1.

Listing 8.1. Testing the Nickname plugin

vendor/plugins/nickname/test/nickname_test.rb
require 'test/unit'

    # Add the path to the nickname library to the load path & require it
    $LOAD_PATH.unshift File.join('..', 'lib')
    require 'nickname'

    class NicknameTest < Test::Unit::TestCase

      # A test class to add the nickname behavior to
      class TestDude
      include Nickname
      def initialize(n)
        @name = n
      end
    end

    # create an instance of TestDude
    def setup
      @dude = TestDude.new('Sean')
    end

    # Test that the nickname method is working
    def test_should_add_nickname_method
      nick = @dude.nickname
      assert_not_nil nick
      assert_not_nil (nick =~ /o$/) || (nick =~ /ster$/)
                     (nick =~ /miester$/)
end

This simple test should be enough to verify that the Nickname module works as expected when it is included in classes. You can now run these tests to verify that the nickname plugin works as advertised:

$ rake test:plugins
(in /Users/james/Code/my_app)
/opt/local/bin/ruby -Ilib:test "/opt/local/lib/ruby/gems/1.8/gems/rake-
0.7.1/lib/rake/rake_test_loader.rb" "vendor/plugins/nickname/test/nickname_test.rb"
Loaded suite /opt/local/lib/ruby/gems/1.8/gems/rake-0.7.1/lib/rake/rake_test_loader
Started
.
Finished in 0.000503 seconds.
1 tests, 2 assertions, 0 failures, 0 errors

Testing Plugins Individually

The test:plugins task is great, but it runs the test for every plugin in your application. However, often we're only interested in running the tests for a single plugin. You can run plugin tests individually in two ways.

The PLUGIN Argument

By specifying a PLUGIN argument to the test:plugins rake task, only the tests for the plugin with that name will be run:

$ rake test:plugins PLUGIN=nickname

The Plugin Rakefile

As seen in Section 5, the plugin generator creates a Rakefile in the plugin directory. By default it contains only two tasks: rdoc and test. When not modified, the test task will behave in almost the same way as the test:plugins rake task, except it only runs the tests for this particular application. There are two notable differences when running plugin tests this way.

First, the Rails environment is never loaded unless the plugin test files explicitly require it. When the test:plugins rake task is invoked, the Rails environment is loaded once as a dependency of that task. As a result of this, any initialization specified in environment.rb will be performed.[4] When running the test task from a plugin's own Rakefile, there is no such dependency, so environment.rb is not loaded. (The Rails environment's role in plugin testing is discussed later in this section.) If your plugin does not need the Rails environment for its tests to run, you can save some time when running the tests by using the Rakefile instead of the test:plugins task.

[4] While the Rails environment is loaded before the plugin tests run, it is not available in your plugin tests unless you specifically require it. See the section "Loading the Rails Test Environment" for details.

Customized Testing Tasks

The second notable difference when testing via a plugin's own Rakefile is the opportunity to customize the test task itself. For instance, you may wish to provide other tasks which the plugin tests depend on (see Listing 8.2 for an example). Because it's not possible to alter the test:plugins task (short of replacing it entirely through the plugin's tasks directory), editing the Rakefile is your only opportunity to add such dependencies.

Listing 8.2. Adding a dependent task to the plugin tests

vendor/plugins/some_plugin/Rakefile (excerpt)
desc 'Generate some initial data for the plugin'
    task :generate_test_data do
      # something in here
    end
    desc 'Run the plugin tests'
    Rake::TestTask.new(:test => :generate_test_data) do |t|
      t.libs << 'lib'
      t.pattern = 'test/**/*_test.rb'
      t.verbose = true
end

Loading the Rails Test Environment

One notable difference between testing plugins and "normal" Rails testing is that the Rails environment usually is not loaded when your plugin tests run. This can be significant, particularly if your plugin needs to access the database or use some of the assertions that Rails adds to Test::Unit.

Listing 8.3 shows how to easily load the Rails environment and testing additions when running your plugin tests. By requiring the test_helper file from the application's own test folder, you ensure that the Rails' environment is loaded (including all your plugins) and any application-specific test configuration is performed.

Listing 8.3. Loading the Rails environment in plugin tests

# Include this line at the top of your test files
    require File.join(File.dirname(__FILE__),
'../../../../test/test_helper')


Testing Plugins Against an Isolated Database

When developing a plugin that enhances or modifies the behavior of ActiveRecord objects, it's often desirable to be able to test these new features against a real database. However, since this is a plugin, it may be dangerous to interfere with the application database in any way (even the test database), since you cannot predict the interaction between your tests and those of the application in general.

In such cases, it is safer to run the plugin tests against their own database. Here is a fairly simple recipe for testing a plugin against an isolated database. The acts_as_checksummed plugin you developed in the previous section is a good example we can use to illustrate this; you should be able to verify that the checksum is updated each time an ActiveRecord subclass instance is saved. Listing 8.4 shows the contents of the test file (the file will have been automatically generated when you created the plugin; simply edit it to match the listing here).

Listing 8.4. The acts_as_checksummed plugin tests

vendor/plugins/acts_as_checksummed/test/acts_as_checksummed_test.rb
require 'test/unit'

    # a simple ActiveRecord::Base subclass to test your
    # simple_checksum behavior
    class Thing < ActiveRecord::Base
      acts_as_checksummed
    end

    class ActsAsChecksummedTest < Test::Unit::TestCase
      fixtures :things

      def test_should_create_a_checksum
        t = Thing.new
        assert t.save
        assert_not_nil t.checksum
      end

      def test_should_change_checksum_upon_save
        t = things('something_with_checksum')
        old_checksum = t.checksum
        t.content = 'apollo'
        assert t.save
        assert_not_equal old_checksum, t.checksum
      end
end

In this test case we have created a throwaway ActiveRecord subclass called Thing, and included the line

acts_as_checksummed

as the only declaration in its definition. Following this, you have out two simple tests. The first asserts that the initial checksum of your model will be initialized when the object is first saved. The second test uses an object loaded from fixtures (see below), and asserts that the checksum really is changed and updated after the object is saved.

Creating the Test Data

To have the save methods of the ActiveRecord subclass actually work, you need to define an independent database configuration, schema, and set of fixtures from the application. First, create the fixtures. In the test folder of your plugin, create a new folder called fixtures, and a file called things.yml, using Listing 8.5 as a guideline for its contents.

Listing 8.5. Fixtures for the acts_as_simply_versioned tests

vendor/plugins/acts_as_checksummed/test/fixtures/things.yml
something_with_checksum:
      id: 1
      content: starbuck
      checksum: aa1706b1003a525af7e23d7a8aa368a8
    another_thing_with_checksum:
      id: 2
      content: muffet
  checksum: 260b5d194b8d3d219340798b2ef105cb

Defining the Test Schema

It's clear from these fixtures that we intend our Thing object to only have id, content and version attributes. You can generate a very simple ActiveRecord schema to represent this table (and also use it to create the table later on). This is shown in Listing 8.6. Create this file in the plugin's test directory alongside the test case itself. This schema defines a single table, things, with two columns (the id column is implicit in the create_table statement).

Listing 8.6. Defining the test schema

vendor/plugins/acts_as_simply_versioned/test/schema.rb
ActiveRecord::Schema.define(:version => 0) do
      create_table :things, :force => true do |t|
        t.column :content, :string
        t.column :checksum, :string
      end
end

Each of the elements needed to describe and populate the test database are now in place, but you need to write some code that will establish a connection to your isolated test database and configure Rails' testing mechanisms to use the plugin fixtures.

Testing Against a Plugin Database with a Test Helper

Listing 8.7 shows a test helper file that performs all of the setup needed, and Listing 8.8 shows a database configuration file. Both of these are placed in the plugin's test directory beside the test case we just created.

Listing 8.7. A plugin test helper for connecting to an isolated database

vendor/plugins/acts_as_simply_versioned/test/test_helper.rb
require 'test/unit'
    plugin_test_dir = File.dirname(__FILE__)

    # Load the Rails environment
    require File.join(plugin_test_dir, '../../../../config/environment'))
    require 'active_record/fixtures'

    databases = YAML::load(IO.read(plugin_test_dir + '/database.yml'))
    ActiveRecord::Base.logger = Logger.new(plugin_test_dir + "/debug.log")

    # A specific database can be used by setting the DB environment variable
    ActiveRecord::Base.establish_connection(databases[ENV['DB'] || 'sqlite'])

    # Load the test schema into the database
    load(File.join(plugin_test_dir, 'schema.rb'))

    # Load fixtures from the plugin
Test::Unit::TestCase.fixture_path = File.join(plugin_test_dir, 'fixtures/')

This test helper loads the Rails environment and, in particular, ActiveRecord's fixtures classes. Next, the custom database.yml file (see Listing 8.8) is used to establish a connection to the plugin test database. In this particular implementation, the sqlite database adapter will be used unless another adapter is specified as a parameter for the test.

Listing 8.8. Specifying the database for plugin tests

vendor/plugins/acts_as_simply_versioned/test/database.yml
sqlite:
      :adapter: sqlite
      :dbfile: my_plugin.sqlite.db
    sqlite3:
      :adapter: sqlite3
      :dbfile: my_plugin.sqlite3.db mysql:
      :adapter:
    mysql
      :host: localhost
      :username: rails
      :password:
      :database: my_plugin_test

# ...and any others...

Once a connection to the database has been established, loading the schema (see Listing 8.6) will reset and create the tables the tests expect. Finally, we ensure that fixtures will be loaded from the plugin instead of from the default location in the application's normal test directory.

Using the Plugin Test Database

The final step is to ensure that this custom database setup and initialization is performed when the tests for the plugin are run. This can be achieved by making sure that the test helper is required by each test file, as in Listing 8.9.

Listing 8.9. The revised acts_as_checksummed plugin tests

vendor/plugins/acts_as_checksummed/test/acts_as_checksummed_test.rb
require File.join(File.dirname(__FILE__), 'test_helper')

    # a simple ActiveRecord::Base subclass to test your
    # simple_version behavior
    class Thing < ActiveRecord::Base
      acts_as_checksummed
    end

    class ActsAsChecksummedTest < Test::Unit::TestCase
      fixtures :things

      # ... the rest of the tests from Listing 8.4...
end

You can now test the acts_as_checksummed plugin to verify that it actually works:

$ rake test:plugins PLUGIN=acts_as_checksummed
(in /Users/james/Code/my_app)
/opt/local/bin/ruby -Ilib:test "/opt/local/lib/ruby/gems/1.8/gems/rake-
0.7.1/lib/rake/rake_test_loader.rb" "vendor/plugins/acts_as_checksummed/test/acts_as_check
summed_test.rb"
-- create_table(:things, {:force=>true})
   -> 0.6913s
-- initialize_schema_information()
   -> 0.0014s
-- columns("schema_info")
   -> 0.0009s
Loaded suite /opt/local/lib/ruby/gems/1.8/gems/rake-0.7.1/lib/rake/rake_test_loader
Started
..
Finished in 1.034633 seconds.
2 tests, 4 assertions, 0 failures, 0 errors

Of course, if you were seriously testing this plugin, you would include many more detailed tests, undoubtedly highlighting some of the assumptions and shortcomings of this particular implementation.


Summary

Writing tests for plugins is very similar to writing tests for a fully formed Rails applications, 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, you can take some extra precautions to ensure that the test behavior of your code is isolated from the rest of the application.

This section again clarified the subtle distinction between running plugin tests using the global test:plugins task and via the plugin's own Rakefile. While the former can test all installed plugins at once, the internal Rakefile can be exploited to add any specific tasks your plugin requires to be tested properly.

You've also seen how to bootstrap a separate database for testing plugins in complete isolation. This is particularly useful when a plugin augments ActiveRecord with additional functionality, since you need to be able to test the new methods in a controlled environment, minimizing the interaction with other plugins and the application's own test data.


Section 9. Sharing Your Plugin

Once you've developed your plugin, tested it, and documented it, the only thing left to do is to share it with the rest of the Rails community. This section describes how to make your code available to the world.

To Share, or Not to Share | 114
Hosting | 115
Releasing Your Plugin | 117
Supporting the Plugin | 119


To Share, or Not to Share

The first thing to consider once you feel you are ready to share your hard work with the rest of the world is quite simple: Is what you have written appropriate for sharing? Here is a quick checklist of things to consider, all of which indicate that your plugin isn't suited for use by other people.

  • Do you have the right to release it? If you developed your plugin as part of some commercial endeavor (e.g., at work or on behalf of a client), you may not be legally allowed to make this code available to the rest of the world.

  • Does it include proprietary business logic?

  • Does it solve a problem that no one else is likely to encounter, for example, integrating with a custom-made database that nobody else could possibly have?

If you're still confident that your plugin is going to help the community at large, here's how to help other developers find and download your plugin.

Licensing

Before you release any code to a general audience, it is also important that you spend some time thinking about what license you release your source code under. The license you assign your plugin will affect the allowed uses of that code in other people's projects.

For instance, the General Public License (GPL) allows use of your source code by end-users, as long as any derived works are also released under the GPL. Essentially this means that to include a GPL component in your application, your whole application must become GPL-licensed if your application can be considered as derived in some way from the plugin.

In contrast, the MIT license places no such restrictions on reuse, and as such MIT-licensed software (Rails itself, for example) can be included in proprietary projects without difficulty.

There are many different open source software licenses, so it's well worth spending some time investigating them and selecting whichever feels most comfortable to you. The Open Source Initiative (OSI) maintains a comprehensive list of approved licenses at http://www.opensource.org/licenses/.


Hosting

In order for other people to download your files, they must be accessible on the Internet. Typically this means that you must either have your own server connected to the Internet with a static IP address or, more likely, an account with a Web hosting provider.

Caution: Hosting and script/plugin

As of the current version of Rails (1.1.4), the script/plugin install command will only work with Subversion repositories, made available either through its native protocols (svn://-style URLs), or via a Web server such as Apache using mod_svn (http://-style URLs).

Subversion Hosting

Many popular Web-hosting companies supply Subversion hosting as part of their package. These include (but are certainly not limited to) TextDrive, PLANET ARGON, DreamHost, and the RailsMachine hosting services (see Table 9.1 for the URLs for each of these). Each of these hosting companies will provide you with publicly accessible Subversion repositories from which you can share your code with the world.

Table 9.1. Some Subversion Hosting Providers

Service

URL

Dreamhost

http://www.dreamhost.com

OpenSVN

http://opensvn.csie.org

PLANET ARGON

http://www.planetargon.com

Rails Machine

http://www.railsmachine.com

RubyForge

http://www.rubyforge.org

TextDrive

http://www.textdrive.com

If you don't have access to a host with Subversion, a number of free Subversion hosts are available. Creating a project on RubyForge will give you access to a repository, along with a very helpful mailing list and issue tracking and collaboration tools without any charge. Alternatively, the OpenSVN project will give you a simple repository, along with an installation of Trac5 for issue tracking.

The many actions required to create a Subversion repository and making it available to the Internet are particular to each hosting provider. Please consult your hosting support for more information about how to set a public repository up.

Sharing Plugins Without Subversion

If for some reason you choose not to set up a publicly accessible Subversion repository, users will not be able to install your plugin using the script/plugin install command. Instead, it is probably simplest to compress your plugin files into a tar.gz or zip file, and share the URL to this file with others who may be interested in using your plugin. They will then have to extract your plugin into their vendor/plugins directory, from which point it will be available in Rails as normal.

It's important to remember that a plugin's install.rb file is only automatically invoked by the script/plugin install command. If you choose to package your plugin as a single redistibutable file, you will need to provide instructions and/or rake tasks for performing any setup your plugin may require.


Releasing Your Plugin

Once your plugin is available on the Internet, the next step is announcing it to the world. We have come across some aspects of this in Section 2 in the discussion about the script/plugin discover command.

Making Your Plugin Discoverable

At present, the script/plugin discover command "scrapes" the Plugin page on the official Rails wiki (http://wiki.rubyonrails.com/rails/pages/Plugins) for the location of new plugin repositories. To add your plugin, you must go to the wiki and edit this page, adding some information about the plugin and, most importantly, the URL from which it can be downloaded. It's a good idea to follow the formatting adopted by the majority of the plugin descriptions on that page. This prevents the page from becoming unreadable.

In order for your plugin repository to be recognized by the script/plugin discover command, it should include the string plugins somewhere in it:

http://www.your-server.com/svn/plugins/my_great_plugin
svn://svn.your-server.com/plugins/acts_as_clown

Once you have added the description and all-important repository link, your plugin should be available to anyone running script/plugin discover, and subsequently available by the list and install commands discussed in Section 2.

Adding to Plugin Repositories

While appending your plugin information to the Plugin page on the wiki is the only required step to take before your plugin becomes discoverable, it is likely at some point in the future that a dedicated plugin directory will be established. While no official repository currently exists, two candidates have recently emerged to fulfill this need:

http://www.agilewebdevelopment.com/plugins
http://plugins.radrails.org/

The RadRails IDE already uses its own plugins repository to help developers find and install plugins, and future versions of the plugin script will almost certainly use a repository like these rather than parsing a wiki page. For the time being, it's a good idea to add your plugin to these repositories so that any tools (such as RadRails) can take advantage of your code.

Announce It!

The final thing to do is to announce your new plugin on the Rails mailing list. Typically, you should prefix the subject with the abbreviation [ANN], and include the contents of your README file as a description of what the plugin does:

[View full width]
From: me@superl33tdevelopers.com
To: rails@lists.rubyonrails.org
Subject: [ANN] acts_as_hasselhoff plugin v1.0
Message:
Hi all,
I've just written the acts_as_hasselhoff plugin, and I thought I'd share it with the world
  . It's
wearing tight denim jeans and is ready to high-kick your models into action! For real.

== Description

  Makes ActiveRecord models run in unfeasibly slow motion. If the model contains the attribute
'kitt', it will be filled with random witty quips in a style of a British butler using a
before_save filter.

== Usage
  To use simply add the line 'acts_as_hasselhoff' to your ActiveRecord model, i.e.,

  class RichardJosephPaul < ActiveRecord::Base
    acts_as_hasselhoff :character => 'michael knight'
    # etc

I'm sure you get the idea.


Supporting the Plugin

Once your masterpiece is releasedto great applause, I have no doubtyou may find yourself on the receiving end of queries, bug reports, and suggestions for improving the plugin. Many of the Web-hosting companies mentioned earlier can either provide or support the use of issue tracking software such as Trac (http://www.edgewall.com/trac/) or Collaboa (http://www.collaboa.org), which can help you organize and manage bugs and improvements.

Issue management software can also provide users with a source of information regarding development progress and a convenient means to supply patches. An example of Collaboa supporting a plugin can be found at the Rails Engines development site (http://dev.rails-engines.org), as shown in Figure 9.1.

Figure 9.1. A Collaboa-powered issue tracking site, managing patches and bugs for the Rails Engines project

[View full size image]


Section 10. Conclusions

I hope that you've found this exploration of the Rails plugin mechanism both interesting and useful. Unfortunately, this brief short cut could never fully plumb the depths of what is possible using the plugin system (those possibilities are really only limited by your imagination, anyway). However, you should now be in a good position to start producing your own plugins for use in your own applications and those of other developers.

Ruby, Ruby, Ruby

122


Ruby, Ruby, Ruby

I would again strongly encourage you to become familiar with Ruby as a language in its own right, and in particular with the mechanisms Ruby employs to extend existing classes and objects using modules. There are already a wealth of examples available in the form of existing plugins.

Don't be afraid to examine code from these plugins when developing your own, since doing so will almost always give you new insights into different techniques for working with Ruby. The Ruby community itself is also an invaluable resource for this type of development. We are truly lucky to have such a wealth of helpful and knowledgeable Ruby developers available to answer questions about what I hope will soon become your favorite language (as it is mine).

So what are you waiting for?


About the Author

Dr. James Adam has been an active member of the Ruby community for more than four years, contributing to a number of projects including the FXRuby GUI toolkit and the Ruby-OpenGL bindings. He is a Rails early-adopter and contributor, working with the plugin system since its first appearance in Edge Rails. He has spoken at both conferences and user groups about using Rails plugins for effective team development.

James is the developer behind the Engines plugin, and works as part of a small internal development team in London producing real-world Rails applications within a corporate environment. He also helps run the London Ruby User Group, spreading the word about his favorite programming language to anyone who will listen.