Skip to content

Commit

Permalink
feat: add duration type
Browse files Browse the repository at this point in the history
  • Loading branch information
Ben Shepheard committed Jun 18, 2015
1 parent 9890e0c commit 0a149a5
Show file tree
Hide file tree
Showing 9 changed files with 156 additions and 16 deletions.
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# @iondrive/config

A [12-factor](http://12factor.net/config) configuration module for Node.js/io.js.
A [12-factor] configuration module for Node.js/io.js.

[![Build Status][travis-image]][travis-url]

Expand Down Expand Up @@ -32,19 +32,21 @@ module.exports = {
BOOL: 'boolean',
INT: 'integer',
NUM: 'number',
ENM: ['a', 'b', 'c']
ENM: ['a', 'b', 'c'],
DUR: 'duration'
};
```

### Types

The type must be one of `string`, `boolean`, `integer`, `number` or `enum` (`enum` is implied when the value is an array).
The type must be one of `string`, `boolean`, `integer`, `number`, `enum` (`enum` is implied when the value is an array) or 'duration'.

* The `string` type will match any value (since environment variables are all strings).
* The `boolean` type will perform a case insenstive match on `'false'`, `'true'`, `'yes'`, `'no'`, `'y'`, `'n'`, `'1'` and `'0'`.
* The `integer` type will only match integers in decimal notation, e.g. `'123'`, `'-555'`.
* The `number` type will only match decimal notation, e.g `'123'`, `'-3.14'`.
* The `enum` type will only match the string values provided.
* The `duration` type will match either integers (where the value represents milliseconds) or a duration string accepted by the [ms] package, e.g. `15m`, `6h` or `14d`.

### Advanced options

Expand Down Expand Up @@ -99,6 +101,21 @@ assert.strictEqual(config.NUM, 3.14);
assert.strictEqual(config.ENM, 'b');
```

#### Duration

Duration types are special in that instead of returning a raw value, a number of conversion methods are available to ensure that at the point of use the duration is in the correct units. These conversion methods always round the value, so be careful with your precision.

```js
// Assuming APP_DUR has the value '2d'

config.DUR.asMilliseconds(); // 172800000
config.DUR.asSeconds(); // 172800
config.DUR.asMinutes(); // 2880
config.DUR.asHours(); // 48
config.DUR.asDays(); // 2
config.DUR.asYears(); // 0
```

## Prefix

By default all variables must be prefixed by `APP` in the environment variables as above in order to prevent any clobbering of existing environment variables.
Expand Down Expand Up @@ -134,5 +151,7 @@ node app.js

[MIT](LICENSE)

[12-factor]: http://12factor.net/config
[travis-image]: https://img.shields.io/travis/iondrive/config.svg
[travis-url]: https://travis-ci.org/iondrive/config
[ms]: https://github.com/rauchg/ms.js
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
"license": "MIT",
"repository": "iondrive/config",
"main": "./lib/config.js",
"dependencies": {},
"dependencies": {
"ms": "^0.7.1"
},
"devDependencies": {
"mocha": "^2.2.4",
"ts-globber": "^0.3.1",
Expand Down
39 changes: 39 additions & 0 deletions src/Duration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import ms = require('ms');

class Duration {
private ms: number;

constructor(duration: number);
constructor(duration: string);
constructor(duration: any) {
if (typeof duration === 'number') {
this.ms = duration;
} else {
this.ms = ms(duration);
}
if (!this.ms) throw new Error('Cannot convert to duration');
}
}

var s = 1000;
var m = s * 60;
var h = m * 60;
var d = h * 24;
var y = d * 365.25;

['milliseconds', 'seconds', 'minutes', 'hours', 'days', 'years'].forEach(key => {
var keySuffix = key[0].toUpperCase() + key.slice(1);
Duration.prototype['as' + keySuffix] =
Duration.prototype['to' + keySuffix] = function () { // Arrow syntax breaks `this` here
switch(key) {
case 'milliseconds': return this.ms;
case 'seconds': return Math.round(this.ms / s);
case 'minutes': return Math.round(this.ms / m);
case 'hours': return Math.round(this.ms / h);
case 'days': return Math.round(this.ms / d);
case 'years': return Math.round(this.ms / y);
}
};
});

export = Duration;
53 changes: 42 additions & 11 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import path = require('path');

import ms = require('ms');

import Duration = require('./Duration');

const CONFIG_PATH = path.resolve(process.env.NODE_CONFIG_PATH || './config.js');
const CONFIG_PREFIX = process.env.NODE_CONFIG_PREFIX || 'APP';

Expand All @@ -8,30 +12,57 @@ var definition;
try {
definition = require(CONFIG_PATH);
} catch (err) {
throw Error('CONFIG: Can\'t access config definition: Expecting a config.js file in the current working directory or an explicit location via NODE_CONFIG_PATH');
throw new Error('CONFIG: Can\'t access config definition: Expecting a config.js file in the current working directory or an explicit location via NODE_CONFIG_PATH');
}

const INTEGER_REGEX = /^(\-|\+)?[0-9]+$/;
const NUMBER_REGEX = /^(\-|\+)?[0-9]+(\.[0-9]*)?$/;

const parsers = Object.create(null);

parsers['string'] = value => value;
parsers['boolean'] = value => {
if (/^true|yes|y|1$/i.test(value)) return true;
if (/^false|no|n|0$/i.test(value)) return false;
throw Error('Cannot convert to a boolean');
throw new Error('Cannot convert to a boolean');
};
parsers['integer'] = value => {
if (/^(\-|\+)?[0-9]+$/.test(value)) return parseInt(value, 10);
throw Error('Cannot convert to an integer');
if (INTEGER_REGEX.test(value)) return parseInt(value, 10);
throw new Error('Cannot convert to an integer');
};
parsers['number'] = value => {
if (/^(\-|\+)?[0-9]+(\.[0-9]*)?$/.test(value)) return parseFloat(value);
throw Error('Cannot convert to a number');
if (NUMBER_REGEX.test(value)) return parseFloat(value);
throw new Error('Cannot convert to a number');
};
parsers['duration'] = value => {
if (INTEGER_REGEX.test(value)) return new Duration(parseInt(value, 10));
return new Duration(value);
};
parsers['enum'] = (value, values) => {
if (values.indexOf(value) > -1) return value;
throw Error('Value not found in enumeration values');
throw new Error('Value not found in enumeration values');
};


// function addDurationParser(name: string, aliases: string[], conversionFunction: (milliseconds: number) => number) {
// [name].concat(aliases).forEach(key => {
// parsers[key] = value => {
// if (INTEGER_REGEX.test(value)) return parseInt(value, 10);
// var milliseconds = ms(value);
// if (!milliseconds) throw new Error(`Cannot convert to ${name}`);
// return Math.floor(conversionFunction(milliseconds));
// }
// });
// }

// addDurationParser('years', ['year', 'yrs', 'yr', 'y'], milliseconds => milliseconds / 1000 / 60 / 60 / 24 / 365);
// addDurationParser('days', ['day', 'd'], milliseconds => milliseconds / 1000 / 60 / 60 / 24);
// addDurationParser('hours', ['hour', 'hrs', 'hr', 'h'], milliseconds => milliseconds / 1000 / 60 / 60);
// addDurationParser('minutes', ['minute', 'mins', 'min', 'm'], milliseconds => milliseconds / 1000 / 60);
// addDurationParser('seconds', ['second', 'secs', 'sec', 's'], milliseconds => milliseconds / 1000);
// addDurationParser('milliseconds', ['millisecond', 'msecs', 'msec', 'ms'], milliseconds => milliseconds);


const config = Object.create(null);

for (let key in definition) {
Expand All @@ -51,17 +82,17 @@ for (let key in definition) {
let value = process.env[envKey];

if (!value) {
throw Error(`CONFIG: Environment variable ${envKey} is missing`);
throw new Error(`CONFIG: Environment variable ${envKey} is missing`);
}

try {
if (!parsers[type]) throw Error('Invalid type');
if (!parsers[type]) throw new Error('Invalid type');
if (typeof def.validator === 'function' && !def.validator(value)) {
throw Error('Value did not pass validator function');
throw new Error('Value did not pass validator function');
}
config[key] = parsers[type](value, values);
} catch (err) {
throw Error(`CONFIG: Error parsing environment variable ${envKey}: ${err.message}`);
throw new Error(`CONFIG: Error parsing environment variable ${envKey}: ${err.message}`);
}
}

Expand Down
6 changes: 6 additions & 0 deletions test/fixtures/duration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
FOO: 'duration',
BAR: {
type: 'duration'
}
};
27 changes: 27 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,33 @@ describe('config', function () {
});
});

describe('duration', function () {
before(function () { fixture = new ConfigFixture('duration'); });

it('should throw when invalid', function () {
assert.throws(function () { fixture.getConfig({ APP_FOO: 'hello', APP_BAR: '1d' }); });
assert.throws(function () { fixture.getConfig({ APP_FOO: '1d', APP_BAR: 'hello' }); });
});

it('should return duration when valid', function () {
var durations = [100, 5000000, '1ms', '1s', '1m', '1h', '1d', '1y'];
durations.forEach(function (value) {
fixture.getConfig({ APP_FOO: value, APP_BAR: value });
});
});

it('should expose conversion methods', function () {
var config = fixture.getConfig({ APP_FOO: '2d', APP_BAR: '2y' });
assert.equal(config.FOO.asMilliseconds(), 172800000);
assert.equal(config.FOO.asSeconds(), 172800);
assert.equal(config.FOO.asMinutes(), 2880);
assert.equal(config.FOO.asHours(), 48);
assert.equal(config.BAR.asDays(), 731); // rounded from 730.5
assert.equal(config.BAR.asYears(), 2);
console.log(config.FOO.asYears())
});
});

describe('advanced', function () {
before(function () { fixture = new ConfigFixture('advanced'); });

Expand Down
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
],
"files": [
"./typings/tsd.d.ts",
"./src/Duration.ts",
"./src/config.ts"
]
}
}
13 changes: 13 additions & 0 deletions typings/ms.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
declare module 'ms' {
interface Options {
long?: boolean;
}

interface Ms {
(val: string): number;
(val: number, options?: Options): string;
}

var ms: Ms;
export = ms;
}
2 changes: 2 additions & 0 deletions typings/tsd.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
/// <reference path="node/node.d.ts" />
/// <reference path="mocha/mocha.d.ts" />

/// <reference path="ms.d.ts" />

0 comments on commit 0a149a5

Please sign in to comment.