blob: fb00dffbb1bac4f6013fb923a2a20fffd84a6203 [file] [log] [blame]
(function (exports) {
exports.validate = validate;
exports.mixin = mixin;
//
// ### function validate (object, schema, options)
// #### {Object} object the object to validate.
// #### {Object} schema (optional) the JSON Schema to validate against.
// #### {Object} options (optional) options controlling the validation
// process. See {@link #validate.defaults) for details.
// Validate <code>object</code> against a JSON Schema.
// If <code>object</code> is self-describing (i.e. has a
// <code>$schema</code> property), it will also be validated
// against the referenced schema. [TODO]: This behaviour bay be
// suppressed by setting the {@link #validate.options.???}
// option to <code>???</code>.[/TODO]
//
// If <code>schema</code> is not specified, and <code>object</code>
// is not self-describing, validation always passes.
//
// <strong>Note:</strong> in order to pass options but no schema,
// <code>schema</code> <em>must</em> be specified in the call to
// <code>validate()</code>; otherwise, <code>options</code> will
// be interpreted as the schema. <code>schema</code> may be passed
// as <code>null</code>, <code>undefinded</code>, or the empty object
// (<code>{}</code>) in this case.
//
function validate(object, schema, options) {
options = mixin({}, options, validate.defaults);
var errors = [];
validateObject(object, schema, options, errors);
//
// TODO: self-described validation
// if (! options.selfDescribing) { ... }
//
return {
valid: !(errors.length),
errors: errors
};
};
/**
* Default validation options. Defaults can be overridden by
* passing an 'options' hash to {@link #validate}. They can
* also be set globally be changing the values in
* <code>validate.defaults</code> directly.
*/
validate.defaults = {
/**
* <p>
* Enforce 'format' constraints.
* </p><p>
* <em>Default: <code>true</code></em>
* </p>
*/
validateFormats: true,
/**
* <p>
* When {@link #validateFormats} is <code>true</code>,
* treat unrecognized formats as validation errors.
* </p><p>
* <em>Default: <code>false</code></em>
* </p>
*
* @see validation.formats for default supported formats.
*/
validateFormatsStrict: false,
/**
* <p>
* When {@link #validateFormats} is <code>true</code>,
* also validate formats defined in {@link #validate.formatExtensions}.
* </p><p>
* <em>Default: <code>true</code></em>
* </p>
*/
validateFormatExtensions: true
};
/**
* Default messages to include with validation errors.
*/
validate.messages = {
required: "is required",
minLength: "is too short (minimum is %{expected} characters)",
maxLength: "is too long (maximum is %{expected} characters)",
pattern: "invalid input",
minimum: "must be greater than or equal to %{expected}",
maximum: "must be less than or equal to %{expected}",
exclusiveMinimum: "must be greater than %{expected}",
exclusiveMaximum: "must be less than %{expected}",
divisibleBy: "must be divisible by %{expected}",
minItems: "must contain more than %{expected} items",
maxItems: "must contain less than %{expected} items",
uniqueItems: "must hold a unique set of values",
format: "is not a valid %{expected}",
conform: "must conform to given constraint",
type: "must be of %{expected} type"
};
validate.messages['enum'] = "must be present in given enumerator";
/**
*
*/
validate.formats = {
'email': /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$/i,
'ip-address': /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/i,
'ipv6': /^([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}$/,
'date-time': /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:.\d{1,3})?Z$/,
'date': /^\d{4}-\d{2}-\d{2}$/,
'time': /^\d{2}:\d{2}:\d{2}$/,
'color': /^#[a-z0-9]{6}|#[a-z0-9]{3}|(?:rgb\(\s*(?:[+-]?\d+%?)\s*,\s*(?:[+-]?\d+%?)\s*,\s*(?:[+-]?\d+%?)\s*\))aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow$/i,
//'style': (not supported)
//'phone': (not supported)
//'uri': (not supported)
'host-name': /^(([a-zA-Z]|[a-zA-Z][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])/,
'utc-millisec': {
test: function (value) {
return typeof(value) === 'number' && value >= 0;
}
},
'regex': {
test: function (value) {
try { new RegExp(value) }
catch (e) { return false }
return true;
}
}
};
/**
*
*/
validate.formatExtensions = {
'url': /^(https?|ftp|git):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i
};
function mixin(obj) {
var sources = Array.prototype.slice.call(arguments, 1);
while (sources.length) {
var source = sources.shift();
if (!source) { continue }
if (typeof(source) !== 'object') {
throw new TypeError('mixin non-object');
}
for (var p in source) {
if (source.hasOwnProperty(p)) {
obj[p] = source[p];
}
}
}
return obj;
};
function validateObject(object, schema, options, errors) {
var props, allProps = Object.keys(object),
visitedProps = [];
// see 5.2
if (schema.properties) {
props = schema.properties;
for (var p in props) {
if (props.hasOwnProperty(p)) {
visitedProps.push(p);
validateProperty(object, object[p], p, props[p], options, errors);
}
}
}
// see 5.3
if (schema.patternProperties) {
props = schema.patternProperties;
for (var p in props) {
if (props.hasOwnProperty(p)) {
var re = new RegExp(p);
// Find all object properties that are matching `re`
for (var k in object) {
if (object.hasOwnProperty(k)) {
visitedProps.push(k);
if (re.exec(k) !== null) {
validateProperty(object, object[k], p, props[p], options, errors);
}
}
}
}
}
}
// see 5.4
if (undefined !== schema.additionalProperties) {
var i, l;
var unvisitedProps = allProps.filter(function(k){
return -1 === visitedProps.indexOf(k);
});
// Prevent additional properties; each unvisited property is therefore an error
if (schema.additionalProperties === false && unvisitedProps.length > 0) {
for (i = 0, l = unvisitedProps.length; i < l; i++) {
error("additionalProperties", unvisitedProps[i], object[unvisitedProps[i]], false, errors);
}
}
// additionalProperties is a schema and validate unvisited properties against that schema
else if (typeof schema.additionalProperties == "object" && unvisitedProps.length > 0) {
for (i = 0, l = unvisitedProps.length; i < l; i++) {
validateProperty(object, object[unvisitedProps[i]], unvisitedProps[i], schema.unvisitedProperties, options, errors);
}
}
}
};
function validateProperty(object, value, property, schema, options, errors) {
var format,
valid,
spec,
type;
function constrain(name, value, assert) {
if (schema[name] !== undefined && !assert(value, schema[name])) {
error(name, property, value, schema, errors);
}
}
if (value === undefined || value === '') {
if (schema.required && schema.type !== 'any') {
return error('required', property, undefined, schema, errors);
} else {
return;
}
}
if (options.cast) {
if (('integer' === schema.type || 'number' === schema.type) && value == +value) {
value = +value;
}
if ('boolean' === schema.type) {
if ('true' === value || '1' === value || 1 === value) {
value = true;
}
if ('false' === value || '0' === value || 0 === value) {
value = false;
}
}
}
if (schema.format && options.validateFormats) {
format = schema.format;
if (options.validateFormatExtensions) { spec = validate.formatExtensions[format] }
if (!spec) { spec = validate.formats[format] }
if (!spec) {
if (options.validateFormatsStrict) {
return error('format', property, value, schema, errors);
}
}
else {
if (!spec.test(value)) {
return error('format', property, value, schema, errors);
}
}
}
if (schema['enum'] && schema['enum'].indexOf(value) === -1) {
error('enum', property, value, schema, errors);
}
// Dependencies (see 5.8)
if (typeof schema.dependencies === 'string' &&
object[schema.dependencies] === undefined) {
error('dependencies', property, null, schema, errors);
}
if (isArray(schema.dependencies)) {
for (var i = 0, l = schema.dependencies.length; i < l; i++) {
if (object[schema.dependencies[i]] === undefined) {
error('dependencies', property, null, schema, errors);
}
}
}
if (typeof schema.dependencies === 'object') {
validateObject(object, schema.dependencies, options, errors);
}
checkType(value, schema.type, function(err, type) {
if (err) return error('type', property, typeof value, schema, errors);
constrain('conform', value, function (a, e) { return e(a, object) });
switch (type || (isArray(value) ? 'array' : typeof value)) {
case 'string':
constrain('minLength', value.length, function (a, e) { return a >= e });
constrain('maxLength', value.length, function (a, e) { return a <= e });
constrain('pattern', value, function (a, e) {
e = typeof e === 'string'
? e = new RegExp(e)
: e;
return e.test(a)
});
break;
case 'integer':
case 'number':
constrain('minimum', value, function (a, e) { return a >= e });
constrain('maximum', value, function (a, e) { return a <= e });
constrain('exclusiveMinimum', value, function (a, e) { return a > e });
constrain('exclusiveMaximum', value, function (a, e) { return a < e });
constrain('divisibleBy', value, function (a, e) {
var multiplier = Math.max((a - Math.floor(a)).toString().length - 2, (e - Math.floor(e)).toString().length - 2);
multiplier = multiplier > 0 ? Math.pow(10, multiplier) : 1;
return (a * multiplier) % (e * multiplier) === 0
});
break;
case 'array':
constrain('items', value, function (a, e) {
for (var i = 0, l = a.length; i < l; i++) {
validateProperty(object, a[i], property, e, options, errors);
}
return true;
});
constrain('minItems', value, function (a, e) { return a.length >= e });
constrain('maxItems', value, function (a, e) { return a.length <= e });
constrain('uniqueItems', value, function (a) {
var h = {};
for (var i = 0, l = a.length; i < l; i++) {
var key = JSON.stringify(a[i]);
if (h[key]) return false;
h[key] = true;
}
return true;
});
break;
case 'object':
// Recursive validation
if (schema.properties || schema.patternProperties || schema.additionalProperties) {
validateObject(value, schema, options, errors);
}
break;
}
});
};
function checkType(val, type, callback) {
var result = false,
types = isArray(type) ? type : [type];
// No type - no check
if (type === undefined) return callback(null, type);
// Go through available types
// And fine first matching
for (var i = 0, l = types.length; i < l; i++) {
type = types[i].toLowerCase().trim();
if (type === 'string' ? typeof val === 'string' :
type === 'array' ? isArray(val) :
type === 'object' ? val && typeof val === 'object' &&
!isArray(val) :
type === 'number' ? typeof val === 'number' :
type === 'integer' ? typeof val === 'number' && ~~val === val :
type === 'null' ? val === null :
type === 'boolean'? typeof val === 'boolean' :
type === 'date' ? isDate(val) :
type === 'any' ? typeof val !== 'undefined' : false) {
return callback(null, type);
}
};
callback(true);
};
function error(attribute, property, actual, schema, errors) {
var lookup = { expected: schema[attribute], attribute: attribute, property: property };
var message = schema.messages && schema.messages[attribute] || schema.message || validate.messages[attribute] || "no default message";
message = message.replace(/%\{([a-z]+)\}/ig, function (_, match) { return lookup[match.toLowerCase()] || ''; });
errors.push({
attribute: attribute,
property: property,
expected: schema[attribute],
actual: actual,
message: message
});
};
function isArray(value) {
var s = typeof value;
if (s === 'object') {
if (value) {
if (typeof value.length === 'number' &&
!(value.propertyIsEnumerable('length')) &&
typeof value.splice === 'function') {
return true;
}
}
}
return false;
}
function isDate(value) {
var s = typeof value;
if (s === 'object') {
if (value) {
if (typeof value.getTime === 'function') {
return true;
}
}
}
return false;
}
})(typeof(window) === 'undefined' ? module.exports : (window.json = window.json || {}));