123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604 |
- var genobj = require('generate-object-property')
- var genfun = require('generate-function')
- var jsonpointer = require('jsonpointer')
- var xtend = require('xtend')
- var formats = require('./formats')
-
- var get = function(obj, additionalSchemas, ptr) {
-
- var visit = function(sub) {
- if (sub && sub.id === ptr) return sub
- if (typeof sub !== 'object' || !sub) return null
- return Object.keys(sub).reduce(function(res, k) {
- return res || visit(sub[k])
- }, null)
- }
-
- var res = visit(obj)
- if (res) return res
-
- ptr = ptr.replace(/^#/, '')
- ptr = ptr.replace(/\/$/, '')
-
- try {
- return jsonpointer.get(obj, decodeURI(ptr))
- } catch (err) {
- var end = ptr.indexOf('#')
- var other
- // external reference
- if (end !== 0) {
- // fragment doesn't exist.
- if (end === -1) {
- other = additionalSchemas[ptr]
- } else {
- var ext = ptr.slice(0, end)
- other = additionalSchemas[ext]
- var fragment = ptr.slice(end).replace(/^#/, '')
- try {
- return jsonpointer.get(other, fragment)
- } catch (err) {}
- }
- } else {
- other = additionalSchemas[ptr]
- }
- return other || null
- }
- }
-
- var formatName = function(field) {
- field = JSON.stringify(field)
- var pattern = /\[([^\[\]"]+)\]/
- while (pattern.test(field)) field = field.replace(pattern, '."+$1+"')
- return field
- }
-
- var types = {}
-
- types.any = function() {
- return 'true'
- }
-
- types.null = function(name) {
- 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" && isFinite('+name+')'
- }
-
- types.integer = function(name) {
- return 'typeof '+name+' === "number" && (Math.floor('+name+') === '+name+' || '+name+' > 9007199254740992 || '+name+' < -9007199254740992)'
- }
-
- types.string = function(name) {
- return 'typeof '+name+' === "string"'
- }
-
- var unique = function(array) {
- var list = []
- for (var i = 0; i < array.length; i++) {
- list.push(typeof array[i] === 'object' ? JSON.stringify(array[i]) : array[i])
- }
- for (var i = 1; i < list.length; i++) {
- if (list.indexOf(list[i]) !== i) return false
- }
- return true
- }
-
- var isMultipleOf = function(name, multipleOf) {
- var res;
- var factor = ((multipleOf | 0) !== multipleOf) ? Math.pow(10, multipleOf.toString().split('.').pop().length) : 1
- if (factor > 1) {
- var factorName = ((name | 0) !== name) ? Math.pow(10, name.toString().split('.').pop().length) : 1
- if (factorName > factor) res = true
- else res = Math.round(factor * name) % (factor * multipleOf)
- }
- else res = name % multipleOf;
- return !res;
- }
-
- var compile = function(schema, cache, root, reporter, opts) {
- var fmts = opts ? xtend(formats, opts.formats) : formats
- var scope = {unique:unique, formats:fmts, isMultipleOf:isMultipleOf}
- var verbose = opts ? !!opts.verbose : false;
- var greedy = opts && opts.greedy !== undefined ?
- opts.greedy : false;
-
- var syms = {}
- var gensym = function(name) {
- return name+(syms[name] = (syms[name] || 0)+1)
- }
-
- var reversePatterns = {}
- var patterns = function(p) {
- if (reversePatterns[p]) return reversePatterns[p]
- var n = gensym('pattern')
- scope[n] = new RegExp(p)
- reversePatterns[p] = n
- return n
- }
-
- 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, reporter, filter, schemaPath) {
- var properties = node.properties
- var type = node.type
- var tuple = false
-
- if (Array.isArray(node.items)) { // tuple type
- properties = {}
- node.items.forEach(function(item, i) {
- properties[i] = item
- })
- type = 'array'
- tuple = true
- }
-
- var indent = 0
- var error = function(msg, prop, value) {
- validate('errors++')
- if (reporter === true) {
- validate('if (validate.errors === null) validate.errors = []')
- if (verbose) {
- validate(
- 'validate.errors.push({field:%s,message:%s,value:%s,type:%s,schemaPath:%s})',
- formatName(prop || name),
- JSON.stringify(msg),
- value || name,
- JSON.stringify(type),
- JSON.stringify(schemaPath)
- )
- } else {
- validate('validate.errors.push({field:%s,message:%s})', formatName(prop || name), JSON.stringify(msg))
- }
- }
- }
-
- if (node.required === true) {
- indent++
- validate('if (%s === undefined) {', name)
- error('is required')
- validate('} else {')
- } else {
- indent++
- validate('if (%s !== undefined) {', name)
- }
-
- var valid = [].concat(type)
- .map(function(t) {
- if (t && !types.hasOwnProperty(t)) {
- throw new Error('Unknown type: ' + t)
- }
-
- return types[t || 'any'](name)
- })
- .join(' || ') || 'true'
-
- if (valid !== 'true') {
- indent++
- validate('if (!(%s)) {', valid)
- error('is the wrong type')
- validate('} else {')
- }
-
- if (tuple) {
- if (node.additionalItems === false) {
- validate('if (%s.length > %d) {', name, node.items.length)
- error('has additional items')
- validate('}')
- } else if (node.additionalItems) {
- var i = genloop()
- validate('for (var %s = %d; %s < %s.length; %s++) {', i, node.items.length, i, name, i)
- visit(name+'['+i+']', node.additionalItems, reporter, filter, schemaPath.concat('additionalItems'))
- validate('}')
- }
- }
-
- if (node.format && fmts[node.format]) {
- if (type !== 'string' && formats[node.format]) validate('if (%s) {', types.string(name))
- var n = gensym('format')
- scope[n] = fmts[node.format]
-
- if (typeof scope[n] === 'function') validate('if (!%s(%s)) {', n, name)
- else validate('if (!%s.test(%s)) {', n, name)
- error('must be '+node.format+' format')
- validate('}')
- if (type !== 'string' && formats[node.format]) validate('}')
- }
-
- if (Array.isArray(node.required)) {
- var checkRequired = function (req) {
- var prop = genobj(name, req);
- validate('if (%s === undefined) {', prop)
- error('is required', prop)
- validate('missing++')
- validate('}')
- }
- validate('if ((%s)) {', type !== 'object' ? types.object(name) : 'true')
- validate('var missing = 0')
- node.required.map(checkRequired)
- validate('}');
- if (!greedy) {
- validate('if (missing === 0) {')
- indent++
- }
- }
-
- if (node.uniqueItems) {
- if (type !== 'array') validate('if (%s) {', types.array(name))
- validate('if (!(unique(%s))) {', name)
- error('must be unique')
- validate('}')
- if (type !== 'array') validate('}')
- }
-
- if (node.enum) {
- var complex = node.enum.some(function(e) {
- return typeof e === 'object'
- })
-
- var compare = complex ?
- function(e) {
- return 'JSON.stringify('+name+')'+' !== JSON.stringify('+JSON.stringify(e)+')'
- } :
- function(e) {
- return name+' !== '+JSON.stringify(e)
- }
-
- validate('if (%s) {', node.enum.map(compare).join(' && ') || 'false')
- error('must be an enum value')
- validate('}')
- }
-
- if (node.dependencies) {
- if (type !== 'object') validate('if (%s) {', types.object(name))
-
- Object.keys(node.dependencies).forEach(function(key) {
- var deps = node.dependencies[key]
- if (typeof deps === 'string') deps = [deps]
-
- var exists = function(k) {
- return genobj(name, k) + ' !== undefined'
- }
-
- if (Array.isArray(deps)) {
- validate('if (%s !== undefined && !(%s)) {', genobj(name, key), deps.map(exists).join(' && ') || 'true')
- error('dependencies not set')
- validate('}')
- }
- if (typeof deps === 'object') {
- validate('if (%s !== undefined) {', genobj(name, key))
- visit(name, deps, reporter, filter, schemaPath.concat(['dependencies', key]))
- validate('}')
- }
- })
-
- if (type !== 'object') validate('}')
- }
-
- if (node.additionalProperties || node.additionalProperties === false) {
- if (type !== 'object') validate('if (%s) {', types.object(name))
-
- var i = genloop()
- var keys = gensym('keys')
-
- var toCompare = function(p) {
- return keys+'['+i+'] !== '+JSON.stringify(p)
- }
-
- var toTest = function(p) {
- return '!'+patterns(p)+'.test('+keys+'['+i+'])'
- }
-
- var additionalProp = Object.keys(properties || {}).map(toCompare)
- .concat(Object.keys(node.patternProperties || {}).map(toTest))
- .join(' && ') || 'true'
-
- validate('var %s = Object.keys(%s)', keys, name)
- ('for (var %s = 0; %s < %s.length; %s++) {', i, i, keys, i)
- ('if (%s) {', additionalProp)
-
- if (node.additionalProperties === false) {
- if (filter) validate('delete %s', name+'['+keys+'['+i+']]')
- error('has additional properties', null, JSON.stringify(name+'.') + ' + ' + keys + '['+i+']')
- } else {
- visit(name+'['+keys+'['+i+']]', node.additionalProperties, reporter, filter, schemaPath.concat(['additionalProperties']))
- }
-
- validate
- ('}')
- ('}')
-
- if (type !== 'object') validate('}')
- }
-
- if (node.$ref) {
- var sub = get(root, opts && opts.schemas || {}, node.$ref)
- if (sub) {
- var fn = cache[node.$ref]
- if (!fn) {
- cache[node.$ref] = function proxy(data) {
- return fn(data)
- }
- fn = compile(sub, cache, root, false, opts)
- }
- var n = gensym('ref')
- scope[n] = fn
- validate('if (!(%s(%s))) {', n, name)
- error('referenced schema does not match')
- validate('}')
- }
- }
-
- if (node.not) {
- var prev = gensym('prev')
- validate('var %s = errors', prev)
- visit(name, node.not, false, filter, schemaPath.concat('not'))
- validate('if (%s === errors) {', prev)
- error('negative schema matches')
- validate('} else {')
- ('errors = %s', prev)
- ('}')
- }
-
- if (node.items && !tuple) {
- if (type !== 'array') validate('if (%s) {', types.array(name))
-
- var i = genloop()
- validate('for (var %s = 0; %s < %s.length; %s++) {', i, i, name, i)
- visit(name+'['+i+']', node.items, reporter, filter, schemaPath.concat('items'))
- validate('}')
-
- if (type !== 'array') validate('}')
- }
-
- if (node.patternProperties) {
- if (type !== 'object') validate('if (%s) {', types.object(name))
- var keys = gensym('keys')
- var i = genloop()
- validate
- ('var %s = Object.keys(%s)', keys, name)
- ('for (var %s = 0; %s < %s.length; %s++) {', i, i, keys, i)
-
- Object.keys(node.patternProperties).forEach(function(key) {
- var p = patterns(key)
- validate('if (%s.test(%s)) {', p, keys+'['+i+']')
- visit(name+'['+keys+'['+i+']]', node.patternProperties[key], reporter, filter, schemaPath.concat(['patternProperties', key]))
- validate('}')
- })
-
- validate('}')
- if (type !== 'object') validate('}')
- }
-
- if (node.pattern) {
- var p = patterns(node.pattern)
- if (type !== 'string') validate('if (%s) {', types.string(name))
- validate('if (!(%s.test(%s))) {', p, name)
- error('pattern mismatch')
- validate('}')
- if (type !== 'string') validate('}')
- }
-
- if (node.allOf) {
- node.allOf.forEach(function(sch, key) {
- visit(name, sch, reporter, filter, schemaPath.concat(['allOf', key]))
- })
- }
-
- if (node.anyOf && node.anyOf.length) {
- var prev = gensym('prev')
-
- node.anyOf.forEach(function(sch, i) {
- if (i === 0) {
- validate('var %s = errors', prev)
- } else {
- validate('if (errors !== %s) {', prev)
- ('errors = %s', prev)
- }
- visit(name, sch, false, false, schemaPath)
- })
- node.anyOf.forEach(function(sch, i) {
- if (i) validate('}')
- })
- validate('if (%s !== errors) {', prev)
- error('no schemas match')
- validate('}')
- }
-
- if (node.oneOf && node.oneOf.length) {
- var prev = gensym('prev')
- var passes = gensym('passes')
-
- validate
- ('var %s = errors', prev)
- ('var %s = 0', passes)
-
- node.oneOf.forEach(function(sch, i) {
- visit(name, sch, false, false, schemaPath)
- validate('if (%s === errors) {', prev)
- ('%s++', passes)
- ('} else {')
- ('errors = %s', prev)
- ('}')
- })
-
- validate('if (%s !== 1) {', passes)
- error('no (or more than one) schemas match')
- validate('}')
- }
-
- if (node.multipleOf !== undefined) {
- if (type !== 'number' && type !== 'integer') validate('if (%s) {', types.number(name))
-
- validate('if (!isMultipleOf(%s, %d)) {', name, node.multipleOf)
-
- error('has a remainder')
- validate('}')
-
- if (type !== 'number' && type !== 'integer') validate('}')
- }
-
- if (node.maxProperties !== undefined) {
- if (type !== 'object') validate('if (%s) {', types.object(name))
-
- validate('if (Object.keys(%s).length > %d) {', name, node.maxProperties)
- error('has more properties than allowed')
- validate('}')
-
- if (type !== 'object') validate('}')
- }
-
- if (node.minProperties !== undefined) {
- if (type !== 'object') validate('if (%s) {', types.object(name))
-
- validate('if (Object.keys(%s).length < %d) {', name, node.minProperties)
- error('has less properties than allowed')
- validate('}')
-
- if (type !== 'object') validate('}')
- }
-
- if (node.maxItems !== undefined) {
- if (type !== 'array') validate('if (%s) {', types.array(name))
-
- validate('if (%s.length > %d) {', name, node.maxItems)
- error('has more items than allowed')
- validate('}')
-
- if (type !== 'array') validate('}')
- }
-
- if (node.minItems !== undefined) {
- if (type !== 'array') validate('if (%s) {', types.array(name))
-
- validate('if (%s.length < %d) {', name, node.minItems)
- error('has less items than allowed')
- validate('}')
-
- if (type !== 'array') validate('}')
- }
-
- if (node.maxLength !== undefined) {
- if (type !== 'string') validate('if (%s) {', types.string(name))
-
- validate('if (%s.length > %d) {', name, node.maxLength)
- error('has longer length than allowed')
- validate('}')
-
- if (type !== 'string') validate('}')
- }
-
- if (node.minLength !== undefined) {
- if (type !== 'string') validate('if (%s) {', types.string(name))
-
- validate('if (%s.length < %d) {', name, node.minLength)
- error('has less length than allowed')
- validate('}')
-
- if (type !== 'string') validate('}')
- }
-
- if (node.minimum !== undefined) {
- if (type !== 'number' && type !== 'integer') validate('if (%s) {', types.number(name))
-
- validate('if (%s %s %d) {', name, node.exclusiveMinimum ? '<=' : '<', node.minimum)
- error('is less than minimum')
- validate('}')
-
- if (type !== 'number' && type !== 'integer') validate('}')
- }
-
- if (node.maximum !== undefined) {
- if (type !== 'number' && type !== 'integer') validate('if (%s) {', types.number(name))
-
- validate('if (%s %s %d) {', name, node.exclusiveMaximum ? '>=' : '>', node.maximum)
- error('is more than maximum')
- validate('}')
-
- if (type !== 'number' && type !== 'integer') validate('}')
- }
-
- if (properties) {
- Object.keys(properties).forEach(function(p) {
- if (Array.isArray(type) && type.indexOf('null') !== -1) validate('if (%s !== null) {', name)
-
- visit(
- genobj(name, p),
- properties[p],
- reporter,
- filter,
- schemaPath.concat(tuple ? p : ['properties', p])
- )
-
- if (Array.isArray(type) && type.indexOf('null') !== -1) validate('}')
- })
- }
-
- while (indent--) validate('}')
- }
-
- var validate = genfun
- ('function validate(data) {')
- // Since undefined is not a valid JSON value, we coerce to null and other checks will catch this
- ('if (data === undefined) data = null')
- ('validate.errors = null')
- ('var errors = 0')
-
- visit('data', schema, reporter, opts && opts.filter, [])
-
- validate
- ('return errors === 0')
- ('}')
-
- validate = validate.toFunction(scope)
- validate.errors = null
-
- if (Object.defineProperty) {
- Object.defineProperty(validate, 'error', {
- get: function() {
- if (!validate.errors) return ''
- return validate.errors.map(function(err) {
- return err.field + ' ' + err.message;
- }).join('\n')
- }
- })
- }
-
- validate.toJSON = function() {
- return schema
- }
-
- return validate
- }
-
- module.exports = function(schema, opts) {
- if (typeof schema === 'string') schema = JSON.parse(schema)
- return compile(schema, {}, schema, true, opts)
- }
-
- module.exports.filter = function(schema, opts) {
- var validate = module.exports(schema, xtend(opts, {filter: true}))
- return function(sch) {
- validate(sch)
- return sch
- }
- }
|