extending rails: understanding and building pluginsassets.en.oreilly.com/1/event/12/extending rails_...

Post on 10-Jun-2018

222 Views

Category:

Documents

0 Downloads

Preview:

Click to see full reader

TRANSCRIPT

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

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

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

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.

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: <%%=@post.name%>

‣ 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 = 'crnixon@gmail.com'PROJ.url = 'friend-feed.rubyforge.org'PROJ.dependencies = ['json']PROJ.version = FriendFeed::VERSION

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

top related