ajv.js (15837B)
1 'use strict'; 2 3 var compileSchema = require('./compile') 4 , resolve = require('./compile/resolve') 5 , Cache = require('./cache') 6 , SchemaObject = require('./compile/schema_obj') 7 , stableStringify = require('fast-json-stable-stringify') 8 , formats = require('./compile/formats') 9 , rules = require('./compile/rules') 10 , $dataMetaSchema = require('./data') 11 , util = require('./compile/util'); 12 13 module.exports = Ajv; 14 15 Ajv.prototype.validate = validate; 16 Ajv.prototype.compile = compile; 17 Ajv.prototype.addSchema = addSchema; 18 Ajv.prototype.addMetaSchema = addMetaSchema; 19 Ajv.prototype.validateSchema = validateSchema; 20 Ajv.prototype.getSchema = getSchema; 21 Ajv.prototype.removeSchema = removeSchema; 22 Ajv.prototype.addFormat = addFormat; 23 Ajv.prototype.errorsText = errorsText; 24 25 Ajv.prototype._addSchema = _addSchema; 26 Ajv.prototype._compile = _compile; 27 28 Ajv.prototype.compileAsync = require('./compile/async'); 29 var customKeyword = require('./keyword'); 30 Ajv.prototype.addKeyword = customKeyword.add; 31 Ajv.prototype.getKeyword = customKeyword.get; 32 Ajv.prototype.removeKeyword = customKeyword.remove; 33 Ajv.prototype.validateKeyword = customKeyword.validate; 34 35 var errorClasses = require('./compile/error_classes'); 36 Ajv.ValidationError = errorClasses.Validation; 37 Ajv.MissingRefError = errorClasses.MissingRef; 38 Ajv.$dataMetaSchema = $dataMetaSchema; 39 40 var META_SCHEMA_ID = 'http://json-schema.org/draft-07/schema'; 41 42 var META_IGNORE_OPTIONS = [ 'removeAdditional', 'useDefaults', 'coerceTypes', 'strictDefaults' ]; 43 var META_SUPPORT_DATA = ['/properties']; 44 45 /** 46 * Creates validator instance. 47 * Usage: `Ajv(opts)` 48 * @param {Object} opts optional options 49 * @return {Object} ajv instance 50 */ 51 function Ajv(opts) { 52 if (!(this instanceof Ajv)) return new Ajv(opts); 53 opts = this._opts = util.copy(opts) || {}; 54 setLogger(this); 55 this._schemas = {}; 56 this._refs = {}; 57 this._fragments = {}; 58 this._formats = formats(opts.format); 59 60 this._cache = opts.cache || new Cache; 61 this._loadingSchemas = {}; 62 this._compilations = []; 63 this.RULES = rules(); 64 this._getId = chooseGetId(opts); 65 66 opts.loopRequired = opts.loopRequired || Infinity; 67 if (opts.errorDataPath == 'property') opts._errorDataPathProperty = true; 68 if (opts.serialize === undefined) opts.serialize = stableStringify; 69 this._metaOpts = getMetaSchemaOptions(this); 70 71 if (opts.formats) addInitialFormats(this); 72 if (opts.keywords) addInitialKeywords(this); 73 addDefaultMetaSchema(this); 74 if (typeof opts.meta == 'object') this.addMetaSchema(opts.meta); 75 if (opts.nullable) this.addKeyword('nullable', {metaSchema: {type: 'boolean'}}); 76 addInitialSchemas(this); 77 } 78 79 80 81 /** 82 * Validate data using schema 83 * Schema will be compiled and cached (using serialized JSON as key. [fast-json-stable-stringify](https://github.com/epoberezkin/fast-json-stable-stringify) is used to serialize. 84 * @this Ajv 85 * @param {String|Object} schemaKeyRef key, ref or schema object 86 * @param {Any} data to be validated 87 * @return {Boolean} validation result. Errors from the last validation will be available in `ajv.errors` (and also in compiled schema: `schema.errors`). 88 */ 89 function validate(schemaKeyRef, data) { 90 var v; 91 if (typeof schemaKeyRef == 'string') { 92 v = this.getSchema(schemaKeyRef); 93 if (!v) throw new Error('no schema with key or ref "' + schemaKeyRef + '"'); 94 } else { 95 var schemaObj = this._addSchema(schemaKeyRef); 96 v = schemaObj.validate || this._compile(schemaObj); 97 } 98 99 var valid = v(data); 100 if (v.$async !== true) this.errors = v.errors; 101 return valid; 102 } 103 104 105 /** 106 * Create validating function for passed schema. 107 * @this Ajv 108 * @param {Object} schema schema object 109 * @param {Boolean} _meta true if schema is a meta-schema. Used internally to compile meta schemas of custom keywords. 110 * @return {Function} validating function 111 */ 112 function compile(schema, _meta) { 113 var schemaObj = this._addSchema(schema, undefined, _meta); 114 return schemaObj.validate || this._compile(schemaObj); 115 } 116 117 118 /** 119 * Adds schema to the instance. 120 * @this Ajv 121 * @param {Object|Array} schema schema or array of schemas. If array is passed, `key` and other parameters will be ignored. 122 * @param {String} key Optional schema key. Can be passed to `validate` method instead of schema object or id/ref. One schema per instance can have empty `id` and `key`. 123 * @param {Boolean} _skipValidation true to skip schema validation. Used internally, option validateSchema should be used instead. 124 * @param {Boolean} _meta true if schema is a meta-schema. Used internally, addMetaSchema should be used instead. 125 * @return {Ajv} this for method chaining 126 */ 127 function addSchema(schema, key, _skipValidation, _meta) { 128 if (Array.isArray(schema)){ 129 for (var i=0; i<schema.length; i++) this.addSchema(schema[i], undefined, _skipValidation, _meta); 130 return this; 131 } 132 var id = this._getId(schema); 133 if (id !== undefined && typeof id != 'string') 134 throw new Error('schema id must be string'); 135 key = resolve.normalizeId(key || id); 136 checkUnique(this, key); 137 this._schemas[key] = this._addSchema(schema, _skipValidation, _meta, true); 138 return this; 139 } 140 141 142 /** 143 * Add schema that will be used to validate other schemas 144 * options in META_IGNORE_OPTIONS are alway set to false 145 * @this Ajv 146 * @param {Object} schema schema object 147 * @param {String} key optional schema key 148 * @param {Boolean} skipValidation true to skip schema validation, can be used to override validateSchema option for meta-schema 149 * @return {Ajv} this for method chaining 150 */ 151 function addMetaSchema(schema, key, skipValidation) { 152 this.addSchema(schema, key, skipValidation, true); 153 return this; 154 } 155 156 157 /** 158 * Validate schema 159 * @this Ajv 160 * @param {Object} schema schema to validate 161 * @param {Boolean} throwOrLogError pass true to throw (or log) an error if invalid 162 * @return {Boolean} true if schema is valid 163 */ 164 function validateSchema(schema, throwOrLogError) { 165 var $schema = schema.$schema; 166 if ($schema !== undefined && typeof $schema != 'string') 167 throw new Error('$schema must be a string'); 168 $schema = $schema || this._opts.defaultMeta || defaultMeta(this); 169 if (!$schema) { 170 this.logger.warn('meta-schema not available'); 171 this.errors = null; 172 return true; 173 } 174 var valid = this.validate($schema, schema); 175 if (!valid && throwOrLogError) { 176 var message = 'schema is invalid: ' + this.errorsText(); 177 if (this._opts.validateSchema == 'log') this.logger.error(message); 178 else throw new Error(message); 179 } 180 return valid; 181 } 182 183 184 function defaultMeta(self) { 185 var meta = self._opts.meta; 186 self._opts.defaultMeta = typeof meta == 'object' 187 ? self._getId(meta) || meta 188 : self.getSchema(META_SCHEMA_ID) 189 ? META_SCHEMA_ID 190 : undefined; 191 return self._opts.defaultMeta; 192 } 193 194 195 /** 196 * Get compiled schema from the instance by `key` or `ref`. 197 * @this Ajv 198 * @param {String} keyRef `key` that was passed to `addSchema` or full schema reference (`schema.id` or resolved id). 199 * @return {Function} schema validating function (with property `schema`). 200 */ 201 function getSchema(keyRef) { 202 var schemaObj = _getSchemaObj(this, keyRef); 203 switch (typeof schemaObj) { 204 case 'object': return schemaObj.validate || this._compile(schemaObj); 205 case 'string': return this.getSchema(schemaObj); 206 case 'undefined': return _getSchemaFragment(this, keyRef); 207 } 208 } 209 210 211 function _getSchemaFragment(self, ref) { 212 var res = resolve.schema.call(self, { schema: {} }, ref); 213 if (res) { 214 var schema = res.schema 215 , root = res.root 216 , baseId = res.baseId; 217 var v = compileSchema.call(self, schema, root, undefined, baseId); 218 self._fragments[ref] = new SchemaObject({ 219 ref: ref, 220 fragment: true, 221 schema: schema, 222 root: root, 223 baseId: baseId, 224 validate: v 225 }); 226 return v; 227 } 228 } 229 230 231 function _getSchemaObj(self, keyRef) { 232 keyRef = resolve.normalizeId(keyRef); 233 return self._schemas[keyRef] || self._refs[keyRef] || self._fragments[keyRef]; 234 } 235 236 237 /** 238 * Remove cached schema(s). 239 * If no parameter is passed all schemas but meta-schemas are removed. 240 * If RegExp is passed all schemas with key/id matching pattern but meta-schemas are removed. 241 * Even if schema is referenced by other schemas it still can be removed as other schemas have local references. 242 * @this Ajv 243 * @param {String|Object|RegExp} schemaKeyRef key, ref, pattern to match key/ref or schema object 244 * @return {Ajv} this for method chaining 245 */ 246 function removeSchema(schemaKeyRef) { 247 if (schemaKeyRef instanceof RegExp) { 248 _removeAllSchemas(this, this._schemas, schemaKeyRef); 249 _removeAllSchemas(this, this._refs, schemaKeyRef); 250 return this; 251 } 252 switch (typeof schemaKeyRef) { 253 case 'undefined': 254 _removeAllSchemas(this, this._schemas); 255 _removeAllSchemas(this, this._refs); 256 this._cache.clear(); 257 return this; 258 case 'string': 259 var schemaObj = _getSchemaObj(this, schemaKeyRef); 260 if (schemaObj) this._cache.del(schemaObj.cacheKey); 261 delete this._schemas[schemaKeyRef]; 262 delete this._refs[schemaKeyRef]; 263 return this; 264 case 'object': 265 var serialize = this._opts.serialize; 266 var cacheKey = serialize ? serialize(schemaKeyRef) : schemaKeyRef; 267 this._cache.del(cacheKey); 268 var id = this._getId(schemaKeyRef); 269 if (id) { 270 id = resolve.normalizeId(id); 271 delete this._schemas[id]; 272 delete this._refs[id]; 273 } 274 } 275 return this; 276 } 277 278 279 function _removeAllSchemas(self, schemas, regex) { 280 for (var keyRef in schemas) { 281 var schemaObj = schemas[keyRef]; 282 if (!schemaObj.meta && (!regex || regex.test(keyRef))) { 283 self._cache.del(schemaObj.cacheKey); 284 delete schemas[keyRef]; 285 } 286 } 287 } 288 289 290 /* @this Ajv */ 291 function _addSchema(schema, skipValidation, meta, shouldAddSchema) { 292 if (typeof schema != 'object' && typeof schema != 'boolean') 293 throw new Error('schema should be object or boolean'); 294 var serialize = this._opts.serialize; 295 var cacheKey = serialize ? serialize(schema) : schema; 296 var cached = this._cache.get(cacheKey); 297 if (cached) return cached; 298 299 shouldAddSchema = shouldAddSchema || this._opts.addUsedSchema !== false; 300 301 var id = resolve.normalizeId(this._getId(schema)); 302 if (id && shouldAddSchema) checkUnique(this, id); 303 304 var willValidate = this._opts.validateSchema !== false && !skipValidation; 305 var recursiveMeta; 306 if (willValidate && !(recursiveMeta = id && id == resolve.normalizeId(schema.$schema))) 307 this.validateSchema(schema, true); 308 309 var localRefs = resolve.ids.call(this, schema); 310 311 var schemaObj = new SchemaObject({ 312 id: id, 313 schema: schema, 314 localRefs: localRefs, 315 cacheKey: cacheKey, 316 meta: meta 317 }); 318 319 if (id[0] != '#' && shouldAddSchema) this._refs[id] = schemaObj; 320 this._cache.put(cacheKey, schemaObj); 321 322 if (willValidate && recursiveMeta) this.validateSchema(schema, true); 323 324 return schemaObj; 325 } 326 327 328 /* @this Ajv */ 329 function _compile(schemaObj, root) { 330 if (schemaObj.compiling) { 331 schemaObj.validate = callValidate; 332 callValidate.schema = schemaObj.schema; 333 callValidate.errors = null; 334 callValidate.root = root ? root : callValidate; 335 if (schemaObj.schema.$async === true) 336 callValidate.$async = true; 337 return callValidate; 338 } 339 schemaObj.compiling = true; 340 341 var currentOpts; 342 if (schemaObj.meta) { 343 currentOpts = this._opts; 344 this._opts = this._metaOpts; 345 } 346 347 var v; 348 try { v = compileSchema.call(this, schemaObj.schema, root, schemaObj.localRefs); } 349 catch(e) { 350 delete schemaObj.validate; 351 throw e; 352 } 353 finally { 354 schemaObj.compiling = false; 355 if (schemaObj.meta) this._opts = currentOpts; 356 } 357 358 schemaObj.validate = v; 359 schemaObj.refs = v.refs; 360 schemaObj.refVal = v.refVal; 361 schemaObj.root = v.root; 362 return v; 363 364 365 /* @this {*} - custom context, see passContext option */ 366 function callValidate() { 367 /* jshint validthis: true */ 368 var _validate = schemaObj.validate; 369 var result = _validate.apply(this, arguments); 370 callValidate.errors = _validate.errors; 371 return result; 372 } 373 } 374 375 376 function chooseGetId(opts) { 377 switch (opts.schemaId) { 378 case 'auto': return _get$IdOrId; 379 case 'id': return _getId; 380 default: return _get$Id; 381 } 382 } 383 384 /* @this Ajv */ 385 function _getId(schema) { 386 if (schema.$id) this.logger.warn('schema $id ignored', schema.$id); 387 return schema.id; 388 } 389 390 /* @this Ajv */ 391 function _get$Id(schema) { 392 if (schema.id) this.logger.warn('schema id ignored', schema.id); 393 return schema.$id; 394 } 395 396 397 function _get$IdOrId(schema) { 398 if (schema.$id && schema.id && schema.$id != schema.id) 399 throw new Error('schema $id is different from id'); 400 return schema.$id || schema.id; 401 } 402 403 404 /** 405 * Convert array of error message objects to string 406 * @this Ajv 407 * @param {Array<Object>} errors optional array of validation errors, if not passed errors from the instance are used. 408 * @param {Object} options optional options with properties `separator` and `dataVar`. 409 * @return {String} human readable string with all errors descriptions 410 */ 411 function errorsText(errors, options) { 412 errors = errors || this.errors; 413 if (!errors) return 'No errors'; 414 options = options || {}; 415 var separator = options.separator === undefined ? ', ' : options.separator; 416 var dataVar = options.dataVar === undefined ? 'data' : options.dataVar; 417 418 var text = ''; 419 for (var i=0; i<errors.length; i++) { 420 var e = errors[i]; 421 if (e) text += dataVar + e.dataPath + ' ' + e.message + separator; 422 } 423 return text.slice(0, -separator.length); 424 } 425 426 427 /** 428 * Add custom format 429 * @this Ajv 430 * @param {String} name format name 431 * @param {String|RegExp|Function} format string is converted to RegExp; function should return boolean (true when valid) 432 * @return {Ajv} this for method chaining 433 */ 434 function addFormat(name, format) { 435 if (typeof format == 'string') format = new RegExp(format); 436 this._formats[name] = format; 437 return this; 438 } 439 440 441 function addDefaultMetaSchema(self) { 442 var $dataSchema; 443 if (self._opts.$data) { 444 $dataSchema = require('./refs/data.json'); 445 self.addMetaSchema($dataSchema, $dataSchema.$id, true); 446 } 447 if (self._opts.meta === false) return; 448 var metaSchema = require('./refs/json-schema-draft-07.json'); 449 if (self._opts.$data) metaSchema = $dataMetaSchema(metaSchema, META_SUPPORT_DATA); 450 self.addMetaSchema(metaSchema, META_SCHEMA_ID, true); 451 self._refs['http://json-schema.org/schema'] = META_SCHEMA_ID; 452 } 453 454 455 function addInitialSchemas(self) { 456 var optsSchemas = self._opts.schemas; 457 if (!optsSchemas) return; 458 if (Array.isArray(optsSchemas)) self.addSchema(optsSchemas); 459 else for (var key in optsSchemas) self.addSchema(optsSchemas[key], key); 460 } 461 462 463 function addInitialFormats(self) { 464 for (var name in self._opts.formats) { 465 var format = self._opts.formats[name]; 466 self.addFormat(name, format); 467 } 468 } 469 470 471 function addInitialKeywords(self) { 472 for (var name in self._opts.keywords) { 473 var keyword = self._opts.keywords[name]; 474 self.addKeyword(name, keyword); 475 } 476 } 477 478 479 function checkUnique(self, id) { 480 if (self._schemas[id] || self._refs[id]) 481 throw new Error('schema with key or id "' + id + '" already exists'); 482 } 483 484 485 function getMetaSchemaOptions(self) { 486 var metaOpts = util.copy(self._opts); 487 for (var i=0; i<META_IGNORE_OPTIONS.length; i++) 488 delete metaOpts[META_IGNORE_OPTIONS[i]]; 489 return metaOpts; 490 } 491 492 493 function setLogger(self) { 494 var logger = self._opts.logger; 495 if (logger === false) { 496 self.logger = {log: noop, warn: noop, error: noop}; 497 } else { 498 if (logger === undefined) logger = console; 499 if (!(typeof logger == 'object' && logger.log && logger.warn && logger.error)) 500 throw new Error('logger must implement log, warn and error methods'); 501 self.logger = logger; 502 } 503 } 504 505 506 function noop() {}