Cooking Up A Custom Rails 3 Template

by Andrea Singh | October 11, 2010

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 url
  • prepend_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 no
  • yes?(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 Gemfile
  • gem(*args) -- Adds an entry for the given gem to the Gemfile
  • route(routing_code) -- adds a route
  • environment(data=nil, options={}, &block) -- insert line into application.rb
  • application(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 directory
  • lib(filename, data=nil, &block) -- Creates a file in the /lib directory
  • rakefile(filename, data=nil, &block) -- Creates a rakefile in the /lib/tasks directory
  • initializer(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 git
  • generate(what, *args) -- Runs a generator from Rails or a plugin. script/rails generate #{what} [flattened args] is run in the background
  • rake(command, options={}) -- Runs the supplied rake task
  • readme(path) -- Reads the file at the given path and prints out its contents
  • plugin(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.