Projekt

Obecné

Profil

Stáhnout (15.5 KB) Statistiky
| Větev: | Revize:
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() {}
(2-2/6)