is-my-json-valid
Version:
A JSONSchema / orderly validator that uses code generation to be extremely fast
241 lines (191 loc) • 6.07 kB
JavaScript
var normalize = require('./normalize')
var formats = require('./formats')
var genobj = require('generate-object-property')
var genfun = require('generate-function')
var a = function(type) {
switch (type) {
case 'array':
case 'object':
case 'integer':
return 'an '+type
default:
return 'a '+type
}
}
var formatName = function(field) {
return field.replace(/\[[^\]]+\]/g, '.*')
}
var types = {}
types.any = function() {
return 'true'
}
types.null = function() {
return name+' === null'
}
types.boolean = function(name) {
return 'typeof '+name+' === "boolean"'
}
types.array = function(name) {
return 'Array.isArray('+name+')'
}
types.object = function(name) {
return 'typeof '+name+' === "object" && '+name+' && !Array.isArray('+name+')'
}
types.number = function(name) {
return 'typeof '+name+' === "number"'
}
types.integer = function(name) {
return 'typeof '+name+' === "number" && '+name+' | 0 === '+name
}
types.string = function(name) {
return 'typeof '+name+' === "string"'
}
var unique = function(array) {
for (var i = 1; i < array.length; i++) {
if (array.indexOf(array[i]) !== i) return false
}
return true
}
var toType = function(node) {
return node.type
}
var compile = function(sch) {
var schema = normalize(sch)
var scope = {unique:unique, formats:formats}
var syms = {}
var gensym = function(name) {
return name+(syms[name] = (syms[name] || 0)+1)
}
var vars = ['i','j','k','l','m','n','o','p','q','r','s','t','u','v','x','y','z']
var genloop = function() {
var v = vars.shift()
vars.push(v+v[0])
return v
}
var visit = function(name, node) {
var error = function(msg) {
var n = gensym('error')
scope[n] = {field:formatName(name), message:msg}
validate
('if (validate.errors === null) validate.errors = []')
('validate.errors.push(%s)', n)
}
if (node.required) {
if (node.nullable) validate('if (%s === undefined) {', name)
else validate('if (%s === undefined || %s === null) {', name, name)
error('is required')
validate('} else {')
} else {
if (node.nullable) validate('if (%s !== undefined) {', name)
else validate('if (%s !== undefined && %s !== null) {', name, name)
}
node.types.forEach(function(node, i) {
var valid = types[node.type](name)
if (i) validate('} else if (%s) {', valid)
else validate('if (%s) {', valid)
if (!node.conditions) validate('// do nothing - just type validate')
if (node.values) {
var toCompare = function(v) {
return name+' !== '+JSON.stringify(v)
}
validate('if (%s) {', node.values.map(toCompare).join(' && ') || 'true')
error('must be one of ['+node.values.join(', ')+']')
validate('}')
return
}
if (node.minimum !== undefined) {
validate('if (%s < %d) {', name, node.minimum)
error('must be more than '+node.minimum)
validate('}')
}
if (node.maximum !== undefined) {
validate('if (%s > %d) {', name, node.maximum)
error('must be less than '+node.maximum)
validate('}')
}
if (node.format && formats[node.format]) {
var n = gensym('format')
scope[n] = formats[node.format]
validate('if (!%s.test(%s)) {', n, name)
error('must be '+node.format+' format')
validate('}')
}
if (node.pattern) {
var n = gensym('pattern')
scope[n] = new RegExp(node.pattern)
validate('if (!%s.test(%s)) {', n, name)
error('must match /'+node.pattern+'/')
validate('}')
}
if (node.type === 'array') {
if (node.minItems) {
validate('if (%s.length < %d) {', name, node.minItems)
error('must contain at least '+node.minItems+' item(s)')
validate('}')
}
if (node.maxItems) {
validate('if (%s.length > %d) {', name, node.maxItems)
error('must contain at most '+node.minItems+' item(s)')
validate('}')
}
if (node.uniqueItems) {
validate('if (!unique(%s)) {', name)
error('must only contain unique values')
validate('}')
}
var i = genloop()
validate('for (var %s = 0; %s < %s.length; %s++) {', i, i, name, i)
visit(name+'['+i+']', node.items)
validate('}')
}
if (node.type === 'object' && node.additionalProperties === false) {
var i = genloop()
var keys = gensym('keys')
var toCompare = function(p) {
return keys+'['+i+'] !== '+JSON.stringify(p)
}
validate
('var %s = Object.keys(%s)', keys, name)
('for (var %s = 0; %s < %s.length; %s++) {', i, i, keys, i)
('if (%s) {', Object.keys(node.properties).map(toCompare).join(' && ') || 'true')
('if (validate.errors === null) validate.errors = []')
('validate.errors.push({"field":"%s."+%s[i], "message":"is not defined in the schema"})', formatName(name), keys)
('}')
('}')
}
if (node.type === 'object') {
Object.keys(node.properties).forEach(function(n) {
visit(genobj(name, n), node.properties[n])
})
}
})
if (node.types.length) {
validate('} else {')
error('must be '+node.types.map(toType).map(a).join(' or '))
validate('}')
}
validate('}')
}
var validate = genfun
('function validate(data) {')
('validate.errors = null')
visit('data', schema)
validate
('return validate.errors === null')
('}')
validate = validate.toFunction(scope)
validate.errors = null
validate.__defineGetter__('error', function() {
if (!validate.errors) return ''
return validate.errors
.map(function(err) {
return err.field+' '+err.message
})
.join('\n')
})
validate.toJSON = function() {
return sch
}
return validate
}
module.exports = compile