Vawks Clamantis

Screaming at the top of my fingers.

Configuring With Confidence

In my previous post on caching with Hapi, I showed this example of how to get your redis cache to work both locally and in production.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var cache_cfg;

if (process.env.REDISTOGO_URL) {
  var rtg = require("url").parse(process.env.REDISTOGO_URL)
  cache_cfg = {
    engine: 'catbox-redis',
    host: rtg.hostname,
    port: rtg.port,
    password: rtg.auth.split(":")[1]
  };
} else {
  cache_cfg = 'catbox-redis';
}

var server = Hapi.createServer('localhost', 8000, {
  cache: cache_cfg
});

Then I said that there are “better ways to write environment-specific configuration”.

Well, I wasn’t just selling you a load of salve. There are better ways!

Let’s take at look at Confidence, Hapi’s config module.

Caveat Lector

The code above doesn’t actually distinguish between local and production environments. It just checks to see if a REDISTOGO_URL variable is defined in the current environment. If it is, the code uses the RedisToGo config. Otherwise it uses the default redis config.

In the following example, we’ll make our app only use RedisToGo in production, which is arguably less useful than what we already had.

A Short Example

Confidence lets you load up a JSON object and then query that object using a slash-separated path structure instead of the normal dot-separated JavaScript syntax.

If this was all Confidence did, it would be useless. But when you retrieve a key from Confidence, you can specify a criteria object which is used to filter the data before returning it. You’ll most often use the $filter helper to match against specific values, but you can also use the $range helper to match an integer value against various buckets (good for AB tests!).

Let’s define a quick config module that loads our data into a Confidence store and then provides a simple accessor. Put the following in a file called config.coffee:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Confidence = require 'confidence'

rtg_url = require("url").parse(process.env.REDISTOGO_URL) if process.env.REDISTOGO_URL

store = new Confidence.Store
  cache:
    $filter: 'env'
    production:
      engine: 'catbox-redis',
      host: rtg_url?.hostname
      port: rtg_url?.port
      password: rtg_url?.auth.split(":")[1]
    $default: 'catbox-redis'

criteria =
  env: process.env.ENVIRONMENT

exports.get = (key)->
  store.get key, criteria

I’m using CoffeeScript for these examples, because writing JSON is painful. There is a JavaScript version of the completed code at the end of the article.

We pass a document hash to Confidence.Store constructor. This is our starting data, which gets filtered based on the criteria we define. You can also use the store’s .load method to clear out the existing data and load up new data.

We use a criteria hash with a single key. It maps the criteria key env to the environment variable ENVIRONMENT. This criteria key name is referenced in the starting data on line 7: $filter: 'env'.

We export a get method which simplifies the process of retrieving data from the Confidence store by automatically applying our criteria.

We can now use this module anywhere in our app. Put the following in a file named try_confidence.coffee:

1
2
3
config = require './config'

console.dir config.get('/cache')

NOTE: Don’t forget that leading slash when you get values out of Confidence!

If you don’t already have a package.json file, create one with npm init, and then install the Confidence module with npm install confidence --save.

If we run the script without setting up any environment variables, we get a string back:

1
2
$ coffee try_confidence.coffee
'catbox-redis'

NOTE: If you get unexpected output, check your local environment by running env.

If we set the ENVIRONMENT and REDISTOGO_URL variables, we get the production config object:

1
2
3
4
5
$ ENVIRONMENT=production REDISTOGO_URL=redis://redistogo:password123@carrot.redistogo.com:1234/ coffee try_confidence.coffee
{ engine: 'catbox-redis',
  host: 'carrot.redistogo.com',
  port: '1234',
  password: 'password123' }

It’s worth noting that Confidence strips null keys from the returned data, so if we don’t specify REDISTOGO_URL, we get:

1
2
$ ENVIRONMENT=production  coffee try_confidence.coffee
{ engine: 'catbox-redis' }

Adding More

Let’s make our example a little bit more useful by adding some database config as well:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Confidence = require 'confidence'

rtg_url = require("url").parse(process.env.REDISTOGO_URL) if process.env.REDISTOGO_URL

store = new Confidence.Store
  cache:
    $filter: 'env'
    production:
      engine: 'catbox-redis',
      host: rtg_url?.hostname
      port: rtg_url?.port
      password: rtg_url?.auth.split(":")[1]
    $default: 'catbox-redis'
  database:
    $filter: 'env'
    production: 'foo_production'
    staging: 'foo_staging'
    development: 'foo_development'

criteria =
  env: process.env.ENVIRONMENT

exports.get = (key)->
  store.get key, criteria

And update try_confidence.coffee:

1
2
3
4
config = require './config'

console.dir config.get('/cache')
console.dir config.get('/database')

We have not defined a $default value for the database key, so if we don’t specify an environment, our /database value is undefined. Confidence does not throw an error when you get an undefined key:

1
2
3
$ coffee try_confidence.coffee
'catbox-redis'
undefined

If we specify an environment, we get the correct data:

1
2
3
$ ENVIRONMENT=development coffee try_confidence.coffee
'catbox-redis'
'foo_development'

If you’re defining a filter, specify a $default key unless you have a really good reason for doing otherwise.

Nesting

You can include filters at any level. We could rewrite the previous example so that we don’t repeat $filter: 'env':

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
27
28
Confidence = require 'confidence'

rtg_url = require("url").parse(process.env.REDISTOGO_URL) if process.env.REDISTOGO_URL
rtg_config =
  engine: 'catbox-redis',
  host: rtg_url?.hostname
  port: rtg_url?.port
  password: rtg_url?.auth.split(":")[1]

store = new Confidence.Store
  $filter: 'env'
  production:
    cache: rtg_config
    database: 'foo_production'
  staging:
    cache: rtg_config
    database: 'foo_staging'
  development:
    cache: 'catbox-redis'
    database: 'foo_development'
  $default:
    cache: 'catbox-redis'

criteria =
  env: process.env.ENVIRONMENT

exports.get = (key)->
  store.get key, criteria

In this case, the result is longer, contains more duplication and is generally less readable. FAIL! But if you have a couple config options which need to remain in lockstep across various platforms, this could actually make your config easier to read.

Javascript examples

config.js:

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
27
28
29
30
31
32
33
34
var Confidence, criteria, rtg_url, store;

Confidence = require('confidence');

if (process.env.REDISTOGO_URL) {
  rtg_url = require("url").parse(process.env.REDISTOGO_URL);
}

store = new Confidence.Store({
  cache: {
    $filter: 'env',
    production: {
      engine: 'catbox-redis',
      host: rtg_url != null ? rtg_url.hostname : void 0,
      port: rtg_url != null ? rtg_url.port : void 0,
      password: rtg_url != null ? rtg_url.auth.split(":")[1] : void 0
    },
    $default: 'catbox-redis'
  },
  database: {
    $filter: 'env',
    production: 'foo_production',
    staging: 'foo_staging',
    development: 'foo_development'
  }
});

criteria = {
  env: process.env.ENVIRONMENT
};

exports.get = function(key) {
  return store.get(key, criteria);
};

try_confidence.js:

1
2
3
4
5
6
7
var config;

config = require('./config');

console.dir(config.get('/cache'));

console.dir(config.get('/database'));