1
h2. The Basics of Creating Rails Plugins
3
A Rails plugin is either an extension or a modification of the core framework. Plugins provide:
5
* a way for developers to share bleeding-edge ideas without hurting the stable code base
6
* a segmented architecture so that units of code can be fixed or updated on their own release schedule
7
* an outlet for the core developers so that they donāt have to include every cool new feature under the sun
9
After reading this guide you should be familiar with:
11
* Creating a plugin from scratch
12
* Writing and running tests for the plugin
13
* Storing models, views, controllers, helpers and even other plugins in your plugins
15
* Writing custom Rake tasks in your plugin
16
* Generating RDoc documentation for your plugin
17
* Avoiding common pitfalls with 'init.rb'
19
This guide describes how to build a test-driven plugin that will:
21
* Extend core ruby classes like Hash and String
22
* Add methods to ActiveRecord::Base in the tradition of the 'acts_as' plugins
23
* Add a view helper that can be used in erb templates
24
* Add a new generator that will generate a migration
25
* Add a custom generator command
26
* A custom route method that can be used in routes.rb
28
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 goodness. First, you need to get setup for development.
34
h4. Create the Basic Application
36
The examples in this guide require that you have a working rails application. To create a simple rails app execute:
42
script/generate scaffold bird name:string
47
Then navigate to http://localhost:3000/birds. Make sure you have a functioning rails app before continuing.
49
NOTE: The aforementioned instructions will work for sqlite3. For more detailed instructions on how to create a rails app for other databases see the API docs.
52
h4. Generate the Plugin Skeleton
54
Rails ships with a plugin generator which creates a basic plugin skeleton. Pass the plugin name, either 'CamelCased' or 'under_scored', as an argument. Pass +--with-generator+ to add an example generator also.
56
This creates a plugin in 'vendor/plugins' including an 'init.rb' and 'README' as well as standard 'lib', 'task', and 'test' directories.
60
./script/generate plugin yaffle
61
./script/generate plugin yaffle --with-generator
64
To get more detailed help on the plugin generator, type +./script/generate plugin+.
66
Later on this guide will describe how to work with generators, so go ahead and generate your plugin with the +--with-generator+ option now:
69
./script/generate plugin yaffle --with-generator
72
You should see the following output:
75
create vendor/plugins/yaffle/lib
76
create vendor/plugins/yaffle/tasks
77
create vendor/plugins/yaffle/test
78
create vendor/plugins/yaffle/README
79
create vendor/plugins/yaffle/MIT-LICENSE
80
create vendor/plugins/yaffle/Rakefile
81
create vendor/plugins/yaffle/init.rb
82
create vendor/plugins/yaffle/install.rb
83
create vendor/plugins/yaffle/uninstall.rb
84
create vendor/plugins/yaffle/lib/yaffle.rb
85
create vendor/plugins/yaffle/tasks/yaffle_tasks.rake
86
create vendor/plugins/yaffle/test/core_ext_test.rb
87
create vendor/plugins/yaffle/generators
88
create vendor/plugins/yaffle/generators/yaffle
89
create vendor/plugins/yaffle/generators/yaffle/templates
90
create vendor/plugins/yaffle/generators/yaffle/yaffle_generator.rb
91
create vendor/plugins/yaffle/generators/yaffle/USAGE
94
h4. Organize Your Files
96
To make it easy to organize your files and to make the plugin more compatible with GemPlugins, start out by altering your file system to look like this:
107
*vendor/plugins/yaffle/rails/init.rb*
113
Now you can add any 'require' statements to 'lib/yaffle.rb' and keep 'init.rb' clean.
117
In this guide you will learn how to test your plugin against multiple different database adapters using Active Record. To setup your plugin to allow for easy testing you'll need to add 3 files:
119
* A 'database.yml' file with all of your connection strings
120
* A 'schema.rb' file with your table definitions
121
* A test helper method that sets up the database
125
*vendor/plugins/yaffle/test/database.yml:*
130
:dbfile: vendor/plugins/yaffle/test/yaffle_plugin.sqlite.db
134
:dbfile: vendor/plugins/yaffle/test/yaffle_plugin.sqlite3.db
140
:database: yaffle_plugin_test
148
:database: yaffle_plugin_test
151
For this guide you'll need 2 tables/models, Hickwalls and Wickwalls, so add the following:
153
*vendor/plugins/yaffle/test/schema.rb:*
156
ActiveRecord::Schema.define(:version => 0) do
157
create_table :hickwalls, :force => true do |t|
159
t.string :last_squawk
160
t.datetime :last_squawked_at
162
create_table :wickwalls, :force => true do |t|
165
t.datetime :last_tweeted_at
167
create_table :woodpeckers, :force => true do |t|
173
*vendor/plugins/yaffle/test/test_helper.rb:*
176
ENV['RAILS_ENV'] = 'test'
177
ENV['RAILS_ROOT'] ||= File.dirname(__FILE__) + '/../../../..'
180
require File.expand_path(File.join(ENV['RAILS_ROOT'], 'config/environment.rb'))
183
config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
184
ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
186
db_adapter = ENV['DB']
188
# no db passed, try one of these fine config-free DBs before bombing.
194
rescue MissingSourceFile
198
rescue MissingSourceFile
203
raise "No DB Adapter selected. Pass the DB= option to pick one, or install Sqlite or Sqlite3."
206
ActiveRecord::Base.establish_connection(config[db_adapter])
207
load(File.dirname(__FILE__) + "/schema.rb")
208
require File.dirname(__FILE__) + '/../rails/init.rb'
212
Now whenever you write a test that requires the database, you can call 'load_schema'.
214
h4. Run the Plugin Tests
216
Once you have these files in place, you can write your first test to ensure that your plugin-testing setup is correct. By default rails generates a file in 'vendor/plugins/yaffle/test/yaffle_test.rb' with a sample test. Replace the contents of that file with:
218
*vendor/plugins/yaffle/test/yaffle_test.rb:*
221
require File.dirname(__FILE__) + '/test_helper.rb'
223
class YaffleTest < Test::Unit::TestCase
226
class Hickwall < ActiveRecord::Base
229
class Wickwall < ActiveRecord::Base
232
def test_schema_has_loaded_correctly
233
assert_equal [], Hickwall.all
234
assert_equal [], Wickwall.all
240
To run this, go to the plugin directory and run +rake+:
243
cd vendor/plugins/yaffle
247
You should see output like:
250
/opt/local/bin/ruby -Ilib:lib "/opt/local/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader.rb" "test/yaffle_test.rb"
251
create_table(:hickwalls, {:force=>true})
253
-- create_table(:wickwalls, {:force=>true})
255
-- initialize_schema_migrations_table()
257
-- assume_migrated_upto_version(0)
259
Loaded suite /opt/local/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader
262
Finished in 0.002236 seconds.
264
1 test, 1 assertion, 0 failures, 0 errors
267
By default the setup above runs your tests with sqlite or sqlite3. To run tests with one of the other connection strings specified in database.yml, pass the DB environment variable to rake:
276
Now you are ready to test-drive your plugin!
278
h3. Extending Core Classes
280
This section will explain how to add a method to String that will be available anywhere in your rails app.
282
In this example you will add a method to String named +to_squawk+. To begin, create a new test file with a few assertions:
284
* *vendor/plugins/yaffle/test/core_ext_test.rb*
287
require File.dirname(__FILE__) + '/test_helper.rb'
289
class CoreExtTest < Test::Unit::TestCase
290
def test_to_squawk_prepends_the_word_squawk
291
assert_equal "squawk! Hello World", "Hello World".to_squawk
296
Navigate to your plugin directory and run +rake test+:
299
cd vendor/plugins/yaffle
303
The test above should fail with the message:
307
test_to_squawk_prepends_the_word_squawk(CoreExtTest):
308
NoMethodError: undefined method `to_squawk' for "Hello World":String
309
./test/core_ext_test.rb:5:in `test_to_squawk_prepends_the_word_squawk'
312
Great - now you are ready to start development.
314
Then in 'lib/yaffle.rb' require 'lib/core_ext.rb':
316
* *vendor/plugins/yaffle/lib/yaffle.rb*
319
require "yaffle/core_ext"
322
Finally, create the 'core_ext.rb' file and add the 'to_squawk' method:
324
* *vendor/plugins/yaffle/lib/yaffle/core_ext.rb*
329
"squawk! #{self}".strip
334
To test that your method does what it says it does, run the unit tests with +rake+ from your plugin directory. To see this in action, fire up a console and start squawking:
338
>> "Hello World".to_squawk
339
=> "squawk! Hello World"
342
h4. Working with +init.rb+
344
When rails loads plugins it looks for the file named 'init.rb' or 'rails/init.rb'. However, when the plugin is initialized, 'init.rb' is invoked via +eval+ (not +require+) so it has slightly different behavior.
346
Under certain circumstances if you reopen classes or modules in 'init.rb' you may inadvertently create a new class, rather than reopening an existing class. A better alternative is to reopen the class in a different file, and require that file from +init.rb+, as shown above.
348
If you must reopen a class in +init.rb+ you can use +module_eval+ or +class_eval+ to avoid any issues:
350
* *vendor/plugins/yaffle/rails/init.rb*
354
def is_a_special_hash?
360
Another way is to explicitly define the top-level module space for all modules and classes, like +::Hash+:
362
* *vendor/plugins/yaffle/rails/init.rb*
366
def is_a_special_hash?
372
h3. Add an "acts_as" Method to Active Record
374
A common pattern in plugins is to add a method called 'acts_as_something' to models. In this case, you want to write a method called 'acts_as_yaffle' that adds a 'squawk' method to your models.
376
To begin, set up your files so that you have:
378
* *vendor/plugins/yaffle/test/acts_as_yaffle_test.rb*
381
require File.dirname(__FILE__) + '/test_helper.rb'
383
class ActsAsYaffleTest < Test::Unit::TestCase
387
* *vendor/plugins/yaffle/lib/yaffle.rb*
390
require 'yaffle/acts_as_yaffle'
393
* *vendor/plugins/yaffle/lib/yaffle/acts_as_yaffle.rb*
397
# your code will go here
401
Note that after requiring 'acts_as_yaffle' you also have to include it into ActiveRecord::Base so that your plugin methods will be available to the rails models.
403
One of the most common plugin patterns for 'acts_as_yaffle' plugins is to structure your file like so:
405
* *vendor/plugins/yaffle/lib/yaffle/acts_as_yaffle.rb*
409
def self.included(base)
410
base.send :extend, ClassMethods
414
# any method placed here will apply to classes, like Hickwall
415
def acts_as_something
416
send :include, InstanceMethods
420
module InstanceMethods
421
# any method placed here will apply to instaces, like @hickwall
426
With structure you can easily separate the methods that will be used for the class (like +Hickwall.some_method+) and the instance (like +@hickwell.some_method+).
428
h4. Add a Class Method
430
This plugin will expect that you've added a method to your model named 'last_squawk'. However, the plugin users might have already defined a method on their model named 'last_squawk' that they use for something else. This plugin will allow the name to be changed by adding a class method called 'yaffle_text_field'.
432
To start out, write a failing test that shows the behavior you'd like:
434
* *vendor/plugins/yaffle/test/acts_as_yaffle_test.rb*
437
require File.dirname(__FILE__) + '/test_helper.rb'
439
class Hickwall < ActiveRecord::Base
443
class Wickwall < ActiveRecord::Base
444
acts_as_yaffle :yaffle_text_field => :last_tweet
447
class ActsAsYaffleTest < Test::Unit::TestCase
450
def test_a_hickwalls_yaffle_text_field_should_be_last_squawk
451
assert_equal "last_squawk", Hickwall.yaffle_text_field
454
def test_a_wickwalls_yaffle_text_field_should_be_last_tweet
455
assert_equal "last_tweet", Wickwall.yaffle_text_field
460
To make these tests pass, you could modify your +acts_as_yaffle+ file like so:
462
* *vendor/plugins/yaffle/lib/yaffle/acts_as_yaffle.rb*
466
def self.included(base)
467
base.send :extend, ClassMethods
471
def acts_as_yaffle(options = {})
472
cattr_accessor :yaffle_text_field
473
self.yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s
478
ActiveRecord::Base.send :include, Yaffle
481
h4. Add an Instance Method
483
This plugin will add a method named 'squawk' to any Active Record objects that call 'acts_as_yaffle'. The 'squawk' method will simply set the value of one of the fields in the database.
485
To start out, write a failing test that shows the behavior you'd like:
487
* *vendor/plugins/yaffle/test/acts_as_yaffle_test.rb*
490
require File.dirname(__FILE__) + '/test_helper.rb'
492
class Hickwall < ActiveRecord::Base
496
class Wickwall < ActiveRecord::Base
497
acts_as_yaffle :yaffle_text_field => :last_tweet
500
class ActsAsYaffleTest < Test::Unit::TestCase
503
def test_a_hickwalls_yaffle_text_field_should_be_last_squawk
504
assert_equal "last_squawk", Hickwall.yaffle_text_field
507
def test_a_wickwalls_yaffle_text_field_should_be_last_tweet
508
assert_equal "last_tweet", Wickwall.yaffle_text_field
511
def test_hickwalls_squawk_should_populate_last_squawk
512
hickwall = Hickwall.new
513
hickwall.squawk("Hello World")
514
assert_equal "squawk! Hello World", hickwall.last_squawk
517
def test_wickwalls_squawk_should_populate_last_tweeted_at
518
wickwall = Wickwall.new
519
wickwall.squawk("Hello World")
520
assert_equal "squawk! Hello World", wickwall.last_tweet
525
Run this test to make sure the last two tests fail, then update 'acts_as_yaffle.rb' to look like this:
527
* *vendor/plugins/yaffle/lib/yaffle/acts_as_yaffle.rb*
531
def self.included(base)
532
base.send :extend, ClassMethods
536
def acts_as_yaffle(options = {})
537
cattr_accessor :yaffle_text_field
538
self.yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s
539
send :include, InstanceMethods
543
module InstanceMethods
545
write_attribute(self.class.yaffle_text_field, string.to_squawk)
550
ActiveRecord::Base.send :include, Yaffle
553
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 +send("#{self.class.yaffle_text_field}=", string.to_squawk)+.
557
This section describes how to add a model named 'Woodpecker' to your plugin that will behave the same as a model in your main app. When storing models, controllers, views and helpers in your plugin, it's customary to keep them in directories that match the rails directories. For this example, create a file structure like this:
560
vendor/plugins/yaffle/
566
| | | `-- woodpecker.rb
569
| | |-- acts_as_yaffle.rb
575
As always, start with a test:
577
* *vendor/plugins/yaffle/test/woodpecker_test.rb:*
580
require File.dirname(__FILE__) + '/test_helper.rb'
582
class WoodpeckerTest < Test::Unit::TestCase
586
assert_kind_of Woodpecker, Woodpecker.new
591
This is just a simple test to make sure the class is being loaded correctly. After watching it fail with +rake+, you can make it pass like so:
593
* *vendor/plugins/yaffle/lib/yaffle.rb:*
596
%w{ models }.each do |dir|
597
path = File.join(File.dirname(__FILE__), 'app', dir)
599
ActiveSupport::Dependencies.load_paths << path
600
ActiveSupport::Dependencies.load_once_paths.delete(path)
604
Adding directories to the load path makes them appear just like files in the the main app directory - except that they are only loaded once, so you have to restart the web server to see the changes in the browser. Removing directories from the 'load_once_paths' allow those changes to picked up as soon as you save the file - without having to restart the web server. This is particularly useful as you develop the plugin.
606
* *vendor/plugins/yaffle/lib/app/models/woodpecker.rb:*
609
class Woodpecker < ActiveRecord::Base
613
Finally, add the following to your plugin's 'schema.rb':
615
* *vendor/plugins/yaffle/test/schema.rb:*
618
create_table :woodpeckers, :force => true do |t|
623
Now your test should be passing, and you should be able to use the Woodpecker model from within your rails app, and any changes made to it are reflected immediately when running in development mode.
627
This section describes how to add a controller named 'woodpeckers' to your plugin that will behave the same as a controller in your main app. This is very similar to adding a model.
629
You can test your plugin's controller as you would test any other controller:
631
* *vendor/plugins/yaffle/test/woodpeckers_controller_test.rb:*
634
require File.dirname(__FILE__) + '/test_helper.rb'
635
require 'woodpeckers_controller'
636
require 'action_controller/test_process'
638
class WoodpeckersController; def rescue_action(e) raise e end; end
640
class WoodpeckersControllerTest < Test::Unit::TestCase
642
@controller = WoodpeckersController.new
643
@request = ActionController::TestRequest.new
644
@response = ActionController::TestResponse.new
646
ActionController::Routing::Routes.draw do |map|
647
map.resources :woodpeckers
653
assert_response :success
658
This is just a simple test to make sure the controller is being loaded correctly. After watching it fail with +rake+, you can make it pass like so:
660
* *vendor/plugins/yaffle/lib/yaffle.rb:*
663
%w{ models controllers }.each do |dir|
664
path = File.join(File.dirname(__FILE__), 'app', dir)
666
ActiveSupport::Dependencies.load_paths << path
667
ActiveSupport::Dependencies.load_once_paths.delete(path)
671
* *vendor/plugins/yaffle/lib/app/controllers/woodpeckers_controller.rb:*
674
class WoodpeckersController < ActionController::Base
677
render :text => "Squawk!"
683
Now your test should be passing, and you should be able to use the Woodpeckers controller in your app. If you add a route for the woodpeckers controller you can start up your server and go to http://localhost:3000/woodpeckers to see your controller in action.
687
This section describes how to add a helper named 'WoodpeckersHelper' to your plugin that will behave the same as a helper in your main app. This is very similar to adding a model and a controller.
689
You can test your plugin's helper as you would test any other helper:
691
* *vendor/plugins/yaffle/test/woodpeckers_helper_test.rb*
694
require File.dirname(__FILE__) + '/test_helper.rb'
695
include WoodpeckersHelper
697
class WoodpeckersHelperTest < Test::Unit::TestCase
699
assert_equal "Tweet! Hello", tweet("Hello")
704
This is just a simple test to make sure the helper is being loaded correctly. After watching it fail with +rake+, you can make it pass like so:
706
* *vendor/plugins/yaffle/lib/yaffle.rb:*
709
%w{ models controllers helpers }.each do |dir|
710
path = File.join(File.dirname(__FILE__), 'app', dir)
712
ActiveSupport::Dependencies.load_paths << path
713
ActiveSupport::Dependencies.load_once_paths.delete(path)
717
* *vendor/plugins/yaffle/lib/app/helpers/woodpeckers_helper.rb:*
720
module WoodpeckersHelper
729
Now your test should be passing, and you should be able to use the Woodpeckers helper in your app.
733
In a standard 'routes.rb' file you use routes like 'map.connect' or 'map.resources'. You can add your own custom routes from a plugin. This section will describe how to add a custom method called that can be called with 'map.yaffles'.
735
Testing routes from plugins is slightly different from testing routes in a standard rails app. To begin, add a test like this:
737
* *vendor/plugins/yaffle/test/routing_test.rb*
740
require "#{File.dirname(__FILE__)}/test_helper"
742
class RoutingTest < Test::Unit::TestCase
745
ActionController::Routing::Routes.draw do |map|
750
def test_yaffles_route
751
assert_recognition :get, "/yaffles", :controller => "yaffles_controller", :action => "index"
756
def assert_recognition(method, path, options)
757
result = ActionController::Routing::Routes.recognize_path(path, :method => method)
758
assert_equal options, result
763
Once you see the tests fail by running 'rake', you can make them pass with:
765
* *vendor/plugins/yaffle/lib/yaffle.rb*
768
require "yaffle/routing"
771
* *vendor/plugins/yaffle/lib/yaffle/routing.rb*
774
module Yaffle #:nodoc:
775
module Routing #:nodoc:
776
module MapperExtensions
778
@set.add_route("/yaffles", {:controller => "yaffles_controller", :action => "index"})
784
ActionController::Routing::RouteSet::Mapper.send :include, Yaffle::Routing::MapperExtensions
790
ActionController::Routing::Routes.draw do |map|
795
You can also see if your routes work by running +rake routes+ from your app directory.
799
Many plugins ship with generators. When you created the plugin above, you specified the +--with-generator+ option, so you already have the generator stubs in 'vendor/plugins/yaffle/generators/yaffle'.
801
Building generators is a complex topic unto itself and this section will cover one small aspect of generators: generating a simple text file.
803
h4. Testing Generators
805
Many rails plugin authors do not test their generators, however testing generators is quite simple. A typical generator test does the following:
807
* Creates a new fake rails root directory that will serve as destination
809
* Asserts that the correct files were generated
810
* Removes the fake rails root
812
This section will describe how to create a simple generator that adds a file. For the generator in this section, the test could look something like this:
814
* *vendor/plugins/yaffle/test/definition_generator_test.rb*
817
require File.dirname(__FILE__) + '/test_helper.rb'
818
require 'rails_generator'
819
require 'rails_generator/scripts/generate'
821
class DefinitionGeneratorTest < Test::Unit::TestCase
824
FileUtils.mkdir_p(fake_rails_root)
825
@original_files = file_list
829
FileUtils.rm_r(fake_rails_root)
832
def test_generates_correct_file_name
833
Rails::Generator::Scripts::Generate.new.run(["yaffle_definition"], :destination => fake_rails_root)
834
new_file = (file_list - @original_files).first
835
assert_equal "definition.txt", File.basename(new_file)
841
File.join(File.dirname(__FILE__), 'rails_root')
845
Dir.glob(File.join(fake_rails_root, "*"))
851
You can run 'rake' from the plugin directory to see this fail. Unless you are doing more advanced generator commands it typically suffices to just test the Generate script, and trust that rails will handle the Destroy and Update commands for you.
853
To make it pass, create the generator:
855
* *vendor/plugins/yaffle/generators/yaffle_definition/yaffle_definition_generator.rb*
858
class YaffleDefinitionGenerator < Rails::Generator::Base
861
m.file "definition.txt", "definition.txt"
869
If you plan to distribute your plugin, developers will expect at least a minimum of documentation. You can add simple documentation to the generator by updating the USAGE file.
871
Rails ships with several built-in generators. You can see all of the generators available to you by typing the following at the command line:
877
You should see something like this:
881
Plugins (vendor/plugins): yaffle_definition
882
Builtin: controller, integration_test, mailer, migration, model, observer, plugin, resource, scaffold, session_migration
885
When you run +script/generate yaffle_definition -h+ you should see the contents of your 'vendor/plugins/yaffle/generators/yaffle_definition/USAGE'.
887
For this plugin, update the USAGE file could look like this:
891
Adds a file with the definition of a Yaffle to the app's main directory
894
h3. Add a Custom Generator Command
896
You may have noticed above that you can used one of the built-in rails migration commands +migration_template+. If your plugin needs to add and remove lines of text from existing files you will need to write your own generator methods.
898
This section describes how you you can create your own commands to add and remove a line of text from 'routes.rb'. This example creates a very simple method that adds or removes a text file.
900
To start, add the following test method:
902
* *vendor/plugins/yaffle/test/generator_test.rb*
905
def test_generates_definition
906
Rails::Generator::Scripts::Generate.new.run(["yaffle", "bird"], :destination => fake_rails_root)
907
definition = File.read(File.join(fake_rails_root, "definition.txt"))
908
assert_match /Yaffle\:/, definition
912
Run +rake+ to watch the test fail, then make the test pass add the following:
914
* *vendor/plugins/yaffle/generators/yaffle/templates/definition.txt*
920
* *vendor/plugins/yaffle/lib/yaffle.rb*
923
require "yaffle/commands"
926
* *vendor/plugins/yaffle/lib/commands.rb*
929
require 'rails_generator'
930
require 'rails_generator/commands'
932
module Yaffle #:nodoc:
933
module Generator #:nodoc:
934
module Commands #:nodoc:
936
def yaffle_definition
937
file("definition.txt", "definition.txt")
942
def yaffle_definition
943
file("definition.txt", "definition.txt")
948
def yaffle_definition
949
file("definition.txt", "definition.txt")
954
def yaffle_definition
955
file("definition.txt", "definition.txt")
962
Rails::Generator::Commands::Create.send :include, Yaffle::Generator::Commands::Create
963
Rails::Generator::Commands::Destroy.send :include, Yaffle::Generator::Commands::Destroy
964
Rails::Generator::Commands::List.send :include, Yaffle::Generator::Commands::List
965
Rails::Generator::Commands::Update.send :include, Yaffle::Generator::Commands::Update
968
Finally, call your new method in the manifest:
970
* *vendor/plugins/yaffle/generators/yaffle/yaffle_generator.rb*
973
class YaffleGenerator < Rails::Generator::NamedBase
980
h3. Generator Commands
982
You may have noticed above that you can used one of the built-in rails migration commands +migration_template+. If your plugin needs to add and remove lines of text from existing files you will need to write your own generator methods.
984
This section describes how you you can create your own commands to add and remove a line of text from 'config/routes.rb'.
986
To start, add the following test method:
988
* *vendor/plugins/yaffle/test/route_generator_test.rb*
991
require File.dirname(__FILE__) + '/test_helper.rb'
992
require 'rails_generator'
993
require 'rails_generator/scripts/generate'
994
require 'rails_generator/scripts/destroy'
996
class RouteGeneratorTest < Test::Unit::TestCase
999
FileUtils.mkdir_p(File.join(fake_rails_root, "config"))
1003
FileUtils.rm_r(fake_rails_root)
1006
def test_generates_route
1008
ActionController::Routing::Routes.draw do |map|
1009
map.connect ':controller/:action/:id'
1010
map.connect ':controller/:action/:id.:format'
1013
File.open(routes_path, 'wb') {|f| f.write(content) }
1015
Rails::Generator::Scripts::Generate.new.run(["yaffle_route"], :destination => fake_rails_root)
1016
assert_match /map\.yaffles/, File.read(routes_path)
1019
def test_destroys_route
1021
ActionController::Routing::Routes.draw do |map|
1023
map.connect ':controller/:action/:id'
1024
map.connect ':controller/:action/:id.:format'
1027
File.open(routes_path, 'wb') {|f| f.write(content) }
1029
Rails::Generator::Scripts::Destroy.new.run(["yaffle_route"], :destination => fake_rails_root)
1030
assert_no_match /map\.yaffles/, File.read(routes_path)
1036
File.join(File.dirname(__FILE__), "rails_root")
1040
File.join(fake_rails_root, "config", "routes.rb")
1046
Run +rake+ to watch the test fail, then make the test pass add the following:
1048
* *vendor/plugins/yaffle/lib/yaffle.rb*
1051
require "yaffle/commands"
1054
* *vendor/plugins/yaffle/lib/yaffle/commands.rb*
1057
require 'rails_generator'
1058
require 'rails_generator/commands'
1060
module Yaffle #:nodoc:
1061
module Generator #:nodoc:
1062
module Commands #:nodoc:
1065
logger.route "map.yaffle"
1066
look_for = 'ActionController::Routing::Routes.draw do |map|'
1067
unless options[:pretend]
1068
gsub_file('config/routes.rb', /(#{Regexp.escape(look_for)})/mi){|match| "#{match}\n map.yaffles\n"}
1075
logger.route "map.yaffle"
1076
gsub_file 'config/routes.rb', /\n.+?map\.yaffles/mi, ''
1093
Rails::Generator::Commands::Create.send :include, Yaffle::Generator::Commands::Create
1094
Rails::Generator::Commands::Destroy.send :include, Yaffle::Generator::Commands::Destroy
1095
Rails::Generator::Commands::List.send :include, Yaffle::Generator::Commands::List
1096
Rails::Generator::Commands::Update.send :include, Yaffle::Generator::Commands::Update
1099
* *vendor/plugins/yaffle/generators/yaffle_route/yaffle_route_generator.rb*
1102
class YaffleRouteGenerator < Rails::Generator::Base
1111
To see this work, type:
1114
./script/generate yaffle_route
1115
./script/destroy yaffle_route
1118
NOTE: If you haven't set up the custom route from above, 'script/destroy' will fail and you'll have to remove it manually.
1122
If your plugin requires changes to the app's database you will likely want to somehow add migrations. Rails does not include any built-in support for calling migrations from plugins, but you can still make it easy for developers to call migrations from plugins.
1124
If you have a very simple needs, like creating a table that will always have the same name and columns, then you can use a more simple solution, like creating a custom rake task or method. If your migration needs user input to supply table names or other options, you probably want to opt for generating a migration.
1126
Let's say you have the following migration in your plugin:
1128
* *vendor/plugins/yaffle/lib/db/migrate/20081116181115_create_birdhouses.rb:*
1131
class CreateBirdhouses < ActiveRecord::Migration
1133
create_table :birdhouses, :force => true do |t|
1140
drop_table :birdhouses
1145
Here are a few possibilities for how to allow developers to use your plugin migrations:
1147
h4. Create a Custom Rake Task
1149
* *vendor/plugins/yaffle/tasks/yaffle_tasks.rake:*
1153
namespace :migrate do
1154
description = "Migrate the database through scripts in vendor/plugins/yaffle/lib/db/migrate"
1155
description << "and update db/schema.rb by invoking db:schema:dump."
1156
description << "Target specific version with VERSION=x. Turn off output with VERBOSE=false."
1159
task :yaffle => :environment do
1160
ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true
1161
ActiveRecord::Migrator.migrate("vendor/plugins/yaffle/lib/db/migrate/", ENV["VERSION"] ? ENV["VERSION"].to_i : nil)
1162
Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby
1168
h4. Call Migrations Directly
1170
* *vendor/plugins/yaffle/lib/yaffle.rb:*
1173
Dir.glob(File.join(File.dirname(__FILE__), "db", "migrate", "*")).each do |file|
1178
* *db/migrate/20081116181115_create_birdhouses.rb:*
1181
class CreateBirdhouses < ActiveRecord::Migration
1183
Yaffle::CreateBirdhouses.up
1187
Yaffle::CreateBirdhouses.down
1192
NOTE: several plugin frameworks such as Desert and Engines provide more advanced plugin functionality.
1194
h4. Generate Migrations
1196
Generating migrations has several advantages over other methods. Namely, you can allow other developers to more easily customize the migration. The flow looks like this:
1198
* call your script/generate script and pass in whatever options they need
1199
* examine the generated migration, adding/removing columns or other options as necessary
1201
This example will demonstrate how to use one of the built-in generator methods named 'migration_template' to create a migration file. Extending the rails migration generator requires a somewhat intimate knowledge of the migration generator internals, so it's best to write a test first:
1203
* *vendor/plugins/yaffle/test/yaffle_migration_generator_test.rb*
1206
require File.dirname(__FILE__) + '/test_helper.rb'
1207
require 'rails_generator'
1208
require 'rails_generator/scripts/generate'
1210
class MigrationGeneratorTest < Test::Unit::TestCase
1213
FileUtils.mkdir_p(fake_rails_root)
1214
@original_files = file_list
1218
ActiveRecord::Base.pluralize_table_names = true
1219
FileUtils.rm_r(fake_rails_root)
1222
def test_generates_correct_file_name
1223
Rails::Generator::Scripts::Generate.new.run(["yaffle_migration", "some_name_nobody_is_likely_to_ever_use_in_a_real_migration"],
1224
:destination => fake_rails_root)
1225
new_file = (file_list - @original_files).first
1226
assert_match /add_yaffle_fields_to_some_name_nobody_is_likely_to_ever_use_in_a_real_migrations/, new_file
1227
assert_match /add_column :some_name_nobody_is_likely_to_ever_use_in_a_real_migrations do |t|/, File.read(new_file)
1230
def test_pluralizes_properly
1231
ActiveRecord::Base.pluralize_table_names = false
1232
Rails::Generator::Scripts::Generate.new.run(["yaffle_migration", "some_name_nobody_is_likely_to_ever_use_in_a_real_migration"],
1233
:destination => fake_rails_root)
1234
new_file = (file_list - @original_files).first
1235
assert_match /add_yaffle_fields_to_some_name_nobody_is_likely_to_ever_use_in_a_real_migration/, new_file
1236
assert_match /add_column :some_name_nobody_is_likely_to_ever_use_in_a_real_migration do |t|/, File.read(new_file)
1241
File.join(File.dirname(__FILE__), 'rails_root')
1245
Dir.glob(File.join(fake_rails_root, "db", "migrate", "*"))
1251
NOTE: the migration generator checks to see if a migation already exists, and it's hard-coded to check the 'db/migrate' directory. As a result, if your test tries to generate a migration that already exists in the app, it will fail. The easy workaround is to make sure that the name you generate in your test is very unlikely to actually appear in the app.
1253
After running the test with 'rake' you can make it pass with:
1255
* *vendor/plugins/yaffle/generators/yaffle_migration/yaffle_migration_generator.rb*
1258
class YaffleMigrationGenerator < Rails::Generator::NamedBase
1261
m.migration_template 'migration:migration.rb', "db/migrate", {:assigns => yaffle_local_assigns,
1262
:migration_file_name => "add_yaffle_fields_to_#{custom_file_name}"
1268
def custom_file_name
1269
custom_name = class_name.underscore.downcase
1270
custom_name = custom_name.pluralize if ActiveRecord::Base.pluralize_table_names
1274
def yaffle_local_assigns
1275
returning(assigns = {}) do
1276
assigns[:migration_action] = "add"
1277
assigns[:class_name] = "add_yaffle_fields_to_#{custom_file_name}"
1278
assigns[:table_name] = custom_file_name
1279
assigns[:attributes] = [Rails::Generator::GeneratedAttribute.new("last_squawk", "string")]
1285
The generator creates a new file in 'db/migrate' with a timestamp and an 'add_column' statement. It reuses the built in rails +migration_template+ method, and reuses the built-in rails migration template.
1287
It's courteous to check to see if table names are being pluralized whenever you create a generator that needs to be aware of table names. This way people using your generator won't have to manually change the generated files if they've turned pluralization off.
1289
To run the generator, type the following at the command line:
1292
./script/generate yaffle_migration bird
1295
and you will see a new file:
1297
* *db/migrate/20080529225649_add_yaffle_fields_to_birds.rb*
1300
class AddYaffleFieldsToBirds < ActiveRecord::Migration
1302
add_column :birds, :last_squawk, :string
1306
remove_column :birds, :last_squawk
1313
When you created the plugin with the built-in rails generator, it generated a rake file for you in 'vendor/plugins/yaffle/tasks/yaffle_tasks.rake'. Any rake task you add here will be available to the app.
1315
Many plugin authors put all of their rake tasks into a common namespace that is the same as the plugin, like so:
1317
* *vendor/plugins/yaffle/tasks/yaffle_tasks.rake*
1320
namespace :yaffle do
1321
desc "Prints out the word 'Yaffle'"
1322
task :squawk => :environment do
1328
When you run +rake -T+ from your plugin you will see:
1331
yaffle:squawk # Prints out the word 'Yaffle'
1334
You can add as many files as you want in the tasks directory, and if they end in .rake Rails will pick them up.
1336
Note that tasks from 'vendor/plugins/yaffle/Rakefile' are not available to the main app.
1340
Turning your rails plugin into a gem is a simple and straightforward task. This section will cover how to turn your plugin into a gem. It will not cover how to distribute that gem.
1342
Historically rails plugins loaded the plugin's 'init.rb' file. In fact some plugins contain all of their code in that one file. To be compatible with plugins, 'init.rb' was moved to 'rails/init.rb'.
1344
It's common practice to put any developer-centric rake tasks (such as tests, rdoc and gem package tasks) in 'Rakefile'. A rake task that packages the gem might look like this:
1346
* *vendor/plugins/yaffle/Rakefile:*
1349
PKG_FILES = FileList[
1358
spec = Gem::Specification.new do |s|
1361
s.author = "Gleeful Yaffler"
1362
s.email = "yaffle@example.com"
1363
s.homepage = "http://yafflers.example.com/"
1364
s.platform = Gem::Platform::RUBY
1365
s.summary = "Sharing Yaffle Goodness"
1366
s.files = PKG_FILES.to_a
1367
s.require_path = "lib"
1369
s.extra_rdoc_files = ["README"]
1372
desc 'Turn this plugin into a gem.'
1373
Rake::GemPackageTask.new(spec) do |pkg|
1378
To build and install the gem locally, run the following commands:
1381
cd vendor/plugins/yaffle
1383
sudo gem install pkg/yaffle-0.0.1.gem
1386
To test this, create a new rails app, add 'config.gem "yaffle"' to environment.rb and all of your plugin's functionality will be available to you.
1388
h3. RDoc Documentation
1390
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.
1392
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:
1396
* How to add the functionality to the app (several examples of common use cases)
1397
* Warning, gotchas or tips that might help save users time
1399
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 part of the public api.
1401
Once your comments are good to go, navigate to your plugin directory and run:
1409
If you prefer to use RSpec instead of Test::Unit, you may be interested in the "RSpec Plugin Generator":http://github.com/pat-maddox/rspec-plugin-generator/tree/master.
1413
* http://nubyonrails.com/articles/the-complete-guide-to-rails-plugins-part-i
1414
* http://nubyonrails.com/articles/the-complete-guide-to-rails-plugins-part-ii
1415
* http://github.com/technoweenie/attachment_fu/tree/master
1416
* http://daddy.platte.name/2007/05/rails-plugins-keep-initrb-thin.html
1417
* http://www.mbleigh.com/2008/6/11/gemplugins-a-brief-introduction-to-the-future-of-rails-plugins
1418
* http://weblog.jamisbuck.org/2006/10/26/monkey-patching-rails-extending-routes-2.
1420
h4. Contents of +lib/yaffle.rb+
1422
* *vendor/plugins/yaffle/lib/yaffle.rb:*
1425
require "yaffle/core_ext"
1426
require "yaffle/acts_as_yaffle"
1427
require "yaffle/commands"
1428
require "yaffle/routing"
1430
%w{ models controllers helpers }.each do |dir|
1431
path = File.join(File.dirname(__FILE__), 'app', dir)
1433
ActiveSupport::Dependencies.load_paths << path
1434
ActiveSupport::Dependencies.load_once_paths.delete(path)
1438
# Dir.glob(File.join(File.dirname(__FILE__), "db", "migrate", "*")).each do |file|
1443
h4. Final Plugin Directory Structure
1445
The final plugin should have a directory structure that looks something like this:
1452
| |-- yaffle_definition
1455
| | | `-- definition.txt
1456
| | `-- yaffle_definition_generator.rb
1457
| |-- yaffle_migration
1460
| | `-- yaffle_migration_generator.rb
1464
| `-- yaffle_route_generator.rb
1469
| | | `-- woodpeckers_controller.rb
1471
| | | `-- woodpeckers_helper.rb
1473
| | `-- woodpecker.rb
1476
| | `-- 20081116181115_create_birdhouses.rb
1478
| | |-- acts_as_yaffle.rb
1484
| `-- yaffle-0.0.1.gem
1488
| `-- yaffle_tasks.rake
1490
| |-- acts_as_yaffle_test.rb
1491
| |-- core_ext_test.rb
1494
| |-- definition_generator_test.rb
1495
| |-- migration_generator_test.rb
1496
| |-- route_generator_test.rb
1497
| |-- routes_test.rb
1499
| |-- test_helper.rb
1500
| |-- woodpecker_test.rb
1501
| |-- woodpeckers_controller_test.rb
1502
| |-- wookpeckers_helper_test.rb
1503
| |-- yaffle_plugin.sqlite3.db
1504
| `-- yaffle_test.rb
1510
"Lighthouse ticket":http://rails.lighthouseapp.com/projects/16213/tickets/32-update-plugins-guide
1512
* November 17, 2008: Major revision by Jeff Dean