2009-02-05 20:57:02 -05:00
h2. The Basics of Creating Rails Plugins
A Rails plugin is either an extension or a modification of the core framework. Plugins provide:
* a way for developers to share bleeding-edge ideas without hurting the stable code base
* a segmented architecture so that units of code can be fixed or updated on their own release schedule
* an outlet for the core developers so that they don’ t have to include every cool new feature under the sun
After reading this guide you should be familiar with:
* Creating a plugin from scratch
* Writing and running tests for the plugin
This guide describes how to build a test-driven plugin that will:
* Extend core ruby classes like Hash and String
* Add methods to ActiveRecord::Base in the tradition of the 'acts_as' plugins
2011-02-13 16:21:17 -05:00
* Give you information about where to put generators in your plugin.
2009-02-05 20:57:02 -05:00
2011-04-13 20:49:14 -04:00
For the purpose of this guide pretend for a moment that you are an avid bird watcher.
Your favorite bird is the Yaffle, and you want to create a plugin that allows other developers to share in the Yaffle
2011-02-13 16:21:17 -05:00
goodness.
2009-02-05 20:57:02 -05:00
endprologue.
h3. Setup
2011-05-22 19:07:47 -04:00
Before you continue, take a moment to decide if your new plugin will be potentially shared across different Rails applications.
2009-02-05 20:57:02 -05:00
2011-05-21 20:33:28 -04:00
* If your plugin is specific to your application, your new plugin will be a _vendored plugin_.
* If you think your plugin may be used across applications, build it as a _gemified plugin_.
h4. Either generate a vendored plugin...
2011-05-22 19:07:47 -04:00
Use the +rails generate plugin+ command in your Rails root directory
2011-05-21 20:33:28 -04:00
to create a new plugin that will live in the +vendor/plugins+
directory. See usage and options by asking for help:
2009-02-05 20:57:02 -05:00
2010-04-04 06:43:19 -04:00
<shell>
2011-07-01 10:58:04 -04:00
$ rails generate plugin --help
2010-04-04 06:43:19 -04:00
</shell>
2009-02-05 20:57:02 -05:00
2011-05-21 20:33:28 -04:00
h4. Or generate a gemified plugin.
2009-02-05 20:57:02 -05:00
2011-05-22 19:07:47 -04:00
Writing your Rails plugin as a gem, rather than as a vendored plugin,
2011-05-21 20:33:28 -04:00
lets you share your plugin across different rails applications using
2011-05-22 19:07:47 -04:00
RubyGems and Bundler.
2009-02-05 20:57:02 -05:00
2011-05-21 20:33:28 -04:00
Rails 3.1 ships with a +rails plugin new+ command which creates a
skeleton for developing any kind of Rails extension with the ability
to run integration tests using a dummy Rails application. See usage
and options by asking for help:
2009-02-05 20:57:02 -05:00
2010-04-04 06:43:19 -04:00
<shell>
2011-05-21 20:33:28 -04:00
$ rails plugin --help
2010-04-04 06:43:19 -04:00
</shell>
2009-02-05 20:57:02 -05:00
2011-02-13 16:21:17 -05:00
h3. Testing your newly generated plugin
2009-02-05 20:57:02 -05:00
2011-02-13 16:21:17 -05:00
You can navigate to the directory that contains the plugin, run the +bundle install+ command
and run the one generated test using the +rake+ command.
2009-02-05 20:57:02 -05:00
2011-02-13 16:21:17 -05:00
You should see:
2009-02-05 20:57:02 -05:00
<shell>
2011-02-13 16:21:17 -05:00
2 tests, 2 assertions, 0 failures, 0 errors, 0 skips
2009-02-05 20:57:02 -05:00
</shell>
2011-02-13 16:21:17 -05:00
This will tell you that everything got generated properly and you are ready to start adding functionality.
2009-02-05 20:57:02 -05:00
2009-03-16 07:28:36 -04:00
h3. Extending Core Classes
2009-02-05 20:57:02 -05:00
2011-02-13 16:21:17 -05:00
This section will explain how to add a method to String that will be available anywhere in your rails application.
2009-02-05 20:57:02 -05:00
2011-04-14 19:37:12 -04:00
In this example you will add a method to String named +to_squawk+. To begin, create a new test file with a few assertions:
2009-02-05 20:57:02 -05:00
<ruby>
2011-02-13 16:21:17 -05:00
# yaffle/test/core_ext_test.rb
2010-04-30 17:19:44 -04:00
2011-02-13 16:21:17 -05:00
require 'test_helper'
2009-02-05 20:57:02 -05:00
class CoreExtTest < Test::Unit::TestCase
def test_to_squawk_prepends_the_word_squawk
assert_equal "squawk! Hello World", "Hello World".to_squawk
end
end
</ruby>
2011-07-15 22:40:17 -04:00
Run +rake+ to run the test. This test should fail because we haven't implemented the +to_squawk+ method:
2009-02-05 20:57:02 -05:00
<shell>
2011-02-13 16:21:17 -05:00
1) Error:
test_to_squawk_prepends_the_word_squawk(CoreExtTest):
NoMethodError: undefined method `to_squawk' for "Hello World":String
test/core_ext_test.rb:5:in `test_to_squawk_prepends_the_word_squawk'
2009-02-05 20:57:02 -05:00
</shell>
Great - now you are ready to start development.
2010-06-13 18:50:38 -04:00
Then in +lib/yaffle.rb+ require +lib/core_ext+:
2009-02-05 20:57:02 -05:00
<ruby>
2011-02-13 16:21:17 -05:00
# yaffle/lib/yaffle.rb
2010-04-30 17:19:44 -04:00
2009-02-05 20:57:02 -05:00
require "yaffle/core_ext"
2011-02-13 16:21:17 -05:00
module Yaffle
end
2009-02-05 20:57:02 -05:00
</ruby>
2010-04-30 17:19:44 -04:00
Finally, create the +core_ext.rb+ file and add the +to_squawk+ method:
2009-02-05 20:57:02 -05:00
<ruby>
2011-02-13 16:21:17 -05:00
# yaffle/lib/yaffle/core_ext.rb
2010-08-14 01:13:00 -04:00
2009-02-05 20:57:02 -05:00
String.class_eval do
def to_squawk
"squawk! #{self}".strip
end
end
</ruby>
2011-02-13 16:21:17 -05:00
To test that your method does what it says it does, run the unit tests with +rake+ from your plugin directory.
<shell>
3 tests, 3 assertions, 0 failures, 0 errors, 0 skips
</shell>
To see this in action, change to the test/dummy directory, fire up a console and start squawking:
2009-02-05 20:57:02 -05:00
<shell>
2010-02-06 11:18:10 -05:00
$ rails console
2009-02-05 20:57:02 -05:00
>> "Hello World".to_squawk
=> "squawk! Hello World"
</shell>
2011-02-13 16:21:17 -05:00
h3. Add an "acts_as" Method to Active Record
2010-02-27 18:14:48 -05:00
2011-04-14 19:37:12 -04:00
A common pattern in plugins is to add a method called 'acts_as_something' to models. In this case, you
2011-02-13 16:21:17 -05:00
want to write a method called 'acts_as_yaffle' that adds a 'squawk' method to your Active Record models.
2009-02-05 20:57:02 -05:00
2011-02-13 16:21:17 -05:00
To begin, set up your files so that you have:
2009-02-05 20:57:02 -05:00
<ruby>
2011-02-13 16:21:17 -05:00
# yaffle/test/acts_as_yaffle_test.rb
2010-04-30 17:19:44 -04:00
2011-02-13 16:21:17 -05:00
require 'test_helper'
class ActsAsYaffleTest < Test::Unit::TestCase
2009-02-05 20:57:02 -05:00
end
</ruby>
<ruby>
2011-02-13 16:21:17 -05:00
# yaffle/lib/yaffle.rb
2010-04-30 17:19:44 -04:00
2011-02-13 16:21:17 -05:00
require "yaffle/core_ext"
require 'yaffle/acts_as_yaffle'
module Yaffle
2009-02-05 20:57:02 -05:00
end
</ruby>
<ruby>
2011-02-13 16:21:17 -05:00
# yaffle/lib/yaffle/acts_as_yaffle.rb
2009-02-05 20:57:02 -05:00
2011-02-13 16:21:17 -05:00
module Yaffle
module ActsAsYaffle
# your code will go here
end
2009-02-05 20:57:02 -05:00
end
</ruby>
2011-02-13 16:21:17 -05:00
h4. Add a Class Method
2009-02-05 20:57:02 -05:00
2011-04-14 19:37:12 -04:00
This plugin will expect that you've added a method to your model named 'last_squawk'. However, the
2011-04-13 20:49:14 -04:00
plugin users might have already defined a method on their model named 'last_squawk' that they use
2011-04-14 19:37:12 -04:00
for something else. This plugin will allow the name to be changed by adding a class method called 'yaffle_text_field'.
2009-02-05 20:57:02 -05:00
2011-02-13 16:21:17 -05:00
To start out, write a failing test that shows the behavior you'd like:
2009-02-05 20:57:02 -05:00
<ruby>
2011-02-13 16:21:17 -05:00
# yaffle/test/acts_as_yaffle_test.rb
2009-02-05 20:57:02 -05:00
2011-02-13 16:21:17 -05:00
require 'test_helper'
2009-02-05 20:57:02 -05:00
2011-02-13 16:21:17 -05:00
class ActsAsYaffleTest < Test::Unit::TestCase
2009-02-05 20:57:02 -05:00
2011-02-13 16:21:17 -05:00
def test_a_hickwalls_yaffle_text_field_should_be_last_squawk
assert_equal "last_squawk", Hickwall.yaffle_text_field
2009-02-05 20:57:02 -05:00
end
2011-02-13 16:21:17 -05:00
def test_a_wickwalls_yaffle_text_field_should_be_last_tweet
assert_equal "last_tweet", Wickwall.yaffle_text_field
2009-02-05 20:57:02 -05:00
end
end
</ruby>
2011-02-13 16:21:17 -05:00
When you run +rake+, you should see the following:
2009-02-05 20:57:02 -05:00
2011-02-13 16:21:17 -05:00
<shell>
1) Error:
test_a_hickwalls_yaffle_text_field_should_be_last_squawk(ActsAsYaffleTest):
NameError: uninitialized constant ActsAsYaffleTest::Hickwall
test/acts_as_yaffle_test.rb:6:in `test_a_hickwalls_yaffle_text_field_should_be_last_squawk'
2009-02-05 20:57:02 -05:00
2011-02-13 16:21:17 -05:00
2) Error:
test_a_wickwalls_yaffle_text_field_should_be_last_tweet(ActsAsYaffleTest):
NameError: uninitialized constant ActsAsYaffleTest::Wickwall
test/acts_as_yaffle_test.rb:10:in `test_a_wickwalls_yaffle_text_field_should_be_last_tweet'
2009-02-05 20:57:02 -05:00
2011-02-13 16:21:17 -05:00
5 tests, 3 assertions, 0 failures, 2 errors, 0 skips
</shell>
This tells us that we don't have the necessary models (Hickwall and Wickwall) that we are trying to test.
2011-04-13 20:49:14 -04:00
We can easily generate these models in our "dummy" Rails application by running the following commands from the
2011-02-13 16:21:17 -05:00
test/dummy directory:
<shell>
2011-02-24 16:29:37 -05:00
$ cd test/dummy
2011-07-15 22:40:17 -04:00
$ rails generate model Hickwall last_squawk:string
$ rails generate model Wickwall last_squawk:string last_tweet:string
2011-02-13 16:21:17 -05:00
</shell>
Now you can create the necessary database tables in your testing database by navigating to your dummy app
2011-04-14 19:37:12 -04:00
and migrating the database. First
2011-02-13 16:21:17 -05:00
<shell>
2011-02-24 16:29:37 -05:00
$ cd test/dummy
$ rake db:migrate
$ rake db:test:prepare
2011-02-13 16:21:17 -05:00
</shell>
2009-02-05 20:57:02 -05:00
2011-02-13 16:21:17 -05:00
While you are here, change the Hickwall and Wickwall models so that they know that they are supposed to act
like yaffles.
2009-02-05 20:57:02 -05:00
<ruby>
2011-02-13 16:21:17 -05:00
# test/dummy/app/models/hickwall.rb
2009-02-05 20:57:02 -05:00
class Hickwall < ActiveRecord::Base
acts_as_yaffle
end
2011-02-13 16:21:17 -05:00
# test/dummy/app/models/wickwall.rb
2009-02-05 20:57:02 -05:00
class Wickwall < ActiveRecord::Base
acts_as_yaffle :yaffle_text_field => :last_tweet
end
2011-02-13 16:21:17 -05:00
</ruby>
2009-02-05 20:57:02 -05:00
2011-02-13 16:21:17 -05:00
We will also add code to define the acts_as_yaffle method.
2009-02-05 20:57:02 -05:00
2011-02-13 16:21:17 -05:00
<ruby>
# yaffle/lib/yaffle/acts_as_yaffle.rb
module Yaffle
module ActsAsYaffle
extend ActiveSupport::Concern
included do
end
module ClassMethods
def acts_as_yaffle(options = {})
# your code will go here
end
end
2009-02-05 20:57:02 -05:00
end
end
2011-02-13 16:21:17 -05:00
ActiveRecord::Base.send :include, Yaffle::ActsAsYaffle
2009-02-05 20:57:02 -05:00
</ruby>
2011-02-13 16:21:17 -05:00
You can then return to the root directory (+cd ../..+) of your plugin and rerun the tests using +rake+.
<shell>
1) Error:
test_a_hickwalls_yaffle_text_field_should_be_last_squawk(ActsAsYaffleTest):
NoMethodError: undefined method `yaffle_text_field' for #<Class:0x000001016661b8>
/Users/xxx/.rvm/gems/ruby-1.9.2-p136@xxx/gems/activerecord-3.0.3/lib/active_record/base.rb:1008:in `method_missing'
test/acts_as_yaffle_test.rb:5:in `test_a_hickwalls_yaffle_text_field_should_be_last_squawk'
2) Error:
test_a_wickwalls_yaffle_text_field_should_be_last_tweet(ActsAsYaffleTest):
NoMethodError: undefined method `yaffle_text_field' for #<Class:0x00000101653748>
Users/xxx/.rvm/gems/ruby-1.9.2-p136@xxx/gems/activerecord-3.0.3/lib/active_record/base.rb:1008:in `method_missing'
test/acts_as_yaffle_test.rb:9:in `test_a_wickwalls_yaffle_text_field_should_be_last_tweet'
5 tests, 3 assertions, 0 failures, 2 errors, 0 skips
</shell>
2009-02-05 20:57:02 -05:00
2011-08-26 11:34:57 -04:00
Getting closer... Now we will implement the code of the acts_as_yaffle method to make the tests pass.
2009-02-05 20:57:02 -05:00
<ruby>
2011-02-13 16:21:17 -05:00
# yaffle/lib/yaffle/acts_as_yaffle.rb
2009-02-05 20:57:02 -05:00
module Yaffle
2011-02-13 16:21:17 -05:00
module ActsAsYaffle
extend ActiveSupport::Concern
included do
end
2009-02-05 20:57:02 -05:00
2011-02-13 16:21:17 -05:00
module ClassMethods
def acts_as_yaffle(options = {})
cattr_accessor :yaffle_text_field
self.yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s
end
2009-02-05 20:57:02 -05:00
end
end
end
2011-02-13 16:21:17 -05:00
ActiveRecord::Base.send :include, Yaffle::ActsAsYaffle
2009-02-05 20:57:02 -05:00
</ruby>
2011-02-13 16:21:17 -05:00
When you run +rake+ you should see the tests all pass:
<shell>
5 tests, 5 assertions, 0 failures, 0 errors, 0 skips
</shell>
2009-03-16 07:28:36 -04:00
h4. Add an Instance Method
2009-02-05 20:57:02 -05:00
2011-08-26 11:34:57 -04:00
This plugin will add a method named 'squawk' to any Active Record object that calls 'acts_as_yaffle'. The 'squawk'
2011-02-13 16:21:17 -05:00
method will simply set the value of one of the fields in the database.
2009-02-05 20:57:02 -05:00
To start out, write a failing test that shows the behavior you'd like:
<ruby>
2011-02-13 16:21:17 -05:00
# yaffle/test/acts_as_yaffle_test.rb
require 'test_helper'
2009-02-05 20:57:02 -05:00
class ActsAsYaffleTest < Test::Unit::TestCase
def test_a_hickwalls_yaffle_text_field_should_be_last_squawk
assert_equal "last_squawk", Hickwall.yaffle_text_field
end
def test_a_wickwalls_yaffle_text_field_should_be_last_tweet
assert_equal "last_tweet", Wickwall.yaffle_text_field
end
def test_hickwalls_squawk_should_populate_last_squawk
hickwall = Hickwall.new
hickwall.squawk("Hello World")
assert_equal "squawk! Hello World", hickwall.last_squawk
end
2011-08-26 11:34:57 -04:00
def test_wickwalls_squawk_should_populate_last_tweet
2009-02-05 20:57:02 -05:00
wickwall = Wickwall.new
wickwall.squawk("Hello World")
assert_equal "squawk! Hello World", wickwall.last_tweet
end
end
</ruby>
2011-08-26 11:34:57 -04:00
Run the test to make sure the last two tests fail with an error that contains "NoMethodError: undefined method `squawk'",
2011-02-13 16:21:17 -05:00
then update 'acts_as_yaffle.rb' to look like this:
2009-02-05 20:57:02 -05:00
<ruby>
2011-02-13 16:21:17 -05:00
# yaffle/lib/yaffle/acts_as_yaffle.rb
2009-02-05 20:57:02 -05:00
module Yaffle
2011-02-13 16:21:17 -05:00
module ActsAsYaffle
extend ActiveSupport::Concern
2009-02-05 20:57:02 -05:00
2011-02-13 16:21:17 -05:00
included do
end
module ClassMethods
def acts_as_yaffle(options = {})
cattr_accessor :yaffle_text_field
self.yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s
end
2009-02-05 20:57:02 -05:00
end
def squawk(string)
write_attribute(self.class.yaffle_text_field, string.to_squawk)
end
2011-02-13 16:21:17 -05:00
2009-02-05 20:57:02 -05:00
end
end
2011-02-13 16:21:17 -05:00
ActiveRecord::Base.send :include, Yaffle::ActsAsYaffle
2009-02-05 20:57:02 -05:00
</ruby>
2011-02-13 16:21:17 -05:00
Run +rake+ one final time and you should see:
2011-08-16 22:48:01 -04:00
2009-02-05 20:57:02 -05:00
<shell>
2011-02-13 16:21:17 -05:00
7 tests, 7 assertions, 0 failures, 0 errors, 0 skips
2009-02-05 20:57:02 -05:00
</shell>
2011-05-23 19:09:58 -04:00
NOTE: The use of +write_attribute+ to write to the field in model is just one example of how a plugin can interact with the model, and will not always be the right method to use. For example, you could also use <tt>send("#{self.class.yaffle_text_field}=", string.to_squawk)</tt>.
2009-02-05 20:57:02 -05:00
2011-02-13 16:21:17 -05:00
h3. Generators
2009-02-05 20:57:02 -05:00
2011-04-14 19:37:12 -04:00
Generators can be included in your gem simply by creating them in a lib/generators directory of your plugin. More information about
2011-02-13 16:21:17 -05:00
the creation of generators can be found in the "Generators Guide":generators.html
2009-02-05 20:57:02 -05:00
2011-02-13 16:21:17 -05:00
h3. Publishing your Gem
2009-02-05 20:57:02 -05:00
2011-08-26 11:34:57 -04:00
Gem plugins currently in development can easily be shared from any Git repository. To share the Yaffle gem with others, simply
commit the code to a Git repository (like Github) and add a line to the Gemfile of the application in question:
2009-02-05 20:57:02 -05:00
2011-03-10 10:08:30 -05:00
<ruby>
2011-08-26 11:34:57 -04:00
gem 'yaffle', :git => 'git://github.com/yaffle_watcher/yaffle.git'
2011-03-10 10:08:30 -05:00
</ruby>
2009-02-05 20:57:02 -05:00
2011-02-13 16:21:17 -05:00
After running +bundle install+, your gem functionality will be available to the application.
2009-02-05 20:57:02 -05:00
2011-02-13 16:21:17 -05:00
When the gem is ready to be shared as a formal release, it can be published to "RubyGems":http://www.rubygems.org.
For more information about publishing gems to RubyGems, see: "http://blog.thepete.net/2010/11/creating-and-publishing-your-first-ruby.html":http://blog.thepete.net/2010/11/creating-and-publishing-your-first-ruby.html
2009-02-05 20:57:02 -05:00
2011-02-13 16:21:17 -05:00
h3. Non-Gem Plugins
2009-02-05 20:57:02 -05:00
2011-04-14 19:37:12 -04:00
Non-gem plugins are useful for functionality that won't be shared with another project. Keeping your custom functionality in the
2011-02-13 16:21:17 -05:00
vendor/plugins directory un-clutters the rest of the application.
2009-02-05 20:57:02 -05:00
2011-02-13 16:21:17 -05:00
Move the directory that you created for the gem based plugin into the vendor/plugins directory of a generated Rails application, create a vendor/plugins/yaffle/init.rb file that contains "require 'yaffle'" and everything will still work.
2009-02-05 20:57:02 -05:00
<ruby>
2011-02-13 16:21:17 -05:00
# yaffle/init.rb
2009-02-05 20:57:02 -05:00
2011-02-13 16:21:17 -05:00
require 'yaffle'
2009-02-05 20:57:02 -05:00
</ruby>
2011-04-14 19:37:12 -04:00
You can test this by changing to the Rails application that you added the plugin to and starting a rails console. Once in the
2011-08-26 11:34:57 -04:00
console we can check to see if the String has an instance method to_squawk:
2011-08-16 22:48:01 -04:00
2011-02-13 16:21:17 -05:00
<shell>
2011-02-24 16:29:37 -05:00
$ cd my_app
$ rails console
2011-08-26 11:34:57 -04:00
$ "Rails plugins are easy!".to_squawk
2011-02-13 16:21:17 -05:00
</shell>
2009-02-05 20:57:02 -05:00
2011-02-13 16:21:17 -05:00
You can also remove the .gemspec, Gemfile and Gemfile.lock files as they will no longer be needed.
2009-02-05 20:57:02 -05:00
2011-02-13 16:21:17 -05:00
h3. RDoc Documentation
2009-02-05 20:57:02 -05:00
2011-04-14 19:37:12 -04:00
Once your plugin is stable and you are ready to deploy do everyone else a favor and document it! Luckily, writing documentation for your plugin is easy.
2009-02-05 20:57:02 -05:00
2011-02-13 16:21:17 -05:00
The first step is to update the README file with detailed information about how to use your plugin. A few key things to include are:
2009-02-05 20:57:02 -05:00
2011-02-13 16:21:17 -05:00
* Your name
* How to install
* How to add the functionality to the app (several examples of common use cases)
2011-08-26 11:34:57 -04:00
* Warnings, gotchas or tips that might help users and save them time
2009-02-05 20:57:02 -05:00
2011-08-26 11:34:57 -04:00
Once your README is solid, go through and add rdoc comments to all of the methods that developers will use. It's also customary to add '#:nodoc:' comments to those parts of the code that are not included in the public api.
2009-02-05 20:57:02 -05:00
2011-02-13 16:21:17 -05:00
Once your comments are good to go, navigate to your plugin directory and run:
2009-02-05 20:57:02 -05:00
2011-02-13 16:21:17 -05:00
<shell>
2011-02-24 16:29:37 -05:00
$ rake rdoc
2011-02-13 16:21:17 -05:00
</shell>
2009-02-05 20:57:02 -05:00
2011-02-13 16:21:17 -05:00
h4. References
2009-02-05 20:57:02 -05:00
2011-02-13 16:21:17 -05:00
* "Developing a RubyGem using Bundler":https://github.com/radar/guides/blob/master/gem-development.md
* "Using Gemspecs As Intended":http://yehudakatz.com/2010/04/02/using-gemspecs-as-intended/
* "Gemspec Reference":http://docs.rubygems.org/read/chapter/20
2010-12-24 14:34:10 -05:00
* "GemPlugins":http://www.mbleigh.com/2008/06/11/gemplugins-a-brief-introduction-to-the-future-of-rails-plugins
2011-02-13 16:21:17 -05:00
* "Keeping init.rb thin":http://daddy.platte.name/2007/05/rails-plugins-keep-initrb-thin.html