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 urilib
|
27
|
#IMPORT common/jsonschema/attribute.js
|
28
|
#IMPORT common/jsonschema/helpers.js
|
29
|
|
30
|
#FROM common/jsonschema/scan.js IMPORT scan AS scanSchema
|
31
|
|
32
|
var ValidatorResult = helpers.ValidatorResult;
|
33
|
var ValidatorResultError = helpers.ValidatorResultError;
|
34
|
var SchemaError = helpers.SchemaError;
|
35
|
var SchemaContext = helpers.SchemaContext;
|
36
|
//var anonymousBase = 'vnd.jsonschema:///';
|
37
|
var anonymousBase = '/';
|
38
|
|
39
|
/**
|
40
|
* Creates a new Validator object
|
41
|
* @name Validator
|
42
|
* @constructor
|
43
|
*/
|
44
|
var Validator = function Validator () {
|
45
|
// Allow a validator instance to override global custom formats or to have their
|
46
|
// own custom formats.
|
47
|
this.customFormats = Object.create(Validator.prototype.customFormats);
|
48
|
this.schemas = {};
|
49
|
this.unresolvedRefs = [];
|
50
|
|
51
|
// Use Object.create to make this extensible without Validator instances stepping on each other's toes.
|
52
|
this.types = Object.create(types);
|
53
|
this.attributes = Object.create(attribute.validators);
|
54
|
};
|
55
|
|
56
|
// Allow formats to be registered globally.
|
57
|
Validator.prototype.customFormats = {};
|
58
|
|
59
|
// Hint at the presence of a property
|
60
|
Validator.prototype.schemas = null;
|
61
|
Validator.prototype.types = null;
|
62
|
Validator.prototype.attributes = null;
|
63
|
Validator.prototype.unresolvedRefs = null;
|
64
|
|
65
|
/**
|
66
|
* Adds a schema with a certain urn to the Validator instance.
|
67
|
* @param schema
|
68
|
* @param urn
|
69
|
* @return {Object}
|
70
|
*/
|
71
|
Validator.prototype.addSchema = function addSchema (schema, base) {
|
72
|
var self = this;
|
73
|
if (!schema) {
|
74
|
return null;
|
75
|
}
|
76
|
var scan = scanSchema(base||anonymousBase, schema);
|
77
|
var ourUri = base || schema.$id || schema.id;
|
78
|
for(var uri in scan.id){
|
79
|
this.schemas[uri] = scan.id[uri];
|
80
|
}
|
81
|
for(var uri in scan.ref){
|
82
|
// If this schema is already defined, it will be filtered out by the next step
|
83
|
this.unresolvedRefs.push(uri);
|
84
|
}
|
85
|
// Remove newly defined schemas from unresolvedRefs
|
86
|
this.unresolvedRefs = this.unresolvedRefs.filter(function(uri){
|
87
|
return typeof self.schemas[uri]==='undefined';
|
88
|
});
|
89
|
return this.schemas[ourUri];
|
90
|
};
|
91
|
|
92
|
Validator.prototype.addSubSchemaArray = function addSubSchemaArray(baseuri, schemas) {
|
93
|
if(!Array.isArray(schemas)) return;
|
94
|
for(var i=0; i<schemas.length; i++){
|
95
|
this.addSubSchema(baseuri, schemas[i]);
|
96
|
}
|
97
|
};
|
98
|
|
99
|
Validator.prototype.addSubSchemaObject = function addSubSchemaArray(baseuri, schemas) {
|
100
|
if(!schemas || typeof schemas!='object') return;
|
101
|
for(var p in schemas){
|
102
|
this.addSubSchema(baseuri, schemas[p]);
|
103
|
}
|
104
|
};
|
105
|
|
106
|
|
107
|
|
108
|
/**
|
109
|
* Sets all the schemas of the Validator instance.
|
110
|
* @param schemas
|
111
|
*/
|
112
|
Validator.prototype.setSchemas = function setSchemas (schemas) {
|
113
|
this.schemas = schemas;
|
114
|
};
|
115
|
|
116
|
/**
|
117
|
* Returns the schema of a certain urn
|
118
|
* @param urn
|
119
|
*/
|
120
|
Validator.prototype.getSchema = function getSchema (urn) {
|
121
|
return this.schemas[urn];
|
122
|
};
|
123
|
|
124
|
/**
|
125
|
* Validates instance against the provided schema
|
126
|
* @param instance
|
127
|
* @param schema
|
128
|
* @param [options]
|
129
|
* @param [ctx]
|
130
|
* @return {Array}
|
131
|
*/
|
132
|
Validator.prototype.validate = function validate (instance, schema, options, ctx) {
|
133
|
if((typeof schema !== 'boolean' && typeof schema !== 'object') || schema === null){
|
134
|
throw new SchemaError('Expected `schema` to be an object or boolean');
|
135
|
}
|
136
|
if (!options) {
|
137
|
options = {};
|
138
|
}
|
139
|
// This section indexes subschemas in the provided schema, so they don't need to be added with Validator#addSchema
|
140
|
// This will work so long as the function at uri.resolve() will resolve a relative URI to a relative URI
|
141
|
var id = schema.$id || schema.id;
|
142
|
var base = urilib.resolve(options.base||anonymousBase, id||'');
|
143
|
if(!ctx){
|
144
|
ctx = new SchemaContext(schema, options, [], base, Object.create(this.schemas));
|
145
|
if (!ctx.schemas[base]) {
|
146
|
ctx.schemas[base] = schema;
|
147
|
}
|
148
|
var found = scanSchema(base, schema);
|
149
|
for(var n in found.id){
|
150
|
var sch = found.id[n];
|
151
|
ctx.schemas[n] = sch;
|
152
|
}
|
153
|
}
|
154
|
if(options.required && instance===undefined){
|
155
|
var result = new ValidatorResult(instance, schema, options, ctx);
|
156
|
result.addError('is required, but is undefined');
|
157
|
return result;
|
158
|
}
|
159
|
var result = this.validateSchema(instance, schema, options, ctx);
|
160
|
if (!result) {
|
161
|
throw new Error('Result undefined');
|
162
|
}else if(options.throwAll && result.errors.length){
|
163
|
throw new ValidatorResultError(result);
|
164
|
}
|
165
|
return result;
|
166
|
};
|
167
|
|
168
|
/**
|
169
|
* @param Object schema
|
170
|
* @return mixed schema uri or false
|
171
|
*/
|
172
|
function shouldResolve(schema) {
|
173
|
var ref = (typeof schema === 'string') ? schema : schema.$ref;
|
174
|
if (typeof ref=='string') return ref;
|
175
|
return false;
|
176
|
}
|
177
|
|
178
|
/**
|
179
|
* Validates an instance against the schema (the actual work horse)
|
180
|
* @param instance
|
181
|
* @param schema
|
182
|
* @param options
|
183
|
* @param ctx
|
184
|
* @private
|
185
|
* @return {ValidatorResult}
|
186
|
*/
|
187
|
Validator.prototype.validateSchema = function validateSchema (instance, schema, options, ctx) {
|
188
|
var result = new ValidatorResult(instance, schema, options, ctx);
|
189
|
|
190
|
// Support for the true/false schemas
|
191
|
if(typeof schema==='boolean') {
|
192
|
if(schema===true){
|
193
|
// `true` is always valid
|
194
|
schema = {};
|
195
|
}else if(schema===false){
|
196
|
// `false` is always invalid
|
197
|
schema = {type: []};
|
198
|
}
|
199
|
}else if(!schema){
|
200
|
// This might be a string
|
201
|
throw new Error("schema is undefined");
|
202
|
}
|
203
|
|
204
|
if (schema['extends']) {
|
205
|
if (Array.isArray(schema['extends'])) {
|
206
|
var schemaobj = {schema: schema, ctx: ctx};
|
207
|
schema['extends'].forEach(this.schemaTraverser.bind(this, schemaobj));
|
208
|
schema = schemaobj.schema;
|
209
|
schemaobj.schema = null;
|
210
|
schemaobj.ctx = null;
|
211
|
schemaobj = null;
|
212
|
} else {
|
213
|
schema = helpers.deepMerge(schema, this.superResolve(schema['extends'], ctx));
|
214
|
}
|
215
|
}
|
216
|
|
217
|
// If passed a string argument, load that schema URI
|
218
|
var switchSchema = shouldResolve(schema);
|
219
|
if (switchSchema) {
|
220
|
var resolved = this.resolve(schema, switchSchema, ctx);
|
221
|
var subctx = new SchemaContext(resolved.subschema, options, ctx.path, resolved.switchSchema, ctx.schemas);
|
222
|
return this.validateSchema(instance, resolved.subschema, options, subctx);
|
223
|
}
|
224
|
|
225
|
var skipAttributes = options && options.skipAttributes || [];
|
226
|
// Validate each schema attribute against the instance
|
227
|
for (var key in schema) {
|
228
|
if (!attribute.ignoreProperties[key] && skipAttributes.indexOf(key) < 0) {
|
229
|
var validatorErr = null;
|
230
|
var validator = this.attributes[key];
|
231
|
if (validator) {
|
232
|
validatorErr = validator.call(this, instance, schema, options, ctx);
|
233
|
} else if (options.allowUnknownAttributes === false) {
|
234
|
// This represents an error with the schema itself, not an invalid instance
|
235
|
throw new SchemaError("Unsupported attribute: " + key, schema);
|
236
|
}
|
237
|
if (validatorErr) {
|
238
|
result.importErrors(validatorErr);
|
239
|
}
|
240
|
}
|
241
|
}
|
242
|
|
243
|
if (typeof options.rewrite == 'function') {
|
244
|
var value = options.rewrite.call(this, instance, schema, options, ctx);
|
245
|
result.instance = value;
|
246
|
}
|
247
|
return result;
|
248
|
};
|
249
|
|
250
|
/**
|
251
|
* @private
|
252
|
* @param Object schema
|
253
|
* @param SchemaContext ctx
|
254
|
* @returns Object schema or resolved schema
|
255
|
*/
|
256
|
Validator.prototype.schemaTraverser = function schemaTraverser (schemaobj, s) {
|
257
|
schemaobj.schema = helpers.deepMerge(schemaobj.schema, this.superResolve(s, schemaobj.ctx));
|
258
|
};
|
259
|
|
260
|
/**
|
261
|
* @private
|
262
|
* @param Object schema
|
263
|
* @param SchemaContext ctx
|
264
|
* @returns Object schema or resolved schema
|
265
|
*/
|
266
|
Validator.prototype.superResolve = function superResolve (schema, ctx) {
|
267
|
var ref = shouldResolve(schema);
|
268
|
if(ref) {
|
269
|
return this.resolve(schema, ref, ctx).subschema;
|
270
|
}
|
271
|
return schema;
|
272
|
};
|
273
|
|
274
|
/**
|
275
|
* @private
|
276
|
* @param Object schema
|
277
|
* @param Object switchSchema
|
278
|
* @param SchemaContext ctx
|
279
|
* @return Object resolved schemas {subschema:String, switchSchema: String}
|
280
|
* @throws SchemaError
|
281
|
*/
|
282
|
Validator.prototype.resolve = function resolve (schema, switchSchema, ctx) {
|
283
|
switchSchema = ctx.resolve(switchSchema);
|
284
|
// First see if the schema exists under the provided URI
|
285
|
if (ctx.schemas[switchSchema]) {
|
286
|
return {subschema: ctx.schemas[switchSchema], switchSchema: switchSchema};
|
287
|
}
|
288
|
// Else try walking the property pointer
|
289
|
var parsed = urilib.parse(switchSchema);
|
290
|
var fragment = parsed && parsed.hash;
|
291
|
var document = fragment && fragment.length && switchSchema.substr(0, switchSchema.length - fragment.length);
|
292
|
if (!document || !ctx.schemas[document]) {
|
293
|
throw new SchemaError("no such schema <" + switchSchema + ">", schema);
|
294
|
}
|
295
|
var subschema = helpers.objectGetPath(ctx.schemas[document], fragment.substr(1));
|
296
|
if(subschema===undefined){
|
297
|
throw new SchemaError("no such schema " + fragment + " located in <" + document + " delme " + ">", [ctx.schemas[document], schema]);
|
298
|
}
|
299
|
return {subschema: subschema, switchSchema: switchSchema};
|
300
|
};
|
301
|
|
302
|
/**
|
303
|
* Tests whether the instance if of a certain type.
|
304
|
* @private
|
305
|
* @param instance
|
306
|
* @param schema
|
307
|
* @param options
|
308
|
* @param ctx
|
309
|
* @param type
|
310
|
* @return {boolean}
|
311
|
*/
|
312
|
Validator.prototype.testType = function validateType (instance, schema, options, ctx, type) {
|
313
|
if(type===undefined){
|
314
|
return;
|
315
|
}else if(type===null){
|
316
|
throw new SchemaError('Unexpected null in "type" keyword');
|
317
|
}
|
318
|
if (typeof this.types[type] == 'function') {
|
319
|
return this.types[type].call(this, instance);
|
320
|
}
|
321
|
if (type && typeof type == 'object') {
|
322
|
var res = this.validateSchema(instance, type, options, ctx);
|
323
|
return res === undefined || !(res && res.errors.length);
|
324
|
}
|
325
|
// Undefined or properties not on the list are acceptable, same as not being defined
|
326
|
return true;
|
327
|
};
|
328
|
|
329
|
var types = Validator.prototype.types = {};
|
330
|
types.string = function testString (instance) {
|
331
|
return typeof instance == 'string';
|
332
|
};
|
333
|
types.number = function testNumber (instance) {
|
334
|
// isFinite returns false for NaN, Infinity, and -Infinity
|
335
|
return typeof instance == 'number' && isFinite(instance);
|
336
|
};
|
337
|
types.integer = function testInteger (instance) {
|
338
|
return (typeof instance == 'number') && instance % 1 === 0;
|
339
|
};
|
340
|
types.boolean = function testBoolean (instance) {
|
341
|
return typeof instance == 'boolean';
|
342
|
};
|
343
|
types.array = function testArray (instance) {
|
344
|
return Array.isArray(instance);
|
345
|
};
|
346
|
types['null'] = function testNull (instance) {
|
347
|
return instance === null;
|
348
|
};
|
349
|
types.date = function testDate (instance) {
|
350
|
return instance instanceof Date;
|
351
|
};
|
352
|
types.any = function testAny (instance) {
|
353
|
return true;
|
354
|
};
|
355
|
types.object = function testObject (instance) {
|
356
|
// TODO: fix this - see #15
|
357
|
return instance && (typeof instance === 'object') && !(Array.isArray(instance)) && !(instance instanceof Date);
|
358
|
};
|
359
|
|
360
|
#EXPORT Validator
|