Rails Plugins and Ruby Gems are the basic mechanism of sharing functionality between multiple projects. This talk will go over extracting functionality into a plugin, testing it, sharing it, and converting it to a gem.
Overview
• The good of plugins
• Reasons for creating your own
• Overview of how to extract
• Tools for extracting
• Case studies
• Distributing
Josh Nichols technicalpickles.com
Ruby and Rails notoriety...
• Productivity!
• Convention over configuration
• Usually sane defaults
• 80/20 rule
• But, can it get any better?
Josh Nichols technicalpickles.com
Enter the third party
• Despite it’s goodness, Rails lacks a lot of functionality
you’d find in an webapp
• Pagination, Authentication, Etc
• Do you really want to have to implement this for every
app?
• The Internet is full of clever bastards that have figured
out good practices for these, and released them as
plugins and gems
Josh Nichols technicalpickles.com
You can be clever
too!
Josh Nichols technicalpickles.com
Why make your own plugin?
• Lets you clean up your code
• Focus on core business concerns, not the other stuff
• Re-use within your application
• DRY
• Re-use between applications
• Extract something useful out of one app...
• ... and have a headstart on the next
Josh Nichols technicalpickles.com
What you’d probably want to
extract
• Model stuff
• View helper stuff
• Controller stuff
Josh Nichols technicalpickles.com
Overview of extraction
• Recognize need/want for extracting
• Make sure the functionality you want to extract has
good coverage
• script/generate plugin to start a plugin
• Move code into the plugin
• Make your code use the plugin
• Make sure tests still pass
Josh Nichols technicalpickles.com
Overview of extraction
• Documentation
• RDoc, README
• Clean up plugin layout
• Test your plugin outside your application
• Pull out of your app
• Gem
Josh Nichols technicalpickles.com
Your toolbox:
• Modules
• Group related things
• Can’t create instances of them
• Can ‘mixin’ to classes
• ‘include’ a module into a class to add instance
methods
• ‘extend’ a module into a class to add class methods
Josh Nichols technicalpickles.com
module ClassMethods
def count()
3
end
end
module InstanceMethods
def yell(message)
puts quot;#{message.upcase}!!!!quot;
end
end
class Person
include InstanceMethods
extend ClassMethods
end
Person.count
@person = Person.new()
@person.yell quot;I don't know what we're yelling aboutquot;
Josh Nichols technicalpickles.com
Your toolbox: More modules
• In your module’s methods, you have access to everything
it was mixed into
• There’s a callback for when a module is included
• Gives you access to the class that included the module
• Use this to include/extend other modules
• ... or call class methods
Josh Nichols technicalpickles.com
module MyPlugin
def self.included(base)
base.class_eval do
include InstanceMethods
extend ClassMethods
validates_presence_of :awesome
end
end
module InstanceMethods
end
module ClassMethods
end
end
Josh Nichols technicalpickles.com
Toolbox: init.rb
• Rails will automatically load this
• Add your special sauce here
Josh Nichols technicalpickles.com
Thinking about how the plugin
would be used
• Make it always available
• Include some modules in init.rb
• Include a module
• include MyAwesomePlugin
• Macro method
• acts_as_awesome
Josh Nichols technicalpickles.com
Toolbox: Always include
• Usually do this in init.rb
• For Model:
• ActiveRecord::Base.class_eval { include MyPlugin }
• For Controller:
• ActionController::Base.class_eval { include MyPlugin }
• For View:
• ActionView::Base.class_eval { include MyPlugin }
Josh Nichols technicalpickles.com
Toolbox: Include a module
• Tell users to include in their classes
class User < ActiveRecord::Base
include Clearance::Models::User
end
class UsersController < ApplicationController
include Clearance::Controllers::UsersController
end
Josh Nichols technicalpickles.com
Toolbox: Macro method
• Just a class method
• Include InstanceMethods
• Extend ClassMethods
• Whatever other class stuff you need to do
Josh Nichols technicalpickles.com
module AwesomePlugin
def self.included(base)
base.class_eval do
extend MacroMethods
end
end
module MacroMethods
def acts_as_awesome()
include InstanceMethods
extend ClassMethods
validates_presence_of :awesome
end
end
module InstanceMethods
end
module ClassMethods
end
end
ActiveRecord::Base.class_eval { include AwesomePlugin }
Josh Nichols technicalpickles.com
Toolbox: Testing view stuff
• Use ActionView::TestCase
• Include the module
• Just call your methods, test the output
• For HTML stuff, assert_dom_equals
Josh Nichols technicalpickles.com
Tools: Testing model stuff
• Fake enough of the environment to get by
• Create ActiveRecord::Base migration to sqlite in
memory db
• Migrate a schema
Josh Nichols technicalpickles.com
Toolbox: Other testing
• Create a rails app within your plugin test layout
• test/rails_root
• Update Rakefile to run tests from within the test/
rails_root
Josh Nichols technicalpickles.com
Rakefile
test_files_pattern = 'test/rails_root/test/{unit,functional,other}/**/*_test.rb'
Rake::TestTask.new do |t|
t.libs << 'lib'
t.pattern = test_files_pattern
t.verbose = false
end
Josh Nichols technicalpickles.com
Case Study: content_given
View helpers, always included
http://github.com/technicalpickles/content_given
Josh Nichols technicalpickles.com
Case study: safety_valve
Controller stuff, opt in by including module
http://github.com/technicalpickles/safety_valve
Josh Nichols technicalpickles.com
Case study: has_markup
Model stuff, macro method
http://github.com/technicalpickles/has_markup
Josh Nichols technicalpickles.com
Distributing
• GitHub
• Free
• Easy to collaborate with others
• script/plugin install git://github.com/technicalpickles/
ambitious-sphinx.git
• Also supports generating RubyGems
Josh Nichols technicalpickles.com
Distributing: Gems
• Create a gemspec for your project
• Enable RubyGems for your repository
• http://hasmygembuiltyet.org/
Josh Nichols technicalpickles.com
Gem::Specification.new do |s|
s.name = %q{jeweler}
s.version = quot;0.1.1quot;
s.required_rubygems_version = Gem::Requirement.new(quot;>= 0quot;) if
s.respond_to? :required_rubygems_version=
s.authors = [quot;Josh Nicholsquot;, quot;Dan Croakquot;]
s.date = %q{2008-10-14}
s.description = %q{Simple and opinionated helper for creating Rubygem projects on
GitHub}
s.email = %q{josh@technicalpickles.com}
s.files = [quot;Rakefilequot;, quot;README.markdownquot;, quot;TODOquot;, quot;VERSION.ymlquot;, quot;lib/jewelerquot;, quot;lib/
jeweler/active_support.rbquot;, quot;lib/jeweler/bumping.rbquot;, quot;lib/jeweler/errors.rbquot;, quot;lib/
jeweler/gemspec.rbquot;, quot;lib/jeweler/singleton.rbquot;, quot;lib/jeweler/tasks.rbquot;, quot;lib/jeweler/
versioning.rbquot;, quot;lib/jeweler.rbquot;, quot;test/jeweler_test.rbquot;, quot;test/test_helper.rbquot;]
s.homepage = %q{http://github.com/technicalpickles/jeweler}
s.require_paths = [quot;libquot;]
s.rubygems_version = %q{1.2.0}
s.summary = %q{Simple and opinionated helper for creating Rubygem projects on GitHub}
end
Josh Nichols technicalpickles.com
Distributing: Versioning
• Update gemspec
• Update files
• Push to github
• Kinda annoying to maintain files
• Can maintain it with Rake
• Give Gem::Spec Rake’s FileList to generate list of file
• Write the spec out
Josh Nichols technicalpickles.com
spec = Gem::Specification.new do |s|
s.name = quot;shouldaquot;
s.version = Thoughtbot::Shoulda::VERSION
s.summary = quot;Making tests easy on the fingers and eyesquot;
s.homepage = quot;http://thoughtbot.com/projects/shouldaquot;
s.rubyforge_project = quot;shouldaquot;
s.files = FileList[quot;[A-Z]*quot;, quot;{bin,lib,rails,test}/**/*quot;]
s.executables = s.files.grep(/^bin/) { |f| File.basename(f) }
s.has_rdoc = true
s.extra_rdoc_files = [quot;README.rdocquot;, quot;CONTRIBUTION_GUIDELINES.rdocquot;]
s.rdoc_options = [quot;--line-numbersquot;, quot;--inline-sourcequot;, quot;--mainquot;, quot;README.rdocquot;]
s.authors = [quot;Tammer Salehquot;]
s.email = quot;tsaleh@thoughtbot.comquot;
s.add_dependency quot;activesupportquot;, quot;>= 2.0.0quot;
end
desc quot;Generate a gemspec file for GitHubquot;
task :gemspec do
File.open(quot;#{spec.name}.gemspecquot;, 'w') do |f|
f.write spec.to_ruby
end
end
Josh Nichols technicalpickles.com
Distributing: Versioning
• Update Rakefile’s Gem::Specification’s version
• Run ‘rake gemspec’
• Commit and push
• Easy to forget to keep Rakefile and gemspec in sync
• Can it get easier?
Josh Nichols technicalpickles.com
Jeweler
Craft the perfect gem
http://github.com/technicalpickles/jeweler
Josh Nichols technicalpickles.com
Jeweler
• Rake tasks for creating and validating gemspec
• Rake tasks for bumping the version
• Will automatically write out updated gemspec
Josh Nichols technicalpickles.com
$ rake version
(in /Users/nichoj/Projects/jeweler)
Current version: 0.1.1
$ rake gemspec
(in /Users/nichoj/Projects/jeweler)
Generated: jeweler.gemspec
jeweler.gemspec is valid.
$ rake version:bump:minor
(in /Users/nichoj/Projects/jeweler)
Current version: 0.1.1
Wrote to VERSION.yml: 0.2.0
Generated: jeweler.gemspec
$ rake version:bump:patch
(in /Users/nichoj/Projects/jeweler)
Current version: 0.2.0
Wrote to VERSION.yml: 0.2.1
Generated: jeweler.gemspec
$ rake version:bump:major
(in /Users/nichoj/Projects/jeweler)
Current version: 0.2.1
Wrote to VERSION.yml: 1.0.0
Generated: jeweler.gemspec
Josh Nichols technicalpickles.com
Rakefile
begin
require 'rubygems'
require 'jeweler'
gemspec = Gem::Specification.new do |s|
s.name = quot;has_markupquot;
s.summary = quot;Manage markup close to home... right in the model! Caching, validation,
etcquot;
s.email = quot;josh@technicalpickles.comquot;
s.homepage = quot;http://github.com/technicalpickles/has_markupquot;
s.description = quot;Manage markup close to home... right in the model! Caching,
validation, etcquot;
s.authors = [quot;Josh Nicholsquot;]
s.files = FileList[quot;[A-Z]*.*quot;, quot;{generators,lib,test,spec}/**/*quot;]
end
Jeweler.craft(gemspec)
rescue LoadError
puts quot;Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler
-s http://gems.github.comquot;
end
Josh Nichols technicalpickles.com