UNPKG

@freemework/common

Version:

Common library of the Freemework Project.

314 lines 13.6 kB
import { FExceptionArgument, FExceptionInvalidOperation } from "../exception/index.js"; import { FConfigurationException } from "./f_configuration_exception.js"; import { FConfigurationValue } from "./f_configuration_value.js"; /** * The `FConfiguration` provides contract to access key-value configuration sources */ export class FConfiguration { /** * Construct configuration from json object * * Complex json object expands into plain dictionary of key-values. * * @example * ```js * const config: FConfiguration = FConfiguration.factoryJson({ a: { b: { c: 42 } } }); * assert.equal(config.get("a.b.c").asInteger, 42); * ``` * * @example * ```js * const config: FConfiguration = FConfiguration.factoryJson({ a: { b: [ { c: 40 }, { c: 41 }, { c: 42 } ] } }); * * assert.equal(config.get("a.b.0.c").asString, "40"); * assert.equal(config.get("a.b.1.c").asString, "41"); * assert.equal(config.get("a.b.2.c").asString, "42"); * * const array: ReadonlyArray<FConfiguration> = config.getArray("a.b"); * assert.equal(array.length, 3); * assert.equal(array[0].get("c").asInteger, 40); * assert.equal(array[1].get("c").asInteger, 41); * assert.equal(array[2].get("c").asInteger, 42); * ``` */ static factoryJson(jsonObject, indexFieldName = FConfiguration.DEFAULT_INDEX_KEY) { /** * Expand json to key-value dict * * @example * ```js * const jsonObject = {"a":{"b":{"c":"42"}}}; * const targetDict: { [key: string]: boolean | number | string; } = {}; * expandJson(jsonObject, targetDict); * console.log(JSON.stringify(targetDict)); // {"a.b.c":"42"} * ``` */ function expandJson(jsonObject, targetDict, parentName) { for (let [name, value] of Object.entries(jsonObject)) { if (Array.isArray(jsonObject)) { if (typeof value === "object" && value !== null && indexFieldName in value) { const { [indexFieldName]: index, ...rest } = value; if (typeof index !== "string") { const itemKeyName = parentName !== undefined ? `${parentName}.${name}` : name; throw new FExceptionArgument(`Unreadable data. Unsupported index '${index}' for value of key '${itemKeyName}'. Expected index type of string.`, "jsonObject"); } else { name = index; value = rest; } } } const fullKeyName = parentName !== undefined ? `${parentName}.${name}` : name; switch (typeof value) { case "boolean": case "number": case "string": targetDict[fullKeyName] = value; break; case "object": // it is applicable for arrays too, due to Array.prototype.keys() returns indexes. expandJson(value, targetDict, fullKeyName); break; default: throw new FExceptionArgument(`Unreadable data. Unsupported type for value of key '${fullKeyName}'`, "jsonObject"); } } } const jsonDict = {}; expandJson(jsonObject, jsonDict); const dict = {}; for (const [name, value] of Object.entries(jsonDict)) { if (typeof value === "string") { dict[name] = value === "" ? null : value; } else if (value === null) { dict[name] = null; } else { dict[name] = value.toString(); } } const encodedJson = encodeURIComponent(JSON.stringify(jsonObject)); const sourceURI = `configuration:json?data=${encodedJson}`; return new FConfigurationDictionary(new URL(sourceURI), Object.freeze(dict)); } static DEFAULT_INDEXES_KEY = "indexes"; static DEFAULT_INDEX_KEY = "index"; toDynamicView(opts = { strict: true, }) { function keyWalker(rootObj, keys, sourceConfig, parentObject) { const target = {}; if (rootObj === null) { rootObj = target; } target["$root"] = rootObj; const dottedKeys = new Map(); for (const key of keys) { const dotIndex = key.indexOf("."); if (dotIndex === -1) { target[key] = sourceConfig.get(key).asString; } else { const parentKey = key.substring(0, dotIndex); const subKey = key.substring(dotIndex + 1); if (dottedKeys.has(parentKey)) { dottedKeys.get(parentKey).push(subKey); } else { dottedKeys.set(parentKey, [subKey]); } } } for (const [parentKey, subKeys] of dottedKeys.entries()) { const configNamespaceParent = sourceConfig.namespaceParent; const isSingle = configNamespaceParent !== null && sourceConfig.keys.length === 1; const inner = keyWalker(rootObj, subKeys, sourceConfig.getNamespace(parentKey), target); target[parentKey] = inner; const array = Object.keys(inner).filter(key => key !== "$parent" && key !== "$root").map(key => { const innerObj = inner[key]; if (typeof innerObj === "string" || typeof innerObj === "number" || typeof innerObj === "boolean") { return innerObj; } const wrap = { ...innerObj }; Object.defineProperty(wrap, "$parent", { get: function () { return innerObj["$parent"]["$parent"]; } }); return wrap; }); inner["$array"] = array; inner["$single"] = function () { if (!isSingle) { throw new Error(`Single constraint violation for property '${parentKey}' in namespace '${configNamespaceParent}'`); } const { "$single": _, ...rest } = inner; return rest; }; } if (parentObject !== null) { target["$parent"] = parentObject; } return target; } const objectConfig = keyWalker(null, this.keys, this, null); function makeProxyAdapter(ns, obj) { return new Proxy(obj, { has(_, property) { if (typeof property === "string") { let objProperty = property; if (property.startsWith("?")) { objProperty = property.substring(1); } return objProperty in obj; } return false; }, get(_, property) { if (typeof property === "string") { let objProperty = property; if (property.startsWith("?")) { objProperty = property.substring(1); } if (objProperty in obj) { const value = obj[objProperty]; if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") { return value; } else { return makeProxyAdapter([ ...ns, property, // yep, we need original property name (with ? prefix for optional) to make Mustache work correctly ], value); } } const fullProperty = [...ns, property].join("."); const isOptionalProperty = fullProperty.includes("?"); if (!isOptionalProperty) { if (opts.strict) { throw new FExceptionInvalidOperation(`Non-existing property request '${fullProperty}'.`); } else { return null; } } } return null; }, }); } const proxyConfig = makeProxyAdapter([], objectConfig); return proxyConfig; } } // import { FConfigurationDictionary } from "./f_configuration_dictionary.js"; // Import here due to cyclic dependencies export class FConfigurationDictionary extends FConfiguration { static NAMESPACE_DELIMITER_SYMBOL = "."; _dict; _sourceURI; _configurationNamespace; _keys; constructor(sourceURI, dict, namespaceFull) { super(); this._dict = Object.freeze({ ...dict }); this._sourceURI = sourceURI; this._configurationNamespace = namespaceFull !== undefined ? namespaceFull : null; this._keys = null; } get namespaceFull() { return this._configurationNamespace; } get namespaceParent() { const configurationNamespace = this._configurationNamespace; if (configurationNamespace === null) { return null; } const indexOfLastDelimiter = configurationNamespace.lastIndexOf(FConfigurationDictionary.NAMESPACE_DELIMITER_SYMBOL); if (indexOfLastDelimiter === -1) { return configurationNamespace; } return configurationNamespace.substring(indexOfLastDelimiter + 1); } get keys() { return this._keys !== null ? this._keys : (this._keys = Object.freeze(Object.keys(this._dict))); } get sourceURI() { return this._sourceURI; } findNamespace(_) { throw new Error("Method not implemented."); } find(key) { if (key in this._dict) { const valueData = this._dict[key]; const namespaceFull = this.namespaceFull; const fullKey = namespaceFull !== null ? `${namespaceFull}.${key}` : key; const value = FConfigurationValue.factory(fullKey, valueData, this.sourceURI, null); return value; } else { return null; } } getNamespace(namespaceFull) { const innerDict = {}; const criteria = namespaceFull + FConfigurationDictionary.NAMESPACE_DELIMITER_SYMBOL; const criteriaLen = criteria.length; Object.keys(this._dict).forEach((key) => { if (key.length > criteriaLen && key.startsWith(criteria)) { const value = this._dict[key]; innerDict[key.substring(criteriaLen)] = value; } }); const innerConfigurationNamespace = this._configurationNamespace !== null ? `${this._configurationNamespace}.${namespaceFull}` : namespaceFull; if (Object.keys(innerDict).length === 0) { throw new FConfigurationException(`Namespace '${innerConfigurationNamespace}' was not found in the configuration.`, innerConfigurationNamespace); } return new FConfigurationDictionary(this.sourceURI, innerDict, innerConfigurationNamespace); } get(key, defaultData) { const namespaceFull = this.namespaceFull; const fullKey = namespaceFull !== null ? `${namespaceFull}.${key}` : key; if (key in this._dict) { let valueData = this._dict[key]; if (valueData === null && defaultData !== undefined) { valueData = defaultData; } const value = FConfigurationValue.factory(fullKey, valueData, this.sourceURI, null); return value; } else if (defaultData !== undefined) { const value = FConfigurationValue.factory(fullKey, defaultData, this.sourceURI, null); return value; } else { throw new FConfigurationException("Current configuration does not have such key. Check your configuration.", key); } } getArray(key, indexesName = FConfiguration.DEFAULT_INDEXES_KEY) { const arrayNS = this.getNamespace(key); const arrayIndexes = arrayNS.get(indexesName).asString .split(" ") .filter(s => s !== ""); const arrayNamespaces = arrayIndexes.map(s => { return arrayNS.getNamespace(s); }); return arrayNamespaces; } hasNamespace(namespaceFull) { const criteria = namespaceFull + FConfigurationDictionary.NAMESPACE_DELIMITER_SYMBOL; const criteriaLen = criteria.length; for (const key of Object.keys(this._dict)) { if (key.length > criteriaLen && key.startsWith(criteria)) { return true; } } return false; } has(key) { return key in this._dict; } } //# sourceMappingURL=f_configuration.js.map