~michaelforrest/use-case-mapper/trunk

« back to all changes in this revision

Viewing changes to vendor/rails/railties/guides/source/plugins.textile

  • Committer: Michael Forrest
  • Date: 2010-10-15 16:28:50 UTC
  • Revision ID: michael.forrest@canonical.com-20101015162850-tj2vchanv0kr0dun
refrozeĀ gems

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
h2. The Basics of Creating Rails Plugins
 
2
 
 
3
A Rails plugin is either an extension or a modification of the core framework. Plugins provide:
 
4
 
 
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
 
8
 
 
9
After reading this guide you should be familiar with:
 
10
 
 
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
 
14
* Writing generators
 
15
* Writing custom Rake tasks in your plugin
 
16
* Generating RDoc documentation for your plugin
 
17
* Avoiding common pitfalls with 'init.rb'
 
18
 
 
19
This guide describes how to build a test-driven plugin that will:
 
20
 
 
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
 
27
 
 
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.
 
29
 
 
30
endprologue.
 
31
 
 
32
h3. Setup
 
33
 
 
34
h4. Create the Basic Application
 
35
 
 
36
The examples in this guide require that you have a working rails application.  To create a simple rails app execute:
 
37
 
 
38
<pre>
 
39
gem install rails
 
40
rails yaffle_guide
 
41
cd yaffle_guide
 
42
script/generate scaffold bird name:string
 
43
rake db:migrate
 
44
script/server
 
45
</pre>
 
46
 
 
47
Then navigate to http://localhost:3000/birds.  Make sure you have a functioning rails app before continuing.
 
48
 
 
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.
 
50
 
 
51
 
 
52
h4. Generate the Plugin Skeleton
 
53
 
 
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.
 
55
 
 
56
This creates a plugin in 'vendor/plugins' including an 'init.rb' and 'README' as well as standard 'lib', 'task', and 'test' directories.
 
57
 
 
58
Examples:
 
59
<pre>
 
60
./script/generate plugin yaffle
 
61
./script/generate plugin yaffle --with-generator
 
62
</pre>
 
63
 
 
64
To get more detailed help on the plugin generator, type +./script/generate plugin+.
 
65
 
 
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:
 
67
 
 
68
<pre>
 
69
./script/generate plugin yaffle --with-generator
 
70
</pre>
 
71
 
 
72
You should see the following output:
 
73
 
 
74
<pre>
 
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
 
92
</pre>
 
93
 
 
94
h4. Organize Your Files
 
95
 
 
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:
 
97
 
 
98
<pre>
 
99
|-- lib
 
100
|   |-- yaffle
 
101
|   `-- yaffle.rb
 
102
`-- rails
 
103
    |
 
104
    `-- init.rb
 
105
</pre>
 
106
 
 
107
*vendor/plugins/yaffle/rails/init.rb*
 
108
 
 
109
<ruby>
 
110
require 'yaffle'
 
111
</ruby>
 
112
 
 
113
Now you can add any 'require' statements to 'lib/yaffle.rb' and keep 'init.rb' clean.
 
114
 
 
115
h3. Tests
 
116
 
 
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:
 
118
 
 
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
 
122
 
 
123
h4. Test Setup
 
124
 
 
125
*vendor/plugins/yaffle/test/database.yml:*
 
126
 
 
127
<pre>
 
128
sqlite:
 
129
  :adapter: sqlite
 
130
  :dbfile: vendor/plugins/yaffle/test/yaffle_plugin.sqlite.db
 
131
 
 
132
sqlite3:
 
133
  :adapter: sqlite3
 
134
  :dbfile: vendor/plugins/yaffle/test/yaffle_plugin.sqlite3.db
 
135
 
 
136
postgresql:
 
137
  :adapter: postgresql
 
138
  :username: postgres
 
139
  :password: postgres
 
140
  :database: yaffle_plugin_test
 
141
  :min_messages: ERROR
 
142
 
 
143
mysql:
 
144
  :adapter: mysql
 
145
  :host: localhost
 
146
  :username: root
 
147
  :password: password
 
148
  :database: yaffle_plugin_test
 
149
</pre>
 
150
 
 
151
For this guide you'll need 2 tables/models, Hickwalls and Wickwalls, so add the following:
 
152
 
 
153
*vendor/plugins/yaffle/test/schema.rb:*
 
154
 
 
155
<ruby>
 
156
ActiveRecord::Schema.define(:version => 0) do
 
157
  create_table :hickwalls, :force => true do |t|
 
158
    t.string :name
 
159
    t.string :last_squawk
 
160
    t.datetime :last_squawked_at
 
161
  end
 
162
  create_table :wickwalls, :force => true do |t|
 
163
    t.string :name
 
164
    t.string :last_tweet
 
165
    t.datetime :last_tweeted_at
 
166
  end
 
167
  create_table :woodpeckers, :force => true do |t|
 
168
    t.string :name
 
169
  end
 
170
end
 
171
</ruby>
 
172
 
 
173
*vendor/plugins/yaffle/test/test_helper.rb:*
 
174
 
 
175
<ruby>
 
176
ENV['RAILS_ENV'] = 'test'
 
177
ENV['RAILS_ROOT'] ||= File.dirname(__FILE__) + '/../../../..'
 
178
 
 
179
require 'test/unit'
 
180
require File.expand_path(File.join(ENV['RAILS_ROOT'], 'config/environment.rb'))
 
181
 
 
182
def load_schema
 
183
  config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
 
184
  ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
 
185
 
 
186
  db_adapter = ENV['DB']
 
187
 
 
188
  # no db passed, try one of these fine config-free DBs before bombing.
 
189
  db_adapter ||=
 
190
    begin
 
191
      require 'rubygems'
 
192
      require 'sqlite'
 
193
      'sqlite'
 
194
    rescue MissingSourceFile
 
195
      begin
 
196
        require 'sqlite3'
 
197
        'sqlite3'
 
198
      rescue MissingSourceFile
 
199
      end
 
200
    end
 
201
 
 
202
  if db_adapter.nil?
 
203
    raise "No DB Adapter selected. Pass the DB= option to pick one, or install Sqlite or Sqlite3."
 
204
  end
 
205
 
 
206
  ActiveRecord::Base.establish_connection(config[db_adapter])
 
207
  load(File.dirname(__FILE__) + "/schema.rb")
 
208
  require File.dirname(__FILE__) + '/../rails/init.rb'
 
209
end
 
210
</ruby>
 
211
 
 
212
Now whenever you write a test that requires the database, you can call 'load_schema'.
 
213
 
 
214
h4. Run the Plugin Tests
 
215
 
 
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:
 
217
 
 
218
*vendor/plugins/yaffle/test/yaffle_test.rb:*
 
219
 
 
220
<ruby>
 
221
require File.dirname(__FILE__) + '/test_helper.rb'
 
222
 
 
223
class YaffleTest < Test::Unit::TestCase
 
224
  load_schema
 
225
 
 
226
  class Hickwall < ActiveRecord::Base
 
227
  end
 
228
 
 
229
  class Wickwall < ActiveRecord::Base
 
230
  end
 
231
 
 
232
  def test_schema_has_loaded_correctly
 
233
    assert_equal [], Hickwall.all
 
234
    assert_equal [], Wickwall.all
 
235
  end
 
236
 
 
237
end
 
238
</ruby>
 
239
 
 
240
To run this, go to the plugin directory and run +rake+:
 
241
 
 
242
<pre>
 
243
cd vendor/plugins/yaffle
 
244
rake
 
245
</pre>
 
246
 
 
247
You should see output like:
 
248
 
 
249
<shell>
 
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})
 
252
   -> 0.0220s
 
253
-- create_table(:wickwalls, {:force=>true})
 
254
   -> 0.0077s
 
255
-- initialize_schema_migrations_table()
 
256
   -> 0.0007s
 
257
-- assume_migrated_upto_version(0)
 
258
   -> 0.0007s
 
259
Loaded suite /opt/local/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader
 
260
Started
 
261
.
 
262
Finished in 0.002236 seconds.
 
263
 
 
264
1 test, 1 assertion, 0 failures, 0 errors
 
265
</shell>
 
266
 
 
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:
 
268
 
 
269
<shell>
 
270
rake DB=sqlite
 
271
rake DB=sqlite3
 
272
rake DB=mysql
 
273
rake DB=postgresql
 
274
</shell>
 
275
 
 
276
Now you are ready to test-drive your plugin!
 
277
 
 
278
h3. Extending Core Classes
 
279
 
 
280
This section will explain how to add a method to String that will be available anywhere in your rails app.
 
281
 
 
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:
 
283
 
 
284
* *vendor/plugins/yaffle/test/core_ext_test.rb*
 
285
 
 
286
<ruby>
 
287
require File.dirname(__FILE__) + '/test_helper.rb'
 
288
 
 
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
 
292
  end
 
293
end
 
294
</ruby>
 
295
 
 
296
Navigate to your plugin directory and run +rake test+:
 
297
 
 
298
<shell>
 
299
cd vendor/plugins/yaffle
 
300
rake test
 
301
</shell>
 
302
 
 
303
The test above should fail with the message:
 
304
 
 
305
<shell>
 
306
 1) Error:
 
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'
 
310
</shell>
 
311
 
 
312
Great - now you are ready to start development.
 
313
 
 
314
Then in 'lib/yaffle.rb' require 'lib/core_ext.rb':
 
315
 
 
316
* *vendor/plugins/yaffle/lib/yaffle.rb*
 
317
 
 
318
<ruby>
 
319
require "yaffle/core_ext"
 
320
</ruby>
 
321
 
 
322
Finally, create the 'core_ext.rb' file and add the 'to_squawk' method:
 
323
 
 
324
* *vendor/plugins/yaffle/lib/yaffle/core_ext.rb*
 
325
 
 
326
<ruby>
 
327
String.class_eval do
 
328
  def to_squawk
 
329
    "squawk! #{self}".strip
 
330
  end
 
331
end
 
332
</ruby>
 
333
 
 
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:
 
335
 
 
336
<shell>
 
337
$ ./script/console
 
338
>> "Hello World".to_squawk
 
339
=> "squawk! Hello World"
 
340
</shell>
 
341
 
 
342
h4. Working with +init.rb+
 
343
 
 
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.
 
345
 
 
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.
 
347
 
 
348
If you must reopen a class in +init.rb+ you can use +module_eval+ or +class_eval+ to avoid any issues:
 
349
 
 
350
* *vendor/plugins/yaffle/rails/init.rb*
 
351
 
 
352
<ruby>
 
353
Hash.class_eval do
 
354
  def is_a_special_hash?
 
355
    true
 
356
  end
 
357
end
 
358
</ruby>
 
359
 
 
360
Another way is to explicitly define the top-level module space for all modules and classes, like +::Hash+:
 
361
 
 
362
* *vendor/plugins/yaffle/rails/init.rb*
 
363
 
 
364
<ruby>
 
365
class ::Hash
 
366
  def is_a_special_hash?
 
367
    true
 
368
  end
 
369
end
 
370
</ruby>
 
371
 
 
372
h3. Add an "acts_as" Method to Active Record
 
373
 
 
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.
 
375
 
 
376
To begin, set up your files so that you have:
 
377
 
 
378
* *vendor/plugins/yaffle/test/acts_as_yaffle_test.rb*
 
379
 
 
380
<ruby>
 
381
require File.dirname(__FILE__) + '/test_helper.rb'
 
382
 
 
383
class ActsAsYaffleTest < Test::Unit::TestCase
 
384
end
 
385
</ruby>
 
386
 
 
387
* *vendor/plugins/yaffle/lib/yaffle.rb*
 
388
 
 
389
<ruby>
 
390
require 'yaffle/acts_as_yaffle'
 
391
</ruby>
 
392
 
 
393
* *vendor/plugins/yaffle/lib/yaffle/acts_as_yaffle.rb*
 
394
 
 
395
<ruby>
 
396
module Yaffle
 
397
  # your code will go here
 
398
end
 
399
</ruby>
 
400
 
 
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.
 
402
 
 
403
One of the most common plugin patterns for 'acts_as_yaffle' plugins is to structure your file like so:
 
404
 
 
405
* *vendor/plugins/yaffle/lib/yaffle/acts_as_yaffle.rb*
 
406
 
 
407
<ruby>
 
408
module Yaffle
 
409
  def self.included(base)
 
410
    base.send :extend, ClassMethods
 
411
  end
 
412
 
 
413
  module ClassMethods
 
414
    # any method placed here will apply to classes, like Hickwall
 
415
    def acts_as_something
 
416
      send :include, InstanceMethods
 
417
    end
 
418
  end
 
419
 
 
420
  module InstanceMethods
 
421
    # any method placed here will apply to instaces, like @hickwall
 
422
  end
 
423
end
 
424
</ruby>
 
425
 
 
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+).
 
427
 
 
428
h4. Add a Class Method
 
429
 
 
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'.
 
431
 
 
432
To start out, write a failing test that shows the behavior you'd like:
 
433
 
 
434
* *vendor/plugins/yaffle/test/acts_as_yaffle_test.rb*
 
435
 
 
436
<ruby>
 
437
require File.dirname(__FILE__) + '/test_helper.rb'
 
438
 
 
439
class Hickwall < ActiveRecord::Base
 
440
  acts_as_yaffle
 
441
end
 
442
 
 
443
class Wickwall < ActiveRecord::Base
 
444
  acts_as_yaffle :yaffle_text_field => :last_tweet
 
445
end
 
446
 
 
447
class ActsAsYaffleTest < Test::Unit::TestCase
 
448
  load_schema
 
449
 
 
450
  def test_a_hickwalls_yaffle_text_field_should_be_last_squawk
 
451
    assert_equal "last_squawk", Hickwall.yaffle_text_field
 
452
  end
 
453
 
 
454
  def test_a_wickwalls_yaffle_text_field_should_be_last_tweet
 
455
    assert_equal "last_tweet", Wickwall.yaffle_text_field
 
456
  end
 
457
end
 
458
</ruby>
 
459
 
 
460
To make these tests pass, you could modify your +acts_as_yaffle+ file like so:
 
461
 
 
462
* *vendor/plugins/yaffle/lib/yaffle/acts_as_yaffle.rb*
 
463
 
 
464
<ruby>
 
465
module Yaffle
 
466
  def self.included(base)
 
467
    base.send :extend, ClassMethods
 
468
  end
 
469
 
 
470
  module 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
 
474
    end
 
475
  end
 
476
end
 
477
 
 
478
ActiveRecord::Base.send :include, Yaffle
 
479
</ruby>
 
480
 
 
481
h4. Add an Instance Method
 
482
 
 
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.
 
484
 
 
485
To start out, write a failing test that shows the behavior you'd like:
 
486
 
 
487
* *vendor/plugins/yaffle/test/acts_as_yaffle_test.rb*
 
488
 
 
489
<ruby>
 
490
require File.dirname(__FILE__) + '/test_helper.rb'
 
491
 
 
492
class Hickwall < ActiveRecord::Base
 
493
  acts_as_yaffle
 
494
end
 
495
 
 
496
class Wickwall < ActiveRecord::Base
 
497
  acts_as_yaffle :yaffle_text_field => :last_tweet
 
498
end
 
499
 
 
500
class ActsAsYaffleTest < Test::Unit::TestCase
 
501
  load_schema
 
502
 
 
503
  def test_a_hickwalls_yaffle_text_field_should_be_last_squawk
 
504
    assert_equal "last_squawk", Hickwall.yaffle_text_field
 
505
  end
 
506
 
 
507
  def test_a_wickwalls_yaffle_text_field_should_be_last_tweet
 
508
    assert_equal "last_tweet", Wickwall.yaffle_text_field
 
509
  end
 
510
 
 
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
 
515
  end
 
516
 
 
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
 
521
  end
 
522
end
 
523
</ruby>
 
524
 
 
525
Run this test to make sure the last two tests fail, then update 'acts_as_yaffle.rb' to look like this:
 
526
 
 
527
* *vendor/plugins/yaffle/lib/yaffle/acts_as_yaffle.rb*
 
528
 
 
529
<ruby>
 
530
module Yaffle
 
531
  def self.included(base)
 
532
    base.send :extend, ClassMethods
 
533
  end
 
534
 
 
535
  module 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
 
540
    end
 
541
  end
 
542
 
 
543
  module InstanceMethods
 
544
    def squawk(string)
 
545
      write_attribute(self.class.yaffle_text_field, string.to_squawk)
 
546
    end
 
547
  end
 
548
end
 
549
 
 
550
ActiveRecord::Base.send :include, Yaffle
 
551
</ruby>
 
552
 
 
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)+.
 
554
 
 
555
h3. Models
 
556
 
 
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:
 
558
 
 
559
<shell>
 
560
vendor/plugins/yaffle/
 
561
|-- lib
 
562
|   |-- app
 
563
|   |   |-- controllers
 
564
|   |   |-- helpers
 
565
|   |   |-- models
 
566
|   |   |   `-- woodpecker.rb
 
567
|   |   `-- views
 
568
|   |-- yaffle
 
569
|   |   |-- acts_as_yaffle.rb
 
570
|   |   |-- commands.rb
 
571
|   |   `-- core_ext.rb
 
572
|   `-- yaffle.rb
 
573
</shell>
 
574
 
 
575
As always, start with a test:
 
576
 
 
577
* *vendor/plugins/yaffle/test/woodpecker_test.rb:*
 
578
 
 
579
<ruby>
 
580
require File.dirname(__FILE__) + '/test_helper.rb'
 
581
 
 
582
class WoodpeckerTest < Test::Unit::TestCase
 
583
  load_schema
 
584
 
 
585
  def test_woodpecker
 
586
    assert_kind_of Woodpecker, Woodpecker.new
 
587
  end
 
588
end
 
589
</ruby>
 
590
 
 
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:
 
592
 
 
593
* *vendor/plugins/yaffle/lib/yaffle.rb:*
 
594
 
 
595
<ruby>
 
596
%w{ models }.each do |dir|
 
597
  path = File.join(File.dirname(__FILE__), 'app', dir)
 
598
  $LOAD_PATH << path
 
599
  ActiveSupport::Dependencies.load_paths << path
 
600
  ActiveSupport::Dependencies.load_once_paths.delete(path)
 
601
end
 
602
</ruby>
 
603
 
 
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.
 
605
 
 
606
* *vendor/plugins/yaffle/lib/app/models/woodpecker.rb:*
 
607
 
 
608
<ruby>
 
609
class Woodpecker < ActiveRecord::Base
 
610
end
 
611
</ruby>
 
612
 
 
613
Finally, add the following to your plugin's 'schema.rb':
 
614
 
 
615
* *vendor/plugins/yaffle/test/schema.rb:*
 
616
 
 
617
<ruby>
 
618
create_table :woodpeckers, :force => true do |t|
 
619
  t.string :name
 
620
end
 
621
</ruby>
 
622
 
 
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.
 
624
 
 
625
h3. Controllers
 
626
 
 
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.
 
628
 
 
629
You can test your plugin's controller as you would test any other controller:
 
630
 
 
631
* *vendor/plugins/yaffle/test/woodpeckers_controller_test.rb:*
 
632
 
 
633
<ruby>
 
634
require File.dirname(__FILE__) + '/test_helper.rb'
 
635
require 'woodpeckers_controller'
 
636
require 'action_controller/test_process'
 
637
 
 
638
class WoodpeckersController; def rescue_action(e) raise e end; end
 
639
 
 
640
class WoodpeckersControllerTest < Test::Unit::TestCase
 
641
  def setup
 
642
    @controller = WoodpeckersController.new
 
643
    @request = ActionController::TestRequest.new
 
644
    @response = ActionController::TestResponse.new
 
645
 
 
646
    ActionController::Routing::Routes.draw do |map|
 
647
      map.resources :woodpeckers
 
648
    end
 
649
  end
 
650
 
 
651
  def test_index
 
652
    get :index
 
653
    assert_response :success
 
654
  end
 
655
end
 
656
</ruby>
 
657
 
 
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:
 
659
 
 
660
* *vendor/plugins/yaffle/lib/yaffle.rb:*
 
661
 
 
662
<ruby>
 
663
%w{ models controllers }.each do |dir|
 
664
  path = File.join(File.dirname(__FILE__), 'app', dir)
 
665
  $LOAD_PATH << path
 
666
  ActiveSupport::Dependencies.load_paths << path
 
667
  ActiveSupport::Dependencies.load_once_paths.delete(path)
 
668
end
 
669
</ruby>
 
670
 
 
671
* *vendor/plugins/yaffle/lib/app/controllers/woodpeckers_controller.rb:*
 
672
 
 
673
<ruby>
 
674
class WoodpeckersController < ActionController::Base
 
675
 
 
676
  def index
 
677
    render :text => "Squawk!"
 
678
  end
 
679
 
 
680
end
 
681
</ruby>
 
682
 
 
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.
 
684
 
 
685
h3. Helpers
 
686
 
 
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.
 
688
 
 
689
You can test your plugin's helper as you would test any other helper:
 
690
 
 
691
* *vendor/plugins/yaffle/test/woodpeckers_helper_test.rb*
 
692
 
 
693
<ruby>
 
694
require File.dirname(__FILE__) + '/test_helper.rb'
 
695
include WoodpeckersHelper
 
696
 
 
697
class WoodpeckersHelperTest < Test::Unit::TestCase
 
698
  def test_tweet
 
699
    assert_equal "Tweet! Hello", tweet("Hello")
 
700
  end
 
701
end
 
702
</ruby>
 
703
 
 
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:
 
705
 
 
706
* *vendor/plugins/yaffle/lib/yaffle.rb:*
 
707
 
 
708
<ruby>
 
709
%w{ models controllers helpers }.each do |dir|
 
710
  path = File.join(File.dirname(__FILE__), 'app', dir)
 
711
  $LOAD_PATH << path
 
712
  ActiveSupport::Dependencies.load_paths << path
 
713
  ActiveSupport::Dependencies.load_once_paths.delete(path)
 
714
end
 
715
</ruby>
 
716
 
 
717
* *vendor/plugins/yaffle/lib/app/helpers/woodpeckers_helper.rb:*
 
718
 
 
719
<ruby>
 
720
module WoodpeckersHelper
 
721
 
 
722
  def tweet(text)
 
723
    "Tweet! #{text}"
 
724
  end
 
725
 
 
726
end
 
727
</ruby>
 
728
 
 
729
Now your test should be passing, and you should be able to use the Woodpeckers helper in your app.
 
730
 
 
731
h3. Routes
 
732
 
 
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'.
 
734
 
 
735
Testing routes from plugins is slightly different from testing routes in a standard rails app.  To begin, add a test like this:
 
736
 
 
737
* *vendor/plugins/yaffle/test/routing_test.rb*
 
738
 
 
739
<ruby>
 
740
require "#{File.dirname(__FILE__)}/test_helper"
 
741
 
 
742
class RoutingTest < Test::Unit::TestCase
 
743
 
 
744
  def setup
 
745
    ActionController::Routing::Routes.draw do |map|
 
746
      map.yaffles
 
747
    end
 
748
  end
 
749
 
 
750
  def test_yaffles_route
 
751
    assert_recognition :get, "/yaffles", :controller => "yaffles_controller", :action => "index"
 
752
  end
 
753
 
 
754
  private
 
755
 
 
756
    def assert_recognition(method, path, options)
 
757
      result = ActionController::Routing::Routes.recognize_path(path, :method => method)
 
758
      assert_equal options, result
 
759
    end
 
760
end
 
761
</ruby>
 
762
 
 
763
Once you see the tests fail by running 'rake', you can make them pass with:
 
764
 
 
765
* *vendor/plugins/yaffle/lib/yaffle.rb*
 
766
 
 
767
<ruby>
 
768
require "yaffle/routing"
 
769
</ruby>
 
770
 
 
771
* *vendor/plugins/yaffle/lib/yaffle/routing.rb*
 
772
 
 
773
<ruby>
 
774
module Yaffle #:nodoc:
 
775
  module Routing #:nodoc:
 
776
    module MapperExtensions
 
777
      def yaffles
 
778
        @set.add_route("/yaffles", {:controller => "yaffles_controller", :action => "index"})
 
779
      end
 
780
    end
 
781
  end
 
782
end
 
783
 
 
784
ActionController::Routing::RouteSet::Mapper.send :include, Yaffle::Routing::MapperExtensions
 
785
</ruby>
 
786
 
 
787
* *config/routes.rb*
 
788
 
 
789
<ruby>
 
790
ActionController::Routing::Routes.draw do |map|
 
791
  map.yaffles
 
792
end
 
793
</ruby>
 
794
 
 
795
You can also see if your routes work by running +rake routes+ from your app directory.
 
796
 
 
797
h3. Generators
 
798
 
 
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'.
 
800
 
 
801
Building generators is a complex topic unto itself and this section will cover one small aspect of generators: generating a simple text file.
 
802
 
 
803
h4. Testing Generators
 
804
 
 
805
Many rails plugin authors do not test their generators, however testing generators is quite simple.  A typical generator test does the following:
 
806
 
 
807
 * Creates a new fake rails root directory that will serve as destination
 
808
 * Runs the generator
 
809
 * Asserts that the correct files were generated
 
810
 * Removes the fake rails root
 
811
 
 
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:
 
813
 
 
814
* *vendor/plugins/yaffle/test/definition_generator_test.rb*
 
815
 
 
816
<ruby>
 
817
require File.dirname(__FILE__) + '/test_helper.rb'
 
818
require 'rails_generator'
 
819
require 'rails_generator/scripts/generate'
 
820
 
 
821
class DefinitionGeneratorTest < Test::Unit::TestCase
 
822
 
 
823
  def setup
 
824
    FileUtils.mkdir_p(fake_rails_root)
 
825
    @original_files = file_list
 
826
  end
 
827
 
 
828
  def teardown
 
829
    FileUtils.rm_r(fake_rails_root)
 
830
  end
 
831
 
 
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)
 
836
  end
 
837
 
 
838
  private
 
839
 
 
840
    def fake_rails_root
 
841
      File.join(File.dirname(__FILE__), 'rails_root')
 
842
    end
 
843
 
 
844
    def file_list
 
845
      Dir.glob(File.join(fake_rails_root, "*"))
 
846
    end
 
847
 
 
848
end
 
849
</ruby>
 
850
 
 
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.
 
852
 
 
853
To make it pass, create the generator:
 
854
 
 
855
* *vendor/plugins/yaffle/generators/yaffle_definition/yaffle_definition_generator.rb*
 
856
 
 
857
<ruby>
 
858
class YaffleDefinitionGenerator < Rails::Generator::Base
 
859
  def manifest
 
860
    record do |m|
 
861
      m.file "definition.txt", "definition.txt"
 
862
    end
 
863
  end
 
864
end
 
865
</ruby>
 
866
 
 
867
h4. The +USAGE+ File
 
868
 
 
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.
 
870
 
 
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:
 
872
 
 
873
<shell>
 
874
./script/generate
 
875
</shell>
 
876
 
 
877
You should see something like this:
 
878
 
 
879
<shell>
 
880
Installed Generators
 
881
  Plugins (vendor/plugins): yaffle_definition
 
882
  Builtin: controller, integration_test, mailer, migration, model, observer, plugin, resource, scaffold, session_migration
 
883
</shell>
 
884
 
 
885
When you run +script/generate yaffle_definition -h+ you should see the contents of your 'vendor/plugins/yaffle/generators/yaffle_definition/USAGE'.
 
886
 
 
887
For this plugin, update the USAGE file could look like this:
 
888
 
 
889
<shell>
 
890
Description:
 
891
    Adds a file with the definition of a Yaffle to the app's main directory
 
892
</shell>
 
893
 
 
894
h3. Add a Custom Generator Command
 
895
 
 
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.
 
897
 
 
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.
 
899
 
 
900
To start, add the following test method:
 
901
 
 
902
* *vendor/plugins/yaffle/test/generator_test.rb*
 
903
 
 
904
<ruby>
 
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
 
909
end
 
910
</ruby>
 
911
 
 
912
Run +rake+ to watch the test fail, then make the test pass add the following:
 
913
 
 
914
* *vendor/plugins/yaffle/generators/yaffle/templates/definition.txt*
 
915
 
 
916
<shell>
 
917
Yaffle: A bird
 
918
</shell>
 
919
 
 
920
* *vendor/plugins/yaffle/lib/yaffle.rb*
 
921
 
 
922
<ruby>
 
923
require "yaffle/commands"
 
924
</ruby>
 
925
 
 
926
* *vendor/plugins/yaffle/lib/commands.rb*
 
927
 
 
928
<ruby>
 
929
require 'rails_generator'
 
930
require 'rails_generator/commands'
 
931
 
 
932
module Yaffle #:nodoc:
 
933
  module Generator #:nodoc:
 
934
    module Commands #:nodoc:
 
935
      module Create
 
936
        def yaffle_definition
 
937
          file("definition.txt", "definition.txt")
 
938
        end
 
939
      end
 
940
 
 
941
      module Destroy
 
942
        def yaffle_definition
 
943
          file("definition.txt", "definition.txt")
 
944
        end
 
945
      end
 
946
 
 
947
      module List
 
948
        def yaffle_definition
 
949
          file("definition.txt", "definition.txt")
 
950
        end
 
951
      end
 
952
 
 
953
      module Update
 
954
        def yaffle_definition
 
955
          file("definition.txt", "definition.txt")
 
956
        end
 
957
      end
 
958
    end
 
959
  end
 
960
end
 
961
 
 
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
 
966
</ruby>
 
967
 
 
968
Finally, call your new method in the manifest:
 
969
 
 
970
* *vendor/plugins/yaffle/generators/yaffle/yaffle_generator.rb*
 
971
 
 
972
<ruby>
 
973
class YaffleGenerator < Rails::Generator::NamedBase
 
974
  def manifest
 
975
    m.yaffle_definition
 
976
  end
 
977
end
 
978
</ruby>
 
979
 
 
980
h3. Generator Commands
 
981
 
 
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.
 
983
 
 
984
This section describes how you you can create your own commands to add and remove a line of text from 'config/routes.rb'.
 
985
 
 
986
To start, add the following test method:
 
987
 
 
988
* *vendor/plugins/yaffle/test/route_generator_test.rb*
 
989
 
 
990
<ruby>
 
991
require File.dirname(__FILE__) + '/test_helper.rb'
 
992
require 'rails_generator'
 
993
require 'rails_generator/scripts/generate'
 
994
require 'rails_generator/scripts/destroy'
 
995
 
 
996
class RouteGeneratorTest < Test::Unit::TestCase
 
997
 
 
998
  def setup
 
999
    FileUtils.mkdir_p(File.join(fake_rails_root, "config"))
 
1000
  end
 
1001
 
 
1002
  def teardown
 
1003
    FileUtils.rm_r(fake_rails_root)
 
1004
  end
 
1005
 
 
1006
  def test_generates_route
 
1007
    content = <<-END
 
1008
      ActionController::Routing::Routes.draw do |map|
 
1009
        map.connect ':controller/:action/:id'
 
1010
        map.connect ':controller/:action/:id.:format'
 
1011
      end
 
1012
    END
 
1013
    File.open(routes_path, 'wb') {|f| f.write(content) }
 
1014
 
 
1015
    Rails::Generator::Scripts::Generate.new.run(["yaffle_route"], :destination => fake_rails_root)
 
1016
    assert_match /map\.yaffles/, File.read(routes_path)
 
1017
  end
 
1018
 
 
1019
  def test_destroys_route
 
1020
    content = <<-END
 
1021
      ActionController::Routing::Routes.draw do |map|
 
1022
        map.yaffles
 
1023
        map.connect ':controller/:action/:id'
 
1024
        map.connect ':controller/:action/:id.:format'
 
1025
      end
 
1026
    END
 
1027
    File.open(routes_path, 'wb') {|f| f.write(content) }
 
1028
 
 
1029
    Rails::Generator::Scripts::Destroy.new.run(["yaffle_route"], :destination => fake_rails_root)
 
1030
    assert_no_match /map\.yaffles/, File.read(routes_path)
 
1031
  end
 
1032
 
 
1033
  private
 
1034
 
 
1035
    def fake_rails_root
 
1036
      File.join(File.dirname(__FILE__), "rails_root")
 
1037
    end
 
1038
 
 
1039
    def routes_path
 
1040
      File.join(fake_rails_root, "config", "routes.rb")
 
1041
    end
 
1042
 
 
1043
end
 
1044
</ruby>
 
1045
 
 
1046
Run +rake+ to watch the test fail, then make the test pass add the following:
 
1047
 
 
1048
* *vendor/plugins/yaffle/lib/yaffle.rb*
 
1049
 
 
1050
<ruby>
 
1051
require "yaffle/commands"
 
1052
</ruby>
 
1053
 
 
1054
* *vendor/plugins/yaffle/lib/yaffle/commands.rb*
 
1055
 
 
1056
<ruby>
 
1057
require 'rails_generator'
 
1058
require 'rails_generator/commands'
 
1059
 
 
1060
module Yaffle #:nodoc:
 
1061
  module Generator #:nodoc:
 
1062
    module Commands #:nodoc:
 
1063
      module Create
 
1064
        def yaffle_route
 
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"}
 
1069
          end
 
1070
        end
 
1071
      end
 
1072
 
 
1073
      module Destroy
 
1074
        def yaffle_route
 
1075
          logger.route "map.yaffle"
 
1076
          gsub_file 'config/routes.rb', /\n.+?map\.yaffles/mi, ''
 
1077
        end
 
1078
      end
 
1079
 
 
1080
      module List
 
1081
        def yaffle_route
 
1082
        end
 
1083
      end
 
1084
 
 
1085
      module Update
 
1086
        def yaffle_route
 
1087
        end
 
1088
      end
 
1089
    end
 
1090
  end
 
1091
end
 
1092
 
 
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
 
1097
</ruby>
 
1098
 
 
1099
* *vendor/plugins/yaffle/generators/yaffle_route/yaffle_route_generator.rb*
 
1100
 
 
1101
<ruby>
 
1102
class YaffleRouteGenerator < Rails::Generator::Base
 
1103
  def manifest
 
1104
    record do |m|
 
1105
      m.yaffle_route
 
1106
    end
 
1107
  end
 
1108
end
 
1109
</ruby>
 
1110
 
 
1111
To see this work, type:
 
1112
 
 
1113
<shell>
 
1114
./script/generate yaffle_route
 
1115
./script/destroy yaffle_route
 
1116
</shell>
 
1117
 
 
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.
 
1119
 
 
1120
h3. Migrations
 
1121
 
 
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.
 
1123
 
 
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.
 
1125
 
 
1126
Let's say you have the following migration in your plugin:
 
1127
 
 
1128
* *vendor/plugins/yaffle/lib/db/migrate/20081116181115_create_birdhouses.rb:*
 
1129
 
 
1130
<ruby>
 
1131
class CreateBirdhouses < ActiveRecord::Migration
 
1132
  def self.up
 
1133
    create_table :birdhouses, :force => true do |t|
 
1134
      t.string :name
 
1135
      t.timestamps
 
1136
    end
 
1137
  end
 
1138
 
 
1139
  def self.down
 
1140
    drop_table :birdhouses
 
1141
  end
 
1142
end
 
1143
</ruby>
 
1144
 
 
1145
Here are a few possibilities for how to allow developers to use your plugin migrations:
 
1146
 
 
1147
h4. Create a Custom Rake Task
 
1148
 
 
1149
* *vendor/plugins/yaffle/tasks/yaffle_tasks.rake:*
 
1150
 
 
1151
<ruby>
 
1152
namespace :db do
 
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."
 
1157
 
 
1158
    desc description
 
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
 
1163
    end
 
1164
  end
 
1165
end
 
1166
</ruby>
 
1167
 
 
1168
h4. Call Migrations Directly
 
1169
 
 
1170
* *vendor/plugins/yaffle/lib/yaffle.rb:*
 
1171
 
 
1172
<ruby>
 
1173
Dir.glob(File.join(File.dirname(__FILE__), "db", "migrate", "*")).each do |file|
 
1174
  require file
 
1175
end
 
1176
</ruby>
 
1177
 
 
1178
* *db/migrate/20081116181115_create_birdhouses.rb:*
 
1179
 
 
1180
<ruby>
 
1181
class CreateBirdhouses < ActiveRecord::Migration
 
1182
  def self.up
 
1183
    Yaffle::CreateBirdhouses.up
 
1184
  end
 
1185
 
 
1186
  def self.down
 
1187
    Yaffle::CreateBirdhouses.down
 
1188
  end
 
1189
end
 
1190
</ruby>
 
1191
 
 
1192
NOTE: several plugin frameworks such as Desert and Engines provide more advanced plugin functionality.
 
1193
 
 
1194
h4. Generate Migrations
 
1195
 
 
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:
 
1197
 
 
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
 
1200
 
 
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:
 
1202
 
 
1203
* *vendor/plugins/yaffle/test/yaffle_migration_generator_test.rb*
 
1204
 
 
1205
<ruby>
 
1206
require File.dirname(__FILE__) + '/test_helper.rb'
 
1207
require 'rails_generator'
 
1208
require 'rails_generator/scripts/generate'
 
1209
 
 
1210
class MigrationGeneratorTest < Test::Unit::TestCase
 
1211
 
 
1212
  def setup
 
1213
    FileUtils.mkdir_p(fake_rails_root)
 
1214
    @original_files = file_list
 
1215
  end
 
1216
 
 
1217
  def teardown
 
1218
    ActiveRecord::Base.pluralize_table_names = true
 
1219
    FileUtils.rm_r(fake_rails_root)
 
1220
  end
 
1221
 
 
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)
 
1228
  end
 
1229
 
 
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)
 
1237
  end
 
1238
 
 
1239
  private
 
1240
    def fake_rails_root
 
1241
      File.join(File.dirname(__FILE__), 'rails_root')
 
1242
    end
 
1243
 
 
1244
    def file_list
 
1245
      Dir.glob(File.join(fake_rails_root, "db", "migrate", "*"))
 
1246
    end
 
1247
 
 
1248
end
 
1249
</ruby>
 
1250
 
 
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.
 
1252
 
 
1253
After running the test with 'rake' you can make it pass with:
 
1254
 
 
1255
* *vendor/plugins/yaffle/generators/yaffle_migration/yaffle_migration_generator.rb*
 
1256
 
 
1257
<ruby>
 
1258
class YaffleMigrationGenerator < Rails::Generator::NamedBase
 
1259
  def manifest
 
1260
    record do |m|
 
1261
      m.migration_template 'migration:migration.rb', "db/migrate", {:assigns => yaffle_local_assigns,
 
1262
        :migration_file_name => "add_yaffle_fields_to_#{custom_file_name}"
 
1263
      }
 
1264
    end
 
1265
  end
 
1266
 
 
1267
  private
 
1268
    def custom_file_name
 
1269
      custom_name = class_name.underscore.downcase
 
1270
      custom_name = custom_name.pluralize if ActiveRecord::Base.pluralize_table_names
 
1271
      custom_name
 
1272
    end
 
1273
 
 
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")]
 
1280
      end
 
1281
    end
 
1282
end
 
1283
</ruby>
 
1284
 
 
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.
 
1286
 
 
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.
 
1288
 
 
1289
To run the generator, type the following at the command line:
 
1290
 
 
1291
<shell>
 
1292
./script/generate yaffle_migration bird
 
1293
</shell>
 
1294
 
 
1295
and you will see a new file:
 
1296
 
 
1297
* *db/migrate/20080529225649_add_yaffle_fields_to_birds.rb*
 
1298
 
 
1299
<ruby>
 
1300
class AddYaffleFieldsToBirds < ActiveRecord::Migration
 
1301
  def self.up
 
1302
    add_column :birds, :last_squawk, :string
 
1303
  end
 
1304
 
 
1305
  def self.down
 
1306
    remove_column :birds, :last_squawk
 
1307
  end
 
1308
end
 
1309
</ruby>
 
1310
 
 
1311
h3. Rake tasks
 
1312
 
 
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.
 
1314
 
 
1315
Many plugin authors put all of their rake tasks into a common namespace that is the same as the plugin, like so:
 
1316
 
 
1317
* *vendor/plugins/yaffle/tasks/yaffle_tasks.rake*
 
1318
 
 
1319
<ruby>
 
1320
namespace :yaffle do
 
1321
  desc "Prints out the word 'Yaffle'"
 
1322
  task :squawk => :environment do
 
1323
    puts "squawk!"
 
1324
  end
 
1325
end
 
1326
</ruby>
 
1327
 
 
1328
When you run +rake -T+ from your plugin you will see:
 
1329
 
 
1330
<shell>
 
1331
yaffle:squawk             # Prints out the word 'Yaffle'
 
1332
</shell>
 
1333
 
 
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.
 
1335
 
 
1336
Note that tasks from 'vendor/plugins/yaffle/Rakefile' are not available to the main app.
 
1337
 
 
1338
h3. PluginGems
 
1339
 
 
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.
 
1341
 
 
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'.
 
1343
 
 
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:
 
1345
 
 
1346
* *vendor/plugins/yaffle/Rakefile:*
 
1347
 
 
1348
<ruby>
 
1349
PKG_FILES = FileList[
 
1350
  '[a-zA-Z]*',
 
1351
  'generators/**/*',
 
1352
  'lib/**/*',
 
1353
  'rails/**/*',
 
1354
  'tasks/**/*',
 
1355
  'test/**/*'
 
1356
]
 
1357
 
 
1358
spec = Gem::Specification.new do |s|
 
1359
  s.name = "yaffle"
 
1360
  s.version = "0.0.1"
 
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"
 
1368
  s.has_rdoc = false
 
1369
  s.extra_rdoc_files = ["README"]
 
1370
end
 
1371
 
 
1372
desc 'Turn this plugin into a gem.'
 
1373
Rake::GemPackageTask.new(spec) do |pkg|
 
1374
  pkg.gem_spec = spec
 
1375
end
 
1376
</ruby>
 
1377
 
 
1378
To build and install the gem locally, run the following commands:
 
1379
 
 
1380
<shell>
 
1381
cd vendor/plugins/yaffle
 
1382
rake gem
 
1383
sudo gem install pkg/yaffle-0.0.1.gem
 
1384
</shell>
 
1385
 
 
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.
 
1387
 
 
1388
h3. RDoc Documentation
 
1389
 
 
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.
 
1391
 
 
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:
 
1393
 
 
1394
* Your name
 
1395
* How to install
 
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
 
1398
 
 
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.
 
1400
 
 
1401
Once your comments are good to go, navigate to your plugin directory and run:
 
1402
 
 
1403
<shell>
 
1404
rake rdoc
 
1405
</shell>
 
1406
 
 
1407
h3. Appendix
 
1408
 
 
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.
 
1410
 
 
1411
h4. References
 
1412
 
 
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.
 
1419
 
 
1420
h4. Contents of +lib/yaffle.rb+
 
1421
 
 
1422
* *vendor/plugins/yaffle/lib/yaffle.rb:*
 
1423
 
 
1424
<ruby>
 
1425
require "yaffle/core_ext"
 
1426
require "yaffle/acts_as_yaffle"
 
1427
require "yaffle/commands"
 
1428
require "yaffle/routing"
 
1429
 
 
1430
%w{ models controllers helpers }.each do |dir|
 
1431
  path = File.join(File.dirname(__FILE__), 'app', dir)
 
1432
  $LOAD_PATH << path
 
1433
  ActiveSupport::Dependencies.load_paths << path
 
1434
  ActiveSupport::Dependencies.load_once_paths.delete(path)
 
1435
end
 
1436
 
 
1437
# optionally:
 
1438
# Dir.glob(File.join(File.dirname(__FILE__), "db", "migrate", "*")).each do |file|
 
1439
#   require file
 
1440
# end
 
1441
</ruby>
 
1442
 
 
1443
h4. Final Plugin Directory Structure
 
1444
 
 
1445
The final plugin should have a directory structure that looks something like this:
 
1446
 
 
1447
<shell>
 
1448
|-- MIT-LICENSE
 
1449
|-- README
 
1450
|-- Rakefile
 
1451
|-- generators
 
1452
|   |-- yaffle_definition
 
1453
|   |   |-- USAGE
 
1454
|   |   |-- templates
 
1455
|   |   |   `-- definition.txt
 
1456
|   |   `-- yaffle_definition_generator.rb
 
1457
|   |-- yaffle_migration
 
1458
|   |   |-- USAGE
 
1459
|   |   |-- templates
 
1460
|   |   `-- yaffle_migration_generator.rb
 
1461
|   `-- yaffle_route
 
1462
|       |-- USAGE
 
1463
|       |-- templates
 
1464
|       `-- yaffle_route_generator.rb
 
1465
|-- install.rb
 
1466
|-- lib
 
1467
|   |-- app
 
1468
|   |   |-- controllers
 
1469
|   |   |   `-- woodpeckers_controller.rb
 
1470
|   |   |-- helpers
 
1471
|   |   |   `-- woodpeckers_helper.rb
 
1472
|   |   `-- models
 
1473
|   |       `-- woodpecker.rb
 
1474
|   |-- db
 
1475
|   |   `-- migrate
 
1476
|   |       `-- 20081116181115_create_birdhouses.rb
 
1477
|   |-- yaffle
 
1478
|   |   |-- acts_as_yaffle.rb
 
1479
|   |   |-- commands.rb
 
1480
|   |   |-- core_ext.rb
 
1481
|   |   `-- routing.rb
 
1482
|   `-- yaffle.rb
 
1483
|-- pkg
 
1484
|   `-- yaffle-0.0.1.gem
 
1485
|-- rails
 
1486
|   `-- init.rb
 
1487
|-- tasks
 
1488
|   `-- yaffle_tasks.rake
 
1489
|-- test
 
1490
|   |-- acts_as_yaffle_test.rb
 
1491
|   |-- core_ext_test.rb
 
1492
|   |-- database.yml
 
1493
|   |-- debug.log
 
1494
|   |-- definition_generator_test.rb
 
1495
|   |-- migration_generator_test.rb
 
1496
|   |-- route_generator_test.rb
 
1497
|   |-- routes_test.rb
 
1498
|   |-- schema.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
 
1505
`-- uninstall.rb
 
1506
</shell>
 
1507
 
 
1508
h3. Changelog
 
1509
 
 
1510
"Lighthouse ticket":http://rails.lighthouseapp.com/projects/16213/tickets/32-update-plugins-guide
 
1511
 
 
1512
* November 17, 2008: Major revision by Jeff Dean