Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 65 additions & 20 deletions lib/convict.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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?
}
}

Expand Down Expand Up @@ -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;
Expand All @@ -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);
};

Expand Down
65 changes: 64 additions & 1 deletion test/format-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -333,12 +333,75 @@ 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();
});

it('successfully fails to validate incorrect children values', 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"/);
});
});
});