/* * Copyright (C) 2018-2019 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. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ import {asyncPool} from 'async-pool-native/dist/async-pool'; import {PathLike} from 'fs'; import {Schema, Validator as JSONSchemaValidator, ValidatorResult} from 'jsonschema'; import * as mustache from 'mustache'; import {basename, join, resolve} from 'path'; import { ExpectableValidationErrors, globPromisified, isThingWithType, logger, readFilePromisified, writeFilePromisified, } from './common'; /** * StAppsCore validator */ export class Validator { /** * Map of schema names to schemas */ private readonly schemas: { [type: string]: Schema } = {}; /** * JSONSchema validator instance */ private readonly validator: JSONSchemaValidator; /** * Create a new validator */ constructor() { this.validator = new JSONSchemaValidator(); } /** * Feed the schema files to the validator * * @param schemaDir Path to directory that contains schema files */ public async addSchemas(schemaDir: PathLike): Promise { const schemaFiles = await globPromisified(join(schemaDir.toString(), '*.json')); if (schemaFiles.length === 0) { throw new Error(`No schema files in ${schemaDir.toString()}!`); } logger.log(`Adding schemas from ${schemaDir} to validator.`); // Iterate over schema files await asyncPool(2, schemaFiles, async (file) => { // 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; logger.info(`Added ${file} to validator.`); }); return schemaFiles; } /** * Validates anything against a given schema name * * @param instance Instance to validate * @param schemaName Name of schema to validate instance against */ public validate(instance: any, schemaName: string): ValidatorResult { if (typeof this.schemas[schemaName] !== 'object') { throw new Error(`No schema available for ${schemaName}.`); } return this.validator.validate(instance, this.schemas[schemaName]); } /** * Validate an instance of a thing against the consumed schema files * * @param instance Instance to validate * @deprecated Use [[validate]] instead */ public validateThing(instance: T): ValidatorResult { if (!isThingWithType(instance)) { throw new Error('Instance.type does not exist.'); } const schemaName = instance.type.split(' ').map((part) => { return part.substr(0, 1).toUpperCase() + part.substr(1); }).join(''); return this.validate(instance, schemaName); } } /** * 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 { // instantiate new validator const v = new Validator(); await v.addSchemas(schemaDir); // get list of files to test const testFiles = await globPromisified(join(resourcesDir, '*.json')); if (testFiles.length === 0) { throw new Error(`No test files in ${resourcesDir}!`); } logger.log(`Found ${testFiles.length} file(s) to test.`); // map of errors per file const errors: ExpectableValidationErrors = {}; // iterate over files to test await asyncPool(2, testFiles, async (testFile) => { const testFileName = basename(testFile); const buffer = await readFilePromisified(join(resourcesDir, testFileName)); // read test description from file const testDescription = JSON.parse(buffer.toString()); // 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); // number of unexpected errors let unexpectedErrors = 0; if (result.errors.length > 0) { errors[testFileName] = []; // iterate over errors result.errors.forEach((error) => { // get idx of expected error const errorIdx = expectedErrors.indexOf(error.name); let expected = false; if (errorIdx >= 0) { // remove from list of expected errors expectedErrors.splice(errorIdx, 1); expected = true; } else { unexpectedErrors++; logger.error(`Unexpected error ${error.name} in ${testFile}`); } // add error to list of errors errors[testFileName].push({ ...error, expected, }); }); } if (expectedErrors.length > 0) { expectedErrors.forEach((error) => { logger.error(`Extraneous expected error '${error}' in ${testFile}.`); errors[testFileName].push({ argument: false, expected: false, instance: testDescription.instance, message: `expected error ${error} did not occur`, name: `expected ${error}`, property: 'unknown', schema: undefined as any, }); }); } else if (unexpectedErrors === 0) { logger.info(`Successfully validated ${testFile}.`); } }); return errors; } /** * Write a report for errors that occurred in validation * * @param reportPath Path to write report to * @param errors Errors that occurred in validation */ export async function writeReport(reportPath: PathLike, errors: ExpectableValidationErrors): Promise { let buffer = await readFilePromisified(resolve(__dirname, '..', 'resources', 'file.html.mustache')); const fileTemplate = buffer.toString(); buffer = await readFilePromisified(resolve(__dirname, '..', 'resources', 'error.html.mustache')); const errorTemplate = buffer.toString(); let output = ''; Object.keys(errors).forEach((fileName) => { let fileOutput = ''; errors[fileName].forEach((error, idx) => { fileOutput += mustache.render(errorTemplate, { idx: idx + 1, instance: JSON.stringify(error.instance, null, 2), message: error.message, property: error.property, schema: JSON.stringify(error.schema, null, 2), status: (error.expected) ? 'alert-success' : 'alert-danger', }); }); output += mustache.render(fileTemplate, { errors: fileOutput, testFile: fileName, }); }); buffer = await readFilePromisified(resolve(__dirname, '..', 'resources', 'report.html.mustache')); const reportTemplate = buffer.toString(); await writeFilePromisified(reportPath, mustache.render(reportTemplate, { report: output, timestamp: (new Date()).toISOString(), })); logger.ok(`Wrote report to ${reportPath}.`); }