Cooking Up A Custom Rails 3 Template
Rails 3 templates allow you to set up your work environment just the way you want it. For example, I prefer jQuery to Prototype, Haml to erb templates, and these days am using RSpec for testing. I also use Compass with Sass and Factory Girl for fixture replacement. Repeating the steps required to swap out these parts every time I generate a new rails app gets old pretty quickly. Templates are plain ruby files that automate these type of setup tasks.
Rails 3 template files can install plugins or gems, run rake tasks or migrations and even place boilerplate code into any file you specify. So besides setting up your basic Rails environment, you can instruct your template file to set up a user system, for example, by automatically installing the devise gem and running its generate command.
Templates have been completely overhauled in Rails 3. Besides sporting more commands, templates now share the API with generators. This makes sense seeing that they both involve generating new files or modifying existing ones. The internals have been rebuilt and now leverage the Thor gem in the background.
Available Generator Methods
To take full advantage of the new Rails 3 template API, you need to be familiar with what methods are available to you. For reference purposes, it is also beneficial to know where these methods are defined. The more general purpose methods now live in the Thor::Actions class. Some of these are:
copy_file(source, *args, &block)create_file(destination, *args, &block)get(source, *args, &block)-- download files from a remote urlprepend_file(path, *args, &block)remove_file(path, config = {})run(command, config = {})-- used to run bash commands
Console Options
Thor::Shell::Basic provides several methods that enable you to capture and process user input:
ask(statement, color = nil)-- Example: "Which design framework? [none(default), compass]: "no?(statement, color = nil)-- Returns true if the user has answered the question with noyes?(statement, color = nil)-- Returns true if the user has answered the question with yes
Injecting Code Into Files
The Rails specific methods can be found in the module Rails::Generators::Actions. The following methods find and write to a particular file:
add_source(source, options={})-- adds a given source to the Gemfilegem(*args)-- Adds an entry for the given gem to the Gemfileroute(routing_code)-- adds a routeenvironment(data=nil, options={}, &block)-- insert line into application.rbapplication(data=nil, options={}, &block)-- Alias for environment
Creating Files
The methods below call Thor's create_file method under the hood. The second argument determines the content of the newly created file. This argument can either be a string or a block:
vendor(filename, data=nil, &block)-- Creates a file in the /vendor directorylib(filename, data=nil, &block)-- Creates a file in the /lib directoryrakefile(filename, data=nil, &block)-- Creates a rakefile in the /lib/tasks directoryinitializer(filename, data=nil, &block)-- Creates a file in the /config/initializers directory
For example, the following illustrates the rakefile method:
rakefile("bootstrap.rake") do
project = ask("What is the UNIX name of your project?")
<<-TASK
namespace :#{project} do
task :bootstrap do
puts "i like boots!"
end
end
TASK
end
rakefile("seed.rake", "puts 'im plantin ur seedz'")
Executing stuff
Finally there are methods that execute a command:
git(command={})-- Runs a command in gitgenerate(what, *args)-- Runs a generator from Rails or a plugin. script/rails generate #{what} [flattened args] is run in the backgroundrake(command, options={})-- Runs the supplied rake taskreadme(path)-- Reads the file at the given path and prints out its contentsplugin(name, options)-- Installs a plugin from github
Template Design
Now that we have an overview of the building blocks, we need to consider the base composition of the template. Because most templates are very opinionated in their choices of what to swap out, tweak, generate, etc., chances are slim that you will find one that perfectly suits your needs right out of the box. However, Rails 3 has made it very easy to customize existing templates for different setups without needing to create an entirely new one for each configuration.
Use Recipes for Modularity and Organization
Much of the flexibility for mixing and remixing template modules derives from the apply method in the Thor::Actions module.
# File 'lib/thor/actions.rb', line 191
def apply(path, config={})
verbose = config.fetch(:verbose, true)
is_uri = path =~ /^https?\:\/\//
path = find_in_source_paths(path) unless is_uri
say_status :apply, path, verbose
shell.padding += 1 if verbose
if is_uri
contents = open(path, "Accept" => "application/x-thor-template") {|io| io.read }
else
contents = open(path) {|io| io.read }
end
instance_eval(contents, path)
shell.padding -= 1 if verbose
end
The apply method takes a path variable which can point to either an external url or a local file. The contents of the file are read and executed using instance_eval. This allows you to separate out functionality you want to invoke into different component or "recipe" files. You make a directory to house each of the recipe files, e.g. one for "jquery", "cucumber", "haml", etc. In the template file you can then pick and choose which of these recipes you want to apply. Since it uses instance_eval, the apply method executes the content of the recipe file within the scope of the template file. This has the same effect as if you had placed the recipe code snippet directly into the template file.
As a basis for my own template, I forked an existing template on github that appealed to me because it was fairly simple and used this recipe approach. It came very close to the setup I needed, but more importantly, it afforded the flexibility for customizing the template.
Out of the box, this is the starting directory structure:
rails-templater
\_bootstrap.rb (to bootstap entire app building process using RVM gemset)
\_core_extensions.rb (extending Rails::Generators::Actions)
\_recipes (houses individual recipe files)
\_design.rb
\_default.rb
\_cucumber.rb
\_haml.rb
\_etc...
\_snippets (misc. snippets)
\_templater.rb (the actual template file)
\_templates
\_git (template for .gitignore file)
\_haml (template for views/layouts/application.html.haml in haml)
\_mongoid
To create a new Rails app based off of this template you would need to use the -m switch with the path to the templater.rb file. This is the actual template file that orchestrates all the things that happen after the default files and folders of the new app have been generated.
Defining And Modifying Recipes
In the main template file you can add new or remove existing recipes. The template code loops through the recipes you specify and applies each of them in turn.
required_recipes = %w(default jquery haml rspec factory_girl remarkable)
required_recipes.each {|required_recipe| apply recipe(required_recipe)}
What if you have recipes that you want to apply sometimes, but not other times? It is very easy to make a recipe optional or allow for live choices between different recipes.
The file core_extensions.rb extends the regular Rails::Generators::Actions with some custom methods. One of those additional methods is called load_options and this is where the template defines some choices and saves them to a Hash called @template_options:
# in core_extensions.rb
def load_options
@template_options[:design] = ask("Which design framework?
[none(default), compass]: ").downcase
@template_options[:design] = "none" if @template_options[:design].nil?
@template_options[:orm] = options["skip_active_record"] ? "mongoid" : "active_record"
end
In the template or recipe files this @template_options can later be queried to make decisions. In simpler cases, the same effect can also be accomplished with a simple yes/no question:
apply(recipe('cucumber')) if yes?("Do you want to some cukes?")
Delayed Code Execution Using Lambdas
Some of the code in the recipe files depends on the presence of certain gems, meaning that it can only be executed after the gem in question has been installed. However, the recipes are being read (applied) before the gems are loaded. So how can you define the gem dependent code in the recipe file, but only run it after the gem is available? Lambdas to the rescue! Take the example of the recipe for compass:
gem 'compass'
compass_sass_dir = "app/stylesheets"
compass_css_dir = "public/stylesheets/compiled"
compass_command = "compass init rails . --using blueprint/semantic --css-dir=#{compass_css_dir} --sass-dir=#{compass_sass_dir} "
stategies << lambda do
puts "Beginning Compass setup"
run compass_command
puts "Compass has been setup"
end
As you can see in the above code snippet, a piece of executable code is being added to the strategies array which was initialized in the beginning. After the template file runs its bundle install command, we invoke a method called execute_strategies that will loop through the lambdas originating from different recipes and call each of them in turn.
def execute_stategies
stategies.each {|stategy| stategy.call }
end
Using RVM
I love RVM (Ruby Version Manager). For each Ruby version installed, RVM allows a specific groups of gems (Gemsets) to be associated with a project. This is a highly useful feature as it ensures that the gems of one application are isolated from those of another.
To utilize the Gemset feature, these are the commands you need to run on the console before actually generating the Rails project:
$ rvm gemset create [app_name]
$ rvm gemset use [app_name]
$ gem install bundler
$ gem install rails
I wanted to automate these steps and somehow merge it into the template generation process. To accomplish this, I created a bootstrap.rb file that employs the RVM API.
rvm_lib_path = "#{`echo $rvm_path`.strip}/lib"
$LOAD_PATH.unshift(rvm_lib_path) unless $LOAD_PATH.include?(rvm_lib_path)
require 'rvm'
rvm_ruby = ARGV[0]
app_name = ARGV[1]
unless rvm_ruby
puts "\n You need to specify a which rvm ruby to use."
end
unless app_name
puts "\n You need to name your app."
end
@env = RVM::Environment.new(rvm_ruby)
puts "Creating gemset #{app_name} in #{rvm_ruby}"
@env.gemset_create(app_name)
puts "Now using gemset #{app_name}"
@env.gemset_use!(app_name)
puts "Installing bundler gem."
puts "Successfully installed bundler" if @env.system("gem", "install", "bundler")
puts "Installing rails gem."
puts "Successfully installed rails" if @env.system("gem", "install", "rails")
template_file = File.join(File.expand_path(File.dirname(__FILE__)), 'templater.rb')
system("rails new #{app_name} -JT -d mysql -m #{template_file}")
The bootstrap.rb file first runs the RVM setup steps and then loads the rails template file. So to create your new Rails app using RVM, simply run
$ ruby /path/to/rails-templater/bootstrap.rb [rvm-ruby] [name of new app]
If you'd like to use my template as a starting place for your own, you can get it here.



