Create a Custom Generator With Project Sprouts V1

Written by

I have finally managed to find the time to dig deeper into the world of Project Sprouts and am excited about the Version 1 release which hosts a collective of changes and improvements in comparison to its predecessor V 0.7. If your unaware of Project Sprouts then I actively encourage you to visit their homepage and have a read over what its all about.

Time saving utilities are crucial as a developer to help cut down on repetitive tedious tasks and also to help reduce errors. Applications such as Text Expander was one great time saving tool and then when I discovered live templates in IntelliJ IDEA, I truly saw the rewards of code generation, especially when combined with ANT for project builds. Project Sprouts takes time saving to a completely new level, accelerating project development workflow in a well structured and logical format.

What I was keen on understanding was where it would help me the most (initially at least) and that is project and class Generators (also read).

Sprout generators should be installed by RubyGems as command line applications on your system. After installing the flashsdk gem, you should have access to a variety of generators…..Some generators are expected to create new projects, others expect to run within existing projects.

Stray kindly hooked me up with her RobotLegs bundle to get me started (this is specific to V0.7 of Sprouts) and I also found the library by Kristofer Joseph which I understand Luke Bayes (Project Sprout creator) has assisted on also. Kristofer’s library has been updated for the V1 release of Project Sprouts and is in turn an excellent resource to understand the hooks required for using Generators in the latest Project Sprouts release (combined with reading the documentation and source of the Sprout-FlashSDK Gem).

I wanted to gain an understanding of building my own Generators and wanted to help detail the steps necessary to achieve this task. The rest of this post outlines how to create a custom class generator using a combination of command line (note I am on Mac OS X) and code editing (I use TextMate, feel free to choose your tool). l define a custom Event template and write a Generator for it, including simple Unit Tests and obviously a project example.

Pre-Requirements:

  • Download and install RVM: rvm.beginrescueend.com. This is not necessarily a requirement but saves a load of potential grief with permissions etc so is highly recommended.
  • Install follow instructions for ProjectSprouts setup: projectsprouts.org.

1. Create a new Ruby project using the sprout-ruby generator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Newtriks-MB:~ newtriks$ cd ~/Development/dev/

Newtriks-MB:dev newtriks$ sprout-ruby CustomGenerators
Created directory: ./custom_generators
Created file:      ./custom_generators/Gemfile
Created file:      ./custom_generators/Rakefile
Created file:      ./custom_generators/custom_generators.gemspec
Created directory: ./custom_generators/lib
Created file:      ./custom_generators/lib/custom_generators.rb
Created directory: ./custom_generators/lib/custom_generators
Created file:      ./custom_generators/lib/custom_generators/base.rb
Created directory: ./custom_generators/test
Created directory: ./custom_generators/test/fixtures
Created directory: ./custom_generators/test/unit
Created file:      ./custom_generators/test/unit/custom_generators_test.rb
Created file:      ./custom_generators/test/unit/test_helper.rb
Created directory: ./custom_generators/bin
Created file:      ./custom_generators/bin/custom-generators

2. CD into the newly created project and create a new Ruby Gem using sprout-generator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Newtriks-MB:dev newtriks$ cd custom_generators
 
Newtriks-MB:custom_generators newtriks$ sprout-generator MyEvent
[INFO] It seems you already have a Gemfile in this project, please be sure it has the following content:
 
  gem "sprout", ">= #{Sprout::VERSION::STRING}"
 
  group :development do
    gem "shoulda"
    gem "mocha"
  end
 
Skipped directory: ./bin
Created file:      ./bin/my-event
Skipped directory: ./lib
Skipped directory: ./lib/
Created directory: ./lib/generators
Created file:      ./lib/generators/my_event_generator.rb
Created directory: ./lib/generators/templates
Created file:      ./lib/generators/templates/MyEvent.as
Skipped directory: ./test
Skipped directory: ./test/unit
Created file:      ./test/unit/my_event_generator_test.rb
Skipped file:      ./test/unit/test_helper.rb
Skipped directory: ./test/fixtures
Created directory: ./test/fixtures/generators

3. Keep things as simple as we can for now and only unit test the custom Event generation, so remove the custom_generators_test:

1
rm ./test/unit/custom_generators_test.rb

4. Run a rake test (we know this will fail):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Newtriks-MB:custom_generators newtriks$ rake test
(in /Users/newtriks/Development/dev/custom_generators)
/Users/newtriks/.rvm/rubies/ruby-1.9.2-p136/bin/ruby -I"lib:test/unit" "/Users/newtriks/.rvm/gems/ruby-1.9.2-p136/gems/rake-0.8.7/lib/rake/rake_test_loader.rb" "test/unit/my_event_generator_test.rb"
Loaded suite /Users/newtriks/.rvm/gems/ruby-1.9.2-p136/gems/rake-0.8.7/lib/rake/rake_test_loader
Started
E
Finished in 0.006820 seconds.
 
  1) Error:
test: A new MyEvent generator should generate a new MyEvent. (MyEventGeneratorTest):
RuntimeError:  - 'This is your generator template' should include '(?-mix:Your content to assert here)'
    /Users/newtriks/.rvm/gems/ruby-1.9.2-p136/gems/sprout-1.1.7.pre/lib/sprout/test_helper.rb:232:in `assert_matches'
    test/unit/my_event_generator_test.rb:54:in `block (3 levels) in <class:MyEventGeneratorTest>'
    /Users/newtriks/.rvm/gems/ruby-1.9.2-p136/gems/sprout-1.1.7.pre/lib/sprout/test_helper.rb:186:in `assert_file'
    test/unit/my_event_generator_test.rb:51:in `block (2 levels) in <class:MyEventGeneratorTest>'
    /Users/newtriks/.rvm/gems/ruby-1.9.2-p136/gems/shoulda-2.11.3/lib/shoulda/context.rb:382:in `call'
    /Users/newtriks/.rvm/gems/ruby-1.9.2-p136/gems/shoulda-2.11.3/lib/shoulda/context.rb:382:in `block in create_test_from_should_hash'
    /Users/newtriks/.rvm/gems/ruby-1.9.2-p136/gems/mocha-0.9.10/lib/mocha/integration/mini_test/version_142_and_above.rb:27:in `run'
 
1 tests, 2 assertions, 0 failures, 1 errors, 0 skips
 
Test run options: --seed 12221
rake aborted!
Command failed with status (1): [/Users/newtriks/.rvm/rubies/ruby-1.9.2-p13...]
 
(See full trace by running task with --trace)

Open project in TextMate (or IDE of your choice):

1
Newtriks-MB:custom_generators newtriks$ mate .

EDIT WITHIN TEXTMATE

5. Create a simple method to test for class creation:

-> edit: test > unit > my_event_generator_test.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
should "generate a new MyEvent" do
    # provide example input:
    @generator.input = "MyEvent"
    @generator.execute
 
    input_dir = File.join @temp, "my_event"
    assert_directory input_dir
 
    input_file = File.join input_dir, "MyEvent.as"
    # test input dir exists
    assert_directory input_dir
    # test input create a class of the same name in input dir
    assert_file File.join(@temp, 'my_event', 'MyEvent.as')
end

/ END EDIT WITHIN TEXTMATE

6. Run the rake test again:

1
2
3
4
5
6
7
8
9
10
11
Newtriks-MB:custom_generators newtriks$ rake test
(in /Users/newtriks/Development/dev/custom_generators)
/Users/newtriks/.rvm/rubies/ruby-1.9.2-p136/bin/ruby -I"lib:test/unit" "/Users/newtriks/.rvm/gems/ruby-1.9.2-p136/gems/rake-0.8.7/lib/rake/rake_test_loader.rb" "test/unit/my_event_generator_test.rb"
Loaded suite /Users/newtriks/.rvm/gems/ruby-1.9.2-p136/gems/rake-0.8.7/lib/rake/rake_test_loader
Started
.
Finished in 0.006958 seconds.
 
1 tests, 3 assertions, 0 failures, 0 errors, 0 skips
 
Test run options: --seed 36007

7. Test a build and install, note does not show up in gemlist:

1
2
3
4
5
6
7
8
9
10
11
Newtriks-MB:custom_generators newtriks$ gem build custom_generators.gemspec
WARNING:  bin/custom-generators is missing #! line
  Successfully built RubyGem
  Name: custom_generators
  Version: 0.0.0.pre
  File: custom_generators-0.0.0.pre.gem
Newtriks-MB:custom_generators newtriks$ gem install --local custom_generators
Successfully installed custom_generators-0.0.0.pre
1 gem installed
Installing ri documentation for custom_generators-0.0.0.pre...
Installing RDoc documentation for custom_generators-0.0.0.pre...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Newtriks-MB:custom_generators newtriks$ gem list
 
*** LOCAL GEMS ***
 
archive-tar-minitar (0.5.2)
asunit4 (4.2.2.pre)
bundler (1.0.7)
custom (0.0.4.pre, 0.0.3.pre, 0.0.2.pre, 0.0.1.pre)
flashsdk (1.0.18.pre)
mocha (0.9.10)
open4 (1.0.1)
rake (0.8.7)
rubygems-update (1.4.2, 1.4.1)
rubyzip (0.9.4)
shoulda (2.11.3)
sprout (1.1.7.pre)

EDIT WITHIN TEXTMATE

8. Increase our RubyGem version number:

-> edit: lib > custom_generators.rb

1
VERSION = '0.0.1.pre'

/ END EDIT WITHIN TEXTMATE

9. Build the custom gem:

1
2
3
4
5
Newtriks-MB:custom_generators newtriks$ gem build custom_generators.gemspec
  Successfully built RubyGem
  Name: custom_generators
  Version: 0.0.0.pre
  File: custom_generators-0.0.0.pre.gem

10. Locally install our custom gem:

1
2
3
4
5
Newtriks-MB:custom_generators newtriks$ gem install --local custom_generators
Successfully installed custom_generators-0.0.1.pre
1 gem installed
Installing ri documentation for custom_generators-0.0.1.pre...
Installing RDoc documentation for custom_generators-0.0.1.pre...

Note it is now displaying in the local gem list:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Newtriks-MB:custom_generators newtriks$ gem list
*** LOCAL GEMS ***

archive-tar-minitar (0.5.2)
asunit4 (4.2.2.pre)
bundler (1.0.7)
custom_generators (0.0.1.pre)
flashsdk (1.0.18.pre)
mocha (0.9.10)
open4 (1.0.1)
rake (0.8.7)
rubygems-update (1.4.2, 1.4.1)
rubyzip (0.9.4)
shoulda (2.11.3)
sprout (1.1.7.pre)

EDIT WITHIN TEXTMATE

11. Start steps to extend the FlashSDK Gem, firstly define the dependency in the Gemfile:

-> edit: Gemfile

Add:

1
gem 'flashsdk', '>= 1.0.13.pre'

Ensure the actual generator includes the library:

-> edit: lib > custom_generators.rb

Add:

1
require 'flashsdk'

12. Define the custom generator template, here we will simply create a custom event making use of the package_name and class_name variables:

These package name and class name variables are processed by Sprouts using your arguments. There are several pre-processed variables you can use: full_class_name, class_name and package_name all use dot syntax (full_class_name is the package.Class value), while class_file and class_dir are path/file values relative to your source directory.

-> edit: lib > generators > templates > MyEvent.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package <%= package_name %>{
    import flash.events.Event;
 
    public class <%= class_name %> extends Event {
 
        public function <%= class_name %>(type:String,
                bubbles:Boolean=false,
                cancelable:Boolean=false) {
            super(type, bubbles, cancelable);
        }
 
        override public function clone():Event {
            return new <%= class_name %>(type, bubbles, cancelable);
        }
    }
}

-> edit: lib > generators > my_event_generator.rb

13. Update the logic within the custom event class generator, note extending the FlashSDK Gem

1
2
3
4
5
6
7
8
9
10
11
12
13
module CustomGenerators
  ##
  # This is where you describe your new Generator.
  class MyEventGenerator <FlashSDK Gem::ClassGenerator
 
    def manifest
        directory class_directory do
          template "#{class_name}.as", 'MyEvent.as'
        end
    end
 
  end
end

14. Amend the gem to include the FlashSDK Gem:

-> edit: bin > my-event

1
2
3
4
5
6
7
8
9
#!/usr/bin/env ruby

require 'rubygems'
require 'flashsdk'
require 'generators/my_event_generator'

generator = CustomGenerators::MyEventGenerator.new
generator.parse! ARGV
generator.execute

15. Increase the version number of the gem:

-> edit: lib > custom_generators.rb

1
VERSION = '0.0.2.pre'

16. Change our custom generator namespace:

-> edit: test > unit > my_event_generator_test.rb

Change:

1
2
# Instantiate the generator:
@generator        = Sprout::MyEventGenerator.new

To:

1
2
# Instantiate the generator:
@generator        = CustomGenerators::MyEventGenerator.new

17. Update the unit test method accordingly:

1
2
3
4
5
6
7
should "generate a new CustomEvent using MyEvent template" do
    # provide example input:
    @generator.input = "CustomEvent"
    @generator.execute
 
    assert_file File.join(@temp, 'src', 'CustomEvent.as')
end

/ END EDIT WITHIN TEXTMATE

18. Install required gems:

1
2
3
4
5
6
7
8
9
10
11
Newtriks-MB:custom_generators newtriks$ bundle install
Using rake (0.8.7) 
Using archive-tar-minitar (0.5.2) 
Using bundler (1.0.7) 
Using open4 (1.0.1) 
Using rubyzip (0.9.4) 
Using sprout (1.1.7.pre) 
Using flashsdk (1.0.18.pre) 
Using mocha (0.9.10) 
Using shoulda (2.11.3) 
Your bundle is complete! Use `bundle show [gemname]` to see where a bundled gem is installed.

19. Test the generator:

1
2
3
4
5
6
7
8
9
10
11
Newtriks-MB:custom_generators newtriks$ rake test
(in /Users/newtriks/Development/dev/custom_generators)
/Users/newtriks/.rvm/rubies/ruby-1.9.2-p136/bin/ruby -I"lib:test/unit" "/Users/newtriks/.rvm/gems/ruby-1.9.2-p136/gems/rake-0.8.7/lib/rake/rake_test_loader.rb" "test/unit/my_event_generator_test.rb"
Loaded suite /Users/newtriks/.rvm/gems/ruby-1.9.2-p136/gems/rake-0.8.7/lib/rake/rake_test_loader
Started
.
Finished in 0.037701 seconds.
 
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
 
Test run options: --seed 52964

20. Build the gem:

1
2
3
4
5
Newtriks-MB:custom_generators newtriks$ gem build custom_generators.gemspec
  Successfully built RubyGem
  Name: custom_generators
  Version: 0.0.2.pre
  File: custom_generators-0.0.2.pre.gem

21. Install the gem locally:

1
2
3
4
5
Newtriks-MB:custom_generators newtriks$ gem install --local custom_generators
Successfully installed custom_generators-0.0.2.pre
1 gem installed
Installing ri documentation for custom_generators-0.0.2.pre...
Installing RDoc documentation for custom_generators-0.0.2.pre...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Newtriks-MB:custom_generators newtriks$ gem list
*** LOCAL GEMS ***

archive-tar-minitar (0.5.2)
asunit4 (4.2.2.pre)
bundler (1.0.7)
custom_generators (0.0.2.pre, 0.0.1.pre)
flashsdk (1.0.18.pre)
mocha (0.9.10)
open4 (1.0.1)
rake (0.8.7)
rubygems-update (1.4.2, 1.4.1)
rubyzip (0.9.4)
shoulda (2.11.3)
sprout (1.1.7.pre)

22. Create a project to test the new generator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Newtriks-MB:custom_generators newtriks$ cd ../

Newtriks-MB:dev newtriks$ sprout-as3 EventTestProject
Created directory: ./EventTestProject
Created file:      ./EventTestProject/rakefile.rb
Created file:      ./EventTestProject/Gemfile
Created directory: ./EventTestProject/src
Created file:      ./EventTestProject/src/EventTestProject.as
Created file:      ./EventTestProject/src/EventTestProjectRunner.as
Created directory: ./EventTestProject/assets
Created directory: ./EventTestProject/assets/skins
Created file:      ./EventTestProject/assets/skins/DefaultProjectImage.png
Created directory: ./EventTestProject/lib
Created directory: ./EventTestProject/bin

Newtriks-MB:dev newtriks$ cd EventTestProject

23. Run my-event generator:

1
2
3
Newtriks-MB:EventTestProject newtriks$ my-event CustomEvent
Skipped directory: ./src
Created file:      ./src/CustomEvent.as

24. Destroy previous generator run logic:

1
2
3
4
Newtriks-MB:EventTestProject newtriks$ my-event CustomEvent --destroy
Removed file:                ./src/CustomEvent.as
Skipped remove directory:    ./src
Skipped remove directory:    .

25. Re-run my-event generator within a package structure:

1
2
3
Newtriks-MB:EventTestProject newtriks$ my-event com.newtriks.events.CustomEvent
Created directory: ./src/com/newtriks/events
Created file:      ./src/com/newtriks/events/CustomEvent.as

Successful custom generator generated Class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.newtriks.events {
  import flash.events.Event;

    public class CustomEvent extends Event {

        public function CustomEvent(type:String,
              bubbles:Boolean=false,
              cancelable:Boolean=false) {
          super(type, bubbles, cancelable);
        }
      
      override public function clone():Event {
          return new CustomEvent(type, bubbles, cancelable);
      }
    }
}

Further reading:

Gem Bundler: gembundler.com

Many thanks to @stray_and_ruby and @lukebayes for proof reading the code in this blog post.

Comments