From feaad28f4514ebd96c0bf6d3115c77169ae1335c Mon Sep 17 00:00:00 2001 From: A-312 Date: Sun, 27 Oct 2019 14:50:28 +0100 Subject: [PATCH] addFormat(name, validate) accepts regExp for name --- lib/convict.js | 85 +++++++++++++++++++++++++++++++++----------- test/format-tests.js | 65 ++++++++++++++++++++++++++++++++- 2 files changed, 129 insertions(+), 21 deletions(-) diff --git a/lib/convict.js b/lib/convict.js index 6c391f54..60514bc6 100644 --- a/lib/convict.js +++ b/lib/convict.js @@ -284,15 +284,29 @@ function normalizeSchema(name, node, props, fullName, env, argv, sensitive) { o.format = Format.name.toLowerCase(); } else if (typeof format === 'string') { - // store declared type - if (!types[format]) { - throw new Error("'" + fullName + "' uses an unknown format type: " + - format); - } - // use a predefined type newFormat = types[format]; + // format with regex name + if (!newFormat) { + let regs = types['>RegExp']; + if (regs) { + for (let i = 0; i < regs.length; i++) { + if (!regs[i][0].test(format)) { + continue; + } + + newFormat = regs[i][1]; // regs[i] -> [RegExp, validate] + break; + } + } + } + + // unknown format type + if (!newFormat) { + throw new Error("'" + fullName + "' uses an unknown format type: " + + format); + } } else if (Array.isArray(format)) { // assert that the value is a valid option newFormat = contains.bind(null, format); @@ -415,28 +429,41 @@ function coerce(k, v, schema, instance) { case 'port': case 'nat': case 'integer': - case 'int': v = parseInt(v, 10); break; - case 'port_or_windows_named_pipe': v = isWindowsNamedPipe(v) ? v : parseInt(v, 10); break; - case 'number': v = parseFloat(v); break; - case 'boolean': v = (String(v).toLowerCase() !== 'false'); break; - case 'array': v = v.split(','); break; - case 'object': v = JSON.parse(v); break; - case 'regexp': v = new RegExp(v); break; - case 'timestamp': v = moment(v).valueOf(); break; + case 'int': return parseInt(v, 10); + case 'port_or_windows_named_pipe': return isWindowsNamedPipe(v) ? v : parseInt(v, 10); + case 'number': return parseFloat(v); + case 'boolean': return (String(v).toLowerCase() !== 'false'); + case 'array': return v.split(','); + case 'object': return JSON.parse(v); + case 'regexp': return new RegExp(v); + case 'timestamp': return moment(v).valueOf(); case 'duration': { let split = v.split(' '); if (split.length == 1) { // It must be an integer in string form. - v = parseInt(v, 10); + return parseInt(v, 10); } else { // Add an "s" as the unit of measurement used in Moment if (!split[1].match(/s$/)) split[1] += 's'; - v = moment.duration(parseInt(split[0], 10), split[1]).valueOf(); + return moment.duration(parseInt(split[0], 10), split[1]).valueOf(); } - break; } default: - // TODO: Should we throw an exception here? + // format with regex name + if (types['>RegExp']) { + let regs = types['>RegExp']; + for (let i = 0; i < regs.length; i++) { + if (regs[i][0].test(format)) { + let name = '>RegExp>' + i; + // coerce with keyname : '>RegExp>0', '>RegExp>1', '>RegExp>2'... + if (converters.has(name)) { + return converters.get(name)(v, instance, k); + } + } + } + } + + // TODO: Should we throw an exception here? } } @@ -723,7 +750,7 @@ let convict = function convict(def, opts) { * Adds a new custom format */ convict.addFormat = function(name, validate, coerce) { - if (typeof name === 'object') { + if (typeof name === 'object' && name.constructor !== RegExp) { validate = name.validate; coerce = name.coerce; name = name.name; @@ -734,7 +761,25 @@ convict.addFormat = function(name, validate, coerce) { if (coerce && typeof coerce !== 'function') { throw new Error('Coerce function for ' + name + ' must be a function.'); } - types[name] = validate; + if (typeof name === 'string' && name.startsWith('>RegExp')) { + throw new Error("'>RegExp' is reserved name. Name can't start with '>RegExp'."); + } else if (typeof name === 'string') { + types[name] = validate; + } else if (name.constructor === RegExp) { + if (!types['>RegExp']) { + types['>RegExp'] = []; + } + + types['>RegExp'].push([name, validate]); + } else { + throw new Error('Invalid format for name : ' + name + ' is ' + (typeof name) + ' must be a RegExp or a String.'); + } + + // set name : >RegExp>0, >RegExp>1, >RegExp>2... for coerce + if (name.constructor === RegExp) { + name = '>RegExp>' + (name.length - 1); + } + if (coerce) converters.set(name, coerce); }; diff --git a/test/format-tests.js b/test/format-tests.js index 44f1649c..320050dc 100644 --- a/test/format-tests.js +++ b/test/format-tests.js @@ -333,7 +333,7 @@ describe('convict formats', function() { }); }); - it('must validate children value without throw an Error', function() { + it('must validate children values without throw an Error', function() { (() => convict(schema).load(config).validate()).must.not.throw(); }); @@ -341,4 +341,67 @@ describe('convict formats', function() { (() => convict(schema).load(configWithError).validate()).must.throw(Error, /url: must be a URL: value was "https:\/\(è_é\)\/github\.com\/mozilla\/node-convict\.git"/); }); }); + + it('must accept regex name in .addFormat(...)', function() { + const schema = { + dependencies: { + format: 'Array[String]', + default: [] + }, + serverips: { + format: 'Array[ipaddress]', + default: [] + } + }; + + const config = { + 'dependencies': ['convict', 'express'], + 'serverips': ['127.0.0.1', '8.8.8.8'] + }; + + const configWithError = { + 'dependencies': ['convict', 'express', []], + 'serverips': ['127.0.0.1', '8.8.8.8'] + }; + + const configWithError2 = { + 'dependencies': ['convict', 'express'], + 'serverips': ['127.0.0.1', '8.8.8.8', '127'] + }; + + it('must parse a config specification', function() { + convict.addFormat({ + name: /^Array\[(.*)]$/, + validate: function(values, schema) { + if (!Array.isArray(values)) { + throw new Error('must be of type Array'); + } + + values.forEach((value, key) => { + const name = `arr[${key}]`; + + const subSchema = {}; + const arr = {}; + + subSchema[name] = { + format: schema.format.match(/^Array\[(.*)]$/)[1], + default: null + }; + arr[name] = value; + + convict(subSchema).load(arr).validate(); + }) + } + }); + }); + + it('must validate sub values without throw an Error', function() { + (() => convict(schema).load(config).validate()).must.not.throw(); + }); + + it('successfully fails to validate incorrect sub values', function() { + (() => convict(schema).load(configWithError).validate()).must.throw(Error, /dependencies: arr\[2]: must be of type String: value was \[]/); + (() => convict(schema).load(configWithError2).validate()).must.throw(Error, /serverips: arr\[2]: must be an IP address: value was "127"/); + }); + }); });