Skip to content

Commit

Permalink
feat(aliases): added support for env. var. aliases, fixes #5
Browse files Browse the repository at this point in the history
  • Loading branch information
FGRibreau committed May 18, 2015
1 parent 25a986b commit dc74a02
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 27 deletions.
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,36 @@ t.strictEqual(config.amqp.exchanges[0].name, 'new_exchange'); // extracted from
t.strictEqual(config.FULL_UPPER_CASE.PORT, 8080);
```

## Specifying multiple aliases

It sometimes useful to be able to specify aliases, for instance [Clever-cloud](http://clever-cloud.com) or [Heroku](https://heroku.com) exposes their own environment variable names while your application's internal code may not want to rely on them.

Common-env adds a [layer of indirection](http://en.wikipedia.org/wiki/Fundamental_theorem_of_software_engineering) enabling you to specify environment aliases that won't impact your codebase.

#### Usage

```javascript
var config = env.getOrElseAll({
amqp: {
login: {
$default: 'guest',
$aliases: ['ADDON_RABBITMQ_LOGIN', 'LOCAL_RABBITMQ_LOGIN']
},
password: 'guest',
host: 'localhost',
port: 5672
},
});

t.strictEqual(config.amqp.login, 'plop'); // converted from env
```

#### How common-env resolves `config.amqp.login`

- Common-env will first read `ADDON_RABBITMQ_LOGIN` environment variable, if it exists, its value will be used.
- If not common-env will read `LOCAL_RABBITMQ_LOGIN`, if it exists, its value will be used.
- If not common-env will read `AMQP_LOGIN`, if it exists, its value will be used.
- If not common-env will fallback on `$default` value.

<p align="center">
<img style="width:100%" src="./docs/Thumbs-Up-Gif.gif"/>
Expand Down
87 changes: 60 additions & 27 deletions lib/common-env.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,58 +16,91 @@ function env(logger, options) {
logger.info.apply(logger, arguments);
}

return {
getOrElse: function (key, def) {
if (_.has(process.env, key)) {
var val = _.isNumber(def) ? toInt(process.env[key]) : process.env[key];

if (seemsBoolean(val)) {
val = String(val).toLowerCase() === 'true';
}
/**
* @param {[type]} key [description]
* @param {[type]} def [description]
* @return {undefined|mixed} yield undefined if not found
*/
function getEnv(key, def) {
if (!_.has(process.env, key)) {
return def;
}

if (!PRINTED[key]) {
PRINTED[key] = 1;
log('[env] ' + key + ' was defined' + (options.displayValues ? ', using ' + String(val) : ''));
}
var val = _.isNumber(def) ? toInt(process.env[key]) : process.env[key];
if (seemsBoolean(val)) {
val = String(val).toLowerCase() === 'true';
}
return val;
}

return val;
}

function getOrElse(key, def) {
var val = getEnv(key, def);
if (val !== void 0) {
if (!PRINTED[key]) {
PRINTED[key] = 1;
log('[env] ' + key + ' was not defined, using default', String(def));
log('[env] ' + key + ' was defined' + (options.displayValues ? ', using ' + String(val) : ''));
}
return def;
},

return val;
}

if (!PRINTED[key]) {
PRINTED[key] = 1;
log('[env] ' + key + ' was not defined, using default', String(def));
}
return def;
}

return {
getOrElse: getOrElse,

/**
* [getOrElseAll description]
* @param {Object} obj pair or {"ENV_VAR": {Number|String} defaultValue, ...}
* @return {Object} [description]
*/
getOrElseAll: function (vars, prefix) {
getOrElseAll: function getOrElseAll(vars, prefix) {
prefix = prefix || '';

return _.reduce(vars, function (out, val, k) {
var keyEnvName = k.toUpperCase();
var fullKeyName = prefix + keyEnvName;

if (_.isPlainObject(val) && _.has(val, '$default')) {
if (!Array.isArray(val.$aliases)) {
throw new Error('Common-env: $aliases must be defined along side $default, key: ' + fullKeyName);
}
/*
"val":{
"$default": 1,
"$aliases": ['VAR_ENV', 'VAR_ENV2']
}
Will first try `VAR_ENV` then `VAR_ENV2` then `VAL` then fallback on `$default` if none of them were set
*/
out[k] = val.$aliases.concat([fullKeyName]).reduce(function (memo, varEnvName, i, arr) {
return memo === void 0 ? getEnv(varEnvName, i === arr.length - 1 ? val.$default : void 0) : memo;
}, void 0);

if (_.isPlainObject(val)) {
out[k] = _.extend({}, this.getOrElseAll(val, prefix + keyEnvName + '_'));
} else if (_.isArray(val)) {
} else if (_.isPlainObject(val)) {
out[k] = _.extend({}, getOrElseAll(val, fullKeyName + '_'));
} else
if (_.isArray(val)) {
out[k] = val.map(function (innerVal, index) {
return this.getOrElseAll(innerVal, prefix + keyEnvName + '[' + index + ']_');
}, this);
return getOrElseAll(innerVal, fullKeyName + '[' + index + ']_');
});
} else {
out[k] = this.getOrElse(prefix + keyEnvName, val);
out[k] = getOrElse(fullKeyName, val);
}
return out;
}.bind(this), {});
}, {});
},

getOrDie: function (key) {
// @todo refactor this shit.
var r = this.getOrElse(key, false);
if (r === false) {
var r = getOrElse(key, undefined);
if (r === void 0) {
throw new Error('(env) ' + key + ' MUST be defined');
}
return r;
Expand Down
25 changes: 25 additions & 0 deletions test/env.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,31 @@ describe('env', function () {
t.ok(logger.hasENV('PLOP_API[1]_A'), 'PLOP_ROOT_TOKEN');
});

it('should handle special $aliases and $default object value', function () {
var config = env.getOrElseAll({
a: {
b: [{
a: {
$default: 'heyheyhey',
$aliases: ['BLABLA_BLABLA', 'AMQP_LOGIN']
}
}, {
a: {
$default: 'plop2',
$aliases: ['BLABLA_BLABLA'] // `BLABLA_BLABLA` does not exist, it should fallback on "plop"
}
}]
},
b: {
$default: 'heyheyhey',
$aliases: ['BLABLA_BLABLA', 'AMQP_LOGIN', 'BLABLA_BLABLA']
}
});
t.strictEqual(config.a.b[0].a, 'plop');
t.strictEqual(config.a.b[1].a, 'plop2');
t.strictEqual(config.b, 'plop');
});

afterEach(function () {
delete process.env.AMQP_LOGIN;
delete process.env.AMQP_CONNECT;
Expand Down

0 comments on commit dc74a02

Please sign in to comment.