refactor: update dependencies and fix resulting errors

Upgraded JSON Schema from version 6 to version 7
Upgraded TypeDoc version to latest
Replaced 'jsonschema' with 'json-schema' package to better comply with 'ts-json-schema-generator'
Replace JSON Schema validation with AJV in areas where it wasn't used previously
Removed commander help output as it causes strange issues
This commit is contained in:
Wieland Schöbl
2020-02-14 11:40:53 +01:00
committed by Rainer Killinger
parent b7cdb6a9ad
commit 5330255b7e
19 changed files with 2058 additions and 1145 deletions

View File

@@ -14,15 +14,20 @@
*/
import {asyncPool} from '@krlwlfrt/async-pool';
import {Logger} from '@openstapps/logger';
import Ajv from 'ajv';
import betterAjvErrors from 'better-ajv-errors';
import {PathLike} from 'fs';
import {Schema, Validator as JSONSchemaValidator, ValidatorResult} from 'jsonschema';
import {JSONSchema7} from 'json-schema';
import * as mustache from 'mustache';
import {basename, join, resolve} from 'path';
import {Schema} from 'ts-json-schema-generator';
import {
ExpectableValidationErrors,
ExpectedValidationErrors,
globPromisified,
isThingWithType,
readFilePromisified,
ValidationError,
ValidationResult,
writeFilePromisified,
} from './common';
@@ -30,21 +35,24 @@ import {
* StAppsCore validator
*/
export class Validator {
/**
* JSON Schema Validator
*/
private readonly ajv = Ajv({verbose: true, jsonPointers: true, extendRefs: true});
/**
* Map of schema names to schemas
*/
private readonly schemas: { [type: string]: Schema; } = {};
/**
* JSONSchema validator instance
* A wrapper function for Ajv that transforms the error into the compatible old error
*
* @param schema the schema that will be validated against
* @param instance the instance that will be validated
*/
private readonly validator: JSONSchemaValidator;
/**
* Create a new validator
*/
constructor() {
this.validator = new JSONSchemaValidator();
private ajvValidateWrapper(schema: JSONSchema7, instance: unknown): ValidationResult {
return fromAjvResult(this.ajv.validate(schema, instance), schema, instance, this.ajv);
}
/**
@@ -65,13 +73,9 @@ export class Validator {
await asyncPool(2, schemaFiles, async (file: string) => {
// read schema file
const buffer = await readFilePromisified(file);
const schema = JSON.parse(buffer.toString());
// add schema to validator
this.validator.addSchema(schema);
// add schema to map
this.schemas[basename(file, '.json')] = schema;
this.schemas[basename(file, '.json')] = JSON.parse(buffer.toString());
Logger.info(`Added ${file} to validator.`);
});
@@ -85,17 +89,17 @@ export class Validator {
* @param instance Instance to validate
* @param schema Name of schema to validate instance against or the schema itself
*/
public validate(instance: unknown, schema?: string | Schema): ValidatorResult {
public validate(instance: unknown, schema?: string | Schema): ValidationResult {
if (typeof schema === 'undefined') {
if (isThingWithType(instance)) {
// schema name can be infered from type string
// schema name can be inferred from type string
// tslint:disable-next-line: completed-docs
const schemaSuffix = (instance as { type: string; }).type.split(' ')
.map((part: string) => {
.map((part: string) => {
return part.substr(0, 1)
.toUpperCase() + part.substr(1);
.toUpperCase() + part.substr(1);
})
.join('');
.join('');
const schemaName = `SC${schemaSuffix}`;
return this.validate(instance, schemaName);
@@ -108,20 +112,61 @@ export class Validator {
throw new Error(`No schema available for ${schema}.`);
}
return this.validator.validate(instance, this.schemas[schema]);
// schema will be cached
return this.ajvValidateWrapper(this.schemas[schema], instance);
}
return this.validator.validate(instance, schema);
return this.ajvValidateWrapper(schema, instance);
}
}
/**
* Creates a ValidationResult from ajv
*
* Implemented for compatibility purposes
*
* @param result the result, now a ValidationResult
* @param schema the schema that has been validated against
* @param instance the data that has been validated
* @param ajvInstance the ajv instance with which the validation was done
*/
function fromAjvResult(
result: boolean | PromiseLike<unknown>,
schema: JSONSchema7,
instance: unknown,
ajvInstance: Ajv.Ajv,
): ValidationResult {
// tslint:disable-next-line
// @ts-ignore function can return void, which at runtime will be undefined. TS doesn't allow to assign void to undefined
const betterErrorObject: betterAjvErrors.IOutputError[] | undefined =
betterAjvErrors(schema, instance, ajvInstance.errors, {format: 'js', indent: null});
return {
errors: ajvInstance.errors?.map((ajvError, index) => {
const error: ValidationError = {
dataPath: ajvError.dataPath,
instance: instance,
message: betterErrorObject?.[index].error!,
name: ajvError.keyword,
schemaPath: ajvError.schemaPath,
suggestion: betterErrorObject?.[index].suggestion,
};
// (validationError as ValidationError).humanReadableError = betterErrorCLI?.[index] as unknown as string;
return error;
}) ?? [],
valid: typeof result === 'boolean' ? result : false,
};
}
/**
* Validate all test files in the given resources directory against schema files in the given (schema) directory
*
* @param schemaDir The directory where the JSON schema files are
* @param resourcesDir The directory where the test files are
*/
export async function validateFiles(schemaDir: string, resourcesDir: string): Promise<ExpectableValidationErrors> {
export async function validateFiles(schemaDir: string, resourcesDir: string): Promise<ExpectedValidationErrors> {
// instantiate new validator
const v = new Validator();
await v.addSchemas(schemaDir);
@@ -136,7 +181,7 @@ export async function validateFiles(schemaDir: string, resourcesDir: string): Pr
Logger.log(`Found ${testFiles.length} file(s) to test.`);
// map of errors per file
const errors: ExpectableValidationErrors = {};
const errors: ExpectedValidationErrors = {};
// tslint:disable-next-line:no-magic-numbers - iterate over files to test
await asyncPool(2, testFiles, async (testFile: string) => {
@@ -162,13 +207,11 @@ export async function validateFiles(schemaDir: string, resourcesDir: string): Pr
// iterate over errors
for (const error of result.errors) {
// get idx of expected error
const errorIdx = expectedErrors.indexOf(error.name);
const errorIndex = expectedErrors.indexOf(error.name);
let expected = false;
if (errorIdx >= 0) {
// remove from list of expected errors
expectedErrors.splice(errorIdx, 1);
if (errorIndex >= 0) {
expectedErrors.splice(errorIndex, 1);
expected = true;
} else {
unexpectedErrors++;
@@ -188,14 +231,14 @@ export async function validateFiles(schemaDir: string, resourcesDir: string): Pr
await Logger.error(`Extraneous expected error '${error}' in ${testFile}.`);
errors[testFileName].push({
argument: false,
dataPath: 'undefined',
expected: false,
instance: testDescription.instance,
message: `expected error ${error} did not occur`,
instance: undefined,
// instance: testDescription.instance,
message: 'undefined',
name: `expected ${error}`,
property: 'unknown',
schema: 'undefined',
stack: 'undefined',
schemaPath: 'undefined',
suggestion: 'undefined',
});
}
} else if (unexpectedErrors === 0) {
@@ -212,7 +255,7 @@ export async function validateFiles(schemaDir: string, resourcesDir: string): Pr
* @param reportPath Path to write report to
* @param errors Errors that occurred in validation
*/
export async function writeReport(reportPath: PathLike, errors: ExpectableValidationErrors): Promise<void> {
export async function writeReport(reportPath: PathLike, errors: ExpectedValidationErrors): Promise<void> {
let buffer = await readFilePromisified(resolve(__dirname, '..', 'resources', 'file.html.mustache'));
const fileTemplate = buffer.toString();
@@ -229,15 +272,16 @@ export async function writeReport(reportPath: PathLike, errors: ExpectableValida
let fileOutput = '';
errors[fileName].forEach((error, idx) => {
fileOutput += mustache.render(errorTemplate, {
idx: idx + 1,
// tslint:disable-next-line:no-magic-numbers
instance: JSON.stringify(error.instance, null, 2),
message: error.message,
property: error.property,
// tslint:disable-next-line:no-magic-numbers
schema: JSON.stringify(error.schema, null, 2),
name: error.name,
schemaPath: error.schemaPath,
status: (error.expected) ? 'alert-success' : 'alert-danger',
suggestion: error.suggestion,
});
});