diff --git a/README.md b/README.md index a97f0ac..9426b7b 100644 --- a/README.md +++ b/README.md @@ -104,11 +104,23 @@ Optional. Define the template file extension. After the template file is parsed, ``` #### --debug -Optional. Turn on debug logging. +Optional. Turn on debug logging. Disabled by default. ```bash --debug ``` +#### --overwrite +Optional. Allow the parameters to be overwritten in parameter files and validator files. Disabled by default. +```bash +--overwrite +``` + +#### --validator +Optional. Accept a list of json files which contain the regular expressions used for validating the parameters loaded from the parameter files. The values in the validator files are loaded by order and can be overwritten. For example +```bash +--validator /home/project/validator1.json,/home/project/validator2.json +``` + ## Templating All objects defined in the parameter json files are flattened and stored in a Map which are used to match the mustache styled template. Json objects and array are supported. @@ -149,7 +161,8 @@ param1.json "array": [ "element1", "element2" - ] + ], + "notAffected": "This will not be overwritten because it doesn't exist in param2.json" } ``` @@ -179,6 +192,37 @@ Two vaules are overwritten here. {{ param.nestedPram2.nestedPram3 }} # overwritten ``` +## Validate parameters +The parameters loaded from parameter files can be validated against given regular expressions. The json path of the object needs to match in both parameter file and validator file so the regular expression logic can kick in to search for a match. + +param.json +```json +{ + "param": { + "nestedParam2": { + "nestedParam3": "nestedParam3" + } + }, + "notValidated": "This will not be validated because it doesn't exist in validator.json" +} +``` + +validator.json +```json +{ + "param": { + "nestedParam2": { + "nestedParam3": "n*3" + } + } +} +``` + +If we load the param.json and validator.json, "nestedParam3" will be validated against the regular expression: "n*3". If the validation fails, the program will display an error and exit. +``` +npx paramplate --params param.json --validator validator.json ... +``` + ## Development ```bash # Build diff --git a/bin/index.js b/bin/index.js index 139c394..7e75eb9 100644 --- a/bin/index.js +++ b/bin/index.js @@ -11,52 +11,77 @@ const COLORS = { fgRed: '\x1b[31m', fgYellow: '\x1b[33m' }; +const EMPTY_STR = ''; +const NO = 'n'; +const YES = 'y'; const INPUT_ARGS = { params: '--params', src: '--src', dest: '--dest', ext: '--ext', - debug: '--debug' + debug: '--debug', + overwrite: '--overwrite', + validator: '--validator' }; // Start console.log('################'); console.log('Hello paramplate'); console.log('################'); console.log(); -const { paramsDirs, srcDir, destDir, templateExt, isDebug } = parseArgs(); -debugLog(`# Inputs`, isDebug); +const { paramsDirs, srcDir, destDir, templateExt, isDebug, isOverwrite, validatorDirs } = parseArgs(); +debugLog('# Inputs', isDebug); debugLog(`- Src dir: ${srcDir}`, isDebug); debugLog(`- Dest dir: ${destDir}`, isDebug); debugLog(`- Template ext: ${templateExt}`, isDebug); +debugLog(`- Enable param overwrite: ${isOverwrite}`, isDebug); const paramsFiles = paramsDirs.split(','); const paramsMap = new Map(); +debugLog('', isDebug); +debugLog('# Parameter files', isDebug); paramsFiles.forEach((p) => { const paramsPath = path_1.default.resolve(path_1.default.normalize(p)); debugLog(`- Param file: ${paramsPath}`, isDebug); - loadParams(paramsPath, paramsMap); + transformJsonFileToMap(paramsPath, paramsMap, isOverwrite); }); if (isDebug) { console.log(); console.log('# All parameters'); for (let [key, value] of paramsMap) { - console.log(key + " = " + value); + console.log(key + "=" + value); } } +const validatorMap = new Map(); +const isValidatorUsed = validatorDirs !== EMPTY_STR; +if (isValidatorUsed) { + debugLog('', isDebug); + debugLog('# Validators', isDebug); + const validatorFiles = validatorDirs.split(','); + validatorFiles.forEach((p) => { + const validatorPath = path_1.default.resolve(path_1.default.normalize(p)); + debugLog(`- Validator file: ${validatorPath}`, isDebug); + transformJsonFileToMap(validatorPath, validatorMap, isOverwrite); + }); + validateParams(paramsMap, validatorMap); +} console.log(); +debugLog('# Parsing starts', isDebug); parseSrcDir(srcDir, paramsMap); -console.log(); console.log('# Done!'); +// Utilities function parseArgs() { - const { params, src, dest, ext, debug } = INPUT_ARGS; + const { params, src, dest, ext, debug, overwrite, validator } = INPUT_ARGS; const defaultExt = '.pp'; - const isDebugYes = 'y'; const argsMap = new Map(); const args = process.argv.slice(2); let i = 0; while (i < args.length) { const key = args[i]; if (key === debug) { - argsMap.set(key, isDebugYes); + argsMap.set(key, YES); + i++; + } + else if (key == overwrite) { + argsMap.set(key, YES); i++; } else if (i + 1 < args.length) { @@ -72,17 +97,21 @@ function parseArgs() { process.exit(0); } } - const srcDir = validateInput(src, argsMap); - const destDir = validateInput(dest, argsMap); - const paramsDirs = validateInput(params, argsMap); - const templateExt = validateInput(ext, argsMap, defaultExt); - const isDebug = validateInput(debug, argsMap) === isDebugYes ? true : false; + const srcDir = validateAndGet(src, argsMap); + const destDir = validateAndGet(dest, argsMap); + const paramsDirs = validateAndGet(params, argsMap); + const templateExt = validateAndGet(ext, argsMap, defaultExt); + const isDebug = validateAndGet(debug, argsMap, NO) === YES ? true : false; + const isOverwrite = validateAndGet(overwrite, argsMap, NO) === YES ? true : false; + const validatorDirs = validateAndGet(validator, argsMap, EMPTY_STR); return { paramsDirs, srcDir, destDir, templateExt, - isDebug + isDebug, + isOverwrite, + validatorDirs }; } function parseSrcDir(dir, paramsMap) { @@ -189,17 +218,35 @@ function parseMustache(fileInput, paramsMap) { } return fileOutput; } -function loadParams(pathname, paramsMap) { +function validateParams(paramsMap, validatorMap) { + for (let [key, value] of validatorMap) { + const reg = new RegExp(value); + if (paramsMap.has(key)) { + const paramValue = paramsMap.get(key); + if (paramValue && reg.test(paramValue)) { + debugLog(`Validating: key=${key}, value=${paramValue}, pattern=${value}`, isDebug); + } + else { + logError(`Validation failed: key=${key}, value=${paramValue}, pattern=${value}`); + process.exit(0); + } + } + else { + logWarning(`Missing validator param: ${key}`); + } + } +} +function transformJsonFileToMap(pathname, paramsMap, isOverwriteAllowed) { const file = fs_1.default.readFileSync(pathname, 'utf8').toString(); const obj = JSON.parse(file); - flattenObject(obj, '', paramsMap); + flattenObject(obj, '', paramsMap, isOverwriteAllowed); } -function flattenObject(obj, path, map) { +function flattenObject(obj, path, map, isOverwriteAllowed) { if (typeof obj === 'object' && obj !== null) { if (Array.isArray(obj)) { // obj is array obj.forEach((nextObj, i) => { - flattenObject(nextObj, path + '[' + i + ']', map); + flattenObject(nextObj, path + '[' + i + ']', map, isOverwriteAllowed); }); } else { @@ -207,26 +254,36 @@ function flattenObject(obj, path, map) { const pathPrefix = path === '' ? '' : path + '.'; Object.keys(obj).forEach((key) => { const nextObj = obj[key]; - flattenObject(nextObj, pathPrefix + key, map); + flattenObject(nextObj, pathPrefix + key, map, isOverwriteAllowed); }); } } else { // obj is value + // Find one existing value in the map. if (map.has(path) && map.get(path) !== obj) { const oldValue = map.get(path); - logWarning(`Param overwritten: ${path} ${oldValue} => ${obj}`); + if (isOverwriteAllowed) { + logWarning(`Param overwritten: ${path} ${oldValue} => ${obj}`); + map.set(path, obj); + } + else { + logError(`Param overwritten: ${path} ${oldValue} => ${obj}`); + process.exit(0); + } + } + else { + map.set(path, obj); } - map.set(path, obj); } } -function validateInput(param, argsMap, defaultValue) { +function validateAndGet(param, argsMap, defaultValue) { const value = argsMap.get(param); if (!value) { - if (defaultValue) { + if (defaultValue || defaultValue === EMPTY_STR) { return defaultValue; } - logError(`${value} value is required`); + logError(`${param} arg is required`); process.exit(0); } return value; diff --git a/package.json b/package.json index 56a186b..2628e31 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "paramplate", - "version": "1.1.2", + "version": "1.2.0", "description": "Simple code generator", "main": "bin/index.js", "scripts": { diff --git a/src/index.ts b/src/index.ts index f14b86a..39b5c64 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,12 +9,18 @@ const COLORS = { fgYellow: '\x1b[33m' } +const EMPTY_STR = ''; +const NO = 'n'; +const YES = 'y'; + const INPUT_ARGS = { params: '--params', src: '--src', dest: '--dest', ext: '--ext', - debug: '--debug' + debug: '--debug', + overwrite: '--overwrite', + validator: '--validator' } // Start @@ -28,56 +34,82 @@ const { srcDir, destDir, templateExt, - isDebug + isDebug, + isOverwrite, + validatorDirs } = parseArgs(); -debugLog(`# Inputs`, isDebug); +debugLog('# Inputs', isDebug); debugLog(`- Src dir: ${srcDir}`, isDebug); debugLog(`- Dest dir: ${destDir}`, isDebug); debugLog(`- Template ext: ${templateExt}`, isDebug); +debugLog(`- Enable param overwrite: ${isOverwrite}`, isDebug); const paramsFiles = paramsDirs.split(','); const paramsMap = new Map(); +debugLog('', isDebug); +debugLog('# Parameter files', isDebug); paramsFiles.forEach((p) => { const paramsPath = path.resolve(path.normalize(p)); debugLog(`- Param file: ${paramsPath}`, isDebug); - loadParams(paramsPath, paramsMap); + transformJsonFileToMap(paramsPath, paramsMap, isOverwrite); }); if (isDebug) { console.log(); console.log('# All parameters'); for (let [key, value] of paramsMap) { - console.log(key + " = " + value); + console.log(key + "=" + value); } } -console.log(); -parseSrcDir(srcDir, paramsMap); +const validatorMap = new Map(); +const isValidatorUsed = validatorDirs !== EMPTY_STR; +if (isValidatorUsed) { + debugLog('', isDebug); + debugLog('# Validators', isDebug); + const validatorFiles = validatorDirs.split(','); + validatorFiles.forEach((p) => { + const validatorPath = path.resolve(path.normalize(p)); + debugLog(`- Validator file: ${validatorPath}`, isDebug); + transformJsonFileToMap(validatorPath, validatorMap, isOverwrite); + }); + + validateParams(paramsMap, validatorMap); +} + console.log(); +debugLog('# Parsing starts', isDebug); +parseSrcDir(srcDir, paramsMap); console.log('# Done!'); // End +// Interface interface Args { paramsDirs: string, srcDir: string, destDir: string, templateExt: string, - isDebug: boolean + isDebug: boolean, + isOverwrite: boolean, + validatorDirs: string } +// Utilities function parseArgs(): Args { - const { params, src, dest, ext, debug } = INPUT_ARGS; + const { params, src, dest, ext, debug, overwrite, validator } = INPUT_ARGS; const defaultExt = '.pp'; - const isDebugYes = 'y'; const argsMap = new Map(); const args = process.argv.slice(2); let i = 0; while (i < args.length) { const key = args[i]; if (key === debug) { - argsMap.set(key, isDebugYes); + argsMap.set(key, YES); + i++; + } else if (key == overwrite) { + argsMap.set(key, YES); i++; } else if (i + 1 < args.length) { let value = args[i + 1]; @@ -92,18 +124,22 @@ function parseArgs(): Args { } } - const srcDir = validateInput(src, argsMap); - const destDir = validateInput(dest, argsMap); - const paramsDirs = validateInput(params, argsMap); - const templateExt = validateInput(ext, argsMap, defaultExt); - const isDebug = validateInput(debug, argsMap) === isDebugYes ? true : false; + const srcDir = validateAndGet(src, argsMap); + const destDir = validateAndGet(dest, argsMap); + const paramsDirs = validateAndGet(params, argsMap); + const templateExt = validateAndGet(ext, argsMap, defaultExt); + const isDebug = validateAndGet(debug, argsMap, NO) === YES ? true : false; + const isOverwrite = validateAndGet(overwrite, argsMap, NO) === YES ? true : false; + const validatorDirs = validateAndGet(validator, argsMap, EMPTY_STR); return { paramsDirs, srcDir, destDir, templateExt, - isDebug + isDebug, + isOverwrite, + validatorDirs }; } @@ -219,45 +255,70 @@ function parseMustache(fileInput: string, paramsMap: Map) { return fileOutput; } -function loadParams(pathname: string, paramsMap: Map) { +function validateParams(paramsMap: Map, validatorMap: Map) { + for (let [key, value] of validatorMap) { + const reg = new RegExp(value); + if (paramsMap.has(key)) { + const paramValue = paramsMap.get(key); + if (paramValue && reg.test(paramValue)) { + debugLog(`Validating: key=${key}, value=${paramValue}, pattern=${value}`, isDebug); + } else { + logError(`Validation failed: key=${key}, value=${paramValue}, pattern=${value}`); + process.exit(0); + } + } else { + logWarning(`Missing validator param: ${key}`); + } + } +} + +function transformJsonFileToMap(pathname: string, paramsMap: Map, isOverwriteAllowed: boolean) { const file = fs.readFileSync(pathname, 'utf8').toString(); const obj = JSON.parse(file); - flattenObject(obj, '', paramsMap); + flattenObject(obj, '', paramsMap, isOverwriteAllowed); } -function flattenObject(obj: any, path: string, map: Map) { +function flattenObject(obj: any, path: string, map: Map, isOverwriteAllowed: boolean) { if (typeof obj === 'object' && obj !== null) { if (Array.isArray(obj)) { // obj is array obj.forEach((nextObj, i) => { - flattenObject(nextObj, path + '[' + i + ']', map); + flattenObject(nextObj, path + '[' + i + ']', map, isOverwriteAllowed); }); } else { // obj is object const pathPrefix = path === '' ? '' : path + '.'; Object.keys(obj).forEach((key) => { const nextObj = obj[key]; - flattenObject(nextObj, pathPrefix + key, map); + flattenObject(nextObj, pathPrefix + key, map, isOverwriteAllowed); }); } } else { // obj is value + // Find one existing value in the map. if (map.has(path) && map.get(path) !== obj) { const oldValue = map.get(path); - logWarning(`Param overwritten: ${path} ${oldValue} => ${obj}`); + if (isOverwriteAllowed) { + logWarning(`Param overwritten: ${path} ${oldValue} => ${obj}`); + map.set(path, obj); + } else { + logError(`Param overwritten: ${path} ${oldValue} => ${obj}`); + process.exit(0); + } + } else { + map.set(path, obj); } - map.set(path, obj); } } -function validateInput(param: string, argsMap: Map, defaultValue?: string): string { +function validateAndGet(param: string, argsMap: Map, defaultValue?: string): string { const value = argsMap.get(param); if (!value) { - if (defaultValue) { + if (defaultValue || defaultValue === EMPTY_STR) { return defaultValue; } - logError(`${value} value is required`); + logError(`${param} arg is required`); process.exit(0); } return value; diff --git a/test/cli-e2e.js b/test/cli-e2e.js index 8c1856b..900f4ae 100644 --- a/test/cli-e2e.js +++ b/test/cli-e2e.js @@ -8,7 +8,14 @@ console.log('Testing CLI e2e'); console.log('Run command'); console.log('----------------------------'); -const testCommand = 'node ./bin/index.js --debug --params ./test/input/param1.json,./test/input/param2.json --src ./test/input/root --dest ./test/output/root'; +const testCommand = `node ./bin/index.js \ + --debug \ + --overwrite \ + --validator ./test/input/validator.json \ + --params ./test/input/param1.json,./test/input/param2.json \ + --src ./test/input/root \ + --dest ./test/output/root`; + execSync(testCommand, {stdio: 'inherit'}); const input = path.resolve(path.normalize('./test/output/root')); diff --git a/test/input/validator.json b/test/input/validator.json new file mode 100644 index 0000000..1784a34 --- /dev/null +++ b/test/input/validator.json @@ -0,0 +1,8 @@ +{ + "param": { + "nestedParam2": { + "nestedParam3": "o*n" + } + }, + "validatorNotFound": "123" +} \ No newline at end of file