Extending Rails: Understanding and Building Plugins
Clinton R. Nixon
Extending Rails: Understanding and Building Plugins Clinton R. - - PowerPoint PPT Presentation
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
Clinton R. Nixon
Welcoming robin by Ian-S (http://flickr.com/photos/ian-s/2301022466/)
The short
The long
History of plugins:
some plugins pulled into core
Fulfills same role as a plugin
Same techniques apply as a standard plugin
Unfortunately, no simple answer
My recommendations
plugins/make_foo
svn/atom_fu/trunk
acts_as_replicator.git
plugins/
plugins/
Scrape Rails wiki for sources:
Plugins are installed, by default, in vendor/
number of intermediary directories” - railties/lib/ rails/plugin/locator.rb
acts_as_long_dir_name/ is fine.
More plugin paths can be defined as configuration.plugin_paths in environment.rb
Installing gem dependencies
Baby monkey (http://flickr.com/photos/7971389@N03/504227772/)
Adds capabilities to ActiveRecord models
Adds new controller capabilities and back-end processing
Automate frequently repeated or complicated tasks; some crossover with ...fu plugins
Adds capabilities to testing in Rails
Plugins which contain a mini-app
Plugins that alter the behavior of other plugins
Shinji the Hedgehog by Narisa (http://flickr.com/photos/narisa/508277874/)
about.yml:
author: Clinton R. Nixon summary: 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.git license: MIT version: 0.9 rails_version: 2.0+
You can see this information with: script/plugin info PLUGIN.
Run automatically
Usually contains code to display instructions
Often not found
puts IO.read(File.join(File.dirname(__FILE__), 'README'))
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_default require 'jrails'
Arbitrary Ruby code to be loaded
Added to require path Because of Rails’ autoloading, all properly named files here will be available without require statements
Rakefile contains tasks internal to the plugin
tasks/ contains tasks external to the plugin
Contains new generators that can be run in Rails
Both generator definitions and generator assets Used to automate creation of models, controllers, views, migrations, and tests
Tests for plugin
License files Further instructions Changelog Contribution guidelines Gemspecs Todo lists Asset files (JavaScript, images)
Baby Hippo by phalinn (http://flickr.com/photos/phalinn)
script/generate plugin create vendor/plugins/test_plugin/lib
create vendor/plugins/test_plugin/tasks create vendor/plugins/test_plugin/test create vendor/plugins/test_plugin/README create vendor/plugins/test_plugin/MIT‐LICENSE create vendor/plugins/test_plugin/Rakefile create vendor/plugins/test_plugin/init.rb create vendor/plugins/test_plugin/install.rb create vendor/plugins/test_plugin/uninstall.rb create vendor/plugins/test_plugin/lib/test_plugin.rb create vendor/plugins/test_plugin/tasks/test_plugin_tasks.rake create vendor/plugins/test_plugin/test/test_plugin_test.rb
Modules
alias_method and alias_method_chain
Allows you to namespace your code
include YourModule
extend YourModule
module YourName::YourPlugin::YourModule def self.included(base) base.extend ClassMethods end def foo ... end module ClassMethods def bar ... end end end User.send(:include, YourName::YourPlugin::YourModule) >> user = User.new >> user.foo >> User.bar
Hook onto any method using this technique
def awesome_find ...
end alias_method :old_find, :find alias_method :find, :awesome_find
Kind of messy and unsustainable
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
class 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_humility end >>> Finder.new.find AWESOME found nothing, really
class 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, :humility end >>> Finder.new.find AWESOME found nothing, really
loaded
Chloe’s Baby #2 by mdprovost (http://flickr.com/photos/anderani/2617606614/)
install.rb - on installation
init.rb - on application load
Run every time the Rails environment is loaded
For a small plugin, may be all you need Should kick off most of your metaprogramming Sometimes used to copy assets
Should only do things you need to do every time your plugin is loaded
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 end end
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.
A solution:
app/models/
We can get assistance from generators if we require a class for our plugin to work.
You can drop a model in lib/ intended to be inherited from.
To affect all models automatically, include code in ActiveRecord::Base.
AKA acts_as... Include module in ActiveRecord::Base, but
module ActiveRecord::Acts::Versioned def self.included(base) base.extend ClassMethods end module ClassMethods def acts_as_versioned(options = {}, &extension) ... end end end ActiveRecord::Base.send :include, ActiveRecord::Acts::Versioned
Baby croc by marcelgermain (http://flickr.com/photos/marcelgermain/1472148914/)
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.
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)
ActionController::Base.send(:include, Stickies::AccessHelpers) ActionController::Base.helper(Stickies::AccessHelpers) ActionController::Base.helper(Stickies::RenderHelpers)
Included modules are the most common way to add controller behavior.
You can use a setup method to add parameters to your behavior, allowing for complex controller manipulation.
Working with views is much like working with controllers. Use ActionController::Base.helper to add new helpers.
class FooController < ActionController::Base self.template_root = \ File.join(File.dirname(__FILE__), '..', 'views') end
Baby tiger (http://flickr.com/photos/modu_li/1788817738/)
Generators can let you make models, controllers,
Rails app. Put assets and generation script in generators/.
Thanks to Brian Landau
Rails::Generator::Base
Rails::Generator::NamedBase
Should inherit from Base or NamedBase Must define a manifest method Can have a banner method Can have an add_options! method
name Blog::Comment or blog/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
Description: The comatose generator creates a migration for the comatose model. The generator takes a migration name as its argument. The migration name may be given in CamelCase or under_score. 'add_comatose_support' is the default. The generator creates a migration class in db/migrate prefixed by its number in the queue. Example: ./script/generate comatose add_comatose_support With 4 existing migrations, this will create an Comatose migration in the file db/migrate/005_add_comatose_support.rb
Use ERB to customize
Rails::Generator::Scripts::Generate.new.run( ['authenticated', 'user', 'sessions'])
No different from Rails
The plugin Rakefile is only for Rake tasks run in the plugin directory.
Simple trick: all plugins in vendor/plugins are loaded in alphabetical order.
One way: require that your plugin is in a Rails app
From plugin dir: rake test
'../../../../test/test_helper')
From Rails app dir
Running from Rails app dir does not run plugin’s Rakefile, so dependencies in there are not executed.
Helper plugins may not need a Rails app For other plugins, you can mock out a Rails app
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 = 'crnixon@gmail.com' PROJ.url = 'friend-feed.rubyforge.org' PROJ.dependencies = ['json'] PROJ.version = FriendFeed::VERSION
Baby tapir (http://flickr.com/photos/su-lin/2087767962/)
Baby duck (http://flickr.com/photos/dizzygirl/437988363/)
Uganda Mbeya (http://flickr.com/photos/youngrobv/2347565498/)
building blog posts