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

2668
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -45,40 +45,42 @@
}, },
"dependencies": { "dependencies": {
"@krlwlfrt/async-pool": "0.3.0", "@krlwlfrt/async-pool": "0.3.0",
"@openstapps/logger": "0.4.0", "@openstapps/logger": "0.5.0",
"@types/glob": "7.1.1", "@types/glob": "7.1.1",
"@types/got": "9.6.9", "@types/got": "9.6.9",
"@types/mustache": "0.8.32", "@types/mustache": "4.0.0",
"@types/node": "10.17.14", "@types/node": "10.17.21",
"@types/json-schema": "7.0.6",
"ajv": "6.11.0", "ajv": "6.11.0",
"better-ajv-errors": "0.6.7",
"chai": "4.2.0", "chai": "4.2.0",
"commander": "2.20.3", "commander": "4.1.1",
"deepmerge": "3.3.0", "deepmerge": "4.2.2",
"del": "4.1.1", "del": "5.1.0",
"flatted": "2.0.1", "flatted": "2.0.1",
"glob": "7.1.6", "glob": "7.1.6",
"got": "9.6.0", "got": "10.5.5",
"humanize-string": "2.1.0", "humanize-string": "2.1.0",
"jsonschema": "1.2.5", "json-schema": "0.2.5",
"mustache": "3.0.1", "mustache": "4.0.0",
"plantuml-encoder": "1.4.0", "plantuml-encoder": "1.4.0",
"toposort": "2.0.2", "toposort": "2.0.2",
"ts-json-schema-generator": "0.42.0", "ts-json-schema-generator": "0.60.0",
"ts-node": "8.6.2", "ts-node": "8.6.2",
"typedoc": "0.14.2" "typedoc": "0.18.0",
"typescript": "3.8.3"
}, },
"devDependencies": { "devDependencies": {
"@openstapps/configuration": "0.23.0", "@openstapps/configuration": "0.25.0",
"@types/chai": "4.2.8", "@types/chai": "4.2.8",
"@types/mocha": "5.2.7", "@types/mocha": "7.0.2",
"@types/rimraf": "2.0.3", "@types/rimraf": "2.0.3",
"conventional-changelog-cli": "2.0.31", "conventional-changelog-cli": "2.1.0",
"mocha": "6.2.2", "mocha": "7.2.0",
"mocha-typescript": "1.1.17", "mocha-typescript": "1.1.17",
"nock": "11.7.2", "nock": "11.9.1",
"prepend-file-cli": "1.0.6", "prepend-file-cli": "1.0.6",
"rimraf": "3.0.1", "rimraf": "3.0.2",
"tslint": "5.20.1", "tslint": "6.1.3"
"typescript": "3.7.5"
} }
} }

View File

@@ -1,10 +1,12 @@
<tr> <tr>
<td><h3>{{idx}}</h3></td> <th scope="row"><h3>{{idx}}</h3></th>
<td> <td>
<div class="alert {{status}}">{{message}}</div> <div class="alert {{status}}">{{name}}</div>
<pre style="max-width: 600px;">{{property}} = {{instance}}</pre> <p>{{message}}</p>
<p>{{suggestion}}</p>
</td> </td>
<td> <td style="max-width: 600px">
<pre>{{schema}}</pre> <p>{{schemaPath}}</p>
<pre>{{instance}}</pre>
</td> </td>
</tr> </tr>

View File

@@ -1,10 +1,11 @@
<div class="mw-100">
<h2>Errors in <code>{{testFile}}</code></h2> <h2>Errors in <code>{{testFile}}</code></h2>
<table class="table table-striped table-hover"> <table class="table table-striped table-hover">
<thead> <thead>
<tr> <tr>
<th>#</th> <th scope="col">#</th>
<th>Error</th> <th scope="col">Error</th>
<th>Schema</th> <th scope="col">Instance</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -13,3 +14,4 @@
</tbody> </tbody>
</table> </table>
</div>

View File

@@ -3,26 +3,22 @@
<head> <head>
<title>Report</title> <title>Report</title>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous"> integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
</head> </head>
<body> <body>
<div class="container-fluid">
<h1>Validation result</h1> <h1>Validation result</h1>
<p>Timestamp: {{timestamp}}</p> <p>Timestamp: {{timestamp}}</p>
{{&report}} {{&report}}
</div> <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"
integrity="sha256-4+XzXVhsDmqanXGHaHvgh1gMQKX40OUvDEBTu8JcmNs=" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" crossorigin="anonymous" <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js"
integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN"></script> integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js" crossorigin="anonymous" <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"
integrity="sha384-vFJXuSJphROIrBnz7yo7oB41mKfc8JzQZiCq4NCceLEaO4IHwicKwpJf9c9IpFgh"></script> integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js" crossorigin="anonymous"
integrity="sha384-alpBpkh1PFOepccYVYDB4do5UnbKysX5WZXm3XxPqe5iKTfUKjNkCk9SaVuEZflJ"></script>
</body> </body>
</html> </html>

View File

@@ -15,7 +15,7 @@
import {Logger} from '@openstapps/logger'; import {Logger} from '@openstapps/logger';
import {Command} from 'commander'; import {Command} from 'commander';
import {existsSync, readFileSync, writeFileSync} from 'fs'; import {existsSync, readFileSync, writeFileSync} from 'fs';
import * as got from 'got'; import got from 'got';
import {join, resolve} from 'path'; import {join, resolve} from 'path';
import {exit} from 'process'; import {exit} from 'process';
import { import {
@@ -158,8 +158,7 @@ commander
} }
const response = await got.put(`${esAddress}_template/${template}`, { const response = await got.put(`${esAddress}_template/${template}`, {
body: result.mappings[template], json: result.mappings[template],
json: true,
}); });
const HTTP_STATUS_OK = 200; const HTTP_STATUS_OK = 200;
@@ -353,8 +352,3 @@ commander
}); });
commander.parse(process.argv); commander.parse(process.argv);
if (commander.args.length < 1) {
commander.outputHelp();
process.exit(1);
}

View File

@@ -15,11 +15,12 @@
import {Logger} from '@openstapps/logger'; import {Logger} from '@openstapps/logger';
import {existsSync, mkdir, PathLike, readFile, unlink, writeFile} from 'fs'; import {existsSync, mkdir, PathLike, readFile, unlink, writeFile} from 'fs';
import {Glob} from 'glob'; import {Glob} from 'glob';
import {Schema as JSONSchema, ValidationError} from 'jsonschema'; import {JSONSchema7 as JSONSchema} from 'json-schema';
import {platform} from 'os'; import {platform} from 'os';
import {join, sep} from 'path'; import {join, sep} from 'path';
import {Definition} from 'ts-json-schema-generator'; import {Definition} from 'ts-json-schema-generator';
import {Application, ProjectReflection} from 'typedoc'; import {Application, ProjectReflection} from 'typedoc';
import {ModuleKind, ScriptTarget} from 'typescript';
import {promisify} from 'util'; import {promisify} from 'util';
import {LightweightType} from './uml/model/lightweight-type'; import {LightweightType} from './uml/model/lightweight-type';
@@ -122,9 +123,67 @@ interface SchemaWithDefinitions extends JSONSchema {
} }
/** /**
* An expectable error * The validation result
*/ */
export interface ExpectableValidationError extends ValidationError { export interface ValidationResult {
/**
* A list of errors that occurred
*/
errors: ValidationError[];
/**
* whether the validation was successful
*/
valid: boolean;
}
/**
* An error that occurred while validating
*
* This is a duplicate of the ValidationError in core/protocol/errors/validation because of incompatibilities
* between TypeDoc and TypeScript
*/
export interface ValidationError {
/**
* JSON schema path
*/
dataPath: string;
/**
* The instance
*/
instance: unknown;
/**
* The message
*
* Provided by https://www.npmjs.com/package/better-ajv-errors
*/
message: string;
/**
* Name of the error
*/
name: string;
/**
* Path within the Schema
*/
schemaPath: string;
/**
* Suggestion to fix the occurring error
*
* Provided by https://www.npmjs.com/package/better-ajv-errors
*/
suggestion?: string;
}
/**
* An expected error
*/
export interface ExpectedValidationError extends ValidationError {
/** /**
* Whether or not the error is expected * Whether or not the error is expected
*/ */
@@ -132,10 +191,10 @@ export interface ExpectableValidationError extends ValidationError {
} }
/** /**
* A map of files and their expectable validation errors * A map of files and their expected validation errors
*/ */
export interface ExpectableValidationErrors { export interface ExpectedValidationErrors {
[fileName: string]: ExpectableValidationError[]; [fileName: string]: ExpectedValidationError[];
} }
/** /**
@@ -150,10 +209,14 @@ export function getProjectReflection(srcPath: PathLike, excludeExternals = true)
const tsconfigPath = getTsconfigPath(srcPath.toString()); const tsconfigPath = getTsconfigPath(srcPath.toString());
// initialize new Typedoc application // initialize new Typedoc application
const app = new Application({ const app = new Application();
app.options.setValues({
excludeExternals: excludeExternals, excludeExternals: excludeExternals,
ignoreCompilerErrors: false, // TODO: true
includeDeclarations: true, includeDeclarations: true,
module: 'commonjs', module: ModuleKind.CommonJS,
target: ScriptTarget.Latest,
tsconfig: join(tsconfigPath, 'tsconfig.json'), tsconfig: join(tsconfigPath, 'tsconfig.json'),
}); });
@@ -196,8 +259,9 @@ export function isThingWithType(thing: unknown): thing is { type: string; } {
return typeof thing === 'object' && return typeof thing === 'object' &&
thing !== null && thing !== null &&
'type' in thing && 'type' in thing &&
typeof (thing as { type: string; }).type === 'string'; typeof (thing as { type: unknown; }).type === 'string';
} }
// tslint:enable: completed-docs // tslint:enable: completed-docs
/** /**
@@ -233,16 +297,16 @@ export function getTsconfigPath(startPath: string): string {
} }
/** /**
* Converts a comma seperated string into a string array * Converts a comma separated string into a string array
* *
* @param val Comma seperated string * @param val Comma separated string
*/ */
export function toArray(val: string): string[] { export function toArray(val: string): string[] {
return val.split(','); return val.split(',');
} }
/** /**
* Creates the full name of a lightweight type recursivly * Creates the full name of a lightweight type recursively
* *
* @param type Type to get the full name of * @param type Type to get the full name of
*/ */

View File

@@ -51,6 +51,9 @@ const indexableTag = 'indexable';
const aggregatableTag = 'aggregatable'; const aggregatableTag = 'aggregatable';
const aggregatableTagParameterGlobal = 'global'; const aggregatableTagParameterGlobal = 'global';
// clamp printed object to 1000 chars to keep error messages readable
const maxErrorObjectChars = 1000;
let ignoredTagsList = ['indexable', 'validatable']; let ignoredTagsList = ['indexable', 'validatable'];
/** /**
@@ -91,7 +94,7 @@ export function getAllIndexableInterfaces(projectReflection: ProjectReflection):
} }
/** /**
* Composes error messages, that are readable and contain a certain minumum of information * Composes error messages, that are readable and contain a certain minimum of information
* *
* @param path the path where the error took place * @param path the path where the error took place
* @param topTypeName the name of the SCThingType * @param topTypeName the name of the SCThingType
@@ -100,14 +103,27 @@ export function getAllIndexableInterfaces(projectReflection: ProjectReflection):
* @param message the error message * @param message the error message
*/ */
function composeErrorMessage(path: string, topTypeName: string, typeName: string, object: string, message: string) { function composeErrorMessage(path: string, topTypeName: string, typeName: string, object: string, message: string) {
const error = `At "${topTypeName}::${path.substr(0, path.length - 1)}" for ${typeName} "${object}": ${message}`; const error = `At "${topTypeName}::${path.substr(0, path.length - 1)}" for ${typeName} "${trimString(object, maxErrorObjectChars)}": ${message}`;
errors.push(error); errors.push(error);
if (showErrors) { if (showErrors) {
// tslint:disable-next-line:no-floating-promises // tslint:disable-next-line:no-floating-promises
Logger.error(error); Logger.error(error)
.then();
} }
} }
/**
* Trims a string to a readable size and appends "..."
*
* @param value the string to trim
* @param maxLength the maximum allowed length before it is clamped
*/
function trimString(value: string, maxLength: number): string {
return value.length > maxLength ?
`${value.substring(0, maxLength)}...` :
value;
}
/** /**
* Gets the Reflections and names for Generics in a ReferenceType of a DeclarationReflection * Gets the Reflections and names for Generics in a ReferenceType of a DeclarationReflection
* *
@@ -116,6 +132,7 @@ function composeErrorMessage(path: string, topTypeName: string, typeName: string
* *
* @param type the ReferenceType of a DeclarationReflection * @param type the ReferenceType of a DeclarationReflection
* @param out the previous reflection, it then overrides all parameters or keeps old ones * @param out the previous reflection, it then overrides all parameters or keeps old ones
* @param topTypeName the name of the object, with which something went wrong
* @param path the current path to the object we are in * @param path the current path to the object we are in
* @param tags any tags attached to the type * @param tags any tags attached to the type
*/ */
@@ -161,7 +178,7 @@ function getReflectionGeneric(type: ReferenceType,
function handleExternalType(ref: ReferenceType, generics: Map<string, ElasticsearchValue>, function handleExternalType(ref: ReferenceType, generics: Map<string, ElasticsearchValue>,
path: string, topTypeName: string, tags: CommentTag[]): ElasticsearchValue { path: string, topTypeName: string, tags: CommentTag[]): ElasticsearchValue {
for (const premap in premaps) { for (const premap in premaps) {
if (premap === ref.name) { if (premap === ref.name && premaps.hasOwnProperty(premap)) {
return readFieldTags(premaps[premap], path, topTypeName, tags); return readFieldTags(premaps[premap], path, topTypeName, tags);
} }
} }
@@ -619,14 +636,16 @@ export function generateTemplate(projectReflection: ProjectReflection,
.replace('"', ''); .replace('"', '');
} else { } else {
// tslint:disable-next-line:no-floating-promises // tslint:disable-next-line:no-floating-promises
Logger.error('Your input files seem to be incorrect, or there is a major bug in the mapping generator.'); Logger.error('Your input files seem to be incorrect, or there is a major bug in the mapping generator.')
.then();
} }
} else if (typeObject.type instanceof StringLiteralType) { } else if (typeObject.type instanceof StringLiteralType) {
Logger.warn(`The interface ${_interface.name} uses a string literal as type, please use SCThingType.`); Logger.warn(`The interface ${_interface.name} uses a string literal as type, please use SCThingType.`);
typeName = typeObject.type.value; typeName = typeObject.type.value;
} else { } else {
// tslint:disable-next-line:no-floating-promises // tslint:disable-next-line:no-floating-promises
Logger.error(`The interface ${_interface.name} is required to use an SCThingType as a type, please do so.`); Logger.error(`The interface ${_interface.name} is required to use an SCThingType as a type, please do so.`)
.then();
} }
// filter out // filter out

View File

@@ -435,9 +435,9 @@ function topologicalSort(modules: JavaScriptModule[]): JavaScriptModule[] {
// add all edges // add all edges
modules.forEach((module) => { modules.forEach((module) => {
module.dependencies.forEach((dependenciePath) => { module.dependencies.forEach((dependencyPath) => {
// add edge from dependency to our module // add edge from dependency to our module
edges.push([basename(dependenciePath), module.name]); edges.push([basename(dependencyPath), module.name]);
}); });
nodes.push(module.name); nodes.push(module.name);

View File

@@ -13,9 +13,9 @@
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import Ajv from 'ajv'; import Ajv from 'ajv';
import {Schema as JSONSchema} from 'jsonschema'; import {JSONSchema7 as JSONSchema} from 'json-schema';
import {join} from 'path'; import {join} from 'path';
import {DEFAULT_CONFIG, Definition, SchemaGenerator} from 'ts-json-schema-generator'; import {Config, DEFAULT_CONFIG, Definition, SchemaGenerator} from 'ts-json-schema-generator';
import {createFormatter} from 'ts-json-schema-generator/dist/factory/formatter'; import {createFormatter} from 'ts-json-schema-generator/dist/factory/formatter';
import {createParser} from 'ts-json-schema-generator/dist/factory/parser'; import {createParser} from 'ts-json-schema-generator/dist/factory/parser';
import {createProgram} from 'ts-json-schema-generator/dist/factory/program'; import {createProgram} from 'ts-json-schema-generator/dist/factory/program';
@@ -45,13 +45,11 @@ export class Converter {
*/ */
constructor(path: string) { constructor(path: string) {
// set config for schema generator // set config for schema generator
const config = { const config: Config = {
...DEFAULT_CONFIG, ...DEFAULT_CONFIG,
// expose: 'exported' as any,
// jsDoc: 'extended' as any,
path: join(getTsconfigPath(path), 'tsconfig.json'),
sortProps: true, sortProps: true,
topRef: false, topRef: false,
tsconfig: join(getTsconfigPath(path), 'tsconfig.json'),
type: 'SC', type: 'SC',
}; };
@@ -82,7 +80,7 @@ export class Converter {
const schema: JSONSchema = this.generator.createSchema(type); const schema: JSONSchema = this.generator.createSchema(type);
// set id of schema // set id of schema
schema.id = `https://core.stapps.tu-berlin.de/v${version}/lib/schema/${type}.json`; schema.$id = `https://core.stapps.tu-berlin.de/v${version}/lib/schema/${type}.json`;
if (isSchemaWithDefinitions(schema)) { if (isSchemaWithDefinitions(schema)) {
const selfReference = { const selfReference = {
@@ -92,7 +90,7 @@ export class Converter {
delete selfReference.$schema; delete selfReference.$schema;
delete selfReference.definitions; delete selfReference.definitions;
delete selfReference.id; delete selfReference.$id;
// add self reference to definitions // add self reference to definitions
schema.definitions[`SC${type}`] = { schema.definitions[`SC${type}`] = {

View File

@@ -102,7 +102,7 @@ export async function createDiagramFromString(
const url = `${plantUmlBaseURL}/svg/${plantUMLCode}`; const url = `${plantUmlBaseURL}/svg/${plantUMLCode}`;
let response; let response;
try { try {
response = await request.get(url); response = await request.default.get(url);
const httpOK = 200; const httpOK = 200;
if (response.statusCode !== httpOK) { if (response.statusCode !== httpOK) {
await Logger.error(`Plantuml Server responded with an error.\n${response.statusMessage}`); await Logger.error(`Plantuml Server responded with an error.\n${response.statusMessage}`);

View File

@@ -15,9 +15,11 @@
import {Logger} from '@openstapps/logger'; import {Logger} from '@openstapps/logger';
import { import {
ArrayType, ArrayType,
ConditionalType,
DeclarationReflection, DeclarationReflection,
IntrinsicType, IntrinsicType,
ProjectReflection, ProjectReflection,
QueryType,
ReferenceType, ReferenceType,
ReflectionKind, ReflectionKind,
ReflectionType, ReflectionType,
@@ -248,10 +250,43 @@ function readTypeInformation(declarationType: Type): LightweightType {
if (declarationType instanceof UnionType) { if (declarationType instanceof UnionType) {
return readAsUnionType(declarationType); return readAsUnionType(declarationType);
} }
if (declarationType instanceof QueryType) {
return readAsQueryType(declarationType);
}
if (declarationType instanceof ConditionalType) {
return readAsConditionalType(declarationType);
}
throw new Error(`Could not read type ${declarationType.type}`); throw new Error(`Could not read type ${declarationType.type}`);
} }
/**
* Conversion method for ConditionalTypes
*
* @param _type Type to be converted
*/
function readAsConditionalType(_type: ConditionalType): LightweightType {
const returnType: LightweightType = new LightweightType();
returnType.specificationTypes = [];
returnType.name = getFullTypeName(returnType);
returnType.isUnion = true;
return returnType;
}
/**
* Conversion method for QueryTypes
*
* @param type Type to be converted
*/
function readAsQueryType(type: QueryType): LightweightType {
const out = readAsReferenceType(type.queryType);
out.isReference = true;
return out;
}
/** /**
* Conversion method for IntrinsicType's * Conversion method for IntrinsicType's
* *
@@ -315,10 +350,8 @@ function readAsReferenceType(type: ReferenceType): LightweightType {
// interfaces and classes in a type are a sink, since their declaration are defined elsewhere // interfaces and classes in a type are a sink, since their declaration are defined elsewhere
if ( if (
typeof tempTypeReflection.kindString !== 'undefined' && typeof tempTypeReflection.kindString !== 'undefined' &&
['Interface', 'Class', 'Enumeration', 'Type alias'].indexOf( ['Interface', 'Class', 'Enumeration', 'Type alias'].includes(
tempTypeReflection.kindString, tempTypeReflection.kindString)) {
) > -1
) {
returnType.isReference = true; returnType.isReference = true;
} }
} }

View File

@@ -14,15 +14,20 @@
*/ */
import {asyncPool} from '@krlwlfrt/async-pool'; import {asyncPool} from '@krlwlfrt/async-pool';
import {Logger} from '@openstapps/logger'; import {Logger} from '@openstapps/logger';
import Ajv from 'ajv';
import betterAjvErrors from 'better-ajv-errors';
import {PathLike} from 'fs'; import {PathLike} from 'fs';
import {Schema, Validator as JSONSchemaValidator, ValidatorResult} from 'jsonschema'; import {JSONSchema7} from 'json-schema';
import * as mustache from 'mustache'; import * as mustache from 'mustache';
import {basename, join, resolve} from 'path'; import {basename, join, resolve} from 'path';
import {Schema} from 'ts-json-schema-generator';
import { import {
ExpectableValidationErrors, ExpectedValidationErrors,
globPromisified, globPromisified,
isThingWithType, isThingWithType,
readFilePromisified, readFilePromisified,
ValidationError,
ValidationResult,
writeFilePromisified, writeFilePromisified,
} from './common'; } from './common';
@@ -30,21 +35,24 @@ import {
* StAppsCore validator * StAppsCore validator
*/ */
export class Validator { export class Validator {
/**
* JSON Schema Validator
*/
private readonly ajv = Ajv({verbose: true, jsonPointers: true, extendRefs: true});
/** /**
* Map of schema names to schemas * Map of schema names to schemas
*/ */
private readonly schemas: { [type: string]: Schema; } = {}; 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; private ajvValidateWrapper(schema: JSONSchema7, instance: unknown): ValidationResult {
return fromAjvResult(this.ajv.validate(schema, instance), schema, instance, this.ajv);
/**
* Create a new validator
*/
constructor() {
this.validator = new JSONSchemaValidator();
} }
/** /**
@@ -65,13 +73,9 @@ export class Validator {
await asyncPool(2, schemaFiles, async (file: string) => { await asyncPool(2, schemaFiles, async (file: string) => {
// read schema file // read schema file
const buffer = await readFilePromisified(file); const buffer = await readFilePromisified(file);
const schema = JSON.parse(buffer.toString());
// add schema to validator
this.validator.addSchema(schema);
// add schema to map // 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.`); Logger.info(`Added ${file} to validator.`);
}); });
@@ -85,10 +89,10 @@ export class Validator {
* @param instance Instance to validate * @param instance Instance to validate
* @param schema Name of schema to validate instance against or the schema itself * @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 (typeof schema === 'undefined') {
if (isThingWithType(instance)) { 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 // 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) => { .map((part: string) => {
@@ -108,20 +112,61 @@ export class Validator {
throw new Error(`No schema available for ${schema}.`); 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 * 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 schemaDir The directory where the JSON schema files are
* @param resourcesDir The directory where the test 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 // instantiate new validator
const v = new Validator(); const v = new Validator();
await v.addSchemas(schemaDir); 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.`); Logger.log(`Found ${testFiles.length} file(s) to test.`);
// map of errors per file // map of errors per file
const errors: ExpectableValidationErrors = {}; const errors: ExpectedValidationErrors = {};
// tslint:disable-next-line:no-magic-numbers - iterate over files to test // tslint:disable-next-line:no-magic-numbers - iterate over files to test
await asyncPool(2, testFiles, async (testFile: string) => { await asyncPool(2, testFiles, async (testFile: string) => {
@@ -162,13 +207,11 @@ export async function validateFiles(schemaDir: string, resourcesDir: string): Pr
// iterate over errors // iterate over errors
for (const error of result.errors) { for (const error of result.errors) {
// get idx of expected error const errorIndex = expectedErrors.indexOf(error.name);
const errorIdx = expectedErrors.indexOf(error.name);
let expected = false; let expected = false;
if (errorIdx >= 0) { if (errorIndex >= 0) {
// remove from list of expected errors expectedErrors.splice(errorIndex, 1);
expectedErrors.splice(errorIdx, 1);
expected = true; expected = true;
} else { } else {
unexpectedErrors++; unexpectedErrors++;
@@ -188,14 +231,14 @@ export async function validateFiles(schemaDir: string, resourcesDir: string): Pr
await Logger.error(`Extraneous expected error '${error}' in ${testFile}.`); await Logger.error(`Extraneous expected error '${error}' in ${testFile}.`);
errors[testFileName].push({ errors[testFileName].push({
argument: false, dataPath: 'undefined',
expected: false, expected: false,
instance: testDescription.instance, instance: undefined,
message: `expected error ${error} did not occur`, // instance: testDescription.instance,
message: 'undefined',
name: `expected ${error}`, name: `expected ${error}`,
property: 'unknown', schemaPath: 'undefined',
schema: 'undefined', suggestion: 'undefined',
stack: 'undefined',
}); });
} }
} else if (unexpectedErrors === 0) { } 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 reportPath Path to write report to
* @param errors Errors that occurred in validation * @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')); let buffer = await readFilePromisified(resolve(__dirname, '..', 'resources', 'file.html.mustache'));
const fileTemplate = buffer.toString(); const fileTemplate = buffer.toString();
@@ -229,15 +272,16 @@ export async function writeReport(reportPath: PathLike, errors: ExpectableValida
let fileOutput = ''; let fileOutput = '';
errors[fileName].forEach((error, idx) => { errors[fileName].forEach((error, idx) => {
fileOutput += mustache.render(errorTemplate, { fileOutput += mustache.render(errorTemplate, {
idx: idx + 1, idx: idx + 1,
// tslint:disable-next-line:no-magic-numbers // tslint:disable-next-line:no-magic-numbers
instance: JSON.stringify(error.instance, null, 2), instance: JSON.stringify(error.instance, null, 2),
message: error.message, message: error.message,
property: error.property, name: error.name,
// tslint:disable-next-line:no-magic-numbers schemaPath: error.schemaPath,
schema: JSON.stringify(error.schema, null, 2),
status: (error.expected) ? 'alert-success' : 'alert-danger', status: (error.expected) ? 'alert-success' : 'alert-danger',
suggestion: error.suggestion,
}); });
}); });

View File

@@ -73,11 +73,11 @@ export class CreateDiagramSpec {
let fileName = await createDiagram(this.definitions, this.plantUmlConfig, "http://plantuml:8080"); let fileName = await createDiagram(this.definitions, this.plantUmlConfig, "http://plantuml:8080");
let filePath = resolve(__dirname, '..', fileName); let filePath = resolve(__dirname, '..', fileName);
expect(await existsSync(filePath)).to.equal(true); expect(await existsSync(filePath)).to.equal(true);
await unlinkSync(fileName); await unlinkSync(fileName);
this.plantUmlConfig.showAssociations = false; this.plantUmlConfig.showAssociations = false;
this.plantUmlConfig.showInheritance = false;
this.plantUmlConfig.showInheritance = false;
fileName = await createDiagram(this.definitions, this.plantUmlConfig, "http://plantuml:8080"); fileName = await createDiagram(this.definitions, this.plantUmlConfig, "http://plantuml:8080");
filePath = resolve(__dirname, '..', fileName); filePath = resolve(__dirname, '..', fileName);
expect(await existsSync(filePath)).to.equal(true); expect(await existsSync(filePath)).to.equal(true);

View File

@@ -22,17 +22,21 @@ import {ElasticsearchDataType} from '../../../../src/mappings/definitions/typema
* @indexable * @indexable
*/ */
export interface ObjectUnion { export interface ObjectUnion {
foo: Bar | Baz; foo: Boo | Buu;
type: ThingType.ObjectUnion type: ThingType.ObjectUnion
} }
interface Bar { // we can't name them Bar or Baz, look here for more info:
// https://gitlab.com/openstapps/core-tools/-/issues/48
// or here
// https://github.com/TypeStrong/typedoc/issues/1373
interface Boo {
a: boolean; a: boolean;
intersection: string; intersection: string;
} }
interface Baz { interface Buu {
b: number; b: number;
intersection: string; intersection: string;
} }

View File

@@ -27,7 +27,6 @@ import {genericTest} from './mapping-model/mappings/src/generics';
import {nestedTest} from './mapping-model/mappings/src/nested'; import {nestedTest} from './mapping-model/mappings/src/nested';
import {indexSignatureTest} from './mapping-model/mappings/src/index-signature'; import {indexSignatureTest} from './mapping-model/mappings/src/index-signature';
import {impossibleUnionTest} from './mapping-model/mappings/src/impossible-union'; import {impossibleUnionTest} from './mapping-model/mappings/src/impossible-union';
import {typeQueryTest} from './mapping-model/mappings/src/type-query';
import {objectUnionTest} from './mapping-model/mappings/src/object-union'; import {objectUnionTest} from './mapping-model/mappings/src/object-union';
import {sortableTagTest} from './mapping-model/mappings/src/sortable-tag'; import {sortableTagTest} from './mapping-model/mappings/src/sortable-tag';
import {enumTest} from './mapping-model/mappings/src/enum'; import {enumTest} from './mapping-model/mappings/src/enum';
@@ -91,10 +90,12 @@ export class MappingSpec {
magAppInstance.testInterfaceAgainstPath(objectUnionTest); magAppInstance.testInterfaceAgainstPath(objectUnionTest);
} }
/*
https://gitlab.com/openstapps/core-tools/-/issues/47
@test @test
async 'Type queries should work'() { async 'Type queries should work'() {
magAppInstance.testInterfaceAgainstPath(typeQueryTest); magAppInstance.testInterfaceAgainstPath(typeQueryTest);
} }*/
@test @test
async 'Impossible union should cause an error'() { async 'Impossible union should cause an error'() {

View File

@@ -16,9 +16,7 @@ import {LightweightClassDefinition} from '../../src/uml/model/lightweight-class-
import {LightweightDefinition} from '../../src/uml/model/lightweight-definition'; import {LightweightDefinition} from '../../src/uml/model/lightweight-definition';
import {LightweightEnumDefinition} from '../../src/uml/model/lightweight-enum-definition'; import {LightweightEnumDefinition} from '../../src/uml/model/lightweight-enum-definition';
export const generatedModel: Array< export const generatedModel: Array<LightweightDefinition | LightweightClassDefinition | LightweightEnumDefinition> = [
LightweightDefinition | LightweightClassDefinition | LightweightEnumDefinition
> = [
{ {
name: 'TestClass', name: 'TestClass',
type: 'class', type: 'class',
@@ -164,31 +162,6 @@ export const generatedModel: Array<
optional: true, optional: true,
inherited: false, inherited: false,
type: { type: {
hasTypeInformation: false,
isArray: false,
isLiteral: false,
isPrimitive: false,
isReference: false,
isReflection: false,
isTyped: false,
isTypeParameter: false,
isUnion: true,
specificationTypes: [
{
hasTypeInformation: true,
isArray: false,
isLiteral: false,
isPrimitive: true,
isReference: false,
isReflection: false,
isTyped: false,
isTypeParameter: false,
isUnion: false,
specificationTypes: [],
genericsTypes: [],
name: 'undefined',
},
{
hasTypeInformation: false, hasTypeInformation: false,
isArray: false, isArray: false,
isLiteral: false, isLiteral: false,
@@ -202,10 +175,6 @@ export const generatedModel: Array<
genericsTypes: [], genericsTypes: [],
name: 'object', name: 'object',
}, },
],
genericsTypes: [],
name: '',
},
}, },
{ {
name: 'inputType', name: 'inputType',
@@ -279,31 +248,6 @@ export const generatedModel: Array<
optional: true, optional: true,
inherited: false, inherited: false,
type: { type: {
hasTypeInformation: false,
isArray: false,
isLiteral: false,
isPrimitive: false,
isReference: false,
isReflection: false,
isTyped: false,
isTypeParameter: false,
isUnion: true,
specificationTypes: [
{
hasTypeInformation: true,
isArray: false,
isLiteral: false,
isPrimitive: true,
isReference: false,
isReflection: false,
isTyped: false,
isTypeParameter: false,
isUnion: false,
specificationTypes: [],
genericsTypes: [],
name: 'undefined',
},
{
hasTypeInformation: true, hasTypeInformation: true,
isArray: false, isArray: false,
isLiteral: false, isLiteral: false,
@@ -317,10 +261,6 @@ export const generatedModel: Array<
genericsTypes: [], genericsTypes: [],
name: 'number', name: 'number',
}, },
],
genericsTypes: [],
name: '',
},
}, },
{ {
name: 'test1', name: 'test1',
@@ -506,7 +446,7 @@ export const generatedModel: Array<
isArray: false, isArray: false,
isLiteral: false, isLiteral: false,
isPrimitive: false, isPrimitive: false,
isReference: false, isReference: true,
isReflection: false, isReflection: false,
isTyped: false, isTyped: false,
isTypeParameter: false, isTypeParameter: false,

View File

@@ -33,7 +33,8 @@ export class SchemaSpec {
const schema = converter.getSchema('Foo', '0.0.1'); const schema = converter.getSchema('Foo', '0.0.1');
expect(schema).to.be.deep.equal({ expect(schema).to.be.deep.equal({
$schema: 'http://json-schema.org/draft-06/schema#', $id: 'https://core.stapps.tu-berlin.de/v0.0.1/lib/schema/Foo.json',
$schema: 'http://json-schema.org/draft-07/schema#',
additionalProperties: false, additionalProperties: false,
definitions: { definitions: {
FooType: { FooType: {
@@ -67,7 +68,6 @@ export class SchemaSpec {
}, },
}, },
description: 'This is a simple interface declaration for\ntesting the schema generation and validation.', description: 'This is a simple interface declaration for\ntesting the schema generation and validation.',
id: 'https://core.stapps.tu-berlin.de/v0.0.1/lib/schema/Foo.json',
properties: { properties: {
lorem: { lorem: {
description: 'Dummy parameter', description: 'Dummy parameter',

View File

@@ -15,7 +15,7 @@
import {Logger} from '@openstapps/logger'; import {Logger} from '@openstapps/logger';
import {expect} from 'chai'; import {expect} from 'chai';
import {existsSync, mkdirSync, writeFileSync} from 'fs'; import {existsSync, mkdirSync, writeFileSync} from 'fs';
import {Schema} from 'jsonschema'; import {JSONSchema7 as Schema} from 'json-schema';
import {slow, suite, test, timeout} from 'mocha-typescript'; import {slow, suite, test, timeout} from 'mocha-typescript';
import {join} from 'path'; import {join} from 'path';
import rimraf from 'rimraf'; import rimraf from 'rimraf';