218 lines
9.2 KiB
JavaScript
218 lines
9.2 KiB
JavaScript
|
"use strict";
|
||
|
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
|
||
|
// See LICENSE in the project root for license information.
|
||
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||
|
if (k2 === undefined) k2 = k;
|
||
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
||
|
}
|
||
|
Object.defineProperty(o, k2, desc);
|
||
|
}) : (function(o, m, k, k2) {
|
||
|
if (k2 === undefined) k2 = k;
|
||
|
o[k2] = m[k];
|
||
|
}));
|
||
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||
|
}) : function(o, v) {
|
||
|
o["default"] = v;
|
||
|
});
|
||
|
var __importStar = (this && this.__importStar) || function (mod) {
|
||
|
if (mod && mod.__esModule) return mod;
|
||
|
var result = {};
|
||
|
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||
|
__setModuleDefault(result, mod);
|
||
|
return result;
|
||
|
};
|
||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||
|
exports.JsonSchema = void 0;
|
||
|
const os = __importStar(require("os"));
|
||
|
const path = __importStar(require("path"));
|
||
|
const JsonFile_1 = require("./JsonFile");
|
||
|
const FileSystem_1 = require("./FileSystem");
|
||
|
const Validator = require('z-schema/dist/ZSchema-browser-min');
|
||
|
/**
|
||
|
* Represents a JSON schema that can be used to validate JSON data files loaded by the JsonFile class.
|
||
|
* @remarks
|
||
|
* The schema itself is normally loaded and compiled later, only if it is actually required to validate
|
||
|
* an input. To avoid schema errors at runtime, it's recommended to create a unit test that calls
|
||
|
* JsonSchema.ensureCompiled() for each of your schema objects.
|
||
|
*
|
||
|
* @public
|
||
|
*/
|
||
|
class JsonSchema {
|
||
|
constructor() {
|
||
|
this._dependentSchemas = [];
|
||
|
this._filename = '';
|
||
|
this._validator = undefined;
|
||
|
this._schemaObject = undefined;
|
||
|
}
|
||
|
/**
|
||
|
* Registers a JsonSchema that will be loaded from a file on disk.
|
||
|
* @remarks
|
||
|
* NOTE: An error occurs if the file does not exist; however, the file itself is not loaded or validated
|
||
|
* until it the schema is actually used.
|
||
|
*/
|
||
|
static fromFile(filename, options) {
|
||
|
// This is a quick and inexpensive test to avoid the catch the most common errors early.
|
||
|
// Full validation will happen later in JsonSchema.compile().
|
||
|
if (!FileSystem_1.FileSystem.exists(filename)) {
|
||
|
throw new Error('Schema file not found: ' + filename);
|
||
|
}
|
||
|
const schema = new JsonSchema();
|
||
|
schema._filename = filename;
|
||
|
if (options) {
|
||
|
schema._dependentSchemas = options.dependentSchemas || [];
|
||
|
}
|
||
|
return schema;
|
||
|
}
|
||
|
/**
|
||
|
* Registers a JsonSchema that will be loaded from a file on disk.
|
||
|
* @remarks
|
||
|
* NOTE: An error occurs if the file does not exist; however, the file itself is not loaded or validated
|
||
|
* until it the schema is actually used.
|
||
|
*/
|
||
|
static fromLoadedObject(schemaObject) {
|
||
|
const schema = new JsonSchema();
|
||
|
schema._schemaObject = schemaObject;
|
||
|
return schema;
|
||
|
}
|
||
|
static _collectDependentSchemas(collectedSchemas, dependentSchemas, seenObjects, seenIds) {
|
||
|
for (const dependentSchema of dependentSchemas) {
|
||
|
// It's okay for the same schema to appear multiple times in the tree, but we only process it once
|
||
|
if (seenObjects.has(dependentSchema)) {
|
||
|
continue;
|
||
|
}
|
||
|
seenObjects.add(dependentSchema);
|
||
|
const schemaId = dependentSchema._ensureLoaded();
|
||
|
if (schemaId === '') {
|
||
|
throw new Error(`This schema ${dependentSchema.shortName} cannot be referenced` +
|
||
|
' because is missing the "id" field');
|
||
|
}
|
||
|
if (seenIds.has(schemaId)) {
|
||
|
throw new Error(`This schema ${dependentSchema.shortName} has the same "id" as another schema in this set`);
|
||
|
}
|
||
|
seenIds.add(schemaId);
|
||
|
collectedSchemas.push(dependentSchema);
|
||
|
JsonSchema._collectDependentSchemas(collectedSchemas, dependentSchema._dependentSchemas, seenObjects, seenIds);
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* Used to nicely format the ZSchema error tree.
|
||
|
*/
|
||
|
static _formatErrorDetails(errorDetails) {
|
||
|
return JsonSchema._formatErrorDetailsHelper(errorDetails, '', '');
|
||
|
}
|
||
|
/**
|
||
|
* Used by _formatErrorDetails.
|
||
|
*/
|
||
|
static _formatErrorDetailsHelper(errorDetails, indent, buffer) {
|
||
|
for (const errorDetail of errorDetails) {
|
||
|
buffer += os.EOL + indent + `Error: ${errorDetail.path}`;
|
||
|
if (errorDetail.description) {
|
||
|
const MAX_LENGTH = 40;
|
||
|
let truncatedDescription = errorDetail.description.trim();
|
||
|
if (truncatedDescription.length > MAX_LENGTH) {
|
||
|
truncatedDescription = truncatedDescription.substr(0, MAX_LENGTH - 3) + '...';
|
||
|
}
|
||
|
buffer += ` (${truncatedDescription})`;
|
||
|
}
|
||
|
buffer += os.EOL + indent + ` ${errorDetail.message}`;
|
||
|
if (errorDetail.inner) {
|
||
|
buffer = JsonSchema._formatErrorDetailsHelper(errorDetail.inner, indent + ' ', buffer);
|
||
|
}
|
||
|
}
|
||
|
return buffer;
|
||
|
}
|
||
|
/**
|
||
|
* Returns a short name for this schema, for use in error messages.
|
||
|
* @remarks
|
||
|
* If the schema was loaded from a file, then the base filename is used. Otherwise, the "id"
|
||
|
* field is used if available.
|
||
|
*/
|
||
|
get shortName() {
|
||
|
if (!this._filename) {
|
||
|
if (this._schemaObject) {
|
||
|
const schemaWithId = this._schemaObject;
|
||
|
if (schemaWithId.id) {
|
||
|
return schemaWithId.id;
|
||
|
}
|
||
|
}
|
||
|
return '(anonymous schema)';
|
||
|
}
|
||
|
else {
|
||
|
return path.basename(this._filename);
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* If not already done, this loads the schema from disk and compiles it.
|
||
|
* @remarks
|
||
|
* Any dependencies will be compiled as well.
|
||
|
*/
|
||
|
ensureCompiled() {
|
||
|
this._ensureLoaded();
|
||
|
if (!this._validator) {
|
||
|
// Don't assign this to _validator until we're sure everything was successful
|
||
|
const newValidator = new Validator({
|
||
|
breakOnFirstError: false,
|
||
|
noTypeless: true,
|
||
|
noExtraKeywords: true
|
||
|
});
|
||
|
const anythingSchema = {
|
||
|
type: ['array', 'boolean', 'integer', 'number', 'object', 'string']
|
||
|
};
|
||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
|
newValidator.setRemoteReference('http://json-schema.org/draft-04/schema', anythingSchema);
|
||
|
const collectedSchemas = [];
|
||
|
const seenObjects = new Set();
|
||
|
const seenIds = new Set();
|
||
|
JsonSchema._collectDependentSchemas(collectedSchemas, this._dependentSchemas, seenObjects, seenIds);
|
||
|
// Validate each schema in order. We specifically do not supply them all together, because we want
|
||
|
// to make sure that circular references will fail to validate.
|
||
|
for (const collectedSchema of collectedSchemas) {
|
||
|
if (!newValidator.validateSchema(collectedSchema._schemaObject)) {
|
||
|
throw new Error(`Failed to validate schema "${collectedSchema.shortName}":` +
|
||
|
os.EOL +
|
||
|
JsonSchema._formatErrorDetails(newValidator.getLastErrors()));
|
||
|
}
|
||
|
}
|
||
|
this._validator = newValidator;
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* Validates the specified JSON object against this JSON schema. If the validation fails,
|
||
|
* an exception will be thrown.
|
||
|
* @param jsonObject - The JSON data to be validated
|
||
|
* @param filenameForErrors - The filename that the JSON data was available, or an empty string
|
||
|
* if not applicable
|
||
|
* @param options - Other options that control the validation
|
||
|
*/
|
||
|
validateObject(jsonObject, filenameForErrors, options) {
|
||
|
this.validateObjectWithCallback(jsonObject, (errorInfo) => {
|
||
|
const prefix = options && options.customErrorHeader ? options.customErrorHeader : 'JSON validation failed:';
|
||
|
throw new Error(prefix + os.EOL + filenameForErrors + os.EOL + errorInfo.details);
|
||
|
});
|
||
|
}
|
||
|
/**
|
||
|
* Validates the specified JSON object against this JSON schema. If the validation fails,
|
||
|
* a callback is called for each validation error.
|
||
|
*/
|
||
|
validateObjectWithCallback(jsonObject, errorCallback) {
|
||
|
this.ensureCompiled();
|
||
|
if (!this._validator.validate(jsonObject, this._schemaObject)) {
|
||
|
const errorDetails = JsonSchema._formatErrorDetails(this._validator.getLastErrors());
|
||
|
const args = {
|
||
|
details: errorDetails
|
||
|
};
|
||
|
errorCallback(args);
|
||
|
}
|
||
|
}
|
||
|
_ensureLoaded() {
|
||
|
if (!this._schemaObject) {
|
||
|
this._schemaObject = JsonFile_1.JsonFile.load(this._filename);
|
||
|
}
|
||
|
return this._schemaObject.id || '';
|
||
|
}
|
||
|
}
|
||
|
exports.JsonSchema = JsonSchema;
|
||
|
//# sourceMappingURL=JsonSchema.js.map
|