index.js 16 KB

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