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