/** * utils.js * @file General purpose utilities * @description General purpose utilities. */ const path = require('path'); const fs = require('fs').promises; /** * The general purpose utilities. */ class Utils { /** * @function getLocalizedPath * @description Returns a localized file path accoring to the locale. * * Localized files are searched in subfolders of a given path, e.g. * * root/ * ├── base/ // base path to files * │ ├── example.html // default file * │ └── de/ // de language folder * │ │ └── example.html // de localized file * │ └── de-AT/ // de-AT locale folder * │ │ └── example.html // de-AT localized file * * Files are matched with the locale in the following order: * 1. Locale match, e.g. locale `de-AT` matches file in folder `de-AT`. * 2. Language match, e.g. locale `de-AT` matches file in folder `de`. * 3. Default; file in base folder is returned. * * @param {String} defaultPath The absolute file path, which is also * the default path returned if localization is not available. * @param {String} locale The locale. * @returns {Promise} The object contains: * - `path`: The path to the localized file, or the original path if * localization is not available. * - `subdir`: The subdirectory of the localized file, or undefined if * there is no matching localized file. */ static async getLocalizedPath(defaultPath, locale) { // Get file name and paths const file = path.basename(defaultPath); const basePath = path.dirname(defaultPath); // If locale is not set return default file if (!locale) { return { path: defaultPath }; } // Check file for locale exists const localePath = path.join(basePath, locale, file); const localeFileExists = await Utils.fileExists(localePath); // If file for locale exists return file if (localeFileExists) { return { path: localePath, subdir: locale }; } // Check file for language exists const language = locale.split('-')[0]; const languagePath = path.join(basePath, language, file); const languageFileExists = await Utils.fileExists(languagePath); // If file for language exists return file if (languageFileExists) { return { path: languagePath, subdir: language }; } // Return default file return { path: defaultPath }; } /** * @function fileExists * @description Checks whether a file exists. * @param {String} path The file path. * @returns {Promise} Is true if the file can be accessed, false otherwise. */ static async fileExists(path) { try { await fs.access(path); return true; } catch (e) { return false; } } /** * @function isPath * @description Evaluates whether a string is a file path (as opposed to a URL for example). * @param {String} s The string to evaluate. * @returns {Boolean} Returns true if the evaluated string is a path. */ static isPath(s) { return /(^\/)|(^\.\/)|(^\.\.\/)/.test(s); } /** * Flattens an object and crates new keys with custom delimiters. * @param {Object} obj The object to flatten. * @param {String} [delimiter='.'] The delimiter of the newly generated keys. * @param {Object} result * @returns {Object} The flattened object. **/ static flattenObject(obj, parentKey, delimiter = '.', result = {}) { for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { const newKey = parentKey ? parentKey + delimiter + key : key; if (typeof obj[key] === 'object' && obj[key] !== null) { this.flattenObject(obj[key], newKey, delimiter, result); } else { result[newKey] = obj[key]; } } } return result; } /** * Determines whether an object is a Promise. * @param {any} object The object to validate. * @returns {Boolean} Returns true if the object is a promise. */ static isPromise(object) { return object instanceof Promise; } /** * Creates an object with all permutations of the original keys. * For example, this definition: * ``` * { * a: [true, false], * b: [1, 2], * c: ['x'] * } * ``` * permutates to: * ``` * [ * { a: true, b: 1, c: 'x' }, * { a: true, b: 2, c: 'x' }, * { a: false, b: 1, c: 'x' }, * { a: false, b: 2, c: 'x' } * ] * ``` * @param {Object} object The object to permutate. * @param {Integer} [index=0] The current key index. * @param {Object} [current={}] The current result entry being composed. * @param {Array} [results=[]] The resulting array of permutations. */ static getObjectKeyPermutations(object, index = 0, current = {}, results = []) { const keys = Object.keys(object); const key = keys[index]; const values = object[key]; for (const value of values) { current[key] = value; const nextIndex = index + 1; if (nextIndex < keys.length) { Utils.getObjectKeyPermutations(object, nextIndex, current, results); } else { const result = Object.assign({}, current); results.push(result); } } return results; } /** * Validates parameters and throws if a parameter is invalid. * Example parameter types syntax: * ``` * { * parameterName: { * t: 'boolean', * v: isBoolean, * o: true * }, * ... * } * ``` * @param {Object} params The parameters to validate. * @param {Array} types The parameter types used for validation. * @param {Object} types.t The parameter type; used for error message, not for validation. * @param {Object} types.v The function to validate the parameter value. * @param {Boolean} [types.o=false] Is true if the parameter is optional. */ static validateParams(params, types) { for (const key of Object.keys(params)) { const type = types[key]; const isOptional = !!type.o; const param = params[key]; if (!(isOptional && param == null) && !type.v(param)) { throw `Invalid parameter ${key} must be of type ${type.t} but is ${typeof param}`; } } } } module.exports = Utils;