If you are a Ruby or Rails developer, chances are you will run into a situation where you want to modify a gem that is on github. In this tutorial I will show you a real-world example of how I do this, using Jeweler as an example.
I like Jeweler.. I like it alot.. but I always end up creating a gem with it and then tweaking a bunch of files to turn it into a Rails 3 Engine. So I am attempting to integrate Rails 3 engines into Jeweler by adding a command line argument that will tell jeweler to create the engine directories and add the engine file templates.
Fork The Repo
First, I need to fork the repo so I can add my changes. I just login to my github account and go to the jeweler page and click the fork button. Now I have a repo under my account. Then I clone the repo on my dev box. In order to pull changes from the original repo when anything changes, I will need to add a remote upstream:
#clone my forked repo git clone git@github.com:johnmcaliley/jeweler.git #add upstream that points to original repo git remote add upstream git://github.com/technicalpickles/jeweler.git
Test Driven Development
I see that this project uses Cucumber for many of its tests. This time I decide to be a good boy and do some TDD from the get-go. The first thing I want to do is create a cuc feature and write 2 scenarios. The first is a scenario without the –rails3_engine argument. The second is with the argument. I used some of the steps from the cucumber.feature in order to get up and running.
In order to use rails 3 engines with jeweler generators
A user should be able to
generate a project setup and specify the rails3_engine option
Scenario: sans rails3 engine setup
Given a working directory
And I have configured git sanely
And I do not want to create a rails 3 engine
When I generate a project named ‘the-perfect-gem’ that is ‘zomg, so good’
Then a file named ‘the-perfect-gem/app’ is not created
And a file named ‘the-perfect-gem/app/controllers’ is not created
And a file named ‘the-perfect-gem/app/helpers’ is not created
And a file named ‘the-perfect-gem/app/models’ is not created
And a file named ‘the-perfect-gem/app/views’ is not created
And a file named ‘the-perfect-gem/lib/the-perfect-gem’ is not created
And a file named ‘the-perfect-gem/lib/the-perfect-gem/engine.rb’ is not created
And a file named ‘the-perfect-gem/lib/generators’ is not created
And a file named ‘the-perfect-gem/lib/the-perfect-gem/railties’ is not created
And a file named ‘the-perfect-gem/lib/the-perfect-gem/railties/tasks.rake’ is not created
Scenario: Rails 3 engine setup
Given a working directory
And I have configured git sanely
And I want to create a rails 3 engine
When I generate a project named ‘the-perfect-gem’ that is ‘zomg, so good’
Then a file named ‘the-perfect-gem/app’ is created
And a file named ‘the-perfect-gem/app/controllers’ is created
And a file named ‘the-perfect-gem/app/helpers’ is created
And a file named ‘the-perfect-gem/app/models’ is created
And a file named ‘the-perfect-gem/app/views’ is created
And a file named ‘the-perfect-gem/lib/the-perfect-gem’ is created
And a file named ‘the-perfect-gem/lib/generators’ is not created
And a file named ‘the-perfect-gem/lib/the-perfect-gem/railties’ is created
And a file named ‘the-perfect-gem/lib/the-perfect-gem/railties/tasks.rake’ is created
And ‘the-perfect-gem/lib/the-perfect-gem/railties/tasks.rake’ is blank
And a file named ‘the-perfect-gem/lib/the-perfect-gem/engine.rb’ is created
And ‘the-perfect-gem/lib/the-perfect-gem/engine.rb’ requires ‘the-perfect-gem’
And ‘the-perfect-gem/lib/the-perfect-gem/engine.rb’ requires ‘rails’
And ‘the-perfect-gem/lib/the-perfect-gem/engine.rb’ has a module based on the class name
And ‘the-perfect-gem/lib/the-perfect-gem/engine.rb’ subclasses rails engine
And ‘the-perfect-gem/lib/the-perfect-gem/engine.rb’ has placeholders for initializers and rake task inclusion
These scenarios have some steps that are not defined, so obviously they will fail. I need to add the missing step definitions.
##features/step_definitions/generator_steps.rb
Given /^I do not want to create a rails3 engine$/ do
@rails3_engine = false
end
Given /^I want to create a rails3 engine$/ do
@rails3_engine = true
end
# one line added here also:
When /^I generate a (.*)project named '((?:\w|-|_)+)' that is '([^']*)' and described as '([^']*)'$
#....
@use_cucumber ? '--cucumber' : nil,
@rails3_engine ? '--rails3_engine' : nil, # I added this
#....
end
Then /^'(.*)' has a module based on the class name$/ do |file|
content = File.read(File.join(@working_dir, @name, file))
assert_match %Q{module ThePerfectGem}, content
end
Then /^'(.*)' is blank$/ do |file|
content = File.read(File.join(@working_dir, @name, file))
assert_match "", content
end
Then /^'(.*)' subclasses rails engine$/ do |file|
content = File.read(File.join(@working_dir, @name, file))
assert_match "class Engine < Rails::Engine", content
end
Then /^'(.*)' has placeholders for initializers and rake task inclusion$/ do |file|
content = File.read(File.join(@working_dir, @name, file))
assert_match "#rake_tasks do", content
assert_match '#load "the-perfect-gem/railties/tasks.rake"', content
assert_match "#initializer 'the-perfect-gem.helper' do |app|", content
assert_match "#ActionView::Base.send :include, ThePerfectGemHelper", content
end
Then /^'(.*)' requires the engine$/ do |file|
content = File.read(File.join(@working_dir, @name, file))
assert_match 'PATH = File.dirname(__FILE__) + "/the-perfect-gem"', content
assert_match 'require "#{PATH}/engine.rb"', content
end
Dig Into The Existing Code
Next I need to figure out how this thing works and add my code.
The gem contains a binary file under /bin that includes the jeweler libraries and then fires off a generator that accepts arguments
require 'jeweler/generator' exit Jeweler::Generator::Application.run!(*ARGV)
The run class method is in lib/generator/application.rb. It does some checks on the options and then creates an instance of Jeweler::Generator and calls its run method
generator = Jeweler::Generator.new(options) generator.run
Make changes to generator.rb
Taking a closer look at the initialize method, I see that I will need to add something for the “rails3_engine” argument that I pass to the jeweler binary. I will add something similar to the cucumber option
self.should_use_cucumber = options[:use_cucumber] self.should_create_rails3_engine = options[:rails3_engine] # added this code
I also need to add an attribute accessor so the object can set this instance variable:
attr_accessor # a bunch of existing attr_accessors listed here.. append mine to end of list ,:should_create_rails3_engine
I think that will do for the initialize stuff. Next I look at the run instance method. It has a method called create_files. I will need to add a condition to this method so Jeweler can check for my rails3_engine option and create the appropriate files. Again I look at the cucumber option for inspiration, since it creates directories in the gem and adds files. Similar to the cucumber condition, the “rails3_engine” option will create some directories and output my engine template files into these directories.
if should_use_cucumber
mkdir_in_target features_dir
output_template_in_target File.join(%w(features default.feature)), File.join('features', feature_filename)
mkdir_in_target features_support_dir
output_template_in_target File.join(features_support_dir, 'env.rb')
mkdir_in_target features_steps_dir
touch_in_target File.join(features_steps_dir, steps_filename)
end
# here is what I added
if should_create_rails3_engine
mkdir_in_target app_dir
mkdir_in_target controllers_dir
mkdir_in_target models_dir
mkdir_in_target helpers_dir
mkdir_in_target views_dir
mkdir_in_target main_dir
output_template_in_target File.join('rails3_engine','engine_template.erb'), File.join(main_dir,'engine.rb')
mkdir_in_target generators_dir
mkdir_in_target railties_dir
touch_in_target File.join(railties_dir,'tasks.rake')
output_template_in_target File.join('rails3_engine','lib_engine_template.erb'), File.join(lib_dir,lib_filename)
end
I also need to add instance variables for the directories that are used in the rails3_engine option condition. Again, I look at what was coded for cucumber:
#check for a dash in the project name and convert to underscore so classify understands it
def project_class
self.project_name.gsub("-","_").classify
end
def app_dir
'app'
end
def controllers_dir
"#{app_dir}/controllers"
end
def models_dir
"#{app_dir}/models"
end
def views_dir
"#{app_dir}/views"
end
def helpers_dir
"#{app_dir}/helpers"
end
def main_dir
"#{lib_dir}/#{self.project_name}"
end
def generators_dir
"#{lib_dir}/generators"
end
def railties_dir
"#{main_dir}/railties"
end
Since I use the pluralize method in “project_class” I need to include active_support inflectors in the Jeweler class
require 'git' require 'erb' require 'active_support/inflector' # I added this one require 'net/http' require 'uri'
Add code to options.rb
One part that I notice is missing while looking at how cucumber is set up is the code in lib/jeweler/generator/options.rb.
o.on('--cucumber', 'generate cucumber stories in addition to the other tests') do
self[:use_cucumber] = true
end
# I added this
o.on('--rails3_engine', 'generate rails3 engine directories and files') do
self[:rails3_engine] = true
end
Add the file templates
generator.rb makes a few calls with methods like this:
output_template_in_target File.join('rails3_engine','engine_template.erb'), File.join(main_dir,'engine.rb')
I created a directory called “lib/jeweler/templates/rails3_engine” that will house my template files. Then I add 2 erb templates that will generate the source code files dynamically in the gem.
Here are the files:
##engine_template.erb
require "<%=require_name%>"
require "rails"
module <%=project_class%>
class Engine < Rails::Engine
#rake_tasks do
#load "<%=require_name%>/railties/tasks.rake"
#end
#initializer '<%=require_name%>.helper' do |app|
#ActionView::Base.send :include, <%=project_class%>Helper
#end
end
end
##lib_engine_template.erb
PATH = File.dirname(__FILE__) + "/<%=require_name%>"
require "#{PATH}/engine.rb"
The first file creates the engine.rb which is necessary to make our gem behave as a Rails 3 engine plugin. Using erb allows me to insert text dynamically based on the project name and the require directory which were set in generator.rb instance methods. The second file is just a simple template that requires engine.rb in the project.
Fire off them tests man!
This tutorial does not reflect what I actually did in development. I run my tests first, look at the failures, write code, refactor, then repeat until everything is green as a cuc. But to simplify, I am just showing that you need to run tests after you write your code. Jeweler uses a rake task to execute all the cuc scenarios, so I run this command:
rake features 103 scenarios (103 passed) 652 steps (652 passed)
Now I can see all of my scenarios as well as existing scenarios are all passing. Good to go!
I also updated the unit test, test_options.rb. But I am getting some errors relating to ‘rr’ -> “NotImplementedError: super from singleton method that is defined to multiple classes is not supported; this will be fixed in 1.9.3 or later”. I don’t think I will be able to run these tests until I get this resolved. Looks like it is an issue with ‘rr’ and ruby 1.9.2. I might try downgrading ruby using RVM and running from there.
Test with a live app
After the cucumber tests are passing, I want to look at this in a real Rails 3 app. So I create a new gem using my jeweler fork. I just execute the jeweler bin from my fork:
>cd jeweler >bin/jeweler myengine --rails3_engine create .gitignore create Rakefile create Gemfile create LICENSE.txt create README.rdoc create .document create lib create lib/myengine.rb create test create test/helper.rb create test/test_myengine.rb create app create app/controllers create app/models create app/helpers create app/views create lib/myengine create lib/myengine/engine.rb create lib/generators create lib/myengine/railties create lib/myengine/railties/tasks.rake create lib/myengine.rb Jeweler has prepared your gem in myengine
I add a controller and a view to the gem, create a gemspec and install my gem. Then I add the gem to a rails 3 app Gemfile (using :path=>/path/to/my/gem), bundle install, fire up the app server and take a look at the view that I added.
It appears, so I know that the Engine was initialized in the app. Good to go!
Here is the fork if you want to look at the code:
https://github.com/johnmcaliley/jeweler



