Upload
duongthuan
View
228
Download
0
Embed Size (px)
Citation preview
Extending Rails:Understanding and Building Plugins
Clinton R. Nixon
Welcome!
Welcoming robin by Ian-S (http://flickr.com/photos/ian-s/2301022466/)
What are we going to talk about?
The short
‣ How plugins work with Ruby on Rails
‣ How to find and install plugins
The long
‣ Types of plugins
‣ How to build plugins
Ruby on Rails and plugins
History of plugins:
‣ Introduced with Rails 1.0 as a way to extract functionality
‣ Made it easy to distribute functionality
‣ In Rails 2.0, some core features were pulled into plugins and some plugins pulled into core
‣ In Rails 2.1, gem dependencies introduced
Gem dependencies
Fulfills same role as a plugin
‣ Major disadvantage of plugins: no dependencies
‣ Your app can now depend on a gem
‣ That depends on other gems
Same techniques apply as a standard plugin
‣ Differences will be pointed out
Where do you find plugins?
Unfortunately, no simple answer
‣ Rails wiki
‣ Giant list of plugins
‣ Used by script/plugindiscover
‣ Lots of out of date information
Where do you find plugins?
My recommendations
‣ Agile Web Development
‣ Core Rails Plugins
‣ Technoweenie (Rick Olson)
How do you install plugins?
Install from a URL:
‣ script/plugininstallhttp://example.com/plugins/make_foo
‣ script/plugininstallsvn://code.mondu.org/svn/atom_fu/trunk
‣ script/plugininstallgit://github.com/bnl/acts_as_replicator.git
How do you install plugins?
Install by name:
‣ script/pluginsourcehttp://example.com/plugins/
‣ script/plugininstallmake_foo
Plugin installation sources
See list of sources:
‣ script/pluginsources
Remove source:
‣ script/pluginunsourcehttp://example.com/plugins/
Autodiscovering plugin sources
Scrape Rails wiki for sources:
‣ script/plugindiscover
vendor/plugins trivia
Plugins are installed, by default, in vendor/plugins. However:
‣ “plugins can be nested arbitrarily deep within an unspecified number of intermediary directories” - railties/lib/rails/plugin/locator.rb
‣ So, vendor/plugins/my_organization/acts_as/acts_as_long_dir_name/ is fine.
Additional plugin paths
More plugin paths can be defined as configuration.plugin_pathsinenvironment.rb‣ Overwrites default plugin paths
How do you install gem plugins?
Installing gem dependencies
‣ Add config.gem'gemname' to environment.rb
‣ rakegems:install makes sure gems are installed locally
‣ rakegems:unpack puts gems in vendor/gems
‣ Even better: rakegems:unpack:dependencies
‣ Recompilation: rakegems:build
Questions
Baby monkey (http://flickr.com/photos/7971389@N03/504227772/)
What types of plugins are there?
‣ acts_as...
‣ ...fu
‣ controller and view helpers
‣ testing helpers
‣ resourceful plugins
‣ piggyback plugins
acts_as... plugins
Adds capabilities to ActiveRecord models
‣ acts_as_versioned
‣ acts_as_paranoid
‣ acts_as_state_machine
‣ acts_as_taggable_on
...fu plugins
Adds new controller capabilities and back-end processing
‣ attachment_fu
‣ GeoKit
‣ BackgrounDRb
‣ Active Merchant
‣ Exception Notification
Helper plugins
Automate frequently repeated or complicated tasks; some crossover with ...fu plugins
‣ will_paginate
‣ Stickies
‣ jRails
‣ ssl_requirement
‣ TinyMCE for Rails
‣ permalink_fu
Testing plugins
Adds capabilities to testing in Rails
‣ Shoulda
‣ Factory Girl
‣ Test::Spec on Rails
‣ RSpec on Rails
Resourceful plugins
Plugins which contain a mini-app
‣ Savage Beast
‣ Comatose
‣ RESTful Authentication
‣ Bloget
‣ Sandstone
Piggyback plugins
Plugins that alter the behavior of other plugins
‣ Also known as “evil twin plugin”
‣ Usually not published
‣ But increasingly found on GitHub
Questions
Shinji the Hedgehog by Narisa (http://flickr.com/photos/narisa/508277874/)
What are the parts of a plugin?
‣ README
‣ about.yml
‣ install.rb
‣ uninstall.rb
‣ init.rb
‣ lib/
‣ Rakefile
‣ tasks/
‣ generators/
‣ test/‣ anything else you want to add
README and about.yml
about.yml:author: Clinton R. Nixonsummary: Adds ability to set foreign key constraints.description: "Adds ability to set foreign key constraints in the database through ActiveRecord migrations. Only works currently with MySQL, PostgreSQL, and SQLite."homepage: http://www.extendviget.com/plugin: git://github.com/vigetlabs/foreign_key_migrations.gitlicense: MITversion: 0.9rails_version: 2.0+
You can see this information with: script/plugininfoPLUGIN.
install.rb & uninstall.rb
Run automatically
‣ script/plugininstall
‣ script/pluginuninstall
Usually contains code to display instructions or move files
Often not found
puts IO.read(File.join(File.dirname(__FILE__), 'README'))
init.rb
Always run at Rails startup
Arbitrary Ruby code
Usually injects plugin code
ActionView::Helpers::AssetTagHelper::JAVASCRIPT_DEFAULT_SOURCES = \ ['jquery','jquery-ui','jrails']ActionView::Helpers::AssetTagHelper::reset_javascript_include_defaultrequire 'jrails'
lib/
Arbitrary Ruby code to be loaded
‣ models
‣ controllers
‣ modules
Added to require path
Because of Rails’ autoloading, all properly named files here will be available without require statements
Rakefile and tasks/
Rakefile contains tasks internal to the plugin
‣ Only executed from plugin directory
tasks/ contains tasks external to the plugin
‣ Available throughout the Rails environment
‣ in .rake files
generators/
Contains new generators that can be run in Rails
‣ script/generate and script/destroy
Both generator definitions and generator assets
Used to automate creation of models, controllers, views, migrations, and tests
test/
Tests for plugin
‣ raketest:plugins
‣ raketest:pluginsPLUGIN=plugin_name
‣ plugin Rakefile test task
Anything else you want to add
License files
Further instructions
Changelog
Contribution guidelines
Gemspecs
Todo lists
Asset files (JavaScript, images)
Questions
Baby Hippo by phalinn (http://flickr.com/photos/phalinn)
How do you create a plugin?
script/generateplugin createvendor/plugins/test_plugin/libcreatevendor/plugins/test_plugin/taskscreatevendor/plugins/test_plugin/testcreatevendor/plugins/test_plugin/READMEcreatevendor/plugins/test_plugin/MIT‐LICENSEcreatevendor/plugins/test_plugin/Rakefilecreatevendor/plugins/test_plugin/init.rbcreatevendor/plugins/test_plugin/install.rbcreatevendor/plugins/test_plugin/uninstall.rbcreatevendor/plugins/test_plugin/lib/test_plugin.rbcreatevendor/plugins/test_plugin/tasks/test_plugin_tasks.rakecreatevendor/plugins/test_plugin/test/test_plugin_test.rb
Plugins and metaprogramming
Modules
‣ include
‣ extend
alias_method and alias_method_chain
Using modules
Allows you to namespace your code
‣ Common idiom: YourName::YourPlugin::ModuleName
includeYourModule‣ adds methods to class instances
extendYourModule‣ adds methods to class
Common module inclusion idiommodule YourName::YourPlugin::YourModule def self.included(base) base.extend ClassMethods end def foo ... end module ClassMethods def bar ... end endend
User.send(:include, YourName::YourPlugin::YourModule)
>> user = User.new>> user.foo>> User.bar
Aliasing methods
Hook onto any method using this technique
def awesome_find ... old_find(params)end
alias_method :old_find, :findalias_method :find, :awesome_find
Kind of messy and unsustainable
alias_method_chain
def find_with_awesome(params) ... find_without_awesome(params)end
alias_method_chain :find, :awesome
# Equivalent to:# alias_method :find_without_awesome, :find# alias_method :find, :find_with_awesome
Multiple aliasingclass Finder def find puts "found" end
def find_with_awesome puts "AWESOME" find_without_awesome end
def find_with_humility find_without_humility puts "nothing, really" end
alias_method :find_without_awesome, :find alias_method :find, :find_with_awesome alias_method :find_without_humility, :find alias_method :find, :find_with_humilityend
>>> Finder.new.findAWESOMEfoundnothing, really
Multiple aliasingclass Finder def find puts "found" end
def find_with_awesome puts "AWESOME" find_without_awesome end
def find_with_humility find_without_humility puts "nothing, really" end
alias_method_chain :find, :awesome alias_method_chain :find, :humilityend
>>> Finder.new.findAWESOMEfoundnothing, really
Plugin initialization order
‣ Framework is initializated (This typo was too great to leave out)
‣ Environment is loaded
‣ Gem dependencies are loaded
‣ Plugins are loaded
‣ config/initializers/*.rb (application initializers) loaded
‣ after_initialize callback executed
‣ Routes and observers loaded
Plugin best practices
‣ Namespace your code
‣ Enhance, not override
‣ Leave choices to plugin users
‣ Do as little as possible
‣ Don’t do anything unexpected
Questions
Chloe’s Baby #2 by mdprovost (http://flickr.com/photos/anderani/2617606614/)
How do you setup a plugin?
install.rb - on installation
‣ only works when script/plugininstall used
init.rb - on application load
init.rb
Run every time the Rails environment is loaded
‣ script/server
‣ script/console
‣ script/runner
init.rb
For a small plugin, may be all you need
Should kick off most of your metaprogramming
Sometimes used to copy assets
‣ Don’t do this unless absolutely necessary
Should only do things you need to do every time your plugin is loaded
init.rb binding
def evaluate_init_rb(initializer) if has_init_file? silence_warnings do # Allow plugins to reference the current configuration object config = initializer.configuration eval(IO.read(init_path), binding, init_path) end endend
Gem plugins and init.rb
init.rb found under rails/ directory in a gem plugin
lib/ still added to load path
How do you work with models?
New models dropped in lib/ will automatically get picked up and be accessible - but are not reloaded in development.
Downside: user cannot easily extend model.
Model behavior in lib/
A solution:
‣ Put a module in lib/ to be included in a model class in app/models/
We can get assistance from generators if we require a class for our plugin to work.
‣ Example: acts_as_taggable_on vs. Bloget
Single table inheritance as a solution
You can drop a model in lib/ intended to be inherited from.
‣ DB table will need a type column
‣ STI has its own downsides
Adding behavior to multiple models
To affect all models automatically, include code in ActiveRecord::Base.
‣ Not very common usage
Granting ability to add behavior
AKA acts_as...
Include module in ActiveRecord::Base, but only add one method to trigger behaviormodule ActiveRecord::Acts::Versioned def self.included(base) base.extend ClassMethods end
module ClassMethods def acts_as_versioned(options = {}, &extension) ... end endendActiveRecord::Base.send :include, ActiveRecord::Acts::Versioned
acts_as_paranoid walkthrough
Baby croc by marcelgermain (http://flickr.com/photos/marcelgermain/1472148914/)
How do you work with controllers?
Like models, you can drop a controller in lib/ and it will work. Again, this has the drawback that the user cannot easily edit it.
Wrapping behavior in a module and including it is smart. Inheritance also works well.
Classes involved with controllers
ActionController::Base and ActionView::Base are the two classes to change.
ActionController::Base.send(:include, Stickies::ControllerActions)ActionController::Base.send(:include, Stickies::AccessHelpers)ActionView::Base.send(:include, Stickies::AccessHelpers)ActionView::Base.send(:include, Stickies::RenderHelpers)
A better way to handle helpers
Since Rails 0.8.5, there’s been a better way to add helpers to controllers and views.
ActionController::Base.send(:include, Stickies::AccessHelpers)ActionController::Base.helper(Stickies::AccessHelpers)ActionController::Base.helper(Stickies::RenderHelpers)
Adding behavior to controllers
Included modules are the most common way to add controller behavior.
resource_this walkthrough
You can use a setup method to add parameters to your behavior, allowing for complex controller manipulation.
How do you work with views?
Working with views is much like working with controllers.
Use ActionController::Base.helper to add new helpers.
Changing template root
class FooController < ActionController::Base self.template_root = \
File.join(File.dirname(__FILE__), '..', 'views') end
One problem with this: the controller will expect all views - including templates and partials - to be found under this directory.
Questions
Baby tiger (http://flickr.com/photos/modu_li/1788817738/)
How do you work with generators?
Generators can let you make models, controllers, or anything else to stick directly into the user’s Rails app.
Put assets and generation script in generators/.
Thanks to Brian Landau
Structure of generators/ directory
• generators/• plugin_name/
• plugin_name_generator.rb• templates/
• a_model.rb• a_controller.rb• some_views/
• view_file.html.erb• USAGE
Two types of generators
Rails::Generator::Base‣ No required argument
‣ More basic
Rails::Generator::NamedBase‣ First argument is a class name
‣ Extra attributes available in generation
‣ Use when you're creating a specific named object
Generator class structure
Should inherit from Base or NamedBase
Must define a manifest method
Can have a banner method
Can have an add_options! method
NamedBase attributes
name Blog::Commentorblog/comment
class_nesting
class_nesting_depth
class_path
file_path
class_name
plural_name
singular_name
table_name
Blog
1
['blog']
blog/comment
Blog::Comment
comments
comment
blog_comments
Manifest directives
‣ class_collisions
‣ directory
‣ file
‣ template
‣ migration_template
‣ route_resources
‣ readme
‣ dependency
USAGE
Description:Thecomatosegeneratorcreatesamigrationforthecomatosemodel.
Thegeneratortakesamigrationnameasitsargument.ThemigrationnamemaybegiveninCamelCaseorunder_score.'add_comatose_support'isthedefault.
Thegeneratorcreatesamigrationclassindb/migrateprefixedbyitsnumberinthequeue.
Example:./script/generatecomatoseadd_comatose_support
With4existingmigrations,thiswillcreateanComatosemigrationinthefiledb/migrate/005_add_comatose_support.rb
Generator templates
Use ERB to customize
‣ For ERB generating ERB: <%%[email protected]%>
‣ Generator methods available
‣ Unlike Rails’ views, generator instance variable not available
Programmatically running generators
Rails::Generator::Scripts::Generate.new.run( ['authenticated', 'user', 'sessions'])
How do you add new tasks?
No different from Rails
‣ Place Rake files in tasks/ named whatever.rake
The plugin Rakefile is only for Rake tasks run in the plugin directory.
‣ raketest
‣ rakerdoc
‣ rakererdoc
‣ rakeclobber_rdoc
How do you deal with other plugins?
Simple trick: all plugins in vendor/plugins are loaded in alphabetical order.
How do you test a plugin?
One way: require that your plugin is in a Rails app
‣ Easy
‣ Ugly
‣ Cannot specify Rails version
‣ May interfere with app classes
‣ Requires your plugin’s setup to be run
Running plugin tests
From plugin dir: raketest
‣ Does not automatically load Rails environment
‣ To load Rails env:
‣ require File.join(File.dirname(__FILE__), '../../../../test/test_helper')
Running plugin tests
From Rails app dir
‣ raketest:plugins
‣ raketest:pluginsPLUGIN=plugin_name
Running from Rails app dir does not run plugin’s Rakefile, so dependencies in there are not executed.
Standalone testing of plugins
Helper plugins may not need a Rails app
For other plugins, you can mock out a Rails app
‣ Generators may need a full directory
‣ Model plugins can get by with a test database
‣ Controller plugins may need very little
‣ But mocking out routing can be very hard
How do you package your plugin?
A public Subversion or Git repository lets users install with script/plugin.
To make a gem plugin, try Mr Bones.
PROJ.name = 'friend-feed'PROJ.authors = 'Clinton R. Nixon'PROJ.email = '[email protected]'PROJ.url = 'friend-feed.rubyforge.org'PROJ.dependencies = ['json']PROJ.version = FriendFeed::VERSION
Bloget walkthrough
Baby tapir (http://flickr.com/photos/su-lin/2087767962/)
Foreign Key Migrations walkthrough
Baby duck (http://flickr.com/photos/dizzygirl/437988363/)
RESTful Authentication walkthrough
Uganda Mbeya (http://flickr.com/photos/youngrobv/2347565498/)
Resources
‣ Peepcode’s Plugin Patterns by Andrew Stewart
‣ Addison-Wesley’s Shortcut Rails Plugins by James Adam
‣ Rick Olson: techno-weenie.net - tons of plugins and plugin building blog posts
‣ This talk: http://crnixon.org/talks/rails-plugins