UNPKG

yaml-config-ts

Version:

Typescript library to get config from YAML file

259 lines 11.7 kB
"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