UNPKG

wj-config

Version:

Javascript configuration module for NodeJS and browser frameworks such as React that works like ASP.net configuration where data sources are specified (usually JSON files) and environment variables can contribute/overwrite values by following a naming con

120 lines (119 loc) 5.07 kB
import { attemptParse, forEachProperty, isConfigNode } from "../helpers.js"; import { DataSource } from "./DataSource.js"; const processKey = (key, hierarchySeparator, prefix) => { if (prefix) { key = key.substring(prefix.length); } return key.split(hierarchySeparator); }; const ensurePropertyValue = (obj, name) => { if (obj[name] === undefined) { obj[name] = {}; } return obj[name]; }; export class DictionaryDataSource extends DataSource { #dictionary; #hierarchySeparator; #prefixOrPredicate; #buildPredicate() { let predicateFn = _ => true; let prefix = ''; if (this.#prefixOrPredicate) { if (typeof this.#prefixOrPredicate === "string") { prefix = this.#prefixOrPredicate; predicateFn = name => name.toString().startsWith(prefix); } else { predicateFn = this.#prefixOrPredicate; } } return [predicateFn, prefix]; } #validateDictionary(dic) { if (!isConfigNode(dic)) { throw new Error('The provided dictionary must be a flat object.'); } const [predicateFn, prefix] = this.#buildPredicate(); forEachProperty(dic, (k, v) => { if (!predicateFn(k)) { // This property does not qualify, so skip its validation. return false; } if (isConfigNode(v)) { throw new Error(`The provided dictionary must be a flat object: Property ${k} has a non-scalar value.`); } }); } #inflateDictionary(dic) { const result = {}; if (!dic) { return result; } const [predicateFn, prefix] = this.#buildPredicate(); forEachProperty(dic, (key, value) => { if (predicateFn(key)) { // Object values are disallowed because a dictionary's source is assumed to be flat. // if (isConfigNode(value)) { // throw new Error(`Dictionary data sources cannot hold object values. Key: ${key}`); // } const keyParts = processKey(key, this.#hierarchySeparator, prefix); let obj = result; let keyPath = ''; for (let i = 0; i < keyParts.length - 1; ++i) { keyPath += (keyPath.length ? '.' : '') + keyParts[i]; obj = ensurePropertyValue(obj, keyParts[i]); if (!isConfigNode(obj)) { throw new Error(`Cannot set the value of property "${key}" because "${keyPath}" has already been created as a leaf value.`); } } // Ensure there is no value override. if (obj[keyParts[keyParts.length - 1]]) { throw new Error(`Cannot set the value of variable "${key}" because "${keyParts[keyParts.length - 1]}" has already been created as an object to hold other values.`); } // If the value is a string, attempt parsing. This is to support data sources that can only hold strings // as values, such as enumerating actual system environment variables. if (typeof value === 'string') { value = attemptParse(value); } obj[keyParts[keyParts.length - 1]] = value; } }); return result; } constructor(dictionary, hierarchySeparator, prefixOrPredicate) { super('Dictionary'); if (!hierarchySeparator) { throw new Error('Dictionaries must specify a hierarchy separator.'); } if (typeof hierarchySeparator !== 'string') { throw new Error('The hierarchy separator must be a string.'); } this.#hierarchySeparator = hierarchySeparator; if (prefixOrPredicate !== undefined) { if (typeof prefixOrPredicate === 'string' && prefixOrPredicate.length === 0) { throw new Error('The provided prefix value cannot be an empty string.'); } if (typeof prefixOrPredicate !== 'string' && typeof prefixOrPredicate !== 'function') { throw new Error('The prefix argument can only be a string or a function.'); } if (typeof prefixOrPredicate === 'string' && prefixOrPredicate.length === 0) { throw new Error('An empty string cannot be used as prefix.'); } } this.#prefixOrPredicate = prefixOrPredicate; if (typeof dictionary !== 'function') { this.#validateDictionary(dictionary); } this.#dictionary = dictionary; } async getObject() { let dic = this.#dictionary; if (dic && typeof dic === 'function') { dic = await dic(); this.#validateDictionary(dic); } const inflatedObject = this.#inflateDictionary(dic); return Promise.resolve(inflatedObject); } }