Project

General

Profile

« Previous | Next » 

Revision 57ce414c

Added by koszko over 1 year ago

validate repository responses against JSON schemas

  • compute_scripts.awk (include_file): don't enforce specific path format on #INCLUDE'd files
  • .gitmodules, schemas: add Haketilo JSON schemas subrepo
  • html/install.js (InstallView): import schema validator and run it against downloaded mapping and resource definitions
  • html/repo_query.js (RepoEntry): import schema validator and run it against obtained query results
  • test/haketilo_test/unit/test_install.py (test_install_normal_usage, test_install_dialogs): use underscore instead of hyphen in item identifiers
  • test/haketilo_test/unit/test_install.py (test_install_dialogs): adapt error message test cases to new handling method of invalid JSON instanced
  • test/haketilo_test/unit/test_repo_query.py (test_repo_query_normal_usage): use underscore instead of hyphen in item identifiers
  • test/haketilo_test/unit/test_repo_query.py (test_repo_query_messages): use higher sample unsupported schema version to avoid having to modify the test case soon
  • test/haketilo_test/world_wide_library.py: use underscore instead of hyphen in item identifiers
  • common/jsonschema.js, common/jsonschema: adapt tdegrunt's jsonschema and include in Haketilo, load schema documents from schemas/

View differences:

.gitmodules
1
[submodule "schemas"]
2
	path = schemas
3
	url = ../hydrilla-json-schemas/
common/jsonschema.js
1
/* SPDX-License-Identifier: MIT AND CC0-1.0
2
 *
3
 * License text of the original lib/index.js from jsonschema library:
4
 *
5
 ***************************************
6
 *
7
 * jsonschema is licensed under MIT license.
8
 *
9
 * Copyright (C) 2012-2015 Tom de Grunt <tom@degrunt.nl>
10
 *
11
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
12
 * this software and associated documentation files (the "Software"), to deal in
13
 * the Software without restriction, including without limitation the rights to
14
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
15
 * of the Software, and to permit persons to whom the Software is furnished to do
16
 * so, subject to the following conditions:
17
 *
18
 * The above copyright notice and this permission notice shall be included in all
19
 * copies or substantial portions of the Software.
20
 *
21
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
 * SOFTWARE.
28
 *
29
 *******************************************************************************
30
 *
31
 * License notice for the adaptation to use in Haketilo:
32
 *
33
 ***************************************
34
 *
35
 * Copyright (C) 2022 Wojtek Kosior <koszko@koszko.org>
36
 *
37
 * This program is free software: you can redistribute it and/or modify
38
 * it under the terms of the CC0 1.0 Universal License as published by
39
 * the Creative Commons Corporation.
40
 *
41
 * This program is distributed in the hope that it will be useful,
42
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
43
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
44
 * CC0 1.0 Universal License for more details.
45
 */
46

  
47
#FROM common/jsonschema/validator.js IMPORT Validator
48
#EXPORT Validator
49

  
50
#FROM common/jsonschema/helpers.js IMPORT ValidatorResult, ValidationError, \
51
                                          ValidatorResultError, SchemaError
52

  
53
#EXPORT ValidatorResult
54
#EXPORT ValidationError
55
#EXPORT ValidatorResultError
56
#EXPORT SchemaError
57

  
58
#FROM common/jsonschema/scan.js IMPORT SchemaScanResult, scan
59

  
60
#EXPORT scan
61
#EXPORT SchemaScanResult
62

  
63
function validate(instance, schema, options) {
64
    var v = new Validator();
65
    return v.validate(instance, schema, options);
66
};
67
#EXPORT validate
68

  
69
const haketilo_schemas = [
70
#INCLUDE schemas/api_query_result-1.0.1.schema.json
71
    ,
72
#INCLUDE schemas/api_mapping_description-1.0.1.schema.json
73
    ,
74
#INCLUDE schemas/api_resource_description-1.0.1.schema.json
75
    ,
76
#INCLUDE schemas/common_definitions-1.0.1.schema.json
77
].reduce((ac, s) => Object.assign(ac, {[s.$id]: s}), {});
78
#EXPORT haketilo_schemas
79

  
80
const haketilo_validator = new Validator();
81
Object.values(haketilo_schemas)
82
    .forEach(s => haketilo_validator.addSchema(s, s.$id));
83
#EXPORT haketilo_validator
common/jsonschema/attribute.js
1
/* SPDX-License-Identifier: MIT
2
 *
3
 * jsonschema is licensed under MIT license.
4
 *
5
 * Copyright (C) 2012-2015 Tom de Grunt <tom@degrunt.nl>
6
 *
7
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
8
 * this software and associated documentation files (the "Software"), to deal in
9
 * the Software without restriction, including without limitation the rights to
10
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
11
 * of the Software, and to permit persons to whom the Software is furnished to do
12
 * so, subject to the following conditions:
13
 *
14
 * The above copyright notice and this permission notice shall be included in all
15
 * copies or substantial portions of the Software.
16
 *
17
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
 * SOFTWARE.
24
 */
25

  
26
#IMPORT common/jsonschema/helpers.js
27

  
28
/** @type ValidatorResult */
29
var ValidatorResult = helpers.ValidatorResult;
30
/** @type SchemaError */
31
var SchemaError = helpers.SchemaError;
32

  
33
var attribute = {};
34

  
35
const ignoreProperties = {
36
  // informative properties
37
  'id': true,
38
  'default': true,
39
  'description': true,
40
  'title': true,
41
  // arguments to other properties
42
  'additionalItems': true,
43
  'then': true,
44
  'else': true,
45
  // special-handled properties
46
  '$schema': true,
47
  '$ref': true,
48
  'extends': true,
49
};
50
#EXPORT ignoreProperties
51

  
52
/**
53
 * @name validators
54
 */
55
const validators = {};
56

  
57
/**
58
 * Validates whether the instance if of a certain type
59
 * @param instance
60
 * @param schema
61
 * @param options
62
 * @param ctx
63
 * @return {ValidatorResult|null}
64
 */
65
validators.type = function validateType (instance, schema, options, ctx) {
66
  // Ignore undefined instances
67
  if (instance === undefined) {
68
    return null;
69
  }
70
  var result = new ValidatorResult(instance, schema, options, ctx);
71
  var types = Array.isArray(schema.type) ? schema.type : [schema.type];
72
  if (!types.some(this.testType.bind(this, instance, schema, options, ctx))) {
73
    var list = types.map(function (v) {
74
      if(!v) return;
75
      var id = v.$id || v.id;
76
      return id ? ('<' + id + '>') : (v+'');
77
    });
78
    result.addError({
79
      name: 'type',
80
      argument: list,
81
      message: "is not of a type(s) " + list,
82
    });
83
  }
84
  return result;
85
};
86

  
87
function testSchemaNoThrow(instance, options, ctx, callback, schema){
88
  var throwError = options.throwError;
89
  var throwAll = options.throwAll;
90
  options.throwError = false;
91
  options.throwAll = false;
92
  var res = this.validateSchema(instance, schema, options, ctx);
93
  options.throwError = throwError;
94
  options.throwAll = throwAll;
95

  
96
  if (!res.valid && callback instanceof Function) {
97
    callback(res);
98
  }
99
  return res.valid;
100
}
101

  
102
/**
103
 * Validates whether the instance matches some of the given schemas
104
 * @param instance
105
 * @param schema
106
 * @param options
107
 * @param ctx
108
 * @return {ValidatorResult|null}
109
 */
110
validators.anyOf = function validateAnyOf (instance, schema, options, ctx) {
111
  // Ignore undefined instances
112
  if (instance === undefined) {
113
    return null;
114
  }
115
  var result = new ValidatorResult(instance, schema, options, ctx);
116
  var inner = new ValidatorResult(instance, schema, options, ctx);
117
  if (!Array.isArray(schema.anyOf)){
118
    throw new SchemaError("anyOf must be an array");
119
  }
120
  if (!schema.anyOf.some(
121
    testSchemaNoThrow.bind(
122
      this, instance, options, ctx, function(res){inner.importErrors(res);}
123
    ))) {
124
    var list = schema.anyOf.map(function (v, i) {
125
      var id = v.$id || v.id;
126
      if(id) return '<' + id + '>';
127
      return(v.title && JSON.stringify(v.title)) || (v['$ref'] && ('<' + v['$ref'] + '>')) || '[subschema '+i+']';
128
    });
129
    if (options.nestedErrors) {
130
      result.importErrors(inner);
131
    }
132
    result.addError({
133
      name: 'anyOf',
134
      argument: list,
135
      message: "is not any of " + list.join(','),
136
    });
137
  }
138
  return result;
139
};
140

  
141
/**
142
 * Validates whether the instance matches every given schema
143
 * @param instance
144
 * @param schema
145
 * @param options
146
 * @param ctx
147
 * @return {String|null}
148
 */
149
validators.allOf = function validateAllOf (instance, schema, options, ctx) {
150
  // Ignore undefined instances
151
  if (instance === undefined) {
152
    return null;
153
  }
154
  if (!Array.isArray(schema.allOf)){
155
    throw new SchemaError("allOf must be an array");
156
  }
157
  var result = new ValidatorResult(instance, schema, options, ctx);
158
  var self = this;
159
  schema.allOf.forEach(function(v, i){
160
    var valid = self.validateSchema(instance, v, options, ctx);
161
    if(!valid.valid){
162
      var id = v.$id || v.id;
163
      var msg = id || (v.title && JSON.stringify(v.title)) || (v['$ref'] && ('<' + v['$ref'] + '>')) || '[subschema '+i+']';
164
      result.addError({
165
        name: 'allOf',
166
        argument: { id: msg, length: valid.errors.length, valid: valid },
167
        message: 'does not match allOf schema ' + msg + ' with ' + valid.errors.length + ' error[s]:',
168
      });
169
      result.importErrors(valid);
170
    }
171
  });
172
  return result;
173
};
174

  
175
/**
176
 * Validates whether the instance matches exactly one of the given schemas
177
 * @param instance
178
 * @param schema
179
 * @param options
180
 * @param ctx
181
 * @return {String|null}
182
 */
183
validators.oneOf = function validateOneOf (instance, schema, options, ctx) {
184
  // Ignore undefined instances
185
  if (instance === undefined) {
186
    return null;
187
  }
188
  if (!Array.isArray(schema.oneOf)){
189
    throw new SchemaError("oneOf must be an array");
190
  }
191
  var result = new ValidatorResult(instance, schema, options, ctx);
192
  var inner = new ValidatorResult(instance, schema, options, ctx);
193
  var count = schema.oneOf.filter(
194
    testSchemaNoThrow.bind(
195
      this, instance, options, ctx, function(res) {inner.importErrors(res);}
196
    ) ).length;
197
  var list = schema.oneOf.map(function (v, i) {
198
    var id = v.$id || v.id;
199
    return id || (v.title && JSON.stringify(v.title)) || (v['$ref'] && ('<' + v['$ref'] + '>')) || '[subschema '+i+']';
200
  });
201
  if (count!==1) {
202
    if (options.nestedErrors) {
203
      result.importErrors(inner);
204
    }
205
    result.addError({
206
      name: 'oneOf',
207
      argument: list,
208
      message: "is not exactly one from " + list.join(','),
209
    });
210
  }
211
  return result;
212
};
213

  
214
/**
215
 * Validates "then" or "else" depending on the result of validating "if"
216
 * @param instance
217
 * @param schema
218
 * @param options
219
 * @param ctx
220
 * @return {String|null}
221
 */
222
validators.if = function validateIf (instance, schema, options, ctx) {
223
  // Ignore undefined instances
224
  if (instance === undefined) return null;
225
  if (!helpers.isSchema(schema.if)) throw new Error('Expected "if" keyword to be a schema');
226
  var ifValid = testSchemaNoThrow.call(this, instance, options, ctx, null, schema.if);
227
  var result = new ValidatorResult(instance, schema, options, ctx);
228
  var res;
229
  if(ifValid){
230
    if (schema.then === undefined) return;
231
    if (!helpers.isSchema(schema.then)) throw new Error('Expected "then" keyword to be a schema');
232
    res = this.validateSchema(instance, schema.then, options, ctx.makeChild(schema.then));
233
    result.importErrors(res);
234
  }else{
235
    if (schema.else === undefined) return;
236
    if (!helpers.isSchema(schema.else)) throw new Error('Expected "else" keyword to be a schema');
237
    res = this.validateSchema(instance, schema.else, options, ctx.makeChild(schema.else));
238
    result.importErrors(res);
239
  }
240
  return result;
241
};
242

  
243
function getEnumerableProperty(object, key){
244
  // Determine if `key` shows up in `for(var key in object)`
245
  // First test Object.hasOwnProperty.call as an optimization: that guarantees it does
246
  if(Object.hasOwnProperty.call(object, key)) return object[key];
247
  // Test `key in object` as an optimization; false means it won't
248
  if(!(key in object)) return;
249
  while( (object = Object.getPrototypeOf(object)) ){
250
    if(Object.propertyIsEnumerable.call(object, key)) return object[key];
251
  }
252
}
253

  
254
/**
255
 * Validates propertyNames
256
 * @param instance
257
 * @param schema
258
 * @param options
259
 * @param ctx
260
 * @return {String|null|ValidatorResult}
261
 */
262
validators.propertyNames = function validatePropertyNames (instance, schema, options, ctx) {
263
  if(!this.types.object(instance)) return;
264
  var result = new ValidatorResult(instance, schema, options, ctx);
265
  var subschema = schema.propertyNames!==undefined ? schema.propertyNames : {};
266
  if(!helpers.isSchema(subschema)) throw new SchemaError('Expected "propertyNames" to be a schema (object or boolean)');
267

  
268
  for (var property in instance) {
269
    if(getEnumerableProperty(instance, property) !== undefined){
270
      var res = this.validateSchema(property, subschema, options, ctx.makeChild(subschema));
271
      result.importErrors(res);
272
    }
273
  }
274

  
275
  return result;
276
};
277

  
278
/**
279
 * Validates properties
280
 * @param instance
281
 * @param schema
282
 * @param options
283
 * @param ctx
284
 * @return {String|null|ValidatorResult}
285
 */
286
validators.properties = function validateProperties (instance, schema, options, ctx) {
287
  if(!this.types.object(instance)) return;
288
  var result = new ValidatorResult(instance, schema, options, ctx);
289
  var properties = schema.properties || {};
290
  for (var property in properties) {
291
    var subschema = properties[property];
292
    if(subschema===undefined){
293
      continue;
294
    }else if(subschema===null){
295
      throw new SchemaError('Unexpected null, expected schema in "properties"');
296
    }
297
    if (typeof options.preValidateProperty == 'function') {
298
      options.preValidateProperty(instance, property, subschema, options, ctx);
299
    }
300
    var prop = getEnumerableProperty(instance, property);
301
    var res = this.validateSchema(prop, subschema, options, ctx.makeChild(subschema, property));
302
    if(res.instance !== result.instance[property]) result.instance[property] = res.instance;
303
    result.importErrors(res);
304
  }
305
  return result;
306
};
307

  
308
/**
309
 * Test a specific property within in instance against the additionalProperties schema attribute
310
 * This ignores properties with definitions in the properties schema attribute, but no other attributes.
311
 * If too many more types of property-existence tests pop up they may need their own class of tests (like `type` has)
312
 * @private
313
 * @return {boolean}
314
 */
315
function testAdditionalProperty (instance, schema, options, ctx, property, result) {
316
  if(!this.types.object(instance)) return;
317
  if (schema.properties && schema.properties[property] !== undefined) {
318
    return;
319
  }
320
  if (schema.additionalProperties === false) {
321
    result.addError({
322
      name: 'additionalProperties',
323
      argument: property,
324
      message: "is not allowed to have the additional property " + JSON.stringify(property),
325
    });
326
  } else {
327
    var additionalProperties = schema.additionalProperties || {};
328

  
329
    if (typeof options.preValidateProperty == 'function') {
330
      options.preValidateProperty(instance, property, additionalProperties, options, ctx);
331
    }
332

  
333
    var res = this.validateSchema(instance[property], additionalProperties, options, ctx.makeChild(additionalProperties, property));
334
    if(res.instance !== result.instance[property]) result.instance[property] = res.instance;
335
    result.importErrors(res);
336
  }
337
}
338

  
339
/**
340
 * Validates patternProperties
341
 * @param instance
342
 * @param schema
343
 * @param options
344
 * @param ctx
345
 * @return {String|null|ValidatorResult}
346
 */
347
validators.patternProperties = function validatePatternProperties (instance, schema, options, ctx) {
348
  if(!this.types.object(instance)) return;
349
  var result = new ValidatorResult(instance, schema, options, ctx);
350
  var patternProperties = schema.patternProperties || {};
351

  
352
  for (var property in instance) {
353
    var test = true;
354
    for (var pattern in patternProperties) {
355
      var subschema = patternProperties[pattern];
356
      if(subschema===undefined){
357
        continue;
358
      }else if(subschema===null){
359
        throw new SchemaError('Unexpected null, expected schema in "patternProperties"');
360
      }
361
      try {
362
        var regexp = new RegExp(pattern, 'u');
363
      } catch(_e) {
364
        // In the event the stricter handling causes an error, fall back on the forgiving handling
365
        // DEPRECATED
366
        regexp = new RegExp(pattern);
367
      }
368
      if (!regexp.test(property)) {
369
        continue;
370
      }
371
      test = false;
372

  
373
      if (typeof options.preValidateProperty == 'function') {
374
        options.preValidateProperty(instance, property, subschema, options, ctx);
375
      }
376

  
377
      var res = this.validateSchema(instance[property], subschema, options, ctx.makeChild(subschema, property));
378
      if(res.instance !== result.instance[property]) result.instance[property] = res.instance;
379
      result.importErrors(res);
380
    }
381
    if (test) {
382
      testAdditionalProperty.call(this, instance, schema, options, ctx, property, result);
383
    }
384
  }
385

  
386
  return result;
387
};
388

  
389
/**
390
 * Validates additionalProperties
391
 * @param instance
392
 * @param schema
393
 * @param options
394
 * @param ctx
395
 * @return {String|null|ValidatorResult}
396
 */
397
validators.additionalProperties = function validateAdditionalProperties (instance, schema, options, ctx) {
398
  if(!this.types.object(instance)) return;
399
  // if patternProperties is defined then we'll test when that one is called instead
400
  if (schema.patternProperties) {
401
    return null;
402
  }
403
  var result = new ValidatorResult(instance, schema, options, ctx);
404
  for (var property in instance) {
405
    testAdditionalProperty.call(this, instance, schema, options, ctx, property, result);
406
  }
407
  return result;
408
};
409

  
410
/**
411
 * Validates whether the instance value is at least of a certain length, when the instance value is a string.
412
 * @param instance
413
 * @param schema
414
 * @return {String|null}
415
 */
416
validators.minProperties = function validateMinProperties (instance, schema, options, ctx) {
417
  if (!this.types.object(instance)) return;
418
  var result = new ValidatorResult(instance, schema, options, ctx);
419
  var keys = Object.keys(instance);
420
  if (!(keys.length >= schema.minProperties)) {
421
    result.addError({
422
      name: 'minProperties',
423
      argument: schema.minProperties,
424
      message: "does not meet minimum property length of " + schema.minProperties,
425
    });
426
  }
427
  return result;
428
};
429

  
430
/**
431
 * Validates whether the instance value is at most of a certain length, when the instance value is a string.
432
 * @param instance
433
 * @param schema
434
 * @return {String|null}
435
 */
436
validators.maxProperties = function validateMaxProperties (instance, schema, options, ctx) {
437
  if (!this.types.object(instance)) return;
438
  var result = new ValidatorResult(instance, schema, options, ctx);
439
  var keys = Object.keys(instance);
440
  if (!(keys.length <= schema.maxProperties)) {
441
    result.addError({
442
      name: 'maxProperties',
443
      argument: schema.maxProperties,
444
      message: "does not meet maximum property length of " + schema.maxProperties,
445
    });
446
  }
447
  return result;
448
};
449

  
450
/**
451
 * Validates items when instance is an array
452
 * @param instance
453
 * @param schema
454
 * @param options
455
 * @param ctx
456
 * @return {String|null|ValidatorResult}
457
 */
458
validators.items = function validateItems (instance, schema, options, ctx) {
459
  var self = this;
460
  if (!this.types.array(instance)) return;
461
  if (schema.items===undefined) return;
462
  var result = new ValidatorResult(instance, schema, options, ctx);
463
  instance.every(function (value, i) {
464
    if(Array.isArray(schema.items)){
465
      var items =  schema.items[i]===undefined ? schema.additionalItems : schema.items[i];
466
    }else{
467
      var items = schema.items;
468
    }
469
    if (items === undefined) {
470
      return true;
471
    }
472
    if (items === false) {
473
      result.addError({
474
        name: 'items',
475
        message: "additionalItems not permitted",
476
      });
477
      return false;
478
    }
479
    var res = self.validateSchema(value, items, options, ctx.makeChild(items, i));
480
    if(res.instance !== result.instance[i]) result.instance[i] = res.instance;
481
    result.importErrors(res);
482
    return true;
483
  });
484
  return result;
485
};
486

  
487
/**
488
 * Validates the "contains" keyword
489
 * @param instance
490
 * @param schema
491
 * @param options
492
 * @param ctx
493
 * @return {String|null|ValidatorResult}
494
 */
495
validators.contains = function validateContains (instance, schema, options, ctx) {
496
  var self = this;
497
  if (!this.types.array(instance)) return;
498
  if (schema.contains===undefined) return;
499
  if (!helpers.isSchema(schema.contains)) throw new Error('Expected "contains" keyword to be a schema');
500
  var result = new ValidatorResult(instance, schema, options, ctx);
501
  var count = instance.some(function (value, i) {
502
    var res = self.validateSchema(value, schema.contains, options, ctx.makeChild(schema.contains, i));
503
    return res.errors.length===0;
504
  });
505
  if(count===false){
506
    result.addError({
507
      name: 'contains',
508
      argument: schema.contains,
509
      message: "must contain an item matching given schema",
510
    });
511
  }
512
  return result;
513
};
514

  
515
/**
516
 * Validates minimum and exclusiveMinimum when the type of the instance value is a number.
517
 * @param instance
518
 * @param schema
519
 * @return {String|null}
520
 */
521
validators.minimum = function validateMinimum (instance, schema, options, ctx) {
522
  if (!this.types.number(instance)) return;
523
  var result = new ValidatorResult(instance, schema, options, ctx);
524
  if (schema.exclusiveMinimum && schema.exclusiveMinimum === true) {
525
    if(!(instance > schema.minimum)){
526
      result.addError({
527
        name: 'minimum',
528
        argument: schema.minimum,
529
        message: "must be greater than " + schema.minimum,
530
      });
531
    }
532
  } else {
533
    if(!(instance >= schema.minimum)){
534
      result.addError({
535
        name: 'minimum',
536
        argument: schema.minimum,
537
        message: "must be greater than or equal to " + schema.minimum,
538
      });
539
    }
540
  }
541
  return result;
542
};
543

  
544
/**
545
 * Validates maximum and exclusiveMaximum when the type of the instance value is a number.
546
 * @param instance
547
 * @param schema
548
 * @return {String|null}
549
 */
550
validators.maximum = function validateMaximum (instance, schema, options, ctx) {
551
  if (!this.types.number(instance)) return;
552
  var result = new ValidatorResult(instance, schema, options, ctx);
553
  if (schema.exclusiveMaximum && schema.exclusiveMaximum === true) {
554
    if(!(instance < schema.maximum)){
555
      result.addError({
556
        name: 'maximum',
557
        argument: schema.maximum,
558
        message: "must be less than " + schema.maximum,
559
      });
560
    }
561
  } else {
562
    if(!(instance <= schema.maximum)){
563
      result.addError({
564
        name: 'maximum',
565
        argument: schema.maximum,
566
        message: "must be less than or equal to " + schema.maximum,
567
      });
568
    }
569
  }
570
  return result;
571
};
572

  
573
/**
574
 * Validates the number form of exclusiveMinimum when the type of the instance value is a number.
575
 * @param instance
576
 * @param schema
577
 * @return {String|null}
578
 */
579
validators.exclusiveMinimum = function validateExclusiveMinimum (instance, schema, options, ctx) {
580
  // Support the boolean form of exclusiveMinimum, which is handled by the "minimum" keyword.
581
  if(typeof schema.exclusiveMinimum === 'boolean') return;
582
  if (!this.types.number(instance)) return;
583
  var result = new ValidatorResult(instance, schema, options, ctx);
584
  var valid = instance > schema.exclusiveMinimum;
585
  if (!valid) {
586
    result.addError({
587
      name: 'exclusiveMinimum',
588
      argument: schema.exclusiveMinimum,
589
      message: "must be strictly greater than " + schema.exclusiveMinimum,
590
    });
591
  }
592
  return result;
593
};
594

  
595
/**
596
 * Validates the number form of exclusiveMaximum when the type of the instance value is a number.
597
 * @param instance
598
 * @param schema
599
 * @return {String|null}
600
 */
601
validators.exclusiveMaximum = function validateExclusiveMaximum (instance, schema, options, ctx) {
602
  // Support the boolean form of exclusiveMaximum, which is handled by the "maximum" keyword.
603
  if(typeof schema.exclusiveMaximum === 'boolean') return;
604
  if (!this.types.number(instance)) return;
605
  var result = new ValidatorResult(instance, schema, options, ctx);
606
  var valid = instance < schema.exclusiveMaximum;
607
  if (!valid) {
608
    result.addError({
609
      name: 'exclusiveMaximum',
610
      argument: schema.exclusiveMaximum,
611
      message: "must be strictly less than " + schema.exclusiveMaximum,
612
    });
613
  }
614
  return result;
615
};
616

  
617
/**
618
 * Perform validation for multipleOf and divisibleBy, which are essentially the same.
619
 * @param instance
620
 * @param schema
621
 * @param validationType
622
 * @param errorMessage
623
 * @returns {String|null}
624
 */
625
var validateMultipleOfOrDivisbleBy = function validateMultipleOfOrDivisbleBy (instance, schema, options, ctx, validationType, errorMessage) {
626
  if (!this.types.number(instance)) return;
627

  
628
  var validationArgument = schema[validationType];
629
  if (validationArgument == 0) {
630
    throw new SchemaError(validationType + " cannot be zero");
631
  }
632

  
633
  var result = new ValidatorResult(instance, schema, options, ctx);
634

  
635
  var instanceDecimals = helpers.getDecimalPlaces(instance);
636
  var divisorDecimals = helpers.getDecimalPlaces(validationArgument);
637

  
638
  var maxDecimals = Math.max(instanceDecimals , divisorDecimals);
639
  var multiplier = Math.pow(10, maxDecimals);
640

  
641
  if (Math.round(instance * multiplier) % Math.round(validationArgument * multiplier) !== 0) {
642
    result.addError({
643
      name: validationType,
644
      argument:  validationArgument,
645
      message: errorMessage + JSON.stringify(validationArgument),
646
    });
647
  }
648

  
649
  return result;
650
};
651

  
652
/**
653
 * Validates divisibleBy when the type of the instance value is a number.
654
 * @param instance
655
 * @param schema
656
 * @return {String|null}
657
 */
658
validators.multipleOf = function validateMultipleOf (instance, schema, options, ctx) {
659
  return validateMultipleOfOrDivisbleBy.call(this, instance, schema, options, ctx, "multipleOf", "is not a multiple of (divisible by) ");
660
};
661

  
662
/**
663
 * Validates multipleOf when the type of the instance value is a number.
664
 * @param instance
665
 * @param schema
666
 * @return {String|null}
667
 */
668
validators.divisibleBy = function validateDivisibleBy (instance, schema, options, ctx) {
669
  return validateMultipleOfOrDivisbleBy.call(this, instance, schema, options, ctx, "divisibleBy", "is not divisible by (multiple of) ");
670
};
671

  
672
/**
673
 * Validates whether the instance value is present.
674
 * @param instance
675
 * @param schema
676
 * @return {String|null}
677
 */
678
validators.required = function validateRequired (instance, schema, options, ctx) {
679
  var result = new ValidatorResult(instance, schema, options, ctx);
680
  if (instance === undefined && schema.required === true) {
681
    // A boolean form is implemented for reverse-compatibility with schemas written against older drafts
682
    result.addError({
683
      name: 'required',
684
      message: "is required",
685
    });
686
  } else if (this.types.object(instance) && Array.isArray(schema.required)) {
687
    schema.required.forEach(function(n){
688
      if(getEnumerableProperty(instance, n)===undefined){
689
        result.addError({
690
          name: 'required',
691
          argument: n,
692
          message: "requires property " + JSON.stringify(n),
693
        });
694
      }
695
    });
696
  }
697
  return result;
698
};
699

  
700
/**
701
 * Validates whether the instance value matches the regular expression, when the instance value is a string.
702
 * @param instance
703
 * @param schema
704
 * @return {String|null}
705
 */
706
validators.pattern = function validatePattern (instance, schema, options, ctx) {
707
  if (!this.types.string(instance)) return;
708
  var result = new ValidatorResult(instance, schema, options, ctx);
709
  var pattern = schema.pattern;
710
  try {
711
    var regexp = new RegExp(pattern, 'u');
712
  } catch(_e) {
713
    // In the event the stricter handling causes an error, fall back on the forgiving handling
714
    // DEPRECATED
715
    regexp = new RegExp(pattern);
716
  }
717
  if (!instance.match(regexp)) {
718
    result.addError({
719
      name: 'pattern',
720
      argument: schema.pattern,
721
      message: "does not match pattern " + JSON.stringify(schema.pattern.toString()),
722
    });
723
  }
724
  return result;
725
};
726

  
727
/**
728
 * Validates whether the instance value is of a certain defined format or a custom
729
 * format.
730
 * The following formats are supported for string types:
731
 *   - date-time
732
 *   - date
733
 *   - time
734
 *   - ip-address
735
 *   - ipv6
736
 *   - uri
737
 *   - color
738
 *   - host-name
739
 *   - alpha
740
 *   - alpha-numeric
741
 *   - utc-millisec
742
 * @param instance
743
 * @param schema
744
 * @param [options]
745
 * @param [ctx]
746
 * @return {String|null}
747
 */
748
validators.format = function validateFormat (instance, schema, options, ctx) {
749
  if (instance===undefined) return;
750
  var result = new ValidatorResult(instance, schema, options, ctx);
751
  if (!result.disableFormat && !helpers.isFormat(instance, schema.format, this)) {
752
    result.addError({
753
      name: 'format',
754
      argument: schema.format,
755
      message: "does not conform to the " + JSON.stringify(schema.format) + " format",
756
    });
757
  }
758
  return result;
759
};
760

  
761
/**
762
 * Validates whether the instance value is at least of a certain length, when the instance value is a string.
763
 * @param instance
764
 * @param schema
765
 * @return {String|null}
766
 */
767
validators.minLength = function validateMinLength (instance, schema, options, ctx) {
768
  if (!this.types.string(instance)) return;
769
  var result = new ValidatorResult(instance, schema, options, ctx);
770
  var hsp = instance.match(/[\uDC00-\uDFFF]/g);
771
  var length = instance.length - (hsp ? hsp.length : 0);
772
  if (!(length >= schema.minLength)) {
773
    result.addError({
774
      name: 'minLength',
775
      argument: schema.minLength,
776
      message: "does not meet minimum length of " + schema.minLength,
777
    });
778
  }
779
  return result;
780
};
781

  
782
/**
783
 * Validates whether the instance value is at most of a certain length, when the instance value is a string.
784
 * @param instance
785
 * @param schema
786
 * @return {String|null}
787
 */
788
validators.maxLength = function validateMaxLength (instance, schema, options, ctx) {
789
  if (!this.types.string(instance)) return;
790
  var result = new ValidatorResult(instance, schema, options, ctx);
791
  // TODO if this was already computed in "minLength", use that value instead of re-computing
792
  var hsp = instance.match(/[\uDC00-\uDFFF]/g);
793
  var length = instance.length - (hsp ? hsp.length : 0);
794
  if (!(length <= schema.maxLength)) {
795
    result.addError({
796
      name: 'maxLength',
797
      argument: schema.maxLength,
798
      message: "does not meet maximum length of " + schema.maxLength,
799
    });
800
  }
801
  return result;
802
};
803

  
804
/**
805
 * Validates whether instance contains at least a minimum number of items, when the instance is an Array.
806
 * @param instance
807
 * @param schema
808
 * @return {String|null}
809
 */
810
validators.minItems = function validateMinItems (instance, schema, options, ctx) {
811
  if (!this.types.array(instance)) return;
812
  var result = new ValidatorResult(instance, schema, options, ctx);
813
  if (!(instance.length >= schema.minItems)) {
814
    result.addError({
815
      name: 'minItems',
816
      argument: schema.minItems,
817
      message: "does not meet minimum length of " + schema.minItems,
818
    });
819
  }
820
  return result;
821
};
822

  
823
/**
824
 * Validates whether instance contains no more than a maximum number of items, when the instance is an Array.
825
 * @param instance
826
 * @param schema
827
 * @return {String|null}
828
 */
829
validators.maxItems = function validateMaxItems (instance, schema, options, ctx) {
830
  if (!this.types.array(instance)) return;
831
  var result = new ValidatorResult(instance, schema, options, ctx);
832
  if (!(instance.length <= schema.maxItems)) {
833
    result.addError({
834
      name: 'maxItems',
835
      argument: schema.maxItems,
836
      message: "does not meet maximum length of " + schema.maxItems,
837
    });
838
  }
839
  return result;
840
};
841

  
842
/**
843
 * Deep compares arrays for duplicates
844
 * @param v
845
 * @param i
846
 * @param a
847
 * @private
848
 * @return {boolean}
849
 */
850
function testArrays (v, i, a) {
851
  var j, len = a.length;
852
  for (j = i + 1, len; j < len; j++) {
853
    if (helpers.deepCompareStrict(v, a[j])) {
854
      return false;
855
    }
856
  }
857
  return true;
858
}
859

  
860
/**
861
 * Validates whether there are no duplicates, when the instance is an Array.
862
 * @param instance
863
 * @return {String|null}
864
 */
865
validators.uniqueItems = function validateUniqueItems (instance, schema, options, ctx) {
866
  if (schema.uniqueItems!==true) return;
867
  if (!this.types.array(instance)) return;
868
  var result = new ValidatorResult(instance, schema, options, ctx);
869
  if (!instance.every(testArrays)) {
870
    result.addError({
871
      name: 'uniqueItems',
872
      message: "contains duplicate item",
873
    });
874
  }
875
  return result;
876
};
877

  
878
/**
879
 * Validate for the presence of dependency properties, if the instance is an object.
880
 * @param instance
881
 * @param schema
882
 * @param options
883
 * @param ctx
884
 * @return {null|ValidatorResult}
885
 */
886
validators.dependencies = function validateDependencies (instance, schema, options, ctx) {
887
  if (!this.types.object(instance)) return;
888
  var result = new ValidatorResult(instance, schema, options, ctx);
889
  for (var property in schema.dependencies) {
890
    if (instance[property] === undefined) {
891
      continue;
892
    }
893
    var dep = schema.dependencies[property];
894
    var childContext = ctx.makeChild(dep, property);
895
    if (typeof dep == 'string') {
896
      dep = [dep];
897
    }
898
    if (Array.isArray(dep)) {
899
      dep.forEach(function (prop) {
900
        if (instance[prop] === undefined) {
901
          result.addError({
902
            // FIXME there's two different "dependencies" errors here with slightly different outputs
903
            // Can we make these the same? Or should we create different error types?
904
            name: 'dependencies',
905
            argument: childContext.propertyPath,
906
            message: "property " + prop + " not found, required by " + childContext.propertyPath,
907
          });
908
        }
909
      });
910
    } else {
911
      var res = this.validateSchema(instance, dep, options, childContext);
912
      if(result.instance !== res.instance) result.instance = res.instance;
913
      if (res && res.errors.length) {
914
        result.addError({
915
          name: 'dependencies',
916
          argument: childContext.propertyPath,
917
          message: "does not meet dependency required by " + childContext.propertyPath,
918
        });
919
        result.importErrors(res);
920
      }
921
    }
922
  }
923
  return result;
924
};
925

  
926
/**
927
 * Validates whether the instance value is one of the enumerated values.
928
 *
929
 * @param instance
930
 * @param schema
931
 * @return {ValidatorResult|null}
932
 */
933
validators['enum'] = function validateEnum (instance, schema, options, ctx) {
934
  if (instance === undefined) {
935
    return null;
936
  }
937
  if (!Array.isArray(schema['enum'])) {
938
    throw new SchemaError("enum expects an array", schema);
939
  }
940
  var result = new ValidatorResult(instance, schema, options, ctx);
941
  if (!schema['enum'].some(helpers.deepCompareStrict.bind(null, instance))) {
942
    result.addError({
943
      name: 'enum',
944
      argument: schema['enum'],
945
      message: "is not one of enum values: " + schema['enum'].map(String).join(','),
946
    });
947
  }
948
  return result;
949
};
950

  
951
/**
952
 * Validates whether the instance exactly matches a given value
953
 *
954
 * @param instance
955
 * @param schema
956
 * @return {ValidatorResult|null}
957
 */
958
validators['const'] = function validateEnum (instance, schema, options, ctx) {
959
  if (instance === undefined) {
960
    return null;
961
  }
962
  var result = new ValidatorResult(instance, schema, options, ctx);
963
  if (!helpers.deepCompareStrict(schema['const'], instance)) {
964
    result.addError({
965
      name: 'const',
966
      argument: schema['const'],
967
      message: "does not exactly match expected constant: " + schema['const'],
968
    });
969
  }
970
  return result;
971
};
972

  
973
/**
974
 * Validates whether the instance if of a prohibited type.
975
 * @param instance
976
 * @param schema
977
 * @param options
978
 * @param ctx
979
 * @return {null|ValidatorResult}
980
 */
981
validators.not = validators.disallow = function validateNot (instance, schema, options, ctx) {
982
  var self = this;
983
  if(instance===undefined) return null;
984
  var result = new ValidatorResult(instance, schema, options, ctx);
985
  var notTypes = schema.not || schema.disallow;
986
  if(!notTypes) return null;
987
  if(!Array.isArray(notTypes)) notTypes=[notTypes];
988
  notTypes.forEach(function (type) {
989
    if (self.testType(instance, schema, options, ctx, type)) {
990
      var id = type && (type.$id || type.id);
991
      var schemaId = id || type;
992
      result.addError({
993
        name: 'not',
994
        argument: schemaId,
995
        message: "is of prohibited type " + schemaId,
996
      });
997
    }
998
  });
999
  return result;
1000
};
1001

  
1002
#EXPORT validators
common/jsonschema/helpers.js
1
/* SPDX-License-Identifier: MIT
2
 *
3
 * jsonschema is licensed under MIT license.
4
 *
5
 * Copyright (C) 2012-2015 Tom de Grunt <tom@degrunt.nl>
6
 *
7
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
8
 * this software and associated documentation files (the "Software"), to deal in
9
 * the Software without restriction, including without limitation the rights to
10
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
11
 * of the Software, and to permit persons to whom the Software is furnished to do
12
 * so, subject to the following conditions:
13
 *
14
 * The above copyright notice and this permission notice shall be included in all
15
 * copies or substantial portions of the Software.
16
 *
17
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
 * SOFTWARE.
24
 */
25

  
26
#IMPORT common/jsonschema/urllib_mock.js AS uri
27

  
28
function ValidationError (message, instance, schema, path, name, argument) {
29
  if(Array.isArray(path)){
30
    this.path = path;
31
    this.property = path.reduce(function(sum, item){
32
      return sum + makeSuffix(item);
33
    }, 'instance');
34
  }else if(path !== undefined){
35
    this.property = path;
36
  }
37
  if (message) {
38
    this.message = message;
39
  }
40
  if (schema) {
41
    var id = schema.$id || schema.id;
42
    this.schema = id || schema;
43
  }
44
  if (instance !== undefined) {
45
    this.instance = instance;
46
  }
47
  this.name = name;
48
  this.argument = argument;
49
  this.stack = this.toString();
50
};
51
#EXPORT ValidationError
52

  
53
ValidationError.prototype.toString = function toString() {
54
  return this.property + ' ' + this.message;
55
};
56

  
57
function ValidatorResult(instance, schema, options, ctx) {
58
  this.instance = instance;
59
  this.schema = schema;
60
  this.options = options;
61
  this.path = ctx.path;
62
  this.propertyPath = ctx.propertyPath;
63
  this.errors = [];
64
  this.throwError = options && options.throwError;
65
  this.throwFirst = options && options.throwFirst;
66
  this.throwAll = options && options.throwAll;
67
  this.disableFormat = options && options.disableFormat === true;
68
};
69

  
70
ValidatorResult.prototype.addError = function addError(detail) {
71
  var err;
72
  if (typeof detail == 'string') {
73
    err = new ValidationError(detail, this.instance, this.schema, this.path);
74
  } else {
75
    if (!detail) throw new Error('Missing error detail');
76
    if (!detail.message) throw new Error('Missing error message');
77
    if (!detail.name) throw new Error('Missing validator type');
78
    err = new ValidationError(detail.message, this.instance, this.schema, this.path, detail.name, detail.argument);
79
  }
80

  
81
  this.errors.push(err);
82
  if (this.throwFirst) {
83
    throw new ValidatorResultError(this);
84
  }else if(this.throwError){
85
    throw err;
86
  }
87
  return err;
88
};
89

  
90
ValidatorResult.prototype.importErrors = function importErrors(res) {
91
  if (typeof res == 'string' || (res && res.validatorType)) {
92
    this.addError(res);
93
  } else if (res && res.errors) {
94
    Array.prototype.push.apply(this.errors, res.errors);
95
  }
96
};
97

  
98
function stringizer (v,i){
99
  return i+': '+v.toString()+'\n';
100
}
101
ValidatorResult.prototype.toString = function toString(res) {
102
  return this.errors.map(stringizer).join('');
103
};
104

  
105
Object.defineProperty(ValidatorResult.prototype, "valid", { get: function() {
106
  return !this.errors.length;
107
} });
108

  
109
#EXPORT ValidatorResult
110

  
111
function ValidatorResultError(result) {
112
  if(Error.captureStackTrace){
113
    Error.captureStackTrace(this, ValidatorResultError);
114
  }
115
  this.instance = result.instance;
116
  this.schema = result.schema;
117
  this.options = result.options;
118
  this.errors = result.errors;
119
}
120
ValidatorResultError.prototype = new Error();
121
ValidatorResultError.prototype.constructor = ValidatorResultError;
122
ValidatorResultError.prototype.name = "Validation Error";
123
#EXPORT ValidatorResultError
124

  
125
/**
126
 * Describes a problem with a Schema which prevents validation of an instance
127
 * @name SchemaError
128
 * @constructor
129
 */
130
function SchemaError (msg, schema) {
131
  this.message = msg;
132
  this.schema = schema;
133
  Error.call(this, msg);
134
  if(Error.captureStackTrace){
135
    Error.captureStackTrace(this, SchemaError);
136
  }
137
};
138
SchemaError.prototype = Object.create(Error.prototype,
139
  {
140
    constructor: {value: SchemaError, enumerable: false},
141
    name: {value: 'SchemaError', enumerable: false},
142
  });
143
#EXPORT SchemaError
144

  
145
function SchemaContext (schema, options, path, base, schemas) {
146
  this.schema = schema;
147
  this.options = options;
148
  if(Array.isArray(path)){
149
    this.path = path;
150
    this.propertyPath = path.reduce(function(sum, item){
151
      return sum + makeSuffix(item);
152
    }, 'instance');
153
  }else{
154
    this.propertyPath = path;
155
  }
156
  this.base = base;
157
  this.schemas = schemas;
158
};
159

  
160
SchemaContext.prototype.resolve = function resolve (target) {
161
  return uri.resolve(this.base, target);
162
};
163

  
164
SchemaContext.prototype.makeChild = function makeChild(schema, propertyName){
165
  var path = (propertyName===undefined) ? this.path : this.path.concat([propertyName]);
166
  var id = schema.$id || schema.id;
167
  var base = uri.resolve(this.base, id||'');
168
  var ctx = new SchemaContext(schema, this.options, path, base, Object.create(this.schemas));
169
  if(id && !ctx.schemas[base]){
170
    ctx.schemas[base] = schema;
171
  }
172
  return ctx;
173
};
174

  
175
#EXPORT SchemaContext
176

  
177
const FORMAT_REGEXPS = {
178
  // 7.3.1. Dates, Times, and Duration
179
  'date-time': /^\d{4}-(?:0[0-9]{1}|1[0-2]{1})-(3[01]|0[1-9]|[12][0-9])[tT ](2[0-4]|[01][0-9]):([0-5][0-9]):(60|[0-5][0-9])(\.\d+)?([zZ]|[+-]([0-5][0-9]):(60|[0-5][0-9]))$/,
180
  'date': /^\d{4}-(?:0[0-9]{1}|1[0-2]{1})-(3[01]|0[1-9]|[12][0-9])$/,
181
  'time': /^(2[0-4]|[01][0-9]):([0-5][0-9]):(60|[0-5][0-9])$/,
182
  'duration': /P(T\d+(H(\d+M(\d+S)?)?|M(\d+S)?|S)|\d+(D|M(\d+D)?|Y(\d+M(\d+D)?)?)(T\d+(H(\d+M(\d+S)?)?|M(\d+S)?|S))?|\d+W)/i,
183

  
184
  // 7.3.2. Email Addresses
185
  // TODO: fix the email production
186
  'email': /^(?:[\w\!\#\$\%\&\'\*\+\-\/\=\?\^\`\{\|\}\~]+\.)*[\w\!\#\$\%\&\'\*\+\-\/\=\?\^\`\{\|\}\~]+@(?:(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-](?!\.)){0,61}[a-zA-Z0-9]?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9\-](?!$)){0,61}[a-zA-Z0-9]?)|(?:\[(?:(?:[01]?\d{1,2}|2[0-4]\d|25[0-5])\.){3}(?:[01]?\d{1,2}|2[0-4]\d|25[0-5])\]))$/,
187
  'idn-email': /^("(?:[!#-\[\]-\u{10FFFF}]|\\[\t -\u{10FFFF}])*"|[!#-'*+\-/-9=?A-Z\^-\u{10FFFF}](?:\.?[!#-'*+\-/-9=?A-Z\^-\u{10FFFF}])*)@([!#-'*+\-/-9=?A-Z\^-\u{10FFFF}](?:\.?[!#-'*+\-/-9=?A-Z\^-\u{10FFFF}])*|\[[!-Z\^-\u{10FFFF}]*\])$/u,
188

  
189
  // 7.3.3. Hostnames
190

  
191
  // 7.3.4. IP Addresses
192
  'ip-address': /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/,
193
  // FIXME whitespace is invalid
194
  'ipv6': /^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/,
195

  
196
  // 7.3.5. Resource Identifiers
197
  // TODO: A more accurate regular expression for "uri" goes:
198
  // [A-Za-z][+\-.0-9A-Za-z]*:((/(/((%[0-9A-Fa-f]{2}|[!$&-.0-9;=A-Z_a-z~])+|(\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~]+)?|[.0-:A-Fa-f]+)\])?)(:\d*)?)?)?#(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~])*|(/(/((%[0-9A-Fa-f]{2}|[!$&-.0-9;=A-Z_a-z~])+|(\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~]+)?|[.0-:A-Fa-f]+)\])?)(:\d*)?[/?]|[!$&-.0-;=?-Z_a-z~])|/?%[0-9A-Fa-f]{2}|[!$&-.0-;=?-Z_a-z~])(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~])*(#(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~])*)?|/(/((%[0-9A-Fa-f]{2}|[!$&-.0-9;=A-Z_a-z~])+(:\d*)?|(\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~]+)?|[.0-:A-Fa-f]+)\])?:\d*|\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~]+)?|[.0-:A-Fa-f]+)\])?)?)?
199
  'uri': /^[a-zA-Z][a-zA-Z0-9+.-]*:[^\s]*$/,
200
  'uri-reference': /^(((([A-Za-z][+\-.0-9A-Za-z]*(:%[0-9A-Fa-f]{2}|:[!$&-.0-;=?-Z_a-z~]|[/?])|\?)(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~])*|([A-Za-z][+\-.0-9A-Za-z]*:?)?)|([A-Za-z][+\-.0-9A-Za-z]*:)?\/((%[0-9A-Fa-f]{2}|\/((%[0-9A-Fa-f]{2}|[!$&-.0-9;=A-Z_a-z~])+|(\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~]+)?|[.0-:A-Fa-f]+)\])?)(:\d*)?[/?]|[!$&-.0-;=?-Z_a-z~])(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~])*|(\/((%[0-9A-Fa-f]{2}|[!$&-.0-9;=A-Z_a-z~])+|(\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~]+)?|[.0-:A-Fa-f]+)\])?)(:\d*)?)?))#(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~])*|(([A-Za-z][+\-.0-9A-Za-z]*)?%[0-9A-Fa-f]{2}|[!$&-.0-9;=@_~]|[A-Za-z][+\-.0-9A-Za-z]*[!$&-*,;=@_~])(%[0-9A-Fa-f]{2}|[!$&-.0-9;=@-Z_a-z~])*((([/?](%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~])*)?#|[/?])(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~])*)?|([A-Za-z][+\-.0-9A-Za-z]*(:%[0-9A-Fa-f]{2}|:[!$&-.0-;=?-Z_a-z~]|[/?])|\?)(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~])*|([A-Za-z][+\-.0-9A-Za-z]*:)?\/((%[0-9A-Fa-f]{2}|\/((%[0-9A-Fa-f]{2}|[!$&-.0-9;=A-Z_a-z~])+|(\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~]+)?|[.0-:A-Fa-f]+)\])?)(:\d*)?[/?]|[!$&-.0-;=?-Z_a-z~])(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~])*|\/((%[0-9A-Fa-f]{2}|[!$&-.0-9;=A-Z_a-z~])+(:\d*)?|(\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~]+)?|[.0-:A-Fa-f]+)\])?:\d*|\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~]+)?|[.0-:A-Fa-f]+)\])?)?|[A-Za-z][+\-.0-9A-Za-z]*:?)?$/,
201
  'iri': /^[a-zA-Z][a-zA-Z0-9+.-]*:[^\s]*$/,
202
  'iri-reference': /^(((([A-Za-z][+\-.0-9A-Za-z]*(:%[0-9A-Fa-f]{2}|:[!$&-.0-;=?-Z_a-z~-\u{10FFFF}]|[/?])|\?)(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~-\u{10FFFF}])*|([A-Za-z][+\-.0-9A-Za-z]*:?)?)|([A-Za-z][+\-.0-9A-Za-z]*:)?\/((%[0-9A-Fa-f]{2}|\/((%[0-9A-Fa-f]{2}|[!$&-.0-9;=A-Z_a-z~-\u{10FFFF}])+|(\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~-\u{10FFFF}]+)?|[.0-:A-Fa-f]+)\])?)(:\d*)?[/?]|[!$&-.0-;=?-Z_a-z~-\u{10FFFF}])(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~-\u{10FFFF}])*|(\/((%[0-9A-Fa-f]{2}|[!$&-.0-9;=A-Z_a-z~-\u{10FFFF}])+|(\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~-\u{10FFFF}]+)?|[.0-:A-Fa-f]+)\])?)(:\d*)?)?))#(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~-\u{10FFFF}])*|(([A-Za-z][+\-.0-9A-Za-z]*)?%[0-9A-Fa-f]{2}|[!$&-.0-9;=@_~-\u{10FFFF}]|[A-Za-z][+\-.0-9A-Za-z]*[!$&-*,;=@_~-\u{10FFFF}])(%[0-9A-Fa-f]{2}|[!$&-.0-9;=@-Z_a-z~-\u{10FFFF}])*((([/?](%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~-\u{10FFFF}])*)?#|[/?])(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~-\u{10FFFF}])*)?|([A-Za-z][+\-.0-9A-Za-z]*(:%[0-9A-Fa-f]{2}|:[!$&-.0-;=?-Z_a-z~-\u{10FFFF}]|[/?])|\?)(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~-\u{10FFFF}])*|([A-Za-z][+\-.0-9A-Za-z]*:)?\/((%[0-9A-Fa-f]{2}|\/((%[0-9A-Fa-f]{2}|[!$&-.0-9;=A-Z_a-z~-\u{10FFFF}])+|(\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~-\u{10FFFF}]+)?|[.0-:A-Fa-f]+)\])?)(:\d*)?[/?]|[!$&-.0-;=?-Z_a-z~-\u{10FFFF}])(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~-\u{10FFFF}])*|\/((%[0-9A-Fa-f]{2}|[!$&-.0-9;=A-Z_a-z~-\u{10FFFF}])+(:\d*)?|(\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~-\u{10FFFF}]+)?|[.0-:A-Fa-f]+)\])?:\d*|\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~-\u{10FFFF}]+)?|[.0-:A-Fa-f]+)\])?)?|[A-Za-z][+\-.0-9A-Za-z]*:?)?$/u,
203
  'uuid': /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i,
204

  
205
  // 7.3.6. uri-template
206
  'uri-template': /(%[0-9a-f]{2}|[!#$&(-;=?@\[\]_a-z~]|\{[!#&+,./;=?@|]?(%[0-9a-f]{2}|[0-9_a-z])(\.?(%[0-9a-f]{2}|[0-9_a-z]))*(:[1-9]\d{0,3}|\*)?(,(%[0-9a-f]{2}|[0-9_a-z])(\.?(%[0-9a-f]{2}|[0-9_a-z]))*(:[1-9]\d{0,3}|\*)?)*\})*/iu,
207

  
208
  // 7.3.7. JSON Pointers
209
  'json-pointer': /^(\/([\x00-\x2e0-@\[-}\x7f]|~[01])*)*$/iu,
210
  'relative-json-pointer': /^\d+(#|(\/([\x00-\x2e0-@\[-}\x7f]|~[01])*)*)$/iu,
211

  
212
  // hostname regex from: http://stackoverflow.com/a/1420225/5628
213
  'hostname': /^(?=.{1,255}$)[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?)*\.?$/,
214
  'host-name': /^(?=.{1,255}$)[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?)*\.?$/,
215

  
216
  'utc-millisec': function (input) {
217
    return (typeof input === 'string') && parseFloat(input) === parseInt(input, 10) && !isNaN(input);
218
  },
219

  
220
  // 7.3.8. regex
221
  'regex': function (input) {
222
    var result = true;
223
    try {
224
      new RegExp(input);
225
    } catch (e) {
226
      result = false;
227
    }
228
    return result;
229
  },
230

  
231
  // Other definitions
232
  // "style" was removed from JSON Schema in draft-4 and is deprecated
233
  'style': /[\r\n\t ]*[^\r\n\t ][^:]*:[\r\n\t ]*[^\r\n\t ;]*[\r\n\t ]*;?/,
234
  // "color" was removed from JSON Schema in draft-4 and is deprecated
235
  'color': /^(#?([0-9A-Fa-f]{3}){1,2}\b|aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow|(rgb\(\s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*,\s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*,\s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*\))|(rgb\(\s*(\d?\d%|100%)+\s*,\s*(\d?\d%|100%)+\s*,\s*(\d?\d%|100%)+\s*\)))$/,
236
  'phone': /^\+(?:[0-9] ?){6,14}[0-9]$/,
237
  'alpha': /^[a-zA-Z]+$/,
238
  'alphanumeric': /^[a-zA-Z0-9]+$/,
239
};
240

  
241
FORMAT_REGEXPS.regexp = FORMAT_REGEXPS.regex;
242
FORMAT_REGEXPS.pattern = FORMAT_REGEXPS.regex;
243
FORMAT_REGEXPS.ipv4 = FORMAT_REGEXPS['ip-address'];
244

  
245
#EXPORT FORMAT_REGEXPS
246

  
247
function isFormat (input, format, validator) {
248
  if (typeof input === 'string' && FORMAT_REGEXPS[format] !== undefined) {
249
    if (FORMAT_REGEXPS[format] instanceof RegExp) {
250
      return FORMAT_REGEXPS[format].test(input);
251
    }
252
    if (typeof FORMAT_REGEXPS[format] === 'function') {
253
      return FORMAT_REGEXPS[format](input);
254
    }
255
  } else if (validator && validator.customFormats &&
256
      typeof validator.customFormats[format] === 'function') {
257
    return validator.customFormats[format](input);
258
  }
259
  return true;
260
};
261

  
262
#EXPORT isFormat
263

  
264
function makeSuffix (key) {
265
  key = key.toString();
266
  // This function could be capable of outputting valid a ECMAScript string, but the
267
  // resulting code for testing which form to use would be tens of thousands of characters long
268
  // That means this will use the name form for some illegal forms
269
  if (!key.match(/[.\s\[\]]/) && !key.match(/^[\d]/)) {
270
    return '.' + key;
271
  }
272
  if (key.match(/^\d+$/)) {
273
    return '[' + key + ']';
274
  }
275
  return '[' + JSON.stringify(key) + ']';
276
};
277
#EXPORT makeSuffix
278

  
279
function deepCompareStrict (a, b) {
280
  if (typeof a !== typeof b) {
281
    return false;
282
  }
283
  if (Array.isArray(a)) {
284
    if (!Array.isArray(b)) {
285
      return false;
286
    }
287
    if (a.length !== b.length) {
288
      return false;
289
    }
290
    return a.every(function (v, i) {
291
      return deepCompareStrict(a[i], b[i]);
292
    });
293
  }
294
  if (typeof a === 'object') {
295
    if (!a || !b) {
296
      return a === b;
297
    }
298
    var aKeys = Object.keys(a);
299
    var bKeys = Object.keys(b);
300
    if (aKeys.length !== bKeys.length) {
301
      return false;
302
    }
303
    return aKeys.every(function (v) {
304
      return deepCompareStrict(a[v], b[v]);
305
    });
306
  }
307
  return a === b;
308
};
309
#EXPORT deepCompareStrict
310

  
311
function deepMerger (target, dst, e, i) {
312
  if (typeof e === 'object') {
313
    dst[i] = deepMerge(target[i], e);
314
  } else {
315
    if (target.indexOf(e) === -1) {
316
      dst.push(e);
317
    }
318
  }
319
}
320

  
321
function copyist (src, dst, key) {
322
  dst[key] = src[key];
323
}
324

  
325
function copyistWithDeepMerge (target, src, dst, key) {
326
  if (typeof src[key] !== 'object' || !src[key]) {
327
    dst[key] = src[key];
328
  }
329
  else {
330
    if (!target[key]) {
331
      dst[key] = src[key];
332
    } else {
333
      dst[key] = deepMerge(target[key], src[key]);
334
    }
335
  }
336
}
337

  
338
function deepMerge (target, src) {
339
  var array = Array.isArray(src);
340
  var dst = array && [] || {};
341

  
342
  if (array) {
343
    target = target || [];
344
    dst = dst.concat(target);
345
    src.forEach(deepMerger.bind(null, target, dst));
346
  } else {
347
    if (target && typeof target === 'object') {
348
      Object.keys(target).forEach(copyist.bind(null, target, dst));
349
    }
350
    Object.keys(src).forEach(copyistWithDeepMerge.bind(null, target, src, dst));
351
  }
352

  
353
  return dst;
354
}
355
#EXPORT deepMerge
356

  
357
/**
358
 * Validates instance against the provided schema
359
 * Implements URI+JSON Pointer encoding, e.g. "%7e"="~0"=>"~", "~1"="%2f"=>"/"
360
 * @param o
361
 * @param s The path to walk o along
362
 * @return any
363
 */
364
function objectGetPath(o, s) {
365
  var parts = s.split('/').slice(1);
366
  var k;
367
  while (typeof (k=parts.shift()) == 'string') {
368
    var n = decodeURIComponent(k.replace(/~0/,'~').replace(/~1/g,'/'));
369
    if (!(n in o)) return;
370
    o = o[n];
371
  }
372
  return o;
... This diff was truncated because it exceeds the maximum size that can be displayed.

Also available in: Unified diff