yaml-config-ts
Version:
Typescript library to get config from YAML file
259 lines • 11.7 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.yamlConfig = void 0;
const fs = __importStar(require("fs"));
const yaml = __importStar(require("yaml"));
const lodash = __importStar(require("lodash"));
const ts_deepmerge_1 = __importDefault(require("ts-deepmerge"));
/**
* Yaml config class definition
*/
class yamlConfig {
/**
* Object constructor
* @param path - path to the file or directory to load
* @param encoding - encoding to use with file(s) beeing loaded
*/
constructor(path, encoding = 'utf8') {
this.log('Initializing');
this.path = path; // Load path from constructor parameters
this.filesEncoding = encoding; // Load files encoding from constructor parameters
// Initialize files data maps with as empty maps by default
this.filesData = new Map();
this.filesParsedData = new Map();
this.requiredSettings = []; // TODO: Add required settings functionality to specify mandatory configuration items
this.config = {}; // Initialize consolidated config object
this.log('Rendering config');
this.readConfig();
this.parseConfig();
this.mergeConfig();
this.config = this.substitute();
this.configYaml = yaml.stringify(this.config);
this.log('Done');
}
/**
* Logs provided message to stderr
* @param message - string
* @returns N/a
*/
log(message) {
console.error('YAML-CONFIG: ', message);
}
/**
* Checks provided file extension and updated files data map
* @param file - path to the file or directory to read config(s) from
*/
readFile(file) {
// If file has YAML extention
if (file.endsWith('.yml') || file.endsWith('.yaml')) {
this.log(`Reading ${file}`);
// Get it's contents and update files data map
this.filesData.set(file, fs.readFileSync(file, (this.filesEncoding)));
}
else {
// If file extention is different - skip it
this.log(`${file} has non YAML extention and will be ignored`);
}
}
/**
* Reads specified path and loads data from file or directory
* Updates files data map with files content
* @param N/a
* @returns N/a
*/
readConfig() {
// If path exists
if (fs.existsSync(this.path)) {
this.log(`Loading config from "${this.path}"`);
// If it is a directory
if (fs.lstatSync(this.path).isDirectory()) {
this.log(`Using directory mode to read configs`);
// Get files list
const files = fs.readdirSync(this.path);
this.log(`Files list: ${files}`);
// Process each file
files.forEach((file) => {
this.log(`Processing file ${file}`);
// Form full file path
file = this.path + '/' + file;
// Update files data array
this.readFile(file);
});
}
else { // If it's a single file
this.log(`Using single file mode to read config`);
// Read it directly
this.readFile(this.path);
}
}
else {
// Log path error
// TODO: Throw exception to stop main program execution
this.log(`${this.path} is not a path to an existent file or directory`);
}
// Output files data array
this.log(`Processed files data: ${this.filesData}`);
}
/**
* Parses files from files data array and updates parsed files map with YAML contents
*/
parseConfig() {
// For each loaded file
for (let [fileName, fileContents] of this.filesData) {
this.log(`Parsing file: ${fileName}`);
this.filesParsedData.set(fileName, yaml.parse(fileContents));
}
}
/**
* Makes merged object from parsed config files
*/
mergeConfig() {
for (let [fileName, yamlContents] of this.filesParsedData) {
this.log(`Merging config: ${fileName}`);
this.config = ts_deepmerge_1.default(this.config, yamlContents);
}
}
/**
* Preform substitution for given string based on the object contents
* Placeholder format: ${path.to.object.property}
* @param mainObject - object to get placeholder values from
* @param stringToProcess - string with placeholder to be processed
* @returns {result: string | number | object, status: boolean}
*/
substituteString(mainObject = this.config, stringToProcess) {
let successFlag = false; // Flag to indicate if substitution is successful
let isNumberFlag = false; // Flag to indicate that number was substituted
const placeholderRegexp = /\${([\w.-]+)}/g; // Regex to find placeholder like ${object.path}
// Find placeholder in the string
let placeholder = placeholderRegexp.exec(stringToProcess); // Find placeholder in the string beeing processed
// Initialize global vars for future processing
let elementToSubstitute = null;
let substitutedString = null;
// If placeholder found - extract it's value
if (placeholder && (stringToProcess === placeholder[0])) {
elementToSubstitute = placeholder[1];
}
// Check that placeholder value (property path) exists in the config object
if (elementToSubstitute && lodash.has(mainObject, elementToSubstitute)) {
// Get it's value from the config
substitutedString = lodash.get(mainObject, elementToSubstitute);
// If target element is an object - raise success flag
if (typeof (substitutedString) === "object") {
successFlag = true;
}
}
// If object is not processed yet => it's a string
if (!successFlag) {
// Perform substitution: replace placeholder with object.path from mainObject
substitutedString = stringToProcess.replace(placeholderRegexp,
// Inline function to get value to replace
function (searchMatch, placeholder) {
// Check if path from placeholder exists in mainObject
if (!successFlag) {
successFlag = lodash.has(mainObject, placeholder);
}
// Return found object value or initial string (unchanged placeholder)
const result = lodash.get(mainObject, placeholder) || searchMatch;
// If result can be converted to number - raise flag
if (typeof (result) === 'number') {
isNumberFlag = true;
}
return result;
});
// If string still has placeholders (updated value also may contain placeholder)
if (substitutedString.match(placeholderRegexp) && successFlag) {
// Preform substitution again for this string
let recursedSubstitutedString = this.substituteString(mainObject, substitutedString);
// Update result and status with inner substitution's
substitutedString = recursedSubstitutedString.result;
}
// If result can be converted to number - do it
if (!isNaN(Number(substitutedString)) && isNumberFlag) {
substitutedString = Number(substitutedString);
}
}
// Return substitution result and status
return Object({
status: successFlag,
result: substitutedString
});
}
/**
* Process substitions in config object(s)
* @param mainObject - Object containing values that should be used for substitution. Equal to config object in most cases
* @param objectToProcess - Object to replace substitutions in
*/
substitute(mainObject = this.config, objectToProcess = this.config) {
this.log(`Processing substitions for ${objectToProcess} with ${mainObject} as values source`);
// Process substitutions: String
if (typeof (objectToProcess) === 'string') {
this.log("String substitution mode used");
// Preform string substitution
const substitutedString = this.substituteString(mainObject, objectToProcess);
// Update property if substitution happened successfully
if (substitutedString.status) {
objectToProcess = substitutedString.result;
}
// Process substitutions: array
}
else if (Array.isArray(objectToProcess)) {
this.log("Array substitution mode used");
// Get array length
let arrayLength = objectToProcess.length;
// Perform substitutions for each array element
for (let i = 0; i < arrayLength; i++) {
// Get substitution result for an array element
let substitutionResult = this.substitute(mainObject, objectToProcess[i]);
// If value to substitute is also an array - insert it's values into the current one
if (Array.isArray(substitutionResult)) {
// Calculate final array length: current array length + insert array length - 1 (because element with subsitution will be deleted)
arrayLength += substitutionResult.length - 1;
let j = 0; // Inner loop counter
// For each insert array element - insert it instead of the full string substition
substitutionResult.forEach(item => {
objectToProcess.splice(i + j, 0, item);
j++;
});
// Remove resolved substitution element from array
objectToProcess.splice(i + substitutionResult.length, 1);
// If values is a string - just replace an element with the result
}
else {
objectToProcess[i] = substitutionResult;
}
}
// Process substitions: object
}
else if (typeof (objectToProcess) === 'object') {
// Perform substitution for each object key
for (let key in objectToProcess) {
objectToProcess[key] = this.substitute(mainObject, objectToProcess[key]);
}
}
return objectToProcess;
}
}
exports.yamlConfig = yamlConfig;
//# sourceMappingURL=index.js.map