demystifying rails plugin developmentptgmedia.pearsoncmg.com/.../ruby2008/.../plante.pdf ·...

47
Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby Conference November 18th, 2008

Upload: others

Post on 25-Jun-2020

1 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

Demystifying Rails Plugin Development

Nick Plante ::Voices That Matter

Professional Ruby Conference

November 18th, 2008

Page 2: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

Obligatory Introduction

Plugins are generalized, reusable code libraries

Extend or override core functionality of Rails

Can save you a lot of time

Provide standard hooks, helpers… Rails scripts, hooks Generators, etc

Page 3: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

vs.

core plugin

Page 4: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

but more like …

Page 5: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

Plugin Examples

User Authentication Restful Authentication, Closure

Pagination Will Paginate, Paginating Find

Asynchronous Processing Workling, Background Job, etc

View Helpers Lightbox helper, Flash media player helper, etc

Page 6: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

Why Develop Plugins?

Internal re-use, productivity boosterOpportunity to refactor, clean up

project code is_rateable vs. gobs of in-line ratings code

Contribute to the Ruby OSS ecosystem Get feedback, contributions, inspire

others Profit^h^h^h^h^h^h“Marketing”

Page 7: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

Is Plugin Development ‘Hard’?

Is developing software in Ruby/Rails hard?

Depends on what you’re trying to accomplish, right?

The plugins system itself is simple Writing plugins can be hard, but it doesn’t

have to be

Page 8: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

Plugin Genesis: Extraction

Most plugins don’t start out as plugins

Usually extracted & generalized from a useful feature created in a larger project

What interesting problems have you solved lately?

Page 9: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

Validating ISBNs

How would we implement thisin a Book modelfor a plain old Rails project?

ISBN-13: 978-1-59059-993-8ISBN-10: 1-59059-993-4

Page 10: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

class Book < ActiveRecord::Base validates_presence_of :title, :author, :isbn

def validate unless self.isbn_valid? errors.add(:isbn, "is not a valid ISBN code") end end

ISBN10_REGEX = /^(?:\d[\ |-]?){9}[\d|X]$/ ISBN13_REGEX = /^(?:\d[\ |-]?){13}$/

def isbn_valid? !self.isbn.nil? && (self.isbn10_valid? || self.isbn13_valid?) end

# ...end

Page 11: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

Wait! There’s More!

Page 12: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

# more code in your model…def isbn10_valid? if self.isbn.match(ISBN10_REGEX) isbn_values = self.isbn.upcase.gsub(/\ |-/, '').split('') check_digit = isbn_values.pop # last digit is check check_digit = (check_digit == 'X') ? 10 : check_digit.to_i

sum = 0 isbn_values.each_with_index do |value, index| sum += (index + 1) * value.to_i end

(sum % 11) == check_digit else false endend

Page 13: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

# and yet more code in your model…def isbn13_valid? if self.isbn.match(ISBN13_REGEX) isbn_values = self.isbn.upcase.gsub(/\ |-/, '').split('') check_digit = isbn_values.pop.to_i # last digit is check

sum = 0 isbn_values.each_with_index do |value, index| multiplier = (index % 2 == 0) ? 1 : 3 sum += multiplier * value.to_i end

(10 - (sum % 10)) == check_digit else false endend

Page 14: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

Your Model Code

Page 15: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

Extract!

Let’s clean up that messy model Encapsulation & Information Hiding Makes our model easier to read

We can move this code to a module in lib Or yank it all out into a plugin!

Either way, we need to build a validation module, right?

Page 16: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

Encapsulation

Page 17: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

Designing the Interface

We need a clean interface DON’T judge a book by its cover But DO judge a plugin by its interface KISS -- there is beauty in simplicity / minimalism

Goal is often to extend the Rails DSL in a natural way We have pre-existing examples to guide our hand ActiveRecord::Validations (see api.rubyonrails.org)

Page 18: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

ActiveRecord Examples

acts_as_list :scope => :todo_list

validates_http_url :link

is_indexed :fields => ['created_at', 'title’]

has_attached_file :cover_image, :styles => { :medium => "300x300>", :thumb => "100x100>” }

Page 19: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

ActionController Examples

class BooksController < ApplicationController sidebar :login, :unless => :logged_in? permit "rubyists and wanna_be_rubyists" include SomePluginModule def index @books = Book.paginate :page => params[:page], :per_page => 20 endend

Page 20: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

ActionView Examples (View Helpers)

<%= lightbox_link_to “My Link”, “image.png” %>

Page 21: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

class Book < ActiveRecord::Base validates_isbn :isbn, :with => :isbn13, :unless => :skip_validationend

A Little Cleaner, Right? Yeah.

Page 22: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby
Page 23: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

validates_isbn

We need to create a validates_isbn class method on ActiveRecord::Base Generalize our code a bit

No longer married to a particular model attribute

Hide it behind a Rails-ish DSL

Extend AR::Base with our own module

Page 24: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

module IsbnValidation # may want to namespace this def validates_isbn(*attr_names) config = { :message => "is not a valid ISBN code" } config.update(attr_names.extract_options!)

validates_each(attr_names, config) do |record,attr_name,value| valid = case config[:with] when :isbn10; validate_with_isbn10(value) when :isbn13; validate_with_isbn13(value) else validate_with_isbn10(value) || validate_with_isbn13(value) end

record.errors.add(attr_name, config[:message]) unless valid end end

# other methods, constants go here tooend

Page 25: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

Making of the Module

Why is it a module, and not a class? A module is like a degenerate abstract class You can mix a module into a class

Include with a module to add instance methods Extend with a module to add class methods

Also use modules for organization Group similar things together, namespacing

Page 26: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

Mixing It Up with Modules

Don’t have to stash this module in a plugin Can use it directly from lib, too…

require ‘isbn_validation’

class Book < ActiveRecord::Base extend IsbnValidation validates_isbn :isbnend

Page 27: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

Pluginizing

But why not go the extra step?

So you can easily reuse it across projectsAnd share with others

Page 28: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

Generate a Plugin Skeleton

Use the supplied plugin generator The less we have to do, the better!

Page 29: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

$ ruby script/generate plugin isbn_validation

in vendor/plugins/isbn_validation:- lib/

- isbn_validation.rb- tasks/

- isbn_validation_tasks.rake- test/

- isbn_validation_test.rb- README- MIT-LICENSE- Rakefile- init.rb- install.rb- uninstall.rb

Page 30: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

Plugin Hooks: Install.rb

Auto-run when plugin is installed via script/plugin install

Potential Uses Display README Copy needed images, styles, scripts

Remove them with uninstall.rb

Page 31: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

Plugin Hooks: Init.rb

Runs whenever your application is startedUse it to inject plugin code into the

framework Add class methods in IsbnValidation module

to AR::Base

ActiveRecord::Base.class_eval do extend IsbnValidation end

Page 32: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

Adding Instance Methods?

Use include instead of extend

self.included class method is special Executed when the module is mixed in with

include Gives us access to the including class Common Ruby idiom allows us to extend the

base class with a new set of class methods here, too

def self.included(base) base.extend(ClassMethods)end

Page 33: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

Should I Test My Plugin?

If you’re extracting a plugin, you probably already have tests for a lot of that functionality, right?

Page 34: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

Testing Strategies

Varies depending on the type of plugin Mock/stub out your environment if possible

Test the behavior of the system with plugin installed

Rather than the eccentricities of the plugin code itself

For model plugins, consider creating an isolated in-memory database (sqlite3)

Rake testing tasks already provided See Rakefile and sample test provided by

generator rake test:plugins

Page 35: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

Test Helper

$:.unshift(File.dirname(__FILE__) + '/../lib')RAILS_ROOT = File.dirname(__FILE__)

require 'rubygems'require 'test/unit'require 'active_record'require "#{File.dirname(__FILE__)}/../init"

config = YAML::load(IO.read( File.dirname(__FILE__) + '/database.yml'))ActiveRecord::Base.logger = Logger.new( File.dirname(__FILE__) + "/debug.log")ActiveRecord::Base.establish_connection( config[ENV['DB'] || 'sqlite3'])

load(File.dirname(__FILE__) + "/schema.rb") if File.exist?( File.dirname(__FILE__) + "/schema.rb")

Page 36: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

Dummy Test Models (models.rb)

class Book < ActiveRecord::Base validates_isbn :isbn, :message => 'is too fantastical!'end

class Book10 < ActiveRecord::Base set_table_name 'books' validates_isbn :isbn, :with => :isbn10end

class Book13 < ActiveRecord::Base set_table_name 'books' validates_isbn :isbn, :with => :isbn13end

Page 37: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

Unit Testing

require File.dirname(__FILE__) + '/test_helper'require File.dirname(__FILE__) + '/models'

class IsbnValidationTest < Test::Unit::TestCase def setup @book = Book.new end

def test_isbn10_should_pass_check_digit_verification @book.isbn = '159059993-4' assert @book.valid? end

# ...

end

Page 38: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

Rspec fan?

Use Pat Maddox’s RSpec plugin generator Uses RSpec stubs instead of Test::Unit Also sets up isolated database for you!

--with-database

Install the pluginruby script/generate rspec_plugin

isbn_validation

http://github.com/pat-maddox/rspec-plugin-generator

Page 39: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

Distributing Plugins

Use a publicly visible Subversion or Git repository.

It’s that easy.

Options: Google Code (Subversion) RubyForge (Subversion) GitHub (Git) <= Recommended!

Page 40: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

ruby script/plugin install \ git://github.com/zapnap/isbn_validation.git

Page 41: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

Distributing Plugins as Gems?

Can also package plugins as RubyGems In environment.rb:

config.gem “isbn_validation”, :source => “gems.github.com”, :version => “>= 0.1.0”

Then, to install it in the project: rake gems:install rake gems:unpack rake gems:unpack:dependencies

Page 42: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

Gem Advantages

Reasons to prefer Gems for packaging Proper versioning Dependency management

GitHub makes Gem creation easy Gems will be automatically created for

you Installable via gems.github.com

Page 43: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

Gemify!

GitHub workflow Create a rails/init.rb file in your repository Change original init.rb to include only:

require File.dirname(__FILE__) + ‘/rails/init.rb’

Create a Gemspec in the root of your repository Rake task to generate a Gemspec!

Check RubyGem box on GitHub project edit page

Can now install as either a RubyGem or a Plugin!

Page 44: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

spec = Gem::Specification.new do |s| s.name = %q{isbn_validation} s.version = "0.1.0" s.summary = %q{adds an isbn validation...} s.description = %q{adds an isbn validation...}

s.files = FileList['[A-Z]*', '{lib,test}/**/*.rb'] s.require_path = 'lib' s.test_files = Dir[*['test/**/*_test.rb']]

s.authors = ["Nick Plante"] s.email = %q{[email protected]}

s.platform = Gem::Platform::RUBY s.add_dependency(%q<activerecord>, [">= 2.1.2"])end

desc "Generate a gemspec file"task :gemspec do File.open("#{spec.name}.gemspec", 'w') do |f| f.write spec.to_ruby endend

Page 45: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

What Else?

Rake tasks Put them in plugin tasks directory

Note: this does not work for GemPlugins yet Namespace them appropriately

namespace :isbn do …

Automatically made available in the host Rails project’s list of Rake tasks

Test and rdoc tasks are free

Page 46: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

PDI

Every plugin will require different strategies for development & testing Model, Controller, View Plugins Plugins that generate code Plugins that wrap third party daemons & libraries

Fortunately, lots of OSS plugins to look to for examples -- no better way to learn! http://github.com http://agilewebdevelopment.com/plugins http://railslodge.com - http://railsify.com Good luck & don’t forget to let us know about your

new plugin!

Page 47: Demystifying Rails Plugin Developmentptgmedia.pearsoncmg.com/.../ruby2008/.../Plante.pdf · Demystifying Rails Plugin Development Nick Plante :: Voices That Matter Professional Ruby

Thanks!

Nick Plante. @zapnap

Partner, Software DeveloperUbikorp Internet Services

[email protected]://blog.zerosum.org

[email protected]://ubikorp.com

http://github.com/zapnap/isbn_validation