export type ConfigPrim = boolean | string | number; export type ConfigVal = ConfigPrim | Array; export type ConfigValue = { [k: string]: ConfigVal | ConfigValue }; export class Config { private _data: any = {}; constructor(options: ConfigValue = {}, argv: string[] = []) { for (let key in options) { if (options.hasOwnProperty(key)) { this._data[key] = options[key]; } } while (argv.length > 0) { let key = argv.shift(); if (key && key.indexOf('--') === 0) { let val = argv.shift() || ''; this.set(key.substr(2), val); } } } set(key: string, value: ConfigVal) { return SET(this._data, key.split('.'), value); } /** * Look up a key, possibly using the environment or setting a value. */ find(key: string, env: V | string = '', def?: V): V { let deflt: V; if (arguments.length === 2) { deflt = env as V; env = ''; } else { deflt = def as V; } if (env !== '') { deflt = FIND(this._data, key.split('.'), deflt); let envDeflt = FIND(process.env, [env], deflt); return SET(this._data, key.split('.'), envDeflt); } else { return FIND(this._data, key.split('.'), deflt); } } map(key: string, fn: (_: string) => string): string[] { const mapped = toConfigArray(this.find(key, [])).map(fn); return toConfigArray(this.set(key, mapped)); } append(key: string, arr: string | string[]): string[] { let configArr = toConfigArray(arr); let list = toConfigArray(this.find(key, [])).concat(configArr); return toConfigArray(this.set(key, list)); } prepend(key: string, arr: string | string[]): string[] { let configArr = toConfigArray(arr); let list = configArr.concat(toConfigArray(this.find(key, []))); return toConfigArray(this.set(key, list)); } } /** * Convert a ConfigVal to an array of ConfigPrim. * It the value is already an array, return it. Otherwise, return a new * array with one element, the value passed. */ function toConfigArray(arr: string | string[]): string[] { return Array.isArray(arr) ? arr : [arr]; } /** * Convenience for `FIND(_, _, _, true)` */ function SET(o: Object, p: string[], v: V): V { return FIND(o, p, v, true); } /** * Utility method to recursively look through an object and return a node. * Creates missing nodes along the way. Sets the found node with a value if * not present, or if told to force updating. * * @param obj {Object} to search in. * @param path {Array|string} path to follow in the object tree. If a * string, nodes should be dot-separated. * @param val {Any} default value to set the found property to if undefined or * if `force` is `true`. * @param force {boolean} if `true`, replace the value even if it is set. */ function FIND(obj: any, path: string[], val: V, force: boolean = false): V { // Let's follow the next key let key = path.shift() as string; const hasKey = Object.hasOwnProperty.call(obj, key); if (path.length === 0) { // We're at the end of the path if (obj !== process.env && (force || !hasKey)) { // Update the property if either forced or unset. obj[key] = val; } if (obj === process.env) { return GET_ENV(key, val); } else { // Return what's there. return obj[key] as V; } } else { // If not set, fill in this position with a new object. obj[key] = hasKey ? obj[key] : {}; // Return `FIND` on the next path part. return FIND(obj[key], path, val, force); } } function GET_ENV(key: string, val: V): V { // TODO Either use proces.env OR use localStorage. const obj = process.env; if (!obj[key]) { return val; } // process.env behaves oddly. if (obj[key].toLowerCase() === 'false') { return false as V; } else if (obj[key].toLowerCase() === 'true') { return true as V; } else if (obj[key].toLowerCase() === '') { return val as V; } else if (val instanceof Number) { if (obj[key].indexOf('.') === -1) { return parseInt(obj[key], 10) as V; } else { return parseFloat(obj[key]) as V; } } else { return obj[key]; } }