mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-20 00:23:03 +00:00
feat: modernize core-tools
This commit is contained in:
241
src/validate.ts
241
src/validate.ts
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2018-2019 StApps
|
||||
* Copyright (C) 2018-2021 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
@@ -12,38 +12,31 @@
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {asyncPool} from '@krlwlfrt/async-pool/lib/async-pool';
|
||||
import {Logger} from '@openstapps/logger';
|
||||
import Ajv from 'ajv';
|
||||
import betterAjvErrors from 'better-ajv-errors';
|
||||
import {PathLike} from 'fs';
|
||||
import {JSONSchema7} from 'json-schema';
|
||||
import * as mustache from 'mustache';
|
||||
import {basename, join, resolve} from 'path';
|
||||
import {Schema} from 'ts-json-schema-generator';
|
||||
import {
|
||||
ExpectedValidationErrors,
|
||||
globPromisified,
|
||||
isThingWithType,
|
||||
readFilePromisified,
|
||||
ValidationError,
|
||||
ValidationResult,
|
||||
writeFilePromisified,
|
||||
} from './common';
|
||||
import {globPromisified, readFilePromisified, writeFilePromisified} from './common';
|
||||
import {ExpectedValidationErrors, ValidationError, ValidationResult} from './types/validator';
|
||||
import {isThingWithType} from './util/guards';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* 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; } = {};
|
||||
private readonly schemas: {[type: string]: Schema} = {};
|
||||
|
||||
/**
|
||||
* A wrapper function for Ajv that transforms the error into the compatible old error
|
||||
@@ -58,27 +51,28 @@ export class Validator {
|
||||
/**
|
||||
* Feed the schema files to the validator
|
||||
*
|
||||
* @param schemaDir Path to directory that contains schema files
|
||||
* @param schemaDirectory Path to directory that contains schema files
|
||||
*/
|
||||
public async addSchemas(schemaDir: PathLike): Promise<string[]> {
|
||||
const schemaFiles = await globPromisified(join(schemaDir.toString(), '*.json'));
|
||||
public async addSchemas(schemaDirectory: PathLike): Promise<string[]> {
|
||||
const schemaFiles = await globPromisified(path.join(schemaDirectory.toString(), '*.json'));
|
||||
|
||||
if (schemaFiles.length === 0) {
|
||||
throw new Error(`No schema files in ${schemaDir.toString()}!`);
|
||||
throw new Error(`No schema files in ${schemaDirectory.toString()}!`);
|
||||
}
|
||||
|
||||
Logger.log(`Adding schemas from ${schemaDir} to validator.`);
|
||||
Logger.log(`Adding schemas from ${schemaDirectory} to validator.`);
|
||||
|
||||
// tslint:disable-next-line:no-magic-numbers - iterate over schema files
|
||||
await asyncPool(2, schemaFiles, async (file: string) => {
|
||||
// read schema file
|
||||
const buffer = await readFilePromisified(file);
|
||||
await Promise.all(
|
||||
schemaFiles.map(async (file: string) => {
|
||||
// read schema file
|
||||
const buffer = await readFilePromisified(file);
|
||||
|
||||
// add schema to map
|
||||
this.schemas[basename(file, '.json')] = JSON.parse(buffer.toString());
|
||||
// add schema to map
|
||||
this.schemas[path.basename(file, '.json')] = JSON.parse(buffer.toString());
|
||||
|
||||
Logger.info(`Added ${file} to validator.`);
|
||||
});
|
||||
Logger.info(`Added ${file} to validator.`);
|
||||
}),
|
||||
);
|
||||
|
||||
return schemaFiles;
|
||||
}
|
||||
@@ -93,11 +87,10 @@ export class Validator {
|
||||
if (typeof schema === 'undefined') {
|
||||
if (isThingWithType(instance)) {
|
||||
// schema name can be inferred from type string
|
||||
// tslint:disable-next-line: completed-docs
|
||||
const schemaSuffix = (instance as { type: string; }).type.split(' ')
|
||||
const schemaSuffix = (instance as {type: string}).type
|
||||
.split(' ')
|
||||
.map((part: string) => {
|
||||
return part.substr(0, 1)
|
||||
.toUpperCase() + part.substr(1);
|
||||
return part.slice(0, 1).toUpperCase() + part.slice(1);
|
||||
})
|
||||
.join('');
|
||||
const schemaName = `SC${schemaSuffix}`;
|
||||
@@ -109,7 +102,7 @@ export class Validator {
|
||||
if (typeof schema === 'string') {
|
||||
// if you want to access a schema that is contained in the validator object
|
||||
if (typeof this.schemas[schema] !== 'object') {
|
||||
throw new Error(`No schema available for ${schema}.`);
|
||||
throw new TypeError(`No schema available for ${schema}.`);
|
||||
}
|
||||
|
||||
// schema will be cached
|
||||
@@ -136,26 +129,31 @@ function fromAjvResult(
|
||||
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});
|
||||
// @ts-expect-error 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,
|
||||
// eslint-disable-next-line unicorn/no-null
|
||||
{format: 'js', indent: null},
|
||||
);
|
||||
|
||||
return {
|
||||
errors: ajvInstance.errors?.map((ajvError, index) => {
|
||||
errors:
|
||||
ajvInstance.errors?.map((ajvError, index) => {
|
||||
const error: ValidationError = {
|
||||
dataPath: ajvError.dataPath,
|
||||
instance: instance,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
|
||||
message: betterErrorObject?.[index].error!,
|
||||
name: ajvError.keyword,
|
||||
schemaPath: ajvError.schemaPath,
|
||||
suggestion: betterErrorObject?.[index].suggestion,
|
||||
};
|
||||
// (validationError as ValidationError).humanReadableError = betterErrorCLI?.[index] as unknown as string;
|
||||
|
||||
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;
|
||||
}) ?? [],
|
||||
return error;
|
||||
}) ?? [],
|
||||
valid: typeof result === 'boolean' ? result : false,
|
||||
};
|
||||
}
|
||||
@@ -163,19 +161,22 @@ function fromAjvResult(
|
||||
/**
|
||||
* 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
|
||||
* @param schemaDirectory The directory where the JSON schema files are
|
||||
* @param resourcesDirectory The directory where the test files are
|
||||
*/
|
||||
export async function validateFiles(schemaDir: string, resourcesDir: string): Promise<ExpectedValidationErrors> {
|
||||
export async function validateFiles(
|
||||
schemaDirectory: string,
|
||||
resourcesDirectory: string,
|
||||
): Promise<ExpectedValidationErrors> {
|
||||
// instantiate new validator
|
||||
const v = new Validator();
|
||||
await v.addSchemas(schemaDir);
|
||||
await v.addSchemas(schemaDirectory);
|
||||
|
||||
// get list of files to test
|
||||
const testFiles = await globPromisified(join(resourcesDir, '*.json'));
|
||||
const testFiles = await globPromisified(path.join(resourcesDirectory, '*.json'));
|
||||
|
||||
if (testFiles.length === 0) {
|
||||
throw new Error(`No test files in ${resourcesDir}!`);
|
||||
throw new Error(`No test files in ${resourcesDirectory}!`);
|
||||
}
|
||||
|
||||
Logger.log(`Found ${testFiles.length} file(s) to test.`);
|
||||
@@ -183,68 +184,68 @@ export async function validateFiles(schemaDir: string, resourcesDir: string): Pr
|
||||
// map of errors per file
|
||||
const errors: ExpectedValidationErrors = {};
|
||||
|
||||
// tslint:disable-next-line:no-magic-numbers - iterate over files to test
|
||||
await asyncPool(2, testFiles, async (testFile: string) => {
|
||||
const testFileName = basename(testFile);
|
||||
await Promise.all(
|
||||
testFiles.map(async (testFile: string) => {
|
||||
const testFileName = path.basename(testFile);
|
||||
|
||||
const buffer = await readFilePromisified(join(resourcesDir, testFileName));
|
||||
const buffer = await readFilePromisified(path.join(resourcesDirectory, testFileName));
|
||||
|
||||
// read test description from file
|
||||
const testDescription = JSON.parse(buffer.toString());
|
||||
// read test description from file
|
||||
const testDescription = JSON.parse(buffer.toString());
|
||||
|
||||
// validate instance from test description
|
||||
const result = v.validate(testDescription.instance, testDescription.schema);
|
||||
// validate instance from test description
|
||||
const result = v.validate(testDescription.instance, testDescription.schema);
|
||||
|
||||
// list of expected errors for this test description
|
||||
const expectedErrors: string[] = [];
|
||||
expectedErrors.push.apply(expectedErrors, testDescription.errorNames);
|
||||
// list of expected errors for this test description
|
||||
const expectedErrors: string[] = [...testDescription.errorNames];
|
||||
|
||||
// number of unexpected errors
|
||||
let unexpectedErrors = 0;
|
||||
// number of unexpected errors
|
||||
let unexpectedErrors = 0;
|
||||
|
||||
if (result.errors.length > 0) {
|
||||
errors[testFileName] = [];
|
||||
if (result.errors.length > 0) {
|
||||
errors[testFileName] = [];
|
||||
|
||||
// iterate over errors
|
||||
for (const error of result.errors) {
|
||||
const errorIndex = expectedErrors.indexOf(error.name);
|
||||
let expected = false;
|
||||
// iterate over errors
|
||||
for (const error of result.errors) {
|
||||
const errorIndex = expectedErrors.indexOf(error.name);
|
||||
let expected = false;
|
||||
|
||||
if (errorIndex >= 0) {
|
||||
expectedErrors.splice(errorIndex, 1);
|
||||
expected = true;
|
||||
} else {
|
||||
unexpectedErrors++;
|
||||
await Logger.error(`Unexpected error ${error.name} in ${testFile}`);
|
||||
if (errorIndex >= 0) {
|
||||
expectedErrors.splice(errorIndex, 1);
|
||||
expected = true;
|
||||
} else {
|
||||
unexpectedErrors++;
|
||||
await Logger.error(`Unexpected error ${error.name} in ${testFile}`);
|
||||
}
|
||||
|
||||
// add error to list of errors
|
||||
errors[testFileName].push({
|
||||
...error,
|
||||
expected,
|
||||
});
|
||||
}
|
||||
|
||||
// add error to list of errors
|
||||
errors[testFileName].push({
|
||||
...error,
|
||||
expected,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (expectedErrors.length > 0) {
|
||||
for (const error of expectedErrors) {
|
||||
await Logger.error(`Extraneous expected error '${error}' in ${testFile}.`);
|
||||
if (expectedErrors.length > 0) {
|
||||
for (const error of expectedErrors) {
|
||||
await Logger.error(`Extraneous expected error '${error}' in ${testFile}.`);
|
||||
|
||||
errors[testFileName].push({
|
||||
dataPath: 'undefined',
|
||||
expected: false,
|
||||
instance: undefined,
|
||||
// instance: testDescription.instance,
|
||||
message: 'undefined',
|
||||
name: `expected ${error}`,
|
||||
schemaPath: 'undefined',
|
||||
suggestion: 'undefined',
|
||||
});
|
||||
errors[testFileName].push({
|
||||
dataPath: 'undefined',
|
||||
expected: false,
|
||||
instance: undefined,
|
||||
// instance: testDescription.instance,
|
||||
message: 'undefined',
|
||||
name: `expected ${error}`,
|
||||
schemaPath: 'undefined',
|
||||
suggestion: 'undefined',
|
||||
});
|
||||
}
|
||||
} else if (unexpectedErrors === 0) {
|
||||
Logger.info(`Successfully validated ${testFile}.`);
|
||||
}
|
||||
} else if (unexpectedErrors === 0) {
|
||||
Logger.info(`Successfully validated ${testFile}.`);
|
||||
}
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return errors;
|
||||
}
|
||||
@@ -256,10 +257,12 @@ export async function validateFiles(schemaDir: string, resourcesDir: string): Pr
|
||||
* @param errors Errors that occurred in validation
|
||||
*/
|
||||
export async function writeReport(reportPath: PathLike, errors: ExpectedValidationErrors): Promise<void> {
|
||||
let buffer = await readFilePromisified(resolve(__dirname, '..', 'resources', 'file.html.mustache'));
|
||||
// eslint-disable-next-line unicorn/prefer-module
|
||||
let buffer = await readFilePromisified(path.resolve(__dirname, '..', 'resources', 'file.html.mustache'));
|
||||
const fileTemplate = buffer.toString();
|
||||
|
||||
buffer = await readFilePromisified(resolve(__dirname, '..', 'resources', 'error.html.mustache'));
|
||||
// eslint-disable-next-line unicorn/prefer-module
|
||||
buffer = await readFilePromisified(path.resolve(__dirname, '..', 'resources', 'error.html.mustache'));
|
||||
const errorTemplate = buffer.toString();
|
||||
|
||||
let output = '';
|
||||
@@ -271,19 +274,17 @@ export async function writeReport(reportPath: PathLike, errors: ExpectedValidati
|
||||
|
||||
let fileOutput = '';
|
||||
|
||||
errors[fileName].forEach((error, idx) => {
|
||||
|
||||
for (const [index, error] of errors[fileName].entries()) {
|
||||
fileOutput += mustache.render(errorTemplate, {
|
||||
idx: idx + 1,
|
||||
// tslint:disable-next-line:no-magic-numbers
|
||||
instance: JSON.stringify(error.instance, null, 2),
|
||||
idx: index + 1,
|
||||
instance: JSON.stringify(error.instance, undefined, 2),
|
||||
message: error.message,
|
||||
name: error.name,
|
||||
schemaPath: error.schemaPath,
|
||||
status: (error.expected) ? 'alert-success' : 'alert-danger',
|
||||
status: error.expected ? 'alert-success' : 'alert-danger',
|
||||
suggestion: error.suggestion,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
output += mustache.render(fileTemplate, {
|
||||
errors: fileOutput,
|
||||
@@ -291,13 +292,17 @@ export async function writeReport(reportPath: PathLike, errors: ExpectedValidati
|
||||
});
|
||||
}
|
||||
|
||||
buffer = await readFilePromisified(resolve(__dirname, '..', 'resources', 'report.html.mustache'));
|
||||
// eslint-disable-next-line unicorn/prefer-module
|
||||
buffer = await readFilePromisified(path.resolve(__dirname, '..', 'resources', 'report.html.mustache'));
|
||||
const reportTemplate = buffer.toString();
|
||||
|
||||
await writeFilePromisified(reportPath, mustache.render(reportTemplate, {
|
||||
report: output,
|
||||
timestamp: (new Date()).toISOString(),
|
||||
}));
|
||||
await writeFilePromisified(
|
||||
reportPath,
|
||||
mustache.render(reportTemplate, {
|
||||
report: output,
|
||||
timestamp: new Date().toISOString(),
|
||||
}),
|
||||
);
|
||||
|
||||
Logger.ok(`Wrote report to ${reportPath}.`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user