You are on page 1of 45

Demystifying Rails

Plugin Development

Nick Plante ::
Voices That Matter
Professional Ruby Conference

November 18th, 2008


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
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
Why Develop Plugins?
 Internal re-use, productivity booster
 Opportunity 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”
Plugin Genesis: Extraction
 Mostplugins don’t start out as
plugins

 Usuallyextracted & generalized from


a useful feature created in a larger
project

 What interesting problems have you


solved lately?
Validating ISBNs

How would we
implement this
in a Book model
for a plain old
Rails project?
ISBN-13: 978-1-59059-993-8
ISBN-10: 1-59059-993-4
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
Wait! There’s
More!
# 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
end
end
Getting Kind of
Messy…
# 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
end
end
Your Model Code
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!

 Eitherway, we need to build a


validation module, right?
Encapsulation
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

 Goalis 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)
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>” }
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
end
end
ActionView Examples (View
Helpers)

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


A Little Cleaner, Right? Yeah.

class Book < ActiveRecord::Base


validates_isbn :isbn,
:with => :isbn13,
:unless => :skip_validation
end
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


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 too


end
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
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 :isbn
end
Pluginizing
 But why not go the extra step?

 Soyou can easily reuse it across projects


 And share with others
Generate a Plugin Skeleton
 Use the supplied plugin generator
 The less we have to do, the better!
$ 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
Plugin Hooks: Install.rb
 Auto-run when plugin is installed
 via script/plugin install

 Potential Uses
 DisplayREADME
 Copy needed images, styles, scripts
 Remove them with uninstall.rb
Plugin Hooks: Init.rb
 Runs whenever your application is started
 Use 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
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
Should I Test My Plugin?

If you’re extracting a plugin, you


probably already have tests for
a lot of that functionality, right?
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
Test Helper
# vendor/plugins/isbn_validation/test/test_helper.rb
$:.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")
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 => :isbn10
end

class Book13 < ActiveRecord::Base


set_table_name 'books'
validates_isbn :isbn, :with => :isbn13
end
Unit Testing

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


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

class IsbnValidationTest < Test::Unit::TestCase


def setup
@book = Book.new
@book10 = Book10.new
end

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

# ...

end
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 plugin


ruby script/generate rspec_plugin isbn_validation

http://github.com/pat-maddox/rspec-plugin-generator
Distributing Plugins
 Use a publicly visible Subversion or Git
repository.
 It’s that easy.

 Options:
 Google Code (Subversion)
 RubyForge (Subversion)
 GitHub (Git) <= Recommended!
ruby script/plugin install \
git://github.com/zapnap/isbn_validation.git
Distributing Plugins as Gems?
 Canalso package plugins as
RubyGems
 In environment.rb:
config.gem “zapnap-isbn_validation”,
:source => “http://gems.github.com”,
:version => “>= 0.1.1”,
:lib => “isbn_validation”

 Then, to install it in the project:


 rake gems:install
 rake gems:unpack

 rake gems:unpack:dependencies
Gem Advantages
 Reasonsto prefer Gems for
packaging
 Properversioning
 Dependency management

 GitHub makes Gem creation easy


 Gems will be automatically created for
you
 Installable via gems.github.com
 Other helpful Gem creation tools: Echoe,
Hoe, Newgem
Gemify: GitHub Workflow
 Create a Gemspec in the root of your repository
 Tip: Create a Rake task to generate a Gemspec!
 Echoe can be handy for this, too

 Push updated code to repository


 Check RubyGem box on GitHub project edit
page

 Can now install as either a Gem or a Plugin!


spec = Gem::Specification.new do |s|
s.name = %q{isbn_validation}
s.version = "0.1.1"
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{nap@zerosum.org}

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
end
end
What Else?
 Rake tasks
 Made available in the host Rails project
 Test and rdoc tasks are free

 Generator plugins
 Plugin patterns
 alias_method_chain
Conclusion ( PDI )
 Everyplugin will require different strategies
for development & testing
 Don’t be afraid to read the Rails source

 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!
Thanks!

Nick Plante. @zapnap

Partner, Software Developer


Ubikorp Internet Services

nap@ubikorp.com
http://ubikorp.com

nap@zerosum.org
http://blog.zerosum.org

http://github.com/zapnap/isbn_validation

You might also like