Skip to content

Instantly share code, notes, and snippets.

@dmitrymatveev
Last active September 13, 2016 04:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dmitrymatveev/473335f9fbb2c3c51d42cff783514fba to your computer and use it in GitHub Desktop.
Save dmitrymatveev/473335f9fbb2c3c51d42cff783514fba to your computer and use it in GitHub Desktop.
"use strict";
/*
The MIT License (MIT)
Copyright (c) 2016 Dmitry Matveev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
const fs = require('fs');
const path = require('path');
const ENV_CONFIG_REFERENCE = /^{{(\w+)}}$/;
/**
* https://gist.github.com/dmitrymatveev/473335f9fbb2c3c51d42cff783514fba
*
* Builds application configuration hash and provides managed access to config properties.
* This module will log warning messages when attempting to access missing config
* properties or referencing missing ENV properties during a build process.
*
* Strings enclosed into {{ENV_VAR_NAME}} will be treated as pointers to ENV variable,
* otherwise it is copied as is.
*
* E.g.
* {
* value1: {{SOME_VAR}} -> will be replaced with `process.env['SOME_VAR']`
* value2: {
* nested1: {{OTHER_VAR}}
* nested2: 123 -> will be copied as `123` number value
* }
* }
*/
class ConfigManager {
constructor(config) {
this._config = config;
}
/**
* Returns value found at the provided path or undefined, supports nested property selectors.
*
* E.g.
* { foo: {value: 1} }
* get('foo.value') // will return 1
* get('foo.notHere') // will return undefined
*
* @param {string} reference - path to variable
* @param {boolean} partial - If true, returned value of object type will be wrapped into a new
* instance of ConfigManager
*/
get(reference, partial = false) {
let value = findObjectProperty(this._config, reference);
if (value === undefined) {
throw new Error(`Attempting to access 'undefined' config value at: ${reference}`);
}
return typeof value === 'object' && partial ? new ConfigManager(value) : value;
}
/**
* @param {object} opt
* @param {string} opt.root - Absolute path to config directory
* @param {string|object[]} opt.environmentLookupMap - Object or Relative path to environment mappings json
* @param {string|object[]} [opt.defaultConfig] - Relative path to default config file
* @param {string|object[]} [opt.overrideConfig] - Relative path to config overrides file
*/
static create(opt) {
let lookupMap = readJson(opt.root, opt.environmentLookupMap);
let defConfig = opt.defaultConfig ? readJson(opt.root, opt.defaultConfig) : {};
let mixin = opt.overrideConfig ? readJson(opt.root, opt.overrideConfig) : {};
let config = deepCopy(lookupMap, deepCopy(defConfig, mixin));
return new ConfigManager(config);
}
}
function readJson(root, target) {
target = !Array.isArray(target) ? [target] : target;
let loaded = {};
target.forEach(function (ref) {
if (typeof ref === 'object') {
loaded = deepCopy(loaded, ref);
}
else {
let file = fs.readFileSync( path.resolve(root, ref) );
loaded = deepCopy(loaded, JSON.parse(file));
}
});
return loaded;
}
function findObjectProperty(obj, ref) {
let addr = Array.isArray(ref) ? ref : ref.split('.');
let key = addr.shift();
if (!key) return undefined;
let next = obj[key];
if (addr.length <= 0) {
return next;
}
if (typeof next === 'object') {
return findObjectProperty(next, addr);
}
else {
return undefined;
}
}
function transformIterator(obj) {
var iterable = {};
iterable[Symbol.iterator] = function () {
var keys = Object.keys(obj);
var i = -1;
return {
next() {
if (++i >= keys.length) return {done: true};
else return {
done: false,
value: {
key: keys[i],
getValue() {
return obj[keys[i]];
},
setValue(v) {
obj[keys[i]] = v;
}
}
};
}
}
};
return iterable;
}
function mapToEnvironmentVariables(cfg) {
for (let item of transformIterator(cfg)) {
let val = item.getValue();
if (typeof val === 'object') {
item.setValue(mapToEnvironmentVariables(val));
}
else {
let envConfig = ENV_CONFIG_REFERENCE.exec(val);
if (envConfig !== null) {
let configValue = process.env[envConfig[1]];
if (configValue === undefined) {
throw new Error(`Environment variable not found: ${item.key}`);
}
item.setValue(configValue);
}
}
}
return cfg;
}
/**
* @param target
* @param src
* @param use
* @returns {boolean|Array|{}}
*/
function deepCopy(target, src, use) {
var array = Array.isArray(src);
var dst = use ? use : array && [] || {};
if (array) {
target = target || [];
dst = dst.concat(target);
src.forEach(function (e, i) {
if (typeof dst[i] === 'undefined') {
dst[i] = e;
} else if (typeof e === 'object') {
dst[i] = deepCopy(target[i], e);
} else {
if (target.indexOf(e) === -1) {
dst.push(e);
}
}
});
} else {
if (target && typeof target === 'object') {
Object.keys(target).forEach(function (key) {
dst[key] = target[key];
})
}
Object.keys(src).forEach(function (key) {
if (typeof src[key] !== 'object' || !src[key]) {
dst[key] = src[key];
}
else {
if (!target[key]) {
dst[key] = src[key];
} else {
dst[key] = deepCopy(target[key], src[key]);
}
}
});
}
return dst;
}
module.exports = ConfigManager;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment