1 var genobj = require('generate-object-property')
2 var genfun = require('generate-function')
3 var jsonpointer = require('jsonpointer')
4 var xtend = require('xtend')
5 var formats = require('./formats')
7 var get = function(obj, additionalSchemas, ptr) {
9 var visit = function(sub) {
10 if (sub && sub.id === ptr) return sub
11 if (typeof sub !== 'object' || !sub) return null
12 return Object.keys(sub).reduce(function(res, k) {
13 return res || visit(sub[k])
20 ptr = ptr.replace(/^#/, '')
21 ptr = ptr.replace(/\/$/, '')
24 return jsonpointer.get(obj, decodeURI(ptr))
26 var end = ptr.indexOf('#')
30 // fragment doesn't exist.
32 other = additionalSchemas[ptr]
34 var ext = ptr.slice(0, end)
35 other = additionalSchemas[ext]
36 var fragment = ptr.slice(end).replace(/^#/, '')
38 return jsonpointer.get(other, fragment)
42 other = additionalSchemas[ptr]
48 var formatName = function(field) {
49 field = JSON.stringify(field)
50 var pattern = /\[([^\[\]"]+)\]/
51 while (pattern.test(field)) field = field.replace(pattern, '."+$1+"')
57 types.any = function() {
61 types.null = function(name) {
62 return name+' === null'
65 types.boolean = function(name) {
66 return 'typeof '+name+' === "boolean"'
69 types.array = function(name) {
70 return 'Array.isArray('+name+')'
73 types.object = function(name) {
74 return 'typeof '+name+' === "object" && '+name+' && !Array.isArray('+name+')'
77 types.number = function(name) {
78 return 'typeof '+name+' === "number"'
81 types.integer = function(name) {
82 return 'typeof '+name+' === "number" && (Math.floor('+name+') === '+name+' || '+name+' > 9007199254740992 || '+name+' < -9007199254740992)'
85 types.string = function(name) {
86 return 'typeof '+name+' === "string"'
89 var unique = function(array) {
91 for (var i = 0; i < array.length; i++) {
92 list.push(typeof array[i] === 'object' ? JSON.stringify(array[i]) : array[i])
94 for (var i = 1; i < list.length; i++) {
95 if (list.indexOf(list[i]) !== i) return false
100 var isMultipleOf = function(name, multipleOf) {
102 var factor = ((multipleOf | 0) !== multipleOf) ? Math.pow(10, multipleOf.toString().split('.').pop().length) : 1
104 var factorName = ((name | 0) !== name) ? Math.pow(10, name.toString().split('.').pop().length) : 1
105 if (factorName > factor) res = true
106 else res = Math.round(factor * name) % (factor * multipleOf)
108 else res = name % multipleOf;
112 var compile = function(schema, cache, root, reporter, opts) {
113 var fmts = opts ? xtend(formats, opts.formats) : formats
114 var scope = {unique:unique, formats:fmts, isMultipleOf:isMultipleOf}
115 var verbose = opts ? !!opts.verbose : false;
116 var greedy = opts && opts.greedy !== undefined ?
120 var gensym = function(name) {
121 return name+(syms[name] = (syms[name] || 0)+1)
124 var reversePatterns = {}
125 var patterns = function(p) {
126 if (reversePatterns[p]) return reversePatterns[p]
127 var n = gensym('pattern')
128 scope[n] = new RegExp(p)
129 reversePatterns[p] = n
133 var vars = ['i','j','k','l','m','n','o','p','q','r','s','t','u','v','x','y','z']
134 var genloop = function() {
140 var visit = function(name, node, reporter, filter) {
141 var properties = node.properties
145 if (Array.isArray(node.items)) { // tuple type
147 node.items.forEach(function(item, i) {
155 var error = function(msg, prop, value) {
157 if (reporter === true) {
158 validate('if (validate.errors === null) validate.errors = []')
160 validate('validate.errors.push({field:%s,message:%s,value:%s,type:%s})', formatName(prop || name), JSON.stringify(msg), value || name, JSON.stringify(type))
162 validate('validate.errors.push({field:%s,message:%s})', formatName(prop || name), JSON.stringify(msg))
167 if (node.required === true) {
169 validate('if (%s === undefined) {', name)
174 validate('if (%s !== undefined) {', name)
177 var valid = [].concat(type)
179 if (t && !types.hasOwnProperty(t)) {
180 throw new Error('Unknown type: ' + t)
183 return types[t || 'any'](name)
185 .join(' || ') || 'true'
187 if (valid !== 'true') {
189 validate('if (!(%s)) {', valid)
190 error('is the wrong type')
195 if (node.additionalItems === false) {
196 validate('if (%s.length > %d) {', name, node.items.length)
197 error('has additional items')
199 } else if (node.additionalItems) {
201 validate('for (var %s = %d; %s < %s.length; %s++) {', i, node.items.length, i, name, i)
202 visit(name+'['+i+']', node.additionalItems, reporter, filter)
207 if (node.format && fmts[node.format]) {
208 if (type !== 'string' && formats[node.format]) validate('if (%s) {', types.string(name))
209 var n = gensym('format')
210 scope[n] = fmts[node.format]
212 if (typeof scope[n] === 'function') validate('if (!%s(%s)) {', n, name)
213 else validate('if (!%s.test(%s)) {', n, name)
214 error('must be '+node.format+' format')
216 if (type !== 'string' && formats[node.format]) validate('}')
219 if (Array.isArray(node.required)) {
220 var checkRequired = function (req) {
221 var prop = genobj(name, req);
222 validate('if (%s === undefined) {', prop)
223 error('is required', prop)
224 validate('missing++')
227 validate('if ((%s)) {', type !== 'object' ? types.object(name) : 'true')
228 validate('var missing = 0')
229 node.required.map(checkRequired)
232 validate('if (missing === 0) {')
237 if (node.uniqueItems) {
238 if (type !== 'array') validate('if (%s) {', types.array(name))
239 validate('if (!(unique(%s))) {', name)
240 error('must be unique')
242 if (type !== 'array') validate('}')
246 var complex = node.enum.some(function(e) {
247 return typeof e === 'object'
250 var compare = complex ?
252 return 'JSON.stringify('+name+')'+' !== JSON.stringify('+JSON.stringify(e)+')'
255 return name+' !== '+JSON.stringify(e)
258 validate('if (%s) {', node.enum.map(compare).join(' && ') || 'false')
259 error('must be an enum value')
263 if (node.dependencies) {
264 if (type !== 'object') validate('if (%s) {', types.object(name))
266 Object.keys(node.dependencies).forEach(function(key) {
267 var deps = node.dependencies[key]
268 if (typeof deps === 'string') deps = [deps]
270 var exists = function(k) {
271 return genobj(name, k) + ' !== undefined'
274 if (Array.isArray(deps)) {
275 validate('if (%s !== undefined && !(%s)) {', genobj(name, key), deps.map(exists).join(' && ') || 'true')
276 error('dependencies not set')
279 if (typeof deps === 'object') {
280 validate('if (%s !== undefined) {', genobj(name, key))
281 visit(name, deps, reporter, filter)
286 if (type !== 'object') validate('}')
289 if (node.additionalProperties || node.additionalProperties === false) {
290 if (type !== 'object') validate('if (%s) {', types.object(name))
293 var keys = gensym('keys')
295 var toCompare = function(p) {
296 return keys+'['+i+'] !== '+JSON.stringify(p)
299 var toTest = function(p) {
300 return '!'+patterns(p)+'.test('+keys+'['+i+'])'
303 var additionalProp = Object.keys(properties || {}).map(toCompare)
304 .concat(Object.keys(node.patternProperties || {}).map(toTest))
305 .join(' && ') || 'true'
307 validate('var %s = Object.keys(%s)', keys, name)
308 ('for (var %s = 0; %s < %s.length; %s++) {', i, i, keys, i)
309 ('if (%s) {', additionalProp)
311 if (node.additionalProperties === false) {
312 if (filter) validate('delete %s', name+'['+keys+'['+i+']]')
313 error('has additional properties', null, JSON.stringify(name+'.') + ' + ' + keys + '['+i+']')
315 visit(name+'['+keys+'['+i+']]', node.additionalProperties, reporter, filter)
322 if (type !== 'object') validate('}')
326 var sub = get(root, opts && opts.schemas || {}, node.$ref)
328 var fn = cache[node.$ref]
330 cache[node.$ref] = function proxy(data) {
333 fn = compile(sub, cache, root, false, opts)
335 var n = gensym('ref')
337 validate('if (!(%s(%s))) {', n, name)
338 error('referenced schema does not match')
344 var prev = gensym('prev')
345 validate('var %s = errors', prev)
346 visit(name, node.not, false, filter)
347 validate('if (%s === errors) {', prev)
348 error('negative schema matches')
350 ('errors = %s', prev)
354 if (node.items && !tuple) {
355 if (type !== 'array') validate('if (%s) {', types.array(name))
358 validate('for (var %s = 0; %s < %s.length; %s++) {', i, i, name, i)
359 visit(name+'['+i+']', node.items, reporter, filter)
362 if (type !== 'array') validate('}')
365 if (node.patternProperties) {
366 if (type !== 'object') validate('if (%s) {', types.object(name))
367 var keys = gensym('keys')
370 ('var %s = Object.keys(%s)', keys, name)
371 ('for (var %s = 0; %s < %s.length; %s++) {', i, i, keys, i)
373 Object.keys(node.patternProperties).forEach(function(key) {
374 var p = patterns(key)
375 validate('if (%s.test(%s)) {', p, keys+'['+i+']')
376 visit(name+'['+keys+'['+i+']]', node.patternProperties[key], reporter, filter)
381 if (type !== 'object') validate('}')
385 var p = patterns(node.pattern)
386 if (type !== 'string') validate('if (%s) {', types.string(name))
387 validate('if (!(%s.test(%s))) {', p, name)
388 error('pattern mismatch')
390 if (type !== 'string') validate('}')
394 node.allOf.forEach(function(sch) {
395 visit(name, sch, reporter, filter)
399 if (node.anyOf && node.anyOf.length) {
400 var prev = gensym('prev')
402 node.anyOf.forEach(function(sch, i) {
404 validate('var %s = errors', prev)
406 validate('if (errors !== %s) {', prev)
407 ('errors = %s', prev)
409 visit(name, sch, false, false)
411 node.anyOf.forEach(function(sch, i) {
414 validate('if (%s !== errors) {', prev)
415 error('no schemas match')
419 if (node.oneOf && node.oneOf.length) {
420 var prev = gensym('prev')
421 var passes = gensym('passes')
424 ('var %s = errors', prev)
425 ('var %s = 0', passes)
427 node.oneOf.forEach(function(sch, i) {
428 visit(name, sch, false, false)
429 validate('if (%s === errors) {', prev)
432 ('errors = %s', prev)
436 validate('if (%s !== 1) {', passes)
437 error('no (or more than one) schemas match')
441 if (node.multipleOf !== undefined) {
442 if (type !== 'number' && type !== 'integer') validate('if (%s) {', types.number(name))
444 validate('if (!isMultipleOf(%s, %d)) {', name, node.multipleOf)
446 error('has a remainder')
449 if (type !== 'number' && type !== 'integer') validate('}')
452 if (node.maxProperties !== undefined) {
453 if (type !== 'object') validate('if (%s) {', types.object(name))
455 validate('if (Object.keys(%s).length > %d) {', name, node.maxProperties)
456 error('has more properties than allowed')
459 if (type !== 'object') validate('}')
462 if (node.minProperties !== undefined) {
463 if (type !== 'object') validate('if (%s) {', types.object(name))
465 validate('if (Object.keys(%s).length < %d) {', name, node.minProperties)
466 error('has less properties than allowed')
469 if (type !== 'object') validate('}')
472 if (node.maxItems !== undefined) {
473 if (type !== 'array') validate('if (%s) {', types.array(name))
475 validate('if (%s.length > %d) {', name, node.maxItems)
476 error('has more items than allowed')
479 if (type !== 'array') validate('}')
482 if (node.minItems !== undefined) {
483 if (type !== 'array') validate('if (%s) {', types.array(name))
485 validate('if (%s.length < %d) {', name, node.minItems)
486 error('has less items than allowed')
489 if (type !== 'array') validate('}')
492 if (node.maxLength !== undefined) {
493 if (type !== 'string') validate('if (%s) {', types.string(name))
495 validate('if (%s.length > %d) {', name, node.maxLength)
496 error('has longer length than allowed')
499 if (type !== 'string') validate('}')
502 if (node.minLength !== undefined) {
503 if (type !== 'string') validate('if (%s) {', types.string(name))
505 validate('if (%s.length < %d) {', name, node.minLength)
506 error('has less length than allowed')
509 if (type !== 'string') validate('}')
512 if (node.minimum !== undefined) {
513 if (type !== 'number' && type !== 'integer') validate('if (%s) {', types.number(name))
515 validate('if (%s %s %d) {', name, node.exclusiveMinimum ? '<=' : '<', node.minimum)
516 error('is less than minimum')
519 if (type !== 'number' && type !== 'integer') validate('}')
522 if (node.maximum !== undefined) {
523 if (type !== 'number' && type !== 'integer') validate('if (%s) {', types.number(name))
525 validate('if (%s %s %d) {', name, node.exclusiveMaximum ? '>=' : '>', node.maximum)
526 error('is more than maximum')
529 if (type !== 'number' && type !== 'integer') validate('}')
533 Object.keys(properties).forEach(function(p) {
534 if (Array.isArray(type) && type.indexOf('null') !== -1) validate('if (%s !== null) {', name)
536 visit(genobj(name, p), properties[p], reporter, filter)
538 if (Array.isArray(type) && type.indexOf('null') !== -1) validate('}')
542 while (indent--) validate('}')
545 var validate = genfun
546 ('function validate(data) {')
547 // Since undefined is not a valid JSON value, we coerce to null and other checks will catch this
548 ('if (data === undefined) data = null')
549 ('validate.errors = null')
552 visit('data', schema, reporter, opts && opts.filter)
555 ('return errors === 0')
558 validate = validate.toFunction(scope)
559 validate.errors = null
561 if (Object.defineProperty) {
562 Object.defineProperty(validate, 'error', {
564 if (!validate.errors) return ''
565 return validate.errors.map(function(err) {
566 return err.field + ' ' + err.message;
572 validate.toJSON = function() {
579 module.exports = function(schema, opts) {
580 if (typeof schema === 'string') schema = JSON.parse(schema)
581 return compile(schema, {}, schema, true, opts)
584 module.exports.filter = function(schema, opts) {
585 var validate = module.exports(schema, xtend(opts, {filter: true}))
586 return function(sch) {