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.
2. 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
3. Ruby and Rails notoriety...
• Productivity!
• Convention over configuration
• Usually sane defaults
• 80/20 rule
• But, can it get any better?
Josh Nichols technicalpickles.com
4. Oh yeah it can.
Josh Nichols technicalpickles.com
5. 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
7. You can be clever
too!
Josh Nichols technicalpickles.com
8. 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
9. What you’d probably want to
extract
• Model stuff
• View helper stuff
• Controller stuff
Josh Nichols technicalpickles.com
10. 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
11. 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
12. 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
13. 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
14. 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
15. 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
16. Toolbox: init.rb
• Rails will automatically load this
• Add your special sauce here
Josh Nichols technicalpickles.com
17. 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
18. 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
19. 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
20. Toolbox: Macro method
• Just a class method
• Include InstanceMethods
• Extend ClassMethods
• Whatever other class stuff you need to do
Josh Nichols technicalpickles.com
21. 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
22. 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
23. 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
25. 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
27. Case Study: content_given
View helpers, always included
http://github.com/technicalpickles/content_given
Josh Nichols technicalpickles.com
28. Case study: safety_valve
Controller stuff, opt in by including module
http://github.com/technicalpickles/safety_valve
Josh Nichols technicalpickles.com
29. Case study: has_markup
Model stuff, macro method
http://github.com/technicalpickles/has_markup
Josh Nichols technicalpickles.com
30. 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
31. Distributing: Gems
• Create a gemspec for your project
• Enable RubyGems for your repository
• http://hasmygembuiltyet.org/
Josh Nichols technicalpickles.com
32. 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
34. 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
35. 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
36. 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
37. Jeweler
Craft the perfect gem
http://github.com/technicalpickles/jeweler
Josh Nichols technicalpickles.com
38. Jeweler
• Rake tasks for creating and validating gemspec
• Rake tasks for bumping the version
• Will automatically write out updated gemspec
Josh Nichols technicalpickles.com
39. $ 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
40. 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