Extracting a Gem from your Rails app
So you got that big pile of useful code.
Why?
- Great open-source side project
- Will simplify your actual codebase
- Can bring collaborators to add features and fix bugs
- Fresh and new ideas
- Useful code for your future (or parallel) projects
- Reduce your test suite size
First gem?
- The implementation already exists
- Focus on the interface
- You already know the requirements and edge cases
- You already have an app to test it
Where to find useful code to extract?
- Custom validations
- Wrapper around an external API
- Additions or changes to Rails
- PORO modelising common things
This talk is a step by step guide to extract a gem from existing code
Based on my recent experience with Has_prerequisite
has_prerequisite
Authorization strategy to limit access to some parts of the application until some prerequisites are met.
Step 1:
Identify and move the key parts of your code.
Tests
Coupling
Dependencies
The tests
require 'rails_helper'
RSpec.describe Prerequisites, type: :controller do
let(:controller) do
Class.new(ActionController::Base) do
include Prerequisites
prerequisite :my_prerequisite, redirection_path: 'path', if: :condition
def index
render text: nil
end
private
def my_prerequisite
true
end
def condition
true
end
end.new
end
subject { get :index }
describe 'redirection' do
it 'does not interfere when the prerequisite is met' do
expect(controller).to receive(:my_prerequisite) { true }
expect(subject).to be_success
end
it 'it redirects to the path when the prerequisite is not met' do
expect(controller).to receive(:my_prerequisite) { true }
expect(subject).to redirect_to 'path'
end
end
describe 'conditionnal' do
it 'it supports an `if` option to skip the check' do
expect(controller).to receive(:condition) { false }
expect(controller).to_not receive(:my_prerequisite)
expect(subject).to be_success
end
end
context 'is a controller that skips the prerequisites' do
let(:controller) do
ActionController::Base.new do
include Prerequisites
prerequisite :pre, redirection_path: 'path'
fulfilling_prerequisite
def index
render text: nil
end
end
end
describe 'fulfilling_prerequisite' do
it 'skips the checks' do
expect(controller).to_not receive(:pre)
expect(subject).to be_success
end
end
end
end
- They exist!
- Tests are isolated from existing controllers
- Rails is all over the place
The code
module Prerequisites
extend ActiveSupport::Concern
included do
class_attribute :prerequisites, :skipping_checks
self.prerequisites = []
self.skipping_checks = false
rescue_from Prerequisites::PrerequisiteNotMet, with: :prerequisite_not_met!
end
module ClassMethods
def prerequisite(method, redirection_path: nil, **options)
prerequisites << { method: method, redirection_path: redirection_path, options: options }
end
def fulfilling_prerequisite
self.skipping_checks = true
end
end
def step_fulfilled!
redirect_to stored_location_for(:user)
end
private
def perform_checks
return if self.class.skipping_checks
return unless failing_preriquisite
store_location_for(:user, request.fullpath)
raise Prerequisites::PrerequisiteNotMet
end
def failing_preriquisite
@failing_preriquisite ||= prerequisites.find do |p|
!send(p[:method]) if p[:options][:if].nil? || send(p[:options][:if])
end
end
def prerequisite_not_met!
redirect_to redirect_path
end
def redirect_path
return send(failing_preriquisite[:redirection_path]) if failing_preriquisite[:redirection_path].is_a? Symbol
failing_preriquisite[:redirection_path]
end
class PrerequisiteNotMet < StandardError; end
end
- One module/one file
- Dependent on Rails and Devise
- Decoupled from the application's existing controllers
Step 2: (optional)
Refactor your code to move as much as possible to lib/
Decouple
Test in isolation
Break dependencies
Step 3:
Setup of the gem
Bundle gem [gemname]
- Code of conduct
- RSpec (minitest available)
- Travis-CI ready
Rails plugin new [gemname]
- Dummy rails app
- Minitest only
- Rake tasks setup
- Useful for Rails Engines
VS.
Small number of variations Gemspec, licence, readme, etc
I did both commands
Using an initial commit, a gem creation commit and staging changes step by step to handpick changes and remove duplication
> bundle gem has_prerequisite
> rails plugin new has_prerequisite \
--skip-test --dummy-path=spec/dummy
Checklist:
- Is it the licence you want?
- Code of conduct?
- Edit the Gemspec with the gems info
- Tests are running?
- Git with github/gitlab/bitbucket/whatever
Step 4:
Move the code!
Move the tests first
- Make sure all dev and test dependencies are in the Gemspec (rspec-rails, shoulda-matchers, its, etc)
- Some minor changes to the test setup are expected
- Make sure they run
- Make sure they fail properly
Move the implementation
- If step 2 was skipped, you might have broken dependencies
- Add them to the gemfile and require
- Implement components you don't want to be dependent on
Decisions
I decided to keep the dependency on Active Support but not Devise.
So I have to implement similar methods
Failures:
1) HasPrerequisite redirection it redirects to the path when the prerequisite is not met
Failure/Error: store_location_for(:user, request.fullpath)
NoMethodError:
undefined method `store_location_for' for #<#<Class:0x007f9d98a359e8>:0x007f9d9b931238>
# ./lib/has_prerequisite.rb:36:in `perform_checks'
# ./spec/has_prerequisite_spec.rb:43:in `block (2 levels) in <top (required)>'
# ./spec/has_prerequisite_spec.rb:55:in `block (3 levels) in <top (required)>'
Refactor
Add features
Refactor again
Railties
Hooks for initialisation
Railties
- Extend core classes
- Initialisers
- Set generators
Railties
module HasPrerequisite
class Railtie > ::Rails::Railtie
initializer "has_prerequisite.configure_view_controller" do |app|
ActiveSupport.on_load :action_controller do
include HasPrerequisite
end
end
end
end
Step 5:
Test locally
Test again?
- yes.
- Integration test (I have a dummy app!)
- Pack the gem locally
- Reference it from your application
Gemfile
gem 'has_prerequisite', path: '~/dev/has_prerequisite'
> bundle install
=> It will use your gem
> rspec
Note: If you changed the interface, you'll have to change the usage and maybe some tests
Step 6:
Publish!
Version number
I recommend following Semantic Versioning
MAJOR . MINOR . PATCH
Version number
- Production ready?
- Stable API?
When to publish 1.0.0 ?
Tip: you can always use the labels (1.0.0.beta, 1.0.0.rc.1, 1.0.0.pre, 1.0.0.racecar-1)
Use Git/Github features
- Readme for documentation
- Push hooks to test
- Issue tracker
- Projects
- Releases automatically tagged
- Use branches to merge and deploy bugfixes across versions
Rubygems.org
- Where gems magically come from
- Create an account
Private servers
-
Run your own with `gem server`
-
Gemfurry
-
Note: configure .gemspec to avoid accidental pushes to Rubygems.org
Publish!
→ bundle exec rake release
has_prerequisite 1.0.0.alpha built to pkg/has_prerequisite-1.0.0.alpha.gem.
Tagged v1.0.0.alpha.
Pushed git commits and tags.
Pushed has_prerequisite 1.0.0.alpha to rubygems.org.
Step 6:
Profit.
Step 6:
Maintenance
Bugs
New features
Pull requests
Never forget about the documentation
Tools
- CI servers are good friends
- Contributing guides are your friends
- Set expectations on PR (fully tested, code style)
- Use automated review tools (CodeClimate, Hound)
Don't burn out
Step 7:
Profit!
Thank you!
Questions?
Extracting a Gem from your Rails app
By Sophie Déziel
Extracting a Gem from your Rails app
Talk I gave for a Montreal.rb meetup
- 2,481