attribute.js
  1  'use strict';
  2  
  3  var helpers = require('./helpers');
  4  
  5  /** @type ValidatorResult */
  6  var ValidatorResult = helpers.ValidatorResult;
  7  /** @type SchemaError */
  8  var SchemaError = helpers.SchemaError;
  9  
 10  var attribute = {};
 11  
 12  attribute.ignoreProperties = {
 13    // informative properties
 14    'id': true,
 15    'default': true,
 16    'description': true,
 17    'title': true,
 18    // arguments to other properties
 19    'additionalItems': true,
 20    'then': true,
 21    'else': true,
 22    // special-handled properties
 23    '$schema': true,
 24    '$ref': true,
 25    'extends': true,
 26  };
 27  
 28  /**
 29   * @name validators
 30   */
 31  var validators = attribute.validators = {};
 32  
 33  /**
 34   * Validates whether the instance if of a certain type
 35   * @param instance
 36   * @param schema
 37   * @param options
 38   * @param ctx
 39   * @return {ValidatorResult|null}
 40   */
 41  validators.type = function validateType (instance, schema, options, ctx) {
 42    // Ignore undefined instances
 43    if (instance === undefined) {
 44      return null;
 45    }
 46    var result = new ValidatorResult(instance, schema, options, ctx);
 47    var types = Array.isArray(schema.type) ? schema.type : [schema.type];
 48    if (!types.some(this.testType.bind(this, instance, schema, options, ctx))) {
 49      var list = types.map(function (v) {
 50        if(!v) return;
 51        var id = v.$id || v.id;
 52        return id ? ('<' + id + '>') : (v+'');
 53      });
 54      result.addError({
 55        name: 'type',
 56        argument: list,
 57        message: "is not of a type(s) " + list,
 58      });
 59    }
 60    return result;
 61  };
 62  
 63  function testSchemaNoThrow(instance, options, ctx, callback, schema){
 64    var throwError = options.throwError;
 65    var throwAll = options.throwAll;
 66    options.throwError = false;
 67    options.throwAll = false;
 68    var res = this.validateSchema(instance, schema, options, ctx);
 69    options.throwError = throwError;
 70    options.throwAll = throwAll;
 71  
 72    if (!res.valid && callback instanceof Function) {
 73      callback(res);
 74    }
 75    return res.valid;
 76  }
 77  
 78  /**
 79   * Validates whether the instance matches some of the given schemas
 80   * @param instance
 81   * @param schema
 82   * @param options
 83   * @param ctx
 84   * @return {ValidatorResult|null}
 85   */
 86  validators.anyOf = function validateAnyOf (instance, schema, options, ctx) {
 87    // Ignore undefined instances
 88    if (instance === undefined) {
 89      return null;
 90    }
 91    var result = new ValidatorResult(instance, schema, options, ctx);
 92    var inner = new ValidatorResult(instance, schema, options, ctx);
 93    if (!Array.isArray(schema.anyOf)){
 94      throw new SchemaError("anyOf must be an array");
 95    }
 96    if (!schema.anyOf.some(
 97      testSchemaNoThrow.bind(
 98        this, instance, options, ctx, function(res){inner.importErrors(res);}
 99      ))) {
100      var list = schema.anyOf.map(function (v, i) {
101        var id = v.$id || v.id;
102        if(id) return '<' + id + '>';
103        return(v.title && JSON.stringify(v.title)) || (v['$ref'] && ('<' + v['$ref'] + '>')) || '[subschema '+i+']';
104      });
105      if (options.nestedErrors) {
106        result.importErrors(inner);
107      }
108      result.addError({
109        name: 'anyOf',
110        argument: list,
111        message: "is not any of " + list.join(','),
112      });
113    }
114    return result;
115  };
116  
117  /**
118   * Validates whether the instance matches every given schema
119   * @param instance
120   * @param schema
121   * @param options
122   * @param ctx
123   * @return {String|null}
124   */
125  validators.allOf = function validateAllOf (instance, schema, options, ctx) {
126    // Ignore undefined instances
127    if (instance === undefined) {
128      return null;
129    }
130    if (!Array.isArray(schema.allOf)){
131      throw new SchemaError("allOf must be an array");
132    }
133    var result = new ValidatorResult(instance, schema, options, ctx);
134    var self = this;
135    schema.allOf.forEach(function(v, i){
136      var valid = self.validateSchema(instance, v, options, ctx);
137      if(!valid.valid){
138        var id = v.$id || v.id;
139        var msg = id || (v.title && JSON.stringify(v.title)) || (v['$ref'] && ('<' + v['$ref'] + '>')) || '[subschema '+i+']';
140        result.addError({
141          name: 'allOf',
142          argument: { id: msg, length: valid.errors.length, valid: valid },
143          message: 'does not match allOf schema ' + msg + ' with ' + valid.errors.length + ' error[s]:',
144        });
145        result.importErrors(valid);
146      }
147    });
148    return result;
149  };
150  
151  /**
152   * Validates whether the instance matches exactly one of the given schemas
153   * @param instance
154   * @param schema
155   * @param options
156   * @param ctx
157   * @return {String|null}
158   */
159  validators.oneOf = function validateOneOf (instance, schema, options, ctx) {
160    // Ignore undefined instances
161    if (instance === undefined) {
162      return null;
163    }
164    if (!Array.isArray(schema.oneOf)){
165      throw new SchemaError("oneOf must be an array");
166    }
167    var result = new ValidatorResult(instance, schema, options, ctx);
168    var inner = new ValidatorResult(instance, schema, options, ctx);
169    var count = schema.oneOf.filter(
170      testSchemaNoThrow.bind(
171        this, instance, options, ctx, function(res) {inner.importErrors(res);}
172      ) ).length;
173    var list = schema.oneOf.map(function (v, i) {
174      var id = v.$id || v.id;
175      return id || (v.title && JSON.stringify(v.title)) || (v['$ref'] && ('<' + v['$ref'] + '>')) || '[subschema '+i+']';
176    });
177    if (count!==1) {
178      if (options.nestedErrors) {
179        result.importErrors(inner);
180      }
181      result.addError({
182        name: 'oneOf',
183        argument: list,
184        message: "is not exactly one from " + list.join(','),
185      });
186    }
187    return result;
188  };
189  
190  /**
191   * Validates "then" or "else" depending on the result of validating "if"
192   * @param instance
193   * @param schema
194   * @param options
195   * @param ctx
196   * @return {String|null}
197   */
198  validators.if = function validateIf (instance, schema, options, ctx) {
199    // Ignore undefined instances
200    if (instance === undefined) return null;
201    if (!helpers.isSchema(schema.if)) throw new Error('Expected "if" keyword to be a schema');
202    var ifValid = testSchemaNoThrow.call(this, instance, options, ctx, null, schema.if);
203    var result = new ValidatorResult(instance, schema, options, ctx);
204    var res;
205    if(ifValid){
206      if (schema.then === undefined) return;
207      if (!helpers.isSchema(schema.then)) throw new Error('Expected "then" keyword to be a schema');
208      res = this.validateSchema(instance, schema.then, options, ctx.makeChild(schema.then));
209      result.importErrors(res);
210    }else{
211      if (schema.else === undefined) return;
212      if (!helpers.isSchema(schema.else)) throw new Error('Expected "else" keyword to be a schema');
213      res = this.validateSchema(instance, schema.else, options, ctx.makeChild(schema.else));
214      result.importErrors(res);
215    }
216    return result;
217  };
218  
219  function getEnumerableProperty(object, key){
220    // Determine if `key` shows up in `for(var key in object)`
221    // First test Object.hasOwnProperty.call as an optimization: that guarantees it does
222    if(Object.hasOwnProperty.call(object, key)) return object[key];
223    // Test `key in object` as an optimization; false means it won't
224    if(!(key in object)) return;
225    while( (object = Object.getPrototypeOf(object)) ){
226      if(Object.propertyIsEnumerable.call(object, key)) return object[key];
227    }
228  }
229  
230  /**
231   * Validates propertyNames
232   * @param instance
233   * @param schema
234   * @param options
235   * @param ctx
236   * @return {String|null|ValidatorResult}
237   */
238  validators.propertyNames = function validatePropertyNames (instance, schema, options, ctx) {
239    if(!this.types.object(instance)) return;
240    var result = new ValidatorResult(instance, schema, options, ctx);
241    var subschema = schema.propertyNames!==undefined ? schema.propertyNames : {};
242    if(!helpers.isSchema(subschema)) throw new SchemaError('Expected "propertyNames" to be a schema (object or boolean)');
243  
244    for (var property in instance) {
245      if(getEnumerableProperty(instance, property) !== undefined){
246        var res = this.validateSchema(property, subschema, options, ctx.makeChild(subschema));
247        result.importErrors(res);
248      }
249    }
250  
251    return result;
252  };
253  
254  /**
255   * Validates properties
256   * @param instance
257   * @param schema
258   * @param options
259   * @param ctx
260   * @return {String|null|ValidatorResult}
261   */
262  validators.properties = function validateProperties (instance, schema, options, ctx) {
263    if(!this.types.object(instance)) return;
264    var result = new ValidatorResult(instance, schema, options, ctx);
265    var properties = schema.properties || {};
266    for (var property in properties) {
267      var subschema = properties[property];
268      if(subschema===undefined){
269        continue;
270      }else if(subschema===null){
271        throw new SchemaError('Unexpected null, expected schema in "properties"');
272      }
273      if (typeof options.preValidateProperty == 'function') {
274        options.preValidateProperty(instance, property, subschema, options, ctx);
275      }
276      var prop = getEnumerableProperty(instance, property);
277      var res = this.validateSchema(prop, subschema, options, ctx.makeChild(subschema, property));
278      if(res.instance !== result.instance[property]) result.instance[property] = res.instance;
279      result.importErrors(res);
280    }
281    return result;
282  };
283  
284  /**
285   * Test a specific property within in instance against the additionalProperties schema attribute
286   * This ignores properties with definitions in the properties schema attribute, but no other attributes.
287   * If too many more types of property-existence tests pop up they may need their own class of tests (like `type` has)
288   * @private
289   * @return {boolean}
290   */
291  function testAdditionalProperty (instance, schema, options, ctx, property, result) {
292    if(!this.types.object(instance)) return;
293    if (schema.properties && schema.properties[property] !== undefined) {
294      return;
295    }
296    if (schema.additionalProperties === false) {
297      result.addError({
298        name: 'additionalProperties',
299        argument: property,
300        message: "is not allowed to have the additional property " + JSON.stringify(property),
301      });
302    } else {
303      var additionalProperties = schema.additionalProperties || {};
304  
305      if (typeof options.preValidateProperty == 'function') {
306        options.preValidateProperty(instance, property, additionalProperties, options, ctx);
307      }
308  
309      var res = this.validateSchema(instance[property], additionalProperties, options, ctx.makeChild(additionalProperties, property));
310      if(res.instance !== result.instance[property]) result.instance[property] = res.instance;
311      result.importErrors(res);
312    }
313  }
314  
315  /**
316   * Validates patternProperties
317   * @param instance
318   * @param schema
319   * @param options
320   * @param ctx
321   * @return {String|null|ValidatorResult}
322   */
323  validators.patternProperties = function validatePatternProperties (instance, schema, options, ctx) {
324    if(!this.types.object(instance)) return;
325    var result = new ValidatorResult(instance, schema, options, ctx);
326    var patternProperties = schema.patternProperties || {};
327  
328    for (var property in instance) {
329      var test = true;
330      for (var pattern in patternProperties) {
331        var subschema = patternProperties[pattern];
332        if(subschema===undefined){
333          continue;
334        }else if(subschema===null){
335          throw new SchemaError('Unexpected null, expected schema in "patternProperties"');
336        }
337        try {
338          var regexp = new RegExp(pattern, 'u');
339        } catch(_e) {
340          // In the event the stricter handling causes an error, fall back on the forgiving handling
341          // DEPRECATED
342          regexp = new RegExp(pattern);
343        }
344        if (!regexp.test(property)) {
345          continue;
346        }
347        test = false;
348  
349        if (typeof options.preValidateProperty == 'function') {
350          options.preValidateProperty(instance, property, subschema, options, ctx);
351        }
352  
353        var res = this.validateSchema(instance[property], subschema, options, ctx.makeChild(subschema, property));
354        if(res.instance !== result.instance[property]) result.instance[property] = res.instance;
355        result.importErrors(res);
356      }
357      if (test) {
358        testAdditionalProperty.call(this, instance, schema, options, ctx, property, result);
359      }
360    }
361  
362    return result;
363  };
364  
365  /**
366   * Validates additionalProperties
367   * @param instance
368   * @param schema
369   * @param options
370   * @param ctx
371   * @return {String|null|ValidatorResult}
372   */
373  validators.additionalProperties = function validateAdditionalProperties (instance, schema, options, ctx) {
374    if(!this.types.object(instance)) return;
375    // if patternProperties is defined then we'll test when that one is called instead
376    if (schema.patternProperties) {
377      return null;
378    }
379    var result = new ValidatorResult(instance, schema, options, ctx);
380    for (var property in instance) {
381      testAdditionalProperty.call(this, instance, schema, options, ctx, property, result);
382    }
383    return result;
384  };
385  
386  /**
387   * Validates whether the instance value is at least of a certain length, when the instance value is a string.
388   * @param instance
389   * @param schema
390   * @return {String|null}
391   */
392  validators.minProperties = function validateMinProperties (instance, schema, options, ctx) {
393    if (!this.types.object(instance)) return;
394    var result = new ValidatorResult(instance, schema, options, ctx);
395    var keys = Object.keys(instance);
396    if (!(keys.length >= schema.minProperties)) {
397      result.addError({
398        name: 'minProperties',
399        argument: schema.minProperties,
400        message: "does not meet minimum property length of " + schema.minProperties,
401      });
402    }
403    return result;
404  };
405  
406  /**
407   * Validates whether the instance value is at most of a certain length, when the instance value is a string.
408   * @param instance
409   * @param schema
410   * @return {String|null}
411   */
412  validators.maxProperties = function validateMaxProperties (instance, schema, options, ctx) {
413    if (!this.types.object(instance)) return;
414    var result = new ValidatorResult(instance, schema, options, ctx);
415    var keys = Object.keys(instance);
416    if (!(keys.length <= schema.maxProperties)) {
417      result.addError({
418        name: 'maxProperties',
419        argument: schema.maxProperties,
420        message: "does not meet maximum property length of " + schema.maxProperties,
421      });
422    }
423    return result;
424  };
425  
426  /**
427   * Validates items when instance is an array
428   * @param instance
429   * @param schema
430   * @param options
431   * @param ctx
432   * @return {String|null|ValidatorResult}
433   */
434  validators.items = function validateItems (instance, schema, options, ctx) {
435    var self = this;
436    if (!this.types.array(instance)) return;
437    if (!schema.items) return;
438    var result = new ValidatorResult(instance, schema, options, ctx);
439    instance.every(function (value, i) {
440      var items = Array.isArray(schema.items) ? (schema.items[i] || schema.additionalItems) : schema.items;
441      if (items === undefined) {
442        return true;
443      }
444      if (items === false) {
445        result.addError({
446          name: 'items',
447          message: "additionalItems not permitted",
448        });
449        return false;
450      }
451      var res = self.validateSchema(value, items, options, ctx.makeChild(items, i));
452      if(res.instance !== result.instance[i]) result.instance[i] = res.instance;
453      result.importErrors(res);
454      return true;
455    });
456    return result;
457  };
458  
459  /**
460   * Validates minimum and exclusiveMinimum when the type of the instance value is a number.
461   * @param instance
462   * @param schema
463   * @return {String|null}
464   */
465  validators.minimum = function validateMinimum (instance, schema, options, ctx) {
466    if (!this.types.number(instance)) return;
467    var result = new ValidatorResult(instance, schema, options, ctx);
468    if (schema.exclusiveMinimum && schema.exclusiveMinimum === true) {
469      if(!(instance > schema.minimum)){
470        result.addError({
471          name: 'minimum',
472          argument: schema.minimum,
473          message: "must be greater than " + schema.minimum,
474        });
475      }
476    } else {
477      if(!(instance >= schema.minimum)){
478        result.addError({
479          name: 'minimum',
480          argument: schema.minimum,
481          message: "must be greater than or equal to " + schema.minimum,
482        });
483      }
484    }
485    return result;
486  };
487  
488  /**
489   * Validates maximum and exclusiveMaximum when the type of the instance value is a number.
490   * @param instance
491   * @param schema
492   * @return {String|null}
493   */
494  validators.maximum = function validateMaximum (instance, schema, options, ctx) {
495    if (!this.types.number(instance)) return;
496    var result = new ValidatorResult(instance, schema, options, ctx);
497    if (schema.exclusiveMaximum && schema.exclusiveMaximum === true) {
498      if(!(instance < schema.maximum)){
499        result.addError({
500          name: 'maximum',
501          argument: schema.maximum,
502          message: "must be less than " + schema.maximum,
503        });
504      }
505    } else {
506      if(!(instance <= schema.maximum)){
507        result.addError({
508          name: 'maximum',
509          argument: schema.maximum,
510          message: "must be less than or equal to " + schema.maximum,
511        });
512      }
513    }
514    return result;
515  };
516  
517  /**
518   * Validates the number form of exclusiveMinimum when the type of the instance value is a number.
519   * @param instance
520   * @param schema
521   * @return {String|null}
522   */
523  validators.exclusiveMinimum = function validateExclusiveMinimum (instance, schema, options, ctx) {
524    // Support the boolean form of exclusiveMinimum, which is handled by the "minimum" keyword.
525    if(typeof schema.exclusiveMaximum === 'boolean') return;
526    if (!this.types.number(instance)) return;
527    var result = new ValidatorResult(instance, schema, options, ctx);
528    var valid = instance > schema.exclusiveMinimum;
529    if (!valid) {
530      result.addError({
531        name: 'exclusiveMinimum',
532        argument: schema.exclusiveMinimum,
533        message: "must be strictly greater than " + schema.exclusiveMinimum,
534      });
535    }
536    return result;
537  };
538  
539  /**
540   * Validates the number form of exclusiveMaximum when the type of the instance value is a number.
541   * @param instance
542   * @param schema
543   * @return {String|null}
544   */
545  validators.exclusiveMaximum = function validateExclusiveMaximum (instance, schema, options, ctx) {
546    // Support the boolean form of exclusiveMaximum, which is handled by the "maximum" keyword.
547    if(typeof schema.exclusiveMaximum === 'boolean') return;
548    if (!this.types.number(instance)) return;
549    var result = new ValidatorResult(instance, schema, options, ctx);
550    var valid = instance < schema.exclusiveMaximum;
551    if (!valid) {
552      result.addError({
553        name: 'exclusiveMaximum',
554        argument: schema.exclusiveMaximum,
555        message: "must be strictly less than " + schema.exclusiveMaximum,
556      });
557    }
558    return result;
559  };
560  
561  /**
562   * Perform validation for multipleOf and divisibleBy, which are essentially the same.
563   * @param instance
564   * @param schema
565   * @param validationType
566   * @param errorMessage
567   * @returns {String|null}
568   */
569  var validateMultipleOfOrDivisbleBy = function validateMultipleOfOrDivisbleBy (instance, schema, options, ctx, validationType, errorMessage) {
570    if (!this.types.number(instance)) return;
571  
572    var validationArgument = schema[validationType];
573    if (validationArgument == 0) {
574      throw new SchemaError(validationType + " cannot be zero");
575    }
576  
577    var result = new ValidatorResult(instance, schema, options, ctx);
578  
579    var instanceDecimals = helpers.getDecimalPlaces(instance);
580    var divisorDecimals = helpers.getDecimalPlaces(validationArgument);
581  
582    var maxDecimals = Math.max(instanceDecimals , divisorDecimals);
583    var multiplier = Math.pow(10, maxDecimals);
584  
585    if (Math.round(instance * multiplier) % Math.round(validationArgument * multiplier) !== 0) {
586      result.addError({
587        name: validationType,
588        argument:  validationArgument,
589        message: errorMessage + JSON.stringify(validationArgument),
590      });
591    }
592  
593    return result;
594  };
595  
596  /**
597   * Validates divisibleBy when the type of the instance value is a number.
598   * @param instance
599   * @param schema
600   * @return {String|null}
601   */
602  validators.multipleOf = function validateMultipleOf (instance, schema, options, ctx) {
603    return validateMultipleOfOrDivisbleBy.call(this, instance, schema, options, ctx, "multipleOf", "is not a multiple of (divisible by) ");
604  };
605  
606  /**
607   * Validates multipleOf when the type of the instance value is a number.
608   * @param instance
609   * @param schema
610   * @return {String|null}
611   */
612  validators.divisibleBy = function validateDivisibleBy (instance, schema, options, ctx) {
613    return validateMultipleOfOrDivisbleBy.call(this, instance, schema, options, ctx, "divisibleBy", "is not divisible by (multiple of) ");
614  };
615  
616  /**
617   * Validates whether the instance value is present.
618   * @param instance
619   * @param schema
620   * @return {String|null}
621   */
622  validators.required = function validateRequired (instance, schema, options, ctx) {
623    var result = new ValidatorResult(instance, schema, options, ctx);
624    if (instance === undefined && schema.required === true) {
625      // A boolean form is implemented for reverse-compatibility with schemas written against older drafts
626      result.addError({
627        name: 'required',
628        message: "is required",
629      });
630    } else if (this.types.object(instance) && Array.isArray(schema.required)) {
631      schema.required.forEach(function(n){
632        if(getEnumerableProperty(instance, n)===undefined){
633          result.addError({
634            name: 'required',
635            argument: n,
636            message: "requires property " + JSON.stringify(n),
637          });
638        }
639      });
640    }
641    return result;
642  };
643  
644  /**
645   * Validates whether the instance value matches the regular expression, when the instance value is a string.
646   * @param instance
647   * @param schema
648   * @return {String|null}
649   */
650  validators.pattern = function validatePattern (instance, schema, options, ctx) {
651    if (!this.types.string(instance)) return;
652    var result = new ValidatorResult(instance, schema, options, ctx);
653    var pattern = schema.pattern;
654    try {
655      var regexp = new RegExp(pattern, 'u');
656    } catch(_e) {
657      // In the event the stricter handling causes an error, fall back on the forgiving handling
658      // DEPRECATED
659      regexp = new RegExp(pattern);
660    }
661    if (!instance.match(regexp)) {
662      result.addError({
663        name: 'pattern',
664        argument: schema.pattern,
665        message: "does not match pattern " + JSON.stringify(schema.pattern.toString()),
666      });
667    }
668    return result;
669  };
670  
671  /**
672   * Validates whether the instance value is of a certain defined format or a custom
673   * format.
674   * The following formats are supported for string types:
675   *   - date-time
676   *   - date
677   *   - time
678   *   - ip-address
679   *   - ipv6
680   *   - uri
681   *   - color
682   *   - host-name
683   *   - alpha
684   *   - alpha-numeric
685   *   - utc-millisec
686   * @param instance
687   * @param schema
688   * @param [options]
689   * @param [ctx]
690   * @return {String|null}
691   */
692  validators.format = function validateFormat (instance, schema, options, ctx) {
693    if (instance===undefined) return;
694    var result = new ValidatorResult(instance, schema, options, ctx);
695    if (!result.disableFormat && !helpers.isFormat(instance, schema.format, this)) {
696      result.addError({
697        name: 'format',
698        argument: schema.format,
699        message: "does not conform to the " + JSON.stringify(schema.format) + " format",
700      });
701    }
702    return result;
703  };
704  
705  /**
706   * Validates whether the instance value is at least of a certain length, when the instance value is a string.
707   * @param instance
708   * @param schema
709   * @return {String|null}
710   */
711  validators.minLength = function validateMinLength (instance, schema, options, ctx) {
712    if (!this.types.string(instance)) return;
713    var result = new ValidatorResult(instance, schema, options, ctx);
714    var hsp = instance.match(/[\uDC00-\uDFFF]/g);
715    var length = instance.length - (hsp ? hsp.length : 0);
716    if (!(length >= schema.minLength)) {
717      result.addError({
718        name: 'minLength',
719        argument: schema.minLength,
720        message: "does not meet minimum length of " + schema.minLength,
721      });
722    }
723    return result;
724  };
725  
726  /**
727   * Validates whether the instance value is at most of a certain length, when the instance value is a string.
728   * @param instance
729   * @param schema
730   * @return {String|null}
731   */
732  validators.maxLength = function validateMaxLength (instance, schema, options, ctx) {
733    if (!this.types.string(instance)) return;
734    var result = new ValidatorResult(instance, schema, options, ctx);
735    // TODO if this was already computed in "minLength", use that value instead of re-computing
736    var hsp = instance.match(/[\uDC00-\uDFFF]/g);
737    var length = instance.length - (hsp ? hsp.length : 0);
738    if (!(length <= schema.maxLength)) {
739      result.addError({
740        name: 'maxLength',
741        argument: schema.maxLength,
742        message: "does not meet maximum length of " + schema.maxLength,
743      });
744    }
745    return result;
746  };
747  
748  /**
749   * Validates whether instance contains at least a minimum number of items, when the instance is an Array.
750   * @param instance
751   * @param schema
752   * @return {String|null}
753   */
754  validators.minItems = function validateMinItems (instance, schema, options, ctx) {
755    if (!this.types.array(instance)) return;
756    var result = new ValidatorResult(instance, schema, options, ctx);
757    if (!(instance.length >= schema.minItems)) {
758      result.addError({
759        name: 'minItems',
760        argument: schema.minItems,
761        message: "does not meet minimum length of " + schema.minItems,
762      });
763    }
764    return result;
765  };
766  
767  /**
768   * Validates whether instance contains no more than a maximum number of items, when the instance is an Array.
769   * @param instance
770   * @param schema
771   * @return {String|null}
772   */
773  validators.maxItems = function validateMaxItems (instance, schema, options, ctx) {
774    if (!this.types.array(instance)) return;
775    var result = new ValidatorResult(instance, schema, options, ctx);
776    if (!(instance.length <= schema.maxItems)) {
777      result.addError({
778        name: 'maxItems',
779        argument: schema.maxItems,
780        message: "does not meet maximum length of " + schema.maxItems,
781      });
782    }
783    return result;
784  };
785  
786  /**
787   * Deep compares arrays for duplicates
788   * @param v
789   * @param i
790   * @param a
791   * @private
792   * @return {boolean}
793   */
794  function testArrays (v, i, a) {
795    var j, len = a.length;
796    for (j = i + 1, len; j < len; j++) {
797      if (helpers.deepCompareStrict(v, a[j])) {
798        return false;
799      }
800    }
801    return true;
802  }
803  
804  /**
805   * Validates whether there are no duplicates, when the instance is an Array.
806   * @param instance
807   * @return {String|null}
808   */
809  validators.uniqueItems = function validateUniqueItems (instance, schema, options, ctx) {
810    if (schema.uniqueItems!==true) return;
811    if (!this.types.array(instance)) return;
812    var result = new ValidatorResult(instance, schema, options, ctx);
813    if (!instance.every(testArrays)) {
814      result.addError({
815        name: 'uniqueItems',
816        message: "contains duplicate item",
817      });
818    }
819    return result;
820  };
821  
822  /**
823   * Validate for the presence of dependency properties, if the instance is an object.
824   * @param instance
825   * @param schema
826   * @param options
827   * @param ctx
828   * @return {null|ValidatorResult}
829   */
830  validators.dependencies = function validateDependencies (instance, schema, options, ctx) {
831    if (!this.types.object(instance)) return;
832    var result = new ValidatorResult(instance, schema, options, ctx);
833    for (var property in schema.dependencies) {
834      if (instance[property] === undefined) {
835        continue;
836      }
837      var dep = schema.dependencies[property];
838      var childContext = ctx.makeChild(dep, property);
839      if (typeof dep == 'string') {
840        dep = [dep];
841      }
842      if (Array.isArray(dep)) {
843        dep.forEach(function (prop) {
844          if (instance[prop] === undefined) {
845            result.addError({
846              // FIXME there's two different "dependencies" errors here with slightly different outputs
847              // Can we make these the same? Or should we create different error types?
848              name: 'dependencies',
849              argument: childContext.propertyPath,
850              message: "property " + prop + " not found, required by " + childContext.propertyPath,
851            });
852          }
853        });
854      } else {
855        var res = this.validateSchema(instance, dep, options, childContext);
856        if(result.instance !== res.instance) result.instance = res.instance;
857        if (res && res.errors.length) {
858          result.addError({
859            name: 'dependencies',
860            argument: childContext.propertyPath,
861            message: "does not meet dependency required by " + childContext.propertyPath,
862          });
863          result.importErrors(res);
864        }
865      }
866    }
867    return result;
868  };
869  
870  /**
871   * Validates whether the instance value is one of the enumerated values.
872   *
873   * @param instance
874   * @param schema
875   * @return {ValidatorResult|null}
876   */
877  validators['enum'] = function validateEnum (instance, schema, options, ctx) {
878    if (instance === undefined) {
879      return null;
880    }
881    if (!Array.isArray(schema['enum'])) {
882      throw new SchemaError("enum expects an array", schema);
883    }
884    var result = new ValidatorResult(instance, schema, options, ctx);
885    if (!schema['enum'].some(helpers.deepCompareStrict.bind(null, instance))) {
886      result.addError({
887        name: 'enum',
888        argument: schema['enum'],
889        message: "is not one of enum values: " + schema['enum'].map(String).join(','),
890      });
891    }
892    return result;
893  };
894  
895  /**
896   * Validates whether the instance exactly matches a given value
897   *
898   * @param instance
899   * @param schema
900   * @return {ValidatorResult|null}
901   */
902  validators['const'] = function validateEnum (instance, schema, options, ctx) {
903    if (instance === undefined) {
904      return null;
905    }
906    var result = new ValidatorResult(instance, schema, options, ctx);
907    if (!helpers.deepCompareStrict(schema['const'], instance)) {
908      result.addError({
909        name: 'const',
910        argument: schema['const'],
911        message: "does not exactly match expected constant: " + schema['const'],
912      });
913    }
914    return result;
915  };
916  
917  /**
918   * Validates whether the instance if of a prohibited type.
919   * @param instance
920   * @param schema
921   * @param options
922   * @param ctx
923   * @return {null|ValidatorResult}
924   */
925  validators.not = validators.disallow = function validateNot (instance, schema, options, ctx) {
926    var self = this;
927    if(instance===undefined) return null;
928    var result = new ValidatorResult(instance, schema, options, ctx);
929    var notTypes = schema.not || schema.disallow;
930    if(!notTypes) return null;
931    if(!Array.isArray(notTypes)) notTypes=[notTypes];
932    notTypes.forEach(function (type) {
933      if (self.testType(instance, schema, options, ctx, type)) {
934        var id = type && (type.$id || type.id);
935        var schemaId = id || type;
936        result.addError({
937          name: 'not',
938          argument: schemaId,
939          message: "is of prohibited type " + schemaId,
940        });
941      }
942    });
943    return result;
944  };
945  
946  module.exports = attribute;