Using Sprout Generators

31 Dec 2010 – Luke Bayes

Generators have been rewritten from scratch for Project Sprouts v1. The new generators are much easier to test, distribute, and modify. This post is going to cover usage (or consumption) of existing generators, we’ll put together a second post where we go into more detail about creating generators.



Sprout Germinator image courtesy of Easy Green.

There are two kinds of generator, an Application generator and a Feature generator. The primary difference between them, is that a Feature generator expects to be run from a folder that contains a Gemfile. This is because feature generators can be easily overridden by gems that are loaded from the Gemfile.

For example, the default sprout-class generator calls a null TestClassGenerator that does nothing. But the AsUnit 4 Rubygem overrides the TestClassGenerator with one that generates an AsUnit 4 test case, and then calls the SuiteClassGenerator – which it also provides. Someone that is so inclined, could easily build a flexunit Rubygem that overrides the TestClassGenerator code so that it gets called instead of AsUnit (hint, hint).

The Application Generator

Let’s start out by demonstrating a few generators and talking about what they do. If you haven’t already, get the Flash SDK (>= 1.0.18) installed on your system:

# Install the ActionScript 3 / Flash / Flex bundle:
gem install flashsdk --pre

You can figure out what version you have installed with:

gem list flashsdk

Now, let’s see what generators we have available to us by running the following and then hitting the TAB key.

sprout-

You should see a collection of executables – something like:

sprout-as3        sprout-class      sprout-flex       sprout-generator  [etc]

We’re going to go ahead and create an ActionScript 3 project named, ‘SomeProject’ with the following:

sprout-as3 SomeProject

You should see something like the following in your terminal:

Created directory: ./SomeProject
Created file:      ./SomeProject/rakefile.rb
Created file:      ./SomeProject/Gemfile
Created directory: ./SomeProject/src
Created file:      ./SomeProject/src/SomeProject.as
Created file:      ./SomeProject/src/SomeProjectRunner.as
Created directory: ./SomeProject/assets
Created directory: ./SomeProject/assets/skins
Created file:      ./SomeProject/assets/skins/DefaultProjectImage.png
Created directory: ./SomeProject/lib
Created directory: ./SomeProject/bin

Let’s pretend we don’t like what just happened. Maybe we meant to call this OtherProject instead. All generator actions can easily be “un-executed” by appending the same command with the argument --destroy. Let’s try this now, by pressing the up-arrow in the terminal, to get the most recently executed command, and then add --destroy to the end.

sprout-as3 SomeProject --destroy

You should see something like:

Removed directory: ./SomeProject/bin
Removed directory: ./SomeProject/lib
Removed file:      ./SomeProject/assets/skins/DefaultProjectImage.png
Removed directory: ./SomeProject/assets/skins
Removed directory: ./SomeProject/assets
Removed file:      ./SomeProject/src/SomeProjectRunner.as
Removed file:      ./SomeProject/src/SomeProject.as
Removed directory: ./SomeProject/src
Removed file:      ./SomeProject/Gemfile
Removed file:      ./SomeProject/rakefile.rb
Removed directory: ./SomeProject

This removal is safe, in that it should only delete directories that the generator created, and it should only delete them if they’re empty. It should also only delete files if they’re identical to what would be generated.

Now, let’s run the generator again with ‘other_project’ for the input: (Note the snake case)

sprout-as3 other_project

You should see something like:

Created directory: ./other_project
Created file:      ./other_project/rakefile.rb
Created file:      ./other_project/Gemfile
Created directory: ./other_project/src
Created file:      ./other_project/src/OtherProject.as
Created file:      ./other_project/src/OtherProjectRunner.as
Created directory: ./other_project/assets
Created directory: ./other_project/assets/skins
Created file:      ./other_project/assets/skins/DefaultProjectImage.png
Created directory: ./other_project/lib
Created directory: ./other_project/bin

Notice how the snake case was respected for the project directory, but it was converted to camel case for the classes and files. This is one of the great features of the new generators. You can see how easy it is to do this by checking out the source file for the class generator.

The Feature Generator

Now, let’s go into the new project and make sure our gems are up to date:

cd other_project
bundle install

Create a new class using the fully-qualified name:

sprout-class utils.MathUtil

You should see output like:

Created directory: ./src/utils
Created file:      ./src/utils/MathUtil.as
Created directory: ./test/utils
Created file:      ./test/utils/MathUtilTest.as
Skipped existing:  ./test
Created file:      ./test/AllTests.as

Notice how a test case and test suite were created. These were only created because the project Gemfile loads the asunit4 gem.

Here’s what’s in that Gemfile:

source "http://rubygems.org"

gem "flashsdk", ">= 1.0.0.pre"
gem "asunit4", ">= 4.2.2.pre"

This Gemfile is how the ClassGenerator, TestClassGenerator and SuiteClassGenerator knew what to build. If you’re interested in learning more about Gemfiles in general, you should check out the GemBundler site.

Even though I’m sure no one would ever want to generate a class without an associated test case, let’s pretend we want to do just that. Since I can never remember exactly what parameters to use, let’s go ahead and run the generator with no arguments in order to see its help content:

sprout-class

You should see something like the following:

[ERROR - Sprout::Errors::MissingArgumentError] input is required and must not be nil

Usage: sprout-class [options]
        --help                       Display this help message
        --path=[STRING]              Default Description
    -d, --destroy                    Default Description
    -f, --force                      Default Description
    -q, --quiet                      Default Description
        --templates+=[a,b,c]         Default Description
    -i, --input=STRING               Default Description
    -p, --package=[STRING]           Default Description
    -a, --assets=[STRING]            Default Description
        --skins=[STRING]             Default Description
        --test=[STRING]              Default Description
    -l, --lib=[STRING]               Default Description
    -b, --bin=[STRING]               Default Description
    -s, --src=[STRING]               Default Description
    -t, --[no-]test-class            Default Description

Of course, now that we see the help output, we see that we could have run the generator with the --help parameter.

Notice how all of these say, “Default Description”, this is because I’d like to extract the rdoc comment directly from the source file in order to avoid duplication. I haven’t gotten around to this task, so if you’re interested in contributing, please fork and send a pull request.

Even though these directives don’t have descriptions, and we can experiment with some confidence thanks to the --destroy argument, I would still check my work into a Git repository before running any generators in an existing project.

In any case, let’s go ahead and try to run this class generator with the --no-test-class argument for a new class.

sprout-class utils.OtherUtil --no-test-class

You should see that the test case and test suite were not created:

Skipped directory: ./src/utils
Created file:      ./src/utils/OtherUtil.as

Working with Templates

The ability to easily customize and control generator templates is one of the main driving forces behind the generator rewrite. You can override a particular template for a single generator execution, a specific project, or globally for your system. Generator templates are simply named erb files – but allowances have been made for alternative template languages. Each template is loaded and rendered individually, so you you only need to replace those templates that you’re interested in.

The first thing you’ll want to figure out, is the name of the template you want to override. For the time being, you can find this out by checking the source code for the project that defined the generator. Following are some template folders to peruse:

Let’s assume we want to replace the template for the ClassGenerator, found in the Flash SDK. You can see on line 59 that the generator is looking for a template file named, ‘ActionScript3Class.as’.

This means that once we decide which scope to operate on, we need to place a file with the same name into the correct directory.

Execution Templates

Let’s start by replacing the template for a single generator execution. Let’s start with the contents of the original template.

package <%= package_name %>{

    public class <%= class_name %> {

        public function <%= class_name %>() {
        }
    }
}

While I’m sure you would never want to unsnuggle your brackets, let’s imagine your boss is going to force you to do it – just for this one file.

Create a new directory in your project (from above) called, “bossypoo” and within that directory, create a new file named ActionScript3Class.as, and paste the following contents into it.

/*********************
Copyright: Pointyhaired Bossypoo, 2010
*********************/
package <%= package_name %>

{

    public class <%= class_name %>

    {

        public function <%= class_name %>()
        {
        }
    }
}

Now run the class generator just like before, but provide the new, custom templates folder:

sprout-class utils.BossyUtil --templates+=bossypoo

You’ll have to forgive the extra carriage returns, as my ERB templating appears to be a little greedy with whitespace. After running the generator, you should see everything looks normal.

Now that approach is pretty neat, but we don’t want to have to remember to add that --templates argument every.single.time we run a generator. Which brings us to;

Project Templates

In addition to setting the --templates parameter manually, there are a number of locations that generators will scan for templates. You can get the definitive list of template paths for any generator that extends Sprout::Generator::Base by running the generator with the --show-template-paths parameter.

On my computer, this results in:

The following paths will be checked for templates:
  * --templates+=[value]
  * config/generators
  * vendor/generators
  * /Users/lbayes/Library/Sprouts/1.1/cache/generators
  * ENV['SPROUT_GENERATORS']
  * /Users/lbayes/.gem/ruby/1.9.2/gems/flashsdk-1.0.17.pre/lib/flashsdk/generators/templates

If you create either of the config/generators or vender/generators paths in your project directory and place that template in there, anytime you run the class generator, it will get picked up.

System Templates

If you look closely at the previous list, you’ll see that one of the folders is in the global Sprout cache, and another is an environmental variable. I hope you can figure out how to create a folder in the cache location. If you place a generator template here, it will be picked up for any Application generators, and any projects that don’t override the template in their own scope.

You can set the environmental variable in a number ways, one of which is on the command line like this:

sprout-class utils.MathUtil SPROUT_GENERATORS=relative/or/absolute/path/to/templates

Or you can set it within your .bash_profile or .bashrc file like:

export SPROUT_GENERATORS=/absolute/path/to/templates

You could also set it in any Ruby source file that gets loaded from your Gemfile. In Ruby, this would look like:

ENV['SPROUT_GENERATORS'] = /absolute/path/to/templates

Conclusion

This brings us to the end of the “Using Generators” tutorial. If you find anything here in error, please let us know, or better yet – fork, fix and send a pull request.

The next article in the series will describe how to create generators.