index.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603
  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')
  6. var get = function(obj, additionalSchemas, ptr) {
  7. var visit = function(sub) {
  8. if (sub && sub.id === ptr) return sub
  9. if (typeof sub !== 'object' || !sub) return null
  10. return Object.keys(sub).reduce(function(res, k) {
  11. return res || visit(sub[k])
  12. }, null)
  13. }
  14. var res = visit(obj)
  15. if (res) return res
  16. ptr = ptr.replace(/^#/, '')
  17. ptr = ptr.replace(/\/$/, '')
  18. try {
  19. return jsonpointer.get(obj, decodeURI(ptr))
  20. } catch (err) {
  21. var end = ptr.indexOf('#')
  22. var other
  23. // external reference
  24. if (end !== 0) {
  25. // fragment doesn't exist.
  26. if (end === -1) {
  27. other = additionalSchemas[ptr]
  28. } else {
  29. var ext = ptr.slice(0, end)
  30. other = additionalSchemas[ext]
  31. var fragment = ptr.slice(end).replace(/^#/, '')
  32. try {
  33. return jsonpointer.get(other, fragment)
  34. } catch (err) {}
  35. }
  36. } else {
  37. other = additionalSchemas[ptr]
  38. }
  39. return other || null
  40. }
  41. }
  42. var formatName = function(field) {
  43. field = JSON.stringify(field)
  44. var pattern = /\[([^\[\]"]+)\]/
  45. while (pattern.test(field)) field = field.replace(pattern, '."+$1+"')
  46. return field
  47. }
  48. var types = {}
  49. types.any = function() {
  50. return 'true'
  51. }
  52. types.null = function(name) {
  53. return name+' === null'
  54. }
  55. types.boolean = function(name) {
  56. return 'typeof '+name+' === "boolean"'
  57. }
  58. types.array = function(name) {
  59. return 'Array.isArray('+name+')'
  60. }
  61. types.object = function(name) {
  62. return 'typeof '+name+' === "object" && '+name+' && !Array.isArray('+name+')'
  63. }
  64. types.number = function(name) {
  65. return 'typeof '+name+' === "number" && isFinite('+name+')'
  66. }
  67. types.integer = function(name) {
  68. return 'typeof '+name+' === "number" && (Math.floor('+name+') === '+name+' || '+name+' > 9007199254740992 || '+name+' < -9007199254740992)'
  69. }
  70. types.string = function(name) {
  71. return 'typeof '+name+' === "string"'
  72. }
  73. var unique = function(array) {
  74. var list = []
  75. for (var i = 0; i < array.length; i++) {
  76. list.push(typeof array[i] === 'object' ? JSON.stringify(array[i]) : array[i])
  77. }
  78. for (var i = 1; i < list.length; i++) {
  79. if (list.indexOf(list[i]) !== i) return false
  80. }
  81. return true
  82. }
  83. var isMultipleOf = function(name, multipleOf) {
  84. var res;
  85. var factor = ((multipleOf | 0) !== multipleOf) ? Math.pow(10, multipleOf.toString().split('.').pop().length) : 1
  86. if (factor > 1) {
  87. var factorName = ((name | 0) !== name) ? Math.pow(10, name.toString().split('.').pop().length) : 1
  88. if (factorName > factor) res = true
  89. else res = Math.round(factor * name) % (factor * multipleOf)
  90. }
  91. else res = name % multipleOf;
  92. return !res;
  93. }
  94. var compile = function(schema, cache, root, reporter, opts) {
  95. var fmts = opts ? xtend(formats, opts.formats) : formats
  96. var scope = {unique:unique, formats:fmts, isMultipleOf:isMultipleOf}
  97. var verbose = opts ? !!opts.verbose : false;
  98. var greedy = opts && opts.greedy !== undefined ?
  99. opts.greedy : false;
  100. var syms = {}
  101. var gensym = function(name) {
  102. return name+(syms[name] = (syms[name] || 0)+1)
  103. }
  104. var reversePatterns = {}
  105. var patterns = function(p) {
  106. if (reversePatterns[p]) return reversePatterns[p]
  107. var n = gensym('pattern')
  108. scope[n] = new RegExp(p)
  109. reversePatterns[p] = n
  110. return n
  111. }
  112. var vars = ['i','j','k','l','m','n','o','p','q','r','s','t','u','v','x','y','z']
  113. var genloop = function() {
  114. var v = vars.shift()
  115. vars.push(v+v[0])
  116. return v
  117. }
  118. var visit = function(name, node, reporter, filter, schemaPath) {
  119. var properties = node.properties
  120. var type = node.type
  121. var tuple = false
  122. if (Array.isArray(node.items)) { // tuple type
  123. properties = {}
  124. node.items.forEach(function(item, i) {
  125. properties[i] = item
  126. })
  127. type = 'array'
  128. tuple = true
  129. }
  130. var indent = 0
  131. var error = function(msg, prop, value) {
  132. validate('errors++')
  133. if (reporter === true) {
  134. validate('if (validate.errors === null) validate.errors = []')
  135. if (verbose) {
  136. validate(
  137. 'validate.errors.push({field:%s,message:%s,value:%s,type:%s,schemaPath:%s})',
  138. formatName(prop || name),
  139. JSON.stringify(msg),
  140. value || name,
  141. JSON.stringify(type),
  142. JSON.stringify(schemaPath)
  143. )
  144. } else {
  145. validate('validate.errors.push({field:%s,message:%s})', formatName(prop || name), JSON.stringify(msg))
  146. }
  147. }
  148. }
  149. if (node.required === true) {
  150. indent++
  151. validate('if (%s === undefined) {', name)
  152. error('is required')
  153. validate('} else {')
  154. } else {
  155. indent++
  156. validate('if (%s !== undefined) {', name)
  157. }
  158. var valid = [].concat(type)
  159. .map(function(t) {
  160. if (t && !types.hasOwnProperty(t)) {
  161. throw new Error('Unknown type: ' + t)
  162. }
  163. return types[t || 'any'](name)
  164. })
  165. .join(' || ') || 'true'
  166. if (valid !== 'true') {
  167. indent++
  168. validate('if (!(%s)) {', valid)
  169. error('is the wrong type')
  170. validate('} else {')
  171. }
  172. if (tuple) {
  173. if (node.additionalItems === false) {
  174. validate('if (%s.length > %d) {', name, node.items.length)
  175. error('has additional items')
  176. validate('}')
  177. } else if (node.additionalItems) {
  178. var i = genloop()
  179. validate('for (var %s = %d; %s < %s.length; %s++) {', i, node.items.length, i, name, i)
  180. visit(name+'['+i+']', node.additionalItems, reporter, filter, schemaPath.concat('additionalItems'))
  181. validate('}')
  182. }
  183. }
  184. if (node.format && fmts[node.format]) {
  185. if (type !== 'string' && formats[node.format]) validate('if (%s) {', types.string(name))
  186. var n = gensym('format')
  187. scope[n] = fmts[node.format]
  188. if (typeof scope[n] === 'function') validate('if (!%s(%s)) {', n, name)
  189. else validate('if (!%s.test(%s)) {', n, name)
  190. error('must be '+node.format+' format')
  191. validate('}')
  192. if (type !== 'string' && formats[node.format]) validate('}')
  193. }
  194. if (Array.isArray(node.required)) {
  195. var checkRequired = function (req) {
  196. var prop = genobj(name, req);
  197. validate('if (%s === undefined) {', prop)
  198. error('is required', prop)
  199. validate('missing++')
  200. validate('}')
  201. }
  202. validate('if ((%s)) {', type !== 'object' ? types.object(name) : 'true')
  203. validate('var missing = 0')
  204. node.required.map(checkRequired)
  205. validate('}');
  206. if (!greedy) {
  207. validate('if (missing === 0) {')
  208. indent++
  209. }
  210. }
  211. if (node.uniqueItems) {
  212. if (type !== 'array') validate('if (%s) {', types.array(name))
  213. validate('if (!(unique(%s))) {', name)
  214. error('must be unique')
  215. validate('}')
  216. if (type !== 'array') validate('}')
  217. }
  218. if (node.enum) {
  219. var complex = node.enum.some(function(e) {
  220. return typeof e === 'object'
  221. })
  222. var compare = complex ?
  223. function(e) {
  224. return 'JSON.stringify('+name+')'+' !== JSON.stringify('+JSON.stringify(e)+')'
  225. } :
  226. function(e) {
  227. return name+' !== '+JSON.stringify(e)
  228. }
  229. validate('if (%s) {', node.enum.map(compare).join(' && ') || 'false')
  230. error('must be an enum value')
  231. validate('}')
  232. }
  233. if (node.dependencies) {
  234. if (type !== 'object') validate('if (%s) {', types.object(name))
  235. Object.keys(node.dependencies).forEach(function(key) {
  236. var deps = node.dependencies[key]
  237. if (typeof deps === 'string') deps = [deps]
  238. var exists = function(k) {
  239. return genobj(name, k) + ' !== undefined'
  240. }
  241. if (Array.isArray(deps)) {
  242. validate('if (%s !== undefined && !(%s)) {', genobj(name, key), deps.map(exists).join(' && ') || 'true')
  243. error('dependencies not set')
  244. validate('}')
  245. }
  246. if (typeof deps === 'object') {
  247. validate('if (%s !== undefined) {', genobj(name, key))
  248. visit(name, deps, reporter, filter, schemaPath.concat(['dependencies', key]))
  249. validate('}')
  250. }
  251. })
  252. if (type !== 'object') validate('}')
  253. }
  254. if (node.additionalProperties || node.additionalProperties === false) {
  255. if (type !== 'object') validate('if (%s) {', types.object(name))
  256. var i = genloop()
  257. var keys = gensym('keys')
  258. var toCompare = function(p) {
  259. return keys+'['+i+'] !== '+JSON.stringify(p)
  260. }
  261. var toTest = function(p) {
  262. return '!'+patterns(p)+'.test('+keys+'['+i+'])'
  263. }
  264. var additionalProp = Object.keys(properties || {}).map(toCompare)
  265. .concat(Object.keys(node.patternProperties || {}).map(toTest))
  266. .join(' && ') || 'true'
  267. validate('var %s = Object.keys(%s)', keys, name)
  268. ('for (var %s = 0; %s < %s.length; %s++) {', i, i, keys, i)
  269. ('if (%s) {', additionalProp)
  270. if (node.additionalProperties === false) {
  271. if (filter) validate('delete %s', name+'['+keys+'['+i+']]')
  272. error('has additional properties', null, JSON.stringify(name+'.') + ' + ' + keys + '['+i+']')
  273. } else {
  274. visit(name+'['+keys+'['+i+']]', node.additionalProperties, reporter, filter, schemaPath.concat(['additionalProperties']))
  275. }
  276. validate
  277. ('}')
  278. ('}')
  279. if (type !== 'object') validate('}')
  280. }
  281. if (node.$ref) {
  282. var sub = get(root, opts && opts.schemas || {}, node.$ref)
  283. if (sub) {
  284. var fn = cache[node.$ref]
  285. if (!fn) {
  286. cache[node.$ref] = function proxy(data) {
  287. return fn(data)
  288. }
  289. fn = compile(sub, cache, root, false, opts)
  290. }
  291. var n = gensym('ref')
  292. scope[n] = fn
  293. validate('if (!(%s(%s))) {', n, name)
  294. error('referenced schema does not match')
  295. validate('}')
  296. }
  297. }
  298. if (node.not) {
  299. var prev = gensym('prev')
  300. validate('var %s = errors', prev)
  301. visit(name, node.not, false, filter, schemaPath.concat('not'))
  302. validate('if (%s === errors) {', prev)
  303. error('negative schema matches')
  304. validate('} else {')
  305. ('errors = %s', prev)
  306. ('}')
  307. }
  308. if (node.items && !tuple) {
  309. if (type !== 'array') validate('if (%s) {', types.array(name))
  310. var i = genloop()
  311. validate('for (var %s = 0; %s < %s.length; %s++) {', i, i, name, i)
  312. visit(name+'['+i+']', node.items, reporter, filter, schemaPath.concat('items'))
  313. validate('}')
  314. if (type !== 'array') validate('}')
  315. }
  316. if (node.patternProperties) {
  317. if (type !== 'object') validate('if (%s) {', types.object(name))
  318. var keys = gensym('keys')
  319. var i = genloop()
  320. validate
  321. ('var %s = Object.keys(%s)', keys, name)
  322. ('for (var %s = 0; %s < %s.length; %s++) {', i, i, keys, i)
  323. Object.keys(node.patternProperties).forEach(function(key) {
  324. var p = patterns(key)
  325. validate('if (%s.test(%s)) {', p, keys+'['+i+']')
  326. visit(name+'['+keys+'['+i+']]', node.patternProperties[key], reporter, filter, schemaPath.concat(['patternProperties', key]))
  327. validate('}')
  328. })
  329. validate('}')
  330. if (type !== 'object') validate('}')
  331. }
  332. if (node.pattern) {
  333. var p = patterns(node.pattern)
  334. if (type !== 'string') validate('if (%s) {', types.string(name))
  335. validate('if (!(%s.test(%s))) {', p, name)
  336. error('pattern mismatch')
  337. validate('}')
  338. if (type !== 'string') validate('}')
  339. }
  340. if (node.allOf) {
  341. node.allOf.forEach(function(sch, key) {
  342. visit(name, sch, reporter, filter, schemaPath.concat(['allOf', key]))
  343. })
  344. }
  345. if (node.anyOf && node.anyOf.length) {
  346. var prev = gensym('prev')
  347. node.anyOf.forEach(function(sch, i) {
  348. if (i === 0) {
  349. validate('var %s = errors', prev)
  350. } else {
  351. validate('if (errors !== %s) {', prev)
  352. ('errors = %s', prev)
  353. }
  354. visit(name, sch, false, false, schemaPath)
  355. })
  356. node.anyOf.forEach(function(sch, i) {
  357. if (i) validate('}')
  358. })
  359. validate('if (%s !== errors) {', prev)
  360. error('no schemas match')
  361. validate('}')
  362. }
  363. if (node.oneOf && node.oneOf.length) {
  364. var prev = gensym('prev')
  365. var passes = gensym('passes')
  366. validate
  367. ('var %s = errors', prev)
  368. ('var %s = 0', passes)
  369. node.oneOf.forEach(function(sch, i) {
  370. visit(name, sch, false, false, schemaPath)
  371. validate('if (%s === errors) {', prev)
  372. ('%s++', passes)
  373. ('} else {')
  374. ('errors = %s', prev)
  375. ('}')
  376. })
  377. validate('if (%s !== 1) {', passes)
  378. error('no (or more than one) schemas match')
  379. validate('}')
  380. }
  381. if (node.multipleOf !== undefined) {
  382. if (type !== 'number' && type !== 'integer') validate('if (%s) {', types.number(name))
  383. validate('if (!isMultipleOf(%s, %d)) {', name, node.multipleOf)
  384. error('has a remainder')
  385. validate('}')
  386. if (type !== 'number' && type !== 'integer') validate('}')
  387. }
  388. if (node.maxProperties !== undefined) {
  389. if (type !== 'object') validate('if (%s) {', types.object(name))
  390. validate('if (Object.keys(%s).length > %d) {', name, node.maxProperties)
  391. error('has more properties than allowed')
  392. validate('}')
  393. if (type !== 'object') validate('}')
  394. }
  395. if (node.minProperties !== undefined) {
  396. if (type !== 'object') validate('if (%s) {', types.object(name))
  397. validate('if (Object.keys(%s).length < %d) {', name, node.minProperties)
  398. error('has less properties than allowed')
  399. validate('}')
  400. if (type !== 'object') validate('}')
  401. }
  402. if (node.maxItems !== undefined) {
  403. if (type !== 'array') validate('if (%s) {', types.array(name))
  404. validate('if (%s.length > %d) {', name, node.maxItems)
  405. error('has more items than allowed')
  406. validate('}')
  407. if (type !== 'array') validate('}')
  408. }
  409. if (node.minItems !== undefined) {
  410. if (type !== 'array') validate('if (%s) {', types.array(name))
  411. validate('if (%s.length < %d) {', name, node.minItems)
  412. error('has less items than allowed')
  413. validate('}')
  414. if (type !== 'array') validate('}')
  415. }
  416. if (node.maxLength !== undefined) {
  417. if (type !== 'string') validate('if (%s) {', types.string(name))
  418. validate('if (%s.length > %d) {', name, node.maxLength)
  419. error('has longer length than allowed')
  420. validate('}')
  421. if (type !== 'string') validate('}')
  422. }
  423. if (node.minLength !== undefined) {
  424. if (type !== 'string') validate('if (%s) {', types.string(name))
  425. validate('if (%s.length < %d) {', name, node.minLength)
  426. error('has less length than allowed')
  427. validate('}')
  428. if (type !== 'string') validate('}')
  429. }
  430. if (node.minimum !== undefined) {
  431. if (type !== 'number' && type !== 'integer') validate('if (%s) {', types.number(name))
  432. validate('if (%s %s %d) {', name, node.exclusiveMinimum ? '<=' : '<', node.minimum)
  433. error('is less than minimum')
  434. validate('}')
  435. if (type !== 'number' && type !== 'integer') validate('}')
  436. }
  437. if (node.maximum !== undefined) {
  438. if (type !== 'number' && type !== 'integer') validate('if (%s) {', types.number(name))
  439. validate('if (%s %s %d) {', name, node.exclusiveMaximum ? '>=' : '>', node.maximum)
  440. error('is more than maximum')
  441. validate('}')
  442. if (type !== 'number' && type !== 'integer') validate('}')
  443. }
  444. if (properties) {
  445. Object.keys(properties).forEach(function(p) {
  446. if (Array.isArray(type) && type.indexOf('null') !== -1) validate('if (%s !== null) {', name)
  447. visit(
  448. genobj(name, p),
  449. properties[p],
  450. reporter,
  451. filter,
  452. schemaPath.concat(tuple ? p : ['properties', p])
  453. )
  454. if (Array.isArray(type) && type.indexOf('null') !== -1) validate('}')
  455. })
  456. }
  457. while (indent--) validate('}')
  458. }
  459. var validate = genfun
  460. ('function validate(data) {')
  461. // Since undefined is not a valid JSON value, we coerce to null and other checks will catch this
  462. ('if (data === undefined) data = null')
  463. ('validate.errors = null')
  464. ('var errors = 0')
  465. visit('data', schema, reporter, opts && opts.filter, [])
  466. validate
  467. ('return errors === 0')
  468. ('}')
  469. validate = validate.toFunction(scope)
  470. validate.errors = null
  471. if (Object.defineProperty) {
  472. Object.defineProperty(validate, 'error', {
  473. get: function() {
  474. if (!validate.errors) return ''
  475. return validate.errors.map(function(err) {
  476. return err.field + ' ' + err.message;
  477. }).join('\n')
  478. }
  479. })
  480. }
  481. validate.toJSON = function() {
  482. return schema
  483. }
  484. return validate
  485. }
  486. module.exports = function(schema, opts) {
  487. if (typeof schema === 'string') schema = JSON.parse(schema)
  488. return compile(schema, {}, schema, true, opts)
  489. }
  490. module.exports.filter = function(schema, opts) {
  491. var validate = module.exports(schema, xtend(opts, {filter: true}))
  492. return function(sch) {
  493. validate(sch)
  494. return sch
  495. }
  496. }