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
JavaScript
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);
}
}