UNPKG

ravel

Version:

Ravel Rapid Application Development Framework

211 lines (173 loc) 6.84 kB
'use strict'; const upath = require('upath'); const symbols = require('./symbols'); /** * A lightweight configuration system for Ravel, allowing * clients, as well as other parts of the application, to * define expected configuration parameters and get/set * their values. * * @param {Class} Ravel - The Ravel prototype. * @private */ module.exports = function (Ravel) { /** * Stores default configuration values, specified at parameter creation-time. * * @private */ const defaults = Object.create(null); const ENVVAR_PATTERN = /\$([a-zA-Z_][0-9a-zA-Z_]*)/g; /** * Interpolates config values with the values of the environment variables of the process. * * @private * @param {any} value - The value specified for a parameter; possibly an environment variable. */ const interpolateEnvironmentVariables = function (value) { const IllegalValue = this.$err.IllegalValue; if (typeof value !== 'string') { return value; } const result = value.replace(ENVVAR_PATTERN, function () { const varname = arguments[1]; if (process.env[varname] === undefined) { throw new IllegalValue(`Environment variable ${varname} was referenced but not set`); } return process.env[varname]; }); return result; }; /** * Load parameters from a `.ravelrc.json` file during `init()`. * This file must be located beside `app.js` or in any parent directory of `app.js`. * * @private */ Ravel.prototype[symbols.loadParameters] = function () { let fromFile = Object.create(null); let nextDir = this.cwd; let currentDir; do { currentDir = nextDir; // load parameters from config file into an empty object // search upwards until we run out of path components or find something // don't require extension; node will search for .js/.json/.node const search = upath.toUnix(upath.posix.join(currentDir, '.ravelrc')); try { fromFile = require(search); if (typeof fromFile === 'string') { fromFile = JSON.parse(fromFile); } break; } catch (err) { if (err.constructor.name === 'SyntaxError') { throw err; } else { this.once('post init', () => { this.$log.trace(`Could not locate .ravelrc.json at ${search}`, err.message); }); nextDir = upath.join(currentDir, '..'); } } } while (nextDir !== currentDir); // verify all parameters in fromFile are known for (const key of Object.keys(fromFile)) { if (!this[symbols.knownParameters][key]) { throw new this.$err.IllegalValue(`Attempted to set unknown parameter: ${key.toString()}`); } } for (const key of Object.keys(fromFile)) { fromFile[key] = interpolateEnvironmentVariables.bind(this)(fromFile[key]); } // merge params from file into defaults, so file params take precendence over defaults Object.assign(defaults, fromFile); // now merge with this[symbols.params], allowing programmatically set params to take precedence Object.assign(defaults, this[symbols.params]); // now defaults contains what we want, so make it this[symbols.params] this[symbols.params] = defaults; // done! this[symbols.parametersLoaded] = true; }; /** * Validate that all defined required parameters have been set. * * @throws {NotFoundError} If any defined required parameters have not been set. * @private */ Ravel.prototype[symbols.validateParameters] = function () { const unknowns = Object.keys(this[symbols.knownParameters]).filter(p => this[symbols.knownParameters][p].required).filter(p => { try { this.get(p); return false; } catch (err) { if (!(err instanceof this.$err.NotFound)) throw err; return true; } }); if (unknowns.length > 0) { throw new this.$err.NotFound(`Required parameters have not been defined yet:\n${unknowns.join('\n')}`); } }; /* eslint-disable jsdoc/check-param-names */ /** * Register an application parameter. * * @param {string} key - The key for the parameter. * @param {boolean} required - True, iff the parameter is required. False otherwise. * @param {(any | undefined)} defaultValue - The default value for the parameter. */ Ravel.prototype.registerParameter = function (key, required) { this[symbols.knownParameters][key] = { required: required }; if (arguments.length === 3 && arguments[2] !== undefined) { defaults[key] = JSON.parse(JSON.stringify(arguments[2])); } else if (arguments.length === 3 && arguments[2] === undefined) { throw new this.$err.IllegalValue(`Undefined default value supplied for parameter "${key}"`); } }; /* eslint-enable jsdoc/check-param-names */ /** * Set the value of an application parameter. * * @param {string} key - The key for the parameter. * @param {Any} value - The value for the parameter. * @throws {IllegalValueError} If key refers to an unregistered parameter. * @returns {any} The parameter value. */ Ravel.prototype.set = function (key, value) { if (this[symbols.knownParameters][key]) { this[symbols.params][key] = value; } else { throw new this.$err.IllegalValue(`Attempted to set unknown parameter: ${key}.`); } }; /** * Get the value of an application parameter. * * @param {string} key - The key for the parameter. * @throws {NotFoundError} If the parameter is required and not set. * @returns {any} The parameter value, or undefined if it is not required and not set. */ Ravel.prototype.get = function (key) { if (!this[symbols.parametersLoaded]) { throw new this.$err.General('Cannot get() parameters until after app.init()'); } else if (!this[symbols.knownParameters][key]) { throw new this.$err.NotFound(`Parameter ${key} was requested, but is unknown.`); } else if (this[symbols.knownParameters][key].required && this[symbols.params][key] === undefined) { throw new this.$err.NotFound(`Known required parameter ${key} was requested, but hasn't been defined yet.`); } else if (this[symbols.params][key] === undefined) { this.$log.trace(`Optional parameter ${key} was requested, but is not defined.`); return undefined; } else { return JSON.parse(JSON.stringify(this[symbols.params][key])); } }; /** * Getter for the Ravel app configuration object. This is only available after `app.init()`. * * @memberof Ravel * @name config * @returns {object} The Ravel app configuration object (read-only). */ Object.defineProperty(Ravel.prototype, 'config', { get: function () { return JSON.parse(JSON.stringify(this[symbols.params])); } }); };