Provide a mechanism for generating a Rails template that offers a number of 'blessed stacks'.
'Template developers' -- folks working on this project -- run rails_app_composer and choose which recipes and prefs to bake into the template.rb file it outputs.
This template.rb makes these recipes available to 'external application developers' -- folks wishing to jumpstart a new rails app by choosing, e.g. a Heroku stack vs an EC2 stack.
make clean template.rb
will ingest defaults.yml
and output a template.rb
file. All recipes named in defaults.yml
will be inlined (but not necessarily marked for execution) in the template.rb
.
The defaults.yml
is also used as the basis for a @prefs
hash.
The @prefs
hash is the basic means of controlling the flow of execution.
At rails new
time, decisions made by inline recipes will hinge on 1) the @prefs
hash baked into the template, combined with 2) answers obtained by prompting the application developer.
We'll pretend we want to add a recipe that describes our blessed Foo stack. We'll set webserver and email provider. We'll also decree that deployments to FooCloud shall include QuuxForm form-builder.
setup.rb
prefs[:stack] = multiple_choice "Choose your stack", [["Heroku", "heroku"], ["EC2", "ec2"], ["FooCloud", "foocloud"]] unless prefs.has_key? :stack
recipes/foocloud.rb
if prefer :stack, "foocloud"
prefs[:webserver] = "BarServer"
prefs[:email] = "BazMail"
prefs[:form_builder] = "QuuxForms"
end
__END__
name: foocloud
description: "Add blessed FooCloud options."
author: RailsApps + Relevance
requires: [setup]
run_after: [setup]
category: configuration
We'll skip showing the YAML matter on the next two recipes, since it's not relevant.
recipes/webserver.rb
if prefer :webserver, "BarServer"
gem 'barserver', '~> 1.1.1'
end
recipes/email.rb
if prefer :email, "BazMail"
gem 'bazmail, '~> 2.2.2'
copy_from_repo some_setup_file
end
recipes/formbuilder.rb
if prefer :form_builder, "QuuxForms"
gem 'quuxforms', '~> 3.3.3'
end
...
run_after: [setup, foocloud]
...
Note that the formbuilder recipe now must be told to run_after
the foocloud recipe, so that the decision made in foocloud can affect it.
The original codebase assumes a bedrock layer of in-house recipes in gems.rb
with the ability to layer in optional custom recipes via the /recipe directory.
We'd prefer to see all recipe logic, including built-in logic, exist as stand-alone recipes with a tiny bit of controller code in setup.rb
to handle any prompts. As we incrementally improve the code, we're taking the opportunity to untangle the older bits accordingly. For example, the database code in gems.rb
should be its own recipes/database.rb
recipe.
An unfortunate reality of Rails application templates is that the output template.rb
file works best as a single file, so the basic function of rails_app_composer is to spit the chosen code portions -- "recipes" -- into the resulting template.rb
, making them available to the application developer later down the road.
These recipes are then switched on and off at rails new
time via the prefs object. (See the 'Recipes' section for more detail.)
This prefs system is simple and effective but as we add more inter-recipe logic, avoiding brittle spaghetti code should be a chief concern.
Note that recipes become a shared namespace! Setting, say, a VERSION constant in recipe X would be a bad idea -- recipe Y may clobber it. Favor prepending the recipe name to any constants/variables for cheap namespacing.
Rails App Composer has some slightly different assumptions about the way the world needs to work. From Anatomy of a Recipe, this is a standard YAML prompt...
config:
- mars_test:
type: boolean
prompt: Do you also want to test your application on Mars?
if: space_test
if_recipe: mars_lander
Here, the 'mars_test' prompt will be skipped if the mars_lander recipe is available. This isn't that helpful; our typical need is to skip a prompt if the recipe was selected for use in the template.
That is, we don't care much about recipe availability (there's no good reason all recipes can't be available) -- we care about what the user has chosen from the recipes and how that should affect later recipes.
Our tack has been to avoid this YAML prompt facility entirely, using our own prompts within setup.rb
, and altering the prefs object from within a recipe to affect later recipes.
We have identified two worthwhile testing patterns.
Testing one or more recipes by driving the Rails App Generator code to actually generate a new Rails app with the given inputs, then asserting various of its contents.
PROS: Can test recipe combinations. This is important when, say, the Heroku recipe must dictate that no form-builder option be present. Uses built-in framework that includes some niceties like auto-clean-up. CONS: ~1 minute per generated app.
A set of tests running nightly on a CI server that generate a Rails App with particular inputs, then interacts with that app to ensure our expectations are met.
PROS: End-to-end testing. CONS: Trail not yet blazed. Will presumably require non-trivial amount of work up-front and potentially on-going for each new test.
Building new recipes is a fabulous opportunity to prove out these and other testing ideas.
if prefer :some_key
will evaluate to true even if the word "false"
was used in the defaults.yml
, as it will remain a string. The defaults are not currently opt-out friendly.
Testing new recipes that use the gem
command may cause template generation to fail with an error message similar to "Could not find gem 'omniauth-twitter (>= 0) ruby' in the gems available on this machine." A gem install [gem]
will fix this.
- Simple is better than complex.
- Try to put all decisions related to a question into one recipe.
** Use
after_bundler
andafter_everything
hooks for this.after_bundler
is for running generators and file manipulation.after_everything
is for running db migrations and other rake tasks. - Use
~>
for pinning most gems. Do not pin dev-only gems likepry
.