From 856fdf8dff154a1fe2e695776c1a57da4006754e Mon Sep 17 00:00:00 2001 From: Matt Hamann Date: Wed, 25 Oct 2017 22:57:58 -0400 Subject: [PATCH] First pass at transform functions (#279) --- README.md | 78 +++++++++++++++++++++++++++++++++++++--- lib/nconf/common.js | 28 +++++++++++++++ lib/nconf/stores/argv.js | 5 +++ lib/nconf/stores/env.js | 5 +++ test/complete-test.js | 56 +++++++++++++++++++++++++++++ 5 files changed, 167 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d1ca8f3..2adf600 100644 --- a/README.md +++ b/README.md @@ -193,6 +193,32 @@ A simple in-memory storage engine that stores a nested JSON representation of th ### Argv Responsible for loading the values parsed from `process.argv` by `yargs` into the configuration hierarchy. See the [yargs option docs](https://github.com/bcoe/yargs#optionskey-opt) for more on the option format. +#### Options + +##### `parseValues: {true|false}` (default: `false`) +Attempt to parse well-known values (e.g. 'false', 'true', 'null', 'undefined', '3', '5.1' and JSON values) +into their proper types. If a value cannot be parsed, it will remain a string. + +##### `transform: function(obj)` +Pass each key/value pair to the specified function for transformation. + +The input `obj` contains two properties passed in the following format: +``` +{ + key: '', + value: '' +} +``` + +The transformation function may alter both the key and the value. + +The function may return either an object in the asme format as the input or a value that evaluates to false. +If the return value is falsey, the entry will be dropped from the store, otherwise it will replace the original key/value. + +*Note: If the return value doesn't adhere to the above rules, an exception will be thrown.* + +#### Examples + ``` js // // Can optionally also be an object literal to pass to `yargs`. @@ -202,16 +228,52 @@ Responsible for loading the values parsed from `process.argv` by `yargs` into th alias: 'example', describe: 'Example description for usage generation', demand: true, - default: 'some-value' + default: 'some-value', + parseValues: true, + transform: function(obj) { + if (obj.key === 'foo') { + obj.value = 'baz'; + } + return obj; + } } }); ``` ### Env Responsible for loading the values parsed from `process.env` into the configuration hierarchy. -Usually the env variables values are loaded into the configuration as strings. -To ensure well-known strings ('false', 'true', 'null', 'undefined', '3', '5.1') and JSON values -are properly parsed, the `parseValues` boolean option is available. +By default, the env variables values are loaded into the configuration as strings. + +#### Options + +##### `lowerCase: {true|false}` (default: `false`) +Convert all input keys to lower case. Values are not modified. + +If this option is enabled, all calls to `nconf.get()` must pass in a lowercase string (e.g. `nconf.get('port')`) + +##### `parseValues: {true|false}` (default: `false`) +Attempt to parse well-known values (e.g. 'false', 'true', 'null', 'undefined', '3', '5.1' and JSON values) +into their proper types. If a value cannot be parsed, it will remain a string. + +##### `transform: function(obj)` +Pass each key/value pair to the specified function for transformation. + +The input `obj` contains two properties passed in the following format: +``` +{ + key: '', + value: '' +} +``` + +The transformation function may alter both the key and the value. + +The function may return either an object in the asme format as the input or a value that evaluates to false. +If the return value is falsey, the entry will be dropped from the store, otherwise it will replace the original key/value. + +*Note: If the return value doesn't adhere to the above rules, an exception will be thrown.* + +#### Examples ``` js // @@ -247,7 +309,13 @@ are properly parsed, the `parseValues` boolean option is available. match: /^whatever_matches_this_will_be_whitelisted/ whitelist: ['database__host', 'only', 'load', 'these', 'values', 'if', 'whatever_doesnt_match_but_is_whitelisted_gets_loaded_too'], lowerCase: true, - parseValues: true + parseValues: true, + transform: function(obj) { + if (obj.key === 'foo') { + obj.value = 'baz'; + } + return obj; + } }); var dbHost = nconf.get('database:host'); ``` diff --git a/lib/nconf/common.js b/lib/nconf/common.js index 3096fab..83c9081 100644 --- a/lib/nconf/common.js +++ b/lib/nconf/common.js @@ -141,3 +141,31 @@ common.parseValues = function (value) { return val; }; + +// +// ### function transform(map, fn) +// #### @map {object} Object of key/value pairs to apply `fn` to +// #### @fn {function} Transformation function that will be applied to every key/value pair +// transform a set of key/value pairs and return the transformed result +common.transform = function(map, fn) { + var pairs = Object.keys(map).map(function(key) { + var obj = { key: key, value: map[key]}; + var result = fn.call(null, obj); + + if (!result) { + return null; + } else if (result.key && typeof result.value !== 'undefined') { + return result; + } + + var error = new Error('Transform function passed to store returned an invalid format: ' + JSON.stringify(result)); + error.name = 'RuntimeError'; + throw error; + }); + + + return pairs.reduce(function(accumulator, pair) { + accumulator[pair.key] = pair.value; + return accumulator; + }, {}); +} diff --git a/lib/nconf/stores/argv.js b/lib/nconf/stores/argv.js index 44ae4ce..8fffce8 100644 --- a/lib/nconf/stores/argv.js +++ b/lib/nconf/stores/argv.js @@ -24,6 +24,7 @@ var Argv = exports.Argv = function (options, usage) { this.options = options; this.usage = usage; this.parseValues = options.parseValues || false; + this.transform = options.transform || false; }; // Inherit from the Memory store @@ -59,6 +60,10 @@ Argv.prototype.loadArgv = function () { return; } + if (this.transform) { + argv = common.transform(argv, this.transform); + } + this.readOnly = false; Object.keys(argv).forEach(function (key) { var val = argv[key]; diff --git a/lib/nconf/stores/env.js b/lib/nconf/stores/env.js index a6a8955..d500c3e 100644 --- a/lib/nconf/stores/env.js +++ b/lib/nconf/stores/env.js @@ -25,6 +25,7 @@ var Env = exports.Env = function (options) { this.separator = options.separator || ''; this.lowerCase = options.lowerCase || false; this.parseValues = options.parseValues || false; + this.transform = options.transform || false; if (({}).toString.call(options.match) === '[object RegExp]' && typeof options !== 'string') { @@ -67,6 +68,10 @@ Env.prototype.loadEnv = function () { }); } + if (this.transform) { + env = common.transform(env, this.transform); + } + this.readOnly = false; Object.keys(env).filter(function (key) { if (self.match && self.whitelist.length) { diff --git a/test/complete-test.js b/test/complete-test.js index 024a07c..40897de 100644 --- a/test/complete-test.js +++ b/test/complete-test.js @@ -184,5 +184,61 @@ vows.describe('nconf/multiple-stores').addBatch({ teardown: function () { nconf.remove('env'); } + }, +}).addBatch({ + // Threw this in it's own batch to make sure it's run separately from the + // sync check + "When using env with transform:fn": { + topic: function () { + + function testTransform(obj) { + if (obj.key === 'FOO') { + obj.key = 'FOOD'; + obj.value = 'BARFOO'; + } + + return obj; + } + + var that = this; + helpers.cp(complete, completeTest, function () { + nconf.env({ transform: testTransform }) + that.callback(); + }); + }, "env vars": { + "port key/value properly transformed": function() { + assert.equal(nconf.get('FOOD'), 'BARFOO'); + } + } + }, + teardown: function () { + nconf.remove('env'); + } +}).addBatch({ + // Threw this in it's own batch to make sure it's run separately from the + // sync check + "When using env with a bad transform:fn": { + topic: function () { + function testTransform() { + return {foo: 'bar'}; + } + + var that = this; + helpers.cp(complete, completeTest, function () { + try { + nconf.env({ transform: testTransform }); + that.callback(); + } catch (err) { + that.callback(null, err); + } + }); + }, "env vars": { + "port key/value throws transformation error": function(err) { + assert.equal(err.name, 'RuntimeError'); + } + } + }, + teardown: function () { + nconf.remove('env'); } }).export(module); \ No newline at end of file