Contact

admin

About Me · Send mail to the author(s) E-mail · Twitter

At GROSSWEBER we practice what we preach. We offer trainings for modern software technologies like Behavior Driven Development, Clean Code and Git. Our staff is fluent in a variety of languages, including English.

Feed Icon

Tags

Open Source Projects

Archives

Blogs of friends

Now playing [?]

Error retrieving information from external service.
Audioscrobbler/Last.fm

ClustrMap

Page 1 of 1 in the Ruby category

Rake, YAML and Inherited Build Configuration

Posted in Build | Ruby at Saturday, 30 January 2010 15:03 W. Europe Standard Time

We’ve been using Rake for quite a while at work. Sometime last year I sat down and converted our ~30 KB NAnt build scripts to Rake, a light-weight Ruby build framework with low friction and no XML. Since then I have written a bunch of Rake tasks to support our builds (we use TeamCity).

I started a bit out of the blue, because frameworks like Albacore didn’t exist back then and other .NET-specific task collections didn’t fit our needs or simply were inconvenient to use.

Without prior Ruby experience it was also a great opportunity to learn Ruby and give the language and design concepts a spin. I have to admit, I like the fluent style of Ruby, it’s almost like the language tries to stay out of your way.

YAML

Soon after I started building the first Rake script I needed to configure the build for different environments. Like: in production, we have to use another database server. You want to externalize such information into a configuration file. Having database connection strings hard coded in your application’s App.config will make tailoring the application for deployment tedious and error-prone. I’ve been there, and I don’t recommend it!

I came across YAML which is an intuitive notation for configuration files (amongst others):

development:
  database:
    server: (local)
    name: Indigo

qa:
  database:
    server: DB
    name: Indigo_QA

production:
  database:
    server: DB
    name: Indigo_Production

Is that legible? I think so!

We use the configatron Ruby Gem to read such files and dereference configuration information in the build script.

configatron.configure_from_yaml 'properties.yml', :hash => 'production'

puts configatron.database.server
# => 'DB'

puts configatron.database.name
# => 'Indigo_Production'

YAML’s “Inheritance”

Another useful aspect of YAML is that it supports a simple form of inheritance by merging hashes.

qa: &customer_config
  database:
    server: DB
    name: Indigo_QA

production:
  <<: *customer_config
  database:
    name: Indigo_Production

Unfortunately this kind of inheritance has some subtleties as it wouldn’t work as you would expect. I read the snippet above like the production configuration inherits all values from qa and overwrites the database.name. Let's see:

configatron.configure_from_yaml 'properties.yml', :hash => 'production'

puts configatron.database.server.nil?
# => true
# Huh? That should be "DB".

puts configatron.database.name
# => 'Indigo_Production'

Actually, there is an article describing the problem with merging hashes in YAML files  that I found after our build broke in interesting ways after loading an incomplete configuration. The proposed solution is either to duplicate all configuration information between qa and production, or to use more anchors (&foo) and merge references (<<: *foo). I think both clutters a YAML file unnecessarily.

Custom Inheritance

After I identifying why composition doesn’t work as one would expect let’s see what we can do about it.

I went with solution based on a convention that inheritance should be defined using a default_to configuration entry.

qa:
  database:
    server: DB
    name: Indigo_QA

production:
  default_to: qa
  database:
    name: Indigo_Production

The default_to entry in the production section refers to another section that the configuration will be inherited from. You could also build inheritance chains like productionqadefault and additionally use ordinary transparent YAML hash merges.

Instead of initializing configatron from the YAML file, we’ll preprocess the deserialized YAML (basically, a Hash), evaluate the configuration inheritance chain and then pass the Hash to configatron:

yaml = Configuration.load_yaml 'properties.yml', :hash => 'production', :inherit => :default_to
configatron.configure_from_hash yaml

puts configatron.database.server.nil?
# => false

puts configatron.database.server
# => 'DB'

puts configatron.database.name
# => 'Indigo_Production'

The code for the Configuration class that accounts for evaluating the inheritance chain is up on GitHub.

Page 1 of 1 in the Ruby category