@freemework/common
Version:
Common library of the Freemework Project.
314 lines • 13.6 kB
JavaScript
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