mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-02-25 02:12:14 +00:00
refactor: move core-tools to monorepo
This commit is contained in:
248
packages/core-tools/src/cli.ts
Normal file
248
packages/core-tools/src/cli.ts
Normal file
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {Logger} from '@openstapps/logger';
|
||||
import {Command} from 'commander';
|
||||
import {existsSync, readFileSync, writeFileSync} from 'fs';
|
||||
import {copy} from 'fs-extra';
|
||||
import path from 'path';
|
||||
import {mkdirPromisified, readFilePromisified} from './common';
|
||||
import {lightweightDefinitionsFromPath, lightweightProjectFromPath} from './easy-ast/easy-ast';
|
||||
import {pack} from './pack';
|
||||
import {openapi3Template} from './resources/openapi-303-template';
|
||||
import {gatherRouteInformation, generateOpenAPIForRoute} from './routes';
|
||||
import {Converter, getValidatableTypesInPath} from './schema';
|
||||
import {createDiagram, createDiagramFromString} from './uml/create-diagram';
|
||||
import {UMLConfig} from './uml/uml-config';
|
||||
import {capitalize} from './util/string';
|
||||
import {validateFiles, writeReport} from './validate';
|
||||
|
||||
// handle unhandled promise rejections
|
||||
process.on('unhandledRejection', async (reason: unknown) => {
|
||||
if (reason instanceof Error) {
|
||||
await Logger.error(reason.message);
|
||||
Logger.info(reason.stack);
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
const commander = new Command('openstapps-core-tools');
|
||||
|
||||
// eslint-disable-next-line unicorn/prefer-module
|
||||
commander.version(JSON.parse(readFileSync(path.resolve(__dirname, '..', 'package.json')).toString()).version);
|
||||
|
||||
commander.command('prototype <srcBundle> <out>').action(async (sourcePath, out) => {
|
||||
const files = lightweightProjectFromPath(sourcePath);
|
||||
writeFileSync(path.resolve(out), JSON.stringify(files, undefined, 2));
|
||||
});
|
||||
|
||||
commander
|
||||
.command('openapi <srcPath> <outDirPath>')
|
||||
.action(async (relativeSourceBundlePath, relativeOutDirectoryPath) => {
|
||||
// get absolute paths
|
||||
const sourcePath = path.resolve(relativeSourceBundlePath);
|
||||
const outDirectoryPath = path.resolve(relativeOutDirectoryPath);
|
||||
const outDirectorySchemasPath = path.join(outDirectoryPath, 'schemas');
|
||||
|
||||
// get information about routes
|
||||
const routes = await gatherRouteInformation(sourcePath);
|
||||
routes.sort((a, b) => a.route.urlPath.localeCompare(b.route.urlPath));
|
||||
|
||||
// change url path parameters to openapi notation
|
||||
for (const routeWithMetaInformation of routes) {
|
||||
routeWithMetaInformation.route.urlPath = routeWithMetaInformation.route.urlPath.replace(
|
||||
/:\w+/g,
|
||||
(match: string) => `{${match.replace(':', '')}}`,
|
||||
);
|
||||
}
|
||||
|
||||
// keep openapi tags for routes that actually share url fragments
|
||||
let tagsToKeep = routes.map(routeWithMetaInformation =>
|
||||
capitalize(routeWithMetaInformation.route.urlPath.split('/')[1]),
|
||||
);
|
||||
tagsToKeep = tagsToKeep.filter(
|
||||
(element, i, array) => array.indexOf(element) === i && array.lastIndexOf(element) !== i,
|
||||
);
|
||||
|
||||
// initialize json output
|
||||
const output = openapi3Template;
|
||||
|
||||
// names of the schemas to copy
|
||||
const schemasToCopy: string[] = [];
|
||||
|
||||
// generate documentation for all routes
|
||||
for (const routeWithMetaInformation of routes) {
|
||||
routeWithMetaInformation.tags = [capitalize(routeWithMetaInformation.route.urlPath.split('/')[1])];
|
||||
|
||||
output.paths[routeWithMetaInformation.route.urlPath] = generateOpenAPIForRoute(
|
||||
routeWithMetaInformation,
|
||||
path.relative(relativeOutDirectoryPath, outDirectorySchemasPath),
|
||||
schemasToCopy,
|
||||
tagsToKeep,
|
||||
);
|
||||
}
|
||||
|
||||
// copy schema json schema files
|
||||
try {
|
||||
if (!existsSync(outDirectorySchemasPath)) {
|
||||
await mkdirPromisified(outDirectorySchemasPath, {
|
||||
recursive: true,
|
||||
});
|
||||
}
|
||||
for (const fileName of schemasToCopy) {
|
||||
await copy(
|
||||
path.join(sourcePath, 'schema', `${fileName}.json`),
|
||||
path.join(outDirectorySchemasPath, `${fileName}.json`),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
await Logger.error(error);
|
||||
process.exit(-2);
|
||||
}
|
||||
|
||||
// write openapi object to file (prettified)
|
||||
writeFileSync(path.join(outDirectoryPath, 'openapi.json'), JSON.stringify(output, undefined, 2));
|
||||
|
||||
Logger.ok(`OpenAPI representation resources written to ${outDirectoryPath} .`);
|
||||
});
|
||||
|
||||
commander.command('schema <srcPath> <schemaPath>').action(async (relativeSourcePath, relativeSchemaPath) => {
|
||||
// get absolute paths
|
||||
const absoluteSourcePath = path.resolve(relativeSourcePath);
|
||||
const schemaPath = path.resolve(relativeSchemaPath);
|
||||
|
||||
// initialize new core converter
|
||||
const coreConverter = new Converter(absoluteSourcePath);
|
||||
|
||||
// get validatable types
|
||||
const validatableTypes = getValidatableTypesInPath(absoluteSourcePath);
|
||||
|
||||
Logger.info(`Found ${validatableTypes.length} type(s) to generate schemas for.`);
|
||||
|
||||
await mkdirPromisified(schemaPath, {
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
Logger.info(`Trying to find a package.json for ${absoluteSourcePath}.`);
|
||||
|
||||
let packagePath = absoluteSourcePath;
|
||||
// TODO: this check should be less ugly! --- What is this doing anyway?
|
||||
while (!existsSync(path.join(packagePath, 'package.json')) && packagePath.length > 5) {
|
||||
packagePath = path.resolve(packagePath, '..');
|
||||
}
|
||||
|
||||
const corePackageJsonPath = path.join(packagePath, 'package.json');
|
||||
|
||||
Logger.info(`Using ${corePackageJsonPath} to determine version for schemas.`);
|
||||
|
||||
const buffer = await readFilePromisified(corePackageJsonPath);
|
||||
const corePackageJson = JSON.parse(buffer.toString());
|
||||
const coreVersion = corePackageJson.version;
|
||||
|
||||
Logger.log(`Using ${coreVersion} as version for schemas.`);
|
||||
|
||||
// generate and write JSONSchema files for validatable types
|
||||
for (const type of validatableTypes) {
|
||||
const schema = coreConverter.getSchema(type, coreVersion);
|
||||
|
||||
const stringifiedSchema = JSON.stringify(schema, undefined, 2);
|
||||
|
||||
const file = path.join(schemaPath, `${type}.json`);
|
||||
|
||||
// write schema to file
|
||||
writeFileSync(file, stringifiedSchema);
|
||||
|
||||
Logger.info(`Generated schema for ${type} and saved to ${file}.`);
|
||||
}
|
||||
|
||||
Logger.ok(`Generated schemas for ${validatableTypes.length} type(s).`);
|
||||
});
|
||||
|
||||
commander
|
||||
.command('validate <schemaPath> <testPath> [reportPath]')
|
||||
.action(async (relativeSchemaPath, relativeTestPath, relativeReportPath) => {
|
||||
// get absolute paths
|
||||
const schemaPath = path.resolve(relativeSchemaPath);
|
||||
const testPath = path.resolve(relativeTestPath);
|
||||
|
||||
const errorsPerFile = await validateFiles(schemaPath, testPath);
|
||||
|
||||
let unexpected = false;
|
||||
for (const file in errorsPerFile) {
|
||||
if (!errorsPerFile.hasOwnProperty(file)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
unexpected = unexpected || errorsPerFile[file].some(error => !error.expected);
|
||||
}
|
||||
|
||||
if (relativeReportPath !== undefined) {
|
||||
const reportPath = path.resolve(relativeReportPath);
|
||||
await writeReport(reportPath, errorsPerFile);
|
||||
}
|
||||
|
||||
if (unexpected) {
|
||||
await Logger.error('Unexpected errors occurred during validation');
|
||||
process.exit(1);
|
||||
} else {
|
||||
Logger.ok('Successfully finished validation.');
|
||||
}
|
||||
});
|
||||
|
||||
commander.command('pack').action(async () => {
|
||||
await pack();
|
||||
});
|
||||
|
||||
commander
|
||||
.command('plantuml <srcPath> <plantumlserver>')
|
||||
.option('--definitions <definitions>', 'Shows these specific definitions (class, interface or enum)', it =>
|
||||
it.split(','),
|
||||
)
|
||||
.option('--showAssociations', 'Shows associations of definitions')
|
||||
.option('--showInheritance', 'Shows extensions and implementations of definitions')
|
||||
.option('--showEnumValues', 'Show enum values')
|
||||
.option('--showProperties', 'Show attributes')
|
||||
.option('--showInheritedProperties', 'Shows inherited attributes, needs --showProperties')
|
||||
.option('--showOptionalProperties', 'Shows optional attributes and relations, needs --showProperties')
|
||||
.option('--excludeExternals', 'Exclude external definitions')
|
||||
.option('--outputFileName <fileName>', 'Defines the filename of the output')
|
||||
.action(async (relativeSourcePath, plantumlServer, options) => {
|
||||
const plantUmlConfig: UMLConfig = {
|
||||
definitions: options.definitions === undefined ? [] : options.definitions,
|
||||
showAssociations: options.showAssociations === undefined ? false : options.showAssociations,
|
||||
showEnumValues: options.showEnumValues === undefined ? false : options.showEnumValues,
|
||||
showInheritance: options.showInheritance === undefined ? false : options.showInheritance,
|
||||
showInheritedProperties:
|
||||
options.showInheritedProperties === undefined ? false : options.showInheritedProperties,
|
||||
showOptionalProperties:
|
||||
options.showOptionalProperties === undefined ? false : options.showOptionalProperties,
|
||||
showProperties: options.showProperties === undefined ? false : options.showProperties,
|
||||
};
|
||||
if (options.outputFileName !== undefined) {
|
||||
plantUmlConfig.outputFileName = options.outputFileName;
|
||||
}
|
||||
|
||||
Logger.log(`PlantUML options: ${JSON.stringify(plantUmlConfig)}`);
|
||||
|
||||
await createDiagram(lightweightDefinitionsFromPath(relativeSourcePath), plantUmlConfig, plantumlServer);
|
||||
});
|
||||
|
||||
commander
|
||||
.command('plantuml-file <inputFile> <plantumlserver> [outputFile]')
|
||||
.action(async (file: string, plantumlServer: string, outputFile: string) => {
|
||||
const fileContent = readFileSync(path.resolve(file)).toString();
|
||||
await createDiagramFromString(fileContent, plantumlServer, outputFile);
|
||||
});
|
||||
|
||||
commander.parse(process.argv);
|
||||
56
packages/core-tools/src/common.ts
Normal file
56
packages/core-tools/src/common.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {Logger} from '@openstapps/logger';
|
||||
import {existsSync, mkdir, readFile, unlink, writeFile} from 'fs';
|
||||
import {Glob} from 'glob';
|
||||
import {platform} from 'os';
|
||||
import {promisify} from 'util';
|
||||
import path from 'path';
|
||||
|
||||
export const globPromisified = promisify(Glob);
|
||||
export const mkdirPromisified = promisify(mkdir);
|
||||
export const readFilePromisified = promisify(readFile);
|
||||
export const writeFilePromisified = promisify(writeFile);
|
||||
export const unlinkPromisified = promisify(unlink);
|
||||
|
||||
/**
|
||||
* Get path that contains a tsconfig.json
|
||||
*
|
||||
* @param startPath Path from where to start searching "upwards"
|
||||
*/
|
||||
export function getTsconfigPath(startPath: string): string {
|
||||
let tsconfigPath = startPath;
|
||||
|
||||
// see https://stackoverflow.com/questions/9652043/identifying-the-file-system-root-with-node-js
|
||||
const root = platform() === 'win32' ? process.cwd().split(path.sep)[0] : '/';
|
||||
|
||||
// repeat until a tsconfig.json is found
|
||||
while (!existsSync(path.join(tsconfigPath, 'tsconfig.json'))) {
|
||||
if (tsconfigPath === root) {
|
||||
throw new Error(
|
||||
`Reached file system root ${root} while searching for 'tsconfig.json' in ${startPath}!`,
|
||||
);
|
||||
}
|
||||
|
||||
// pop last directory
|
||||
const tsconfigPathParts = tsconfigPath.split(path.sep);
|
||||
tsconfigPathParts.pop();
|
||||
tsconfigPath = tsconfigPathParts.join(path.sep);
|
||||
}
|
||||
|
||||
Logger.info(`Using 'tsconfig.json' from ${tsconfigPath}.`);
|
||||
|
||||
return tsconfigPath;
|
||||
}
|
||||
143
packages/core-tools/src/easy-ast/ast-internal-util.ts
Normal file
143
packages/core-tools/src/easy-ast/ast-internal-util.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright (C) 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.
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {first, last, tail, filter} from 'lodash';
|
||||
import {
|
||||
ArrayTypeNode,
|
||||
ClassDeclaration,
|
||||
ClassElement,
|
||||
EnumDeclaration,
|
||||
Identifier,
|
||||
InterfaceDeclaration,
|
||||
isArrayTypeNode,
|
||||
isClassDeclaration,
|
||||
isComputedPropertyName,
|
||||
isEnumDeclaration,
|
||||
isInterfaceDeclaration,
|
||||
isPropertyDeclaration,
|
||||
isPropertySignature,
|
||||
isTypeAliasDeclaration,
|
||||
isTypeReferenceNode,
|
||||
NodeArray,
|
||||
PropertyDeclaration,
|
||||
PropertyName,
|
||||
PropertySignature,
|
||||
TypeAliasDeclaration,
|
||||
TypeElement,
|
||||
TypeNode,
|
||||
TypeReferenceNode,
|
||||
} from 'typescript';
|
||||
import * as ts from 'typescript';
|
||||
import {cleanupEmpty} from '../util/collections';
|
||||
import {LightweightComment} from './types/lightweight-comment';
|
||||
|
||||
/** @internal */
|
||||
export function extractComment(node: ts.Node): LightweightComment | undefined {
|
||||
const jsDocument = last(
|
||||
// @ts-expect-error jsDoc exists in reality
|
||||
node.jsDoc as
|
||||
| Array<{
|
||||
comment?: string;
|
||||
tags?: Array<{comment?: string; tagName?: {escapedText?: string}}>;
|
||||
}>
|
||||
| undefined,
|
||||
);
|
||||
const comment = jsDocument?.comment?.split('\n\n');
|
||||
|
||||
return jsDocument === undefined
|
||||
? undefined
|
||||
: cleanupEmpty({
|
||||
shortSummary: first(comment),
|
||||
description: tail(comment)?.join('\n\n'),
|
||||
tags: jsDocument?.tags?.map(tag =>
|
||||
cleanupEmpty({
|
||||
name: tag.tagName?.escapedText ?? 'UNRESOLVED_NAME',
|
||||
parameters: tag.comment?.split(' '),
|
||||
}),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function isProperty(
|
||||
node: ClassElement | TypeElement,
|
||||
): node is PropertyDeclaration | PropertySignature {
|
||||
return isPropertyDeclaration(node) || isPropertySignature(node);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function filterNodeTo<T extends ts.Node, S extends T>(
|
||||
node: NodeArray<T>,
|
||||
check: (node: T) => node is S,
|
||||
): S[] {
|
||||
return filter(node, check);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function filterChildrenTo<T extends ts.Node>(node: ts.Node, check: (node: ts.Node) => node is T): T[] {
|
||||
const out: T[] = [];
|
||||
node.forEachChild(child => {
|
||||
if (check(child)) {
|
||||
out.push(child);
|
||||
}
|
||||
});
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function getModifiers(text: string, kind: string): string[] {
|
||||
return [
|
||||
...text
|
||||
.split(kind)[0]
|
||||
.split(/\s+/)
|
||||
.filter(it => it !== ''),
|
||||
kind,
|
||||
];
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function resolvePropertyName(name?: PropertyName): string | undefined {
|
||||
return name === undefined
|
||||
? undefined
|
||||
: isComputedPropertyName(name)
|
||||
? 'UNSUPPORTED_IDENTIFIER_TYPE'
|
||||
: name.getText();
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function resolveTypeName(type?: TypeNode): string | undefined {
|
||||
// @ts-expect-error typeName exists in reality
|
||||
return type?.typeName?.escapedText ?? type?.typeName?.right?.escapedText;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function isArrayLikeType(typeNode?: TypeNode): typeNode is ArrayTypeNode | TypeReferenceNode {
|
||||
return typeNode !== undefined && (isArrayTypeNode(typeNode) || isArrayReference(typeNode));
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function isArrayReference(typeNode: TypeNode): boolean {
|
||||
return isTypeReferenceNode(typeNode) && (typeNode.typeName as Identifier).escapedText === 'Array';
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function isClassLikeNode(node: ts.Node): node is ClassDeclaration | InterfaceDeclaration {
|
||||
return isClassDeclaration(node) || isInterfaceDeclaration(node);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function isEnumLikeNode(node: ts.Node): node is EnumDeclaration | TypeAliasDeclaration {
|
||||
return isEnumDeclaration(node) || isTypeAliasDeclaration(node);
|
||||
}
|
||||
83
packages/core-tools/src/easy-ast/ast-util.ts
Normal file
83
packages/core-tools/src/easy-ast/ast-util.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
/*
|
||||
* Copyright (C) 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.
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {flatMap, keyBy, isEmpty} from 'lodash';
|
||||
import {TypeFlags} from 'typescript';
|
||||
import {LightweightAliasDefinition} from './types/lightweight-alias-definition';
|
||||
import {LightweightClassDefinition} from './types/lightweight-class-definition';
|
||||
import {LightweightDefinition} from './types/lightweight-definition';
|
||||
import {LightweightDefinitionKind} from './types/lightweight-definition-kind';
|
||||
import {LightweightProject} from './types/lightweight-project';
|
||||
import {LightweightType} from './types/lightweight-type';
|
||||
|
||||
/**
|
||||
* Creates a printable name of a type
|
||||
*/
|
||||
export function expandTypeValue(type: LightweightType): string | undefined {
|
||||
if (type.isArray) {
|
||||
return `${type.value}[]`;
|
||||
}
|
||||
if (isStringLiteralType(type)) {
|
||||
return `'${type.value}'`;
|
||||
}
|
||||
if (isUnionOrIntersectionType(type)) {
|
||||
return type.specificationTypes?.map(expandTypeValue).join(isUnionType(type) ? ' | ' : ' & ');
|
||||
}
|
||||
if (isEmpty(type.genericsTypes)) {
|
||||
return `${type.value}<${type.genericsTypes?.map(expandTypeValue).join(', ')}>`;
|
||||
}
|
||||
|
||||
return type.value?.toString();
|
||||
}
|
||||
|
||||
export function definitionsOf(project: LightweightProject): Record<string, LightweightDefinition> {
|
||||
return keyBy(flatMap(project, Object.values), 'name');
|
||||
}
|
||||
|
||||
export function isPrimitiveType(type: {flags: TypeFlags}): boolean {
|
||||
return (type.flags & TypeFlags.NonPrimitive) === 0;
|
||||
}
|
||||
|
||||
export function isLiteralType(type: {flags: TypeFlags}): boolean {
|
||||
return (type.flags & TypeFlags.Literal) !== 0;
|
||||
}
|
||||
|
||||
export function isEnumLiteralType(type: {flags: TypeFlags}): boolean {
|
||||
return (type.flags & TypeFlags.EnumLiteral) !== 0;
|
||||
}
|
||||
|
||||
export function isStringLiteralType(type: {flags: TypeFlags}): boolean {
|
||||
return (type.flags & TypeFlags.StringLiteral) !== 0;
|
||||
}
|
||||
|
||||
export function isUnionOrIntersectionType(type: {flags: TypeFlags}): boolean {
|
||||
return (type.flags & TypeFlags.UnionOrIntersection) !== 0;
|
||||
}
|
||||
|
||||
export function isUnionType(type: {flags: TypeFlags}): boolean {
|
||||
return (type.flags & TypeFlags.Union) !== 0;
|
||||
}
|
||||
|
||||
export function isLightweightClass(node?: LightweightDefinition): node is LightweightClassDefinition {
|
||||
return node?.kind === LightweightDefinitionKind.CLASS_LIKE;
|
||||
}
|
||||
|
||||
export function isLightweightEnum(node?: LightweightDefinition): node is LightweightAliasDefinition {
|
||||
return node?.kind === LightweightDefinitionKind.ALIAS_LIKE;
|
||||
}
|
||||
|
||||
export function isTypeVariable(type: {flags: TypeFlags}): boolean {
|
||||
return (type.flags & TypeFlags.TypeVariable) !== 0;
|
||||
}
|
||||
273
packages/core-tools/src/easy-ast/easy-ast.ts
Normal file
273
packages/core-tools/src/easy-ast/easy-ast.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */
|
||||
/*
|
||||
* Copyright (C) 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.
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {flatMap, groupBy, keyBy, mapValues} from 'lodash';
|
||||
import * as ts from 'typescript';
|
||||
import {
|
||||
ClassDeclaration,
|
||||
ClassElement,
|
||||
EnumDeclaration,
|
||||
InterfaceDeclaration,
|
||||
isArrayTypeNode,
|
||||
isClassDeclaration,
|
||||
isEnumDeclaration,
|
||||
isIndexSignatureDeclaration,
|
||||
isPropertyDeclaration,
|
||||
isTypeLiteralNode,
|
||||
isTypeReferenceNode,
|
||||
NodeArray,
|
||||
Program,
|
||||
SourceFile,
|
||||
SyntaxKind,
|
||||
Type,
|
||||
TypeAliasDeclaration,
|
||||
TypeChecker,
|
||||
TypeElement,
|
||||
TypeFlags,
|
||||
TypeLiteralNode,
|
||||
TypeNode,
|
||||
} from 'typescript';
|
||||
import {cleanupEmpty, mapNotNil, rejectNil} from '../util/collections';
|
||||
import {expandPathToFilesSync} from '../util/io';
|
||||
import {
|
||||
extractComment,
|
||||
filterChildrenTo,
|
||||
filterNodeTo,
|
||||
getModifiers,
|
||||
isArrayLikeType,
|
||||
isClassLikeNode,
|
||||
isEnumLikeNode,
|
||||
isProperty,
|
||||
resolvePropertyName,
|
||||
resolveTypeName,
|
||||
} from './ast-internal-util';
|
||||
import {isEnumLiteralType, isTypeVariable} from './ast-util';
|
||||
import {LightweightAliasDefinition} from './types/lightweight-alias-definition';
|
||||
import {LightweightClassDefinition} from './types/lightweight-class-definition';
|
||||
import {LightweightDefinition} from './types/lightweight-definition';
|
||||
import {LightweightDefinitionKind} from './types/lightweight-definition-kind';
|
||||
import {LightweightProject} from './types/lightweight-project';
|
||||
import {LightweightType} from './types/lightweight-type';
|
||||
import path from 'path';
|
||||
import {LightweightProperty} from './types/lightweight-property';
|
||||
|
||||
/**
|
||||
* Convert a TypeScript project to a lightweight Type-AST representation of the project
|
||||
*
|
||||
* @param sourcePath either a directory or a set of input files
|
||||
* @param includeComments if comments should be included (default true)
|
||||
*/
|
||||
export function lightweightProjectFromPath(
|
||||
sourcePath: string | string[],
|
||||
includeComments = true,
|
||||
): LightweightProject {
|
||||
return new LightweightDefinitionBuilder(sourcePath, includeComments).convert();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a TypeScript project to a set of lightweight definition ASTs
|
||||
*
|
||||
* @param sourcePath either a directory or a set of input files
|
||||
* @param includeComments if comments should be included (default true)
|
||||
*/
|
||||
export function lightweightDefinitionsFromPath(
|
||||
sourcePath: string | string[],
|
||||
includeComments = true,
|
||||
): LightweightDefinition[] {
|
||||
return rejectNil(new LightweightDefinitionBuilder(sourcePath, includeComments).convertToList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the reflection model and converts it into a flatter, easier to handle model
|
||||
*/
|
||||
class LightweightDefinitionBuilder {
|
||||
readonly program: Program;
|
||||
|
||||
readonly sourceFiles: readonly SourceFile[];
|
||||
|
||||
readonly typeChecker: TypeChecker;
|
||||
|
||||
constructor(sourcePath: string | string[], readonly includeComments: boolean) {
|
||||
const rootNames = Array.isArray(sourcePath)
|
||||
? sourcePath
|
||||
: expandPathToFilesSync(path.resolve(sourcePath), file => file.endsWith('ts'));
|
||||
|
||||
this.program = ts.createProgram({
|
||||
rootNames: rootNames,
|
||||
options: {
|
||||
alwaysStrict: true,
|
||||
charset: 'utf8',
|
||||
declaration: true,
|
||||
esModuleInterop: true,
|
||||
experimentalDecorators: true,
|
||||
inlineSourceMap: true,
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
strict: true,
|
||||
target: ts.ScriptTarget.ES2015,
|
||||
},
|
||||
});
|
||||
|
||||
this.typeChecker = this.program.getTypeChecker();
|
||||
this.sourceFiles = mapNotNil(this.program.getRootFileNames(), it => this.program.getSourceFile(it));
|
||||
}
|
||||
|
||||
private convertAliasLike(enumLike: EnumDeclaration | TypeAliasDeclaration): LightweightAliasDefinition {
|
||||
return cleanupEmpty({
|
||||
comment: this.includeComments ? extractComment(enumLike) : undefined,
|
||||
name: enumLike.name.getText() ?? 'ERROR',
|
||||
kind: LightweightDefinitionKind.ALIAS_LIKE,
|
||||
modifiers: getModifiers(enumLike.getText(), isEnumDeclaration(enumLike) ? 'enum' : 'type'),
|
||||
type: isEnumDeclaration(enumLike)
|
||||
? enumLike.members.length > 0
|
||||
? {
|
||||
flags: 1_048_576,
|
||||
specificationTypes: enumLike.members.map(it => this.lightweightTypeAtNode(it)),
|
||||
}
|
||||
: undefined
|
||||
: this.lightweightTypeFromType(this.typeChecker.getTypeFromTypeNode(enumLike.type), enumLike.type),
|
||||
});
|
||||
}
|
||||
|
||||
private convertClassLike(classLike: ClassDeclaration | InterfaceDeclaration): LightweightClassDefinition {
|
||||
const heritages = mapValues(
|
||||
groupBy(classLike.heritageClauses, it => it.token),
|
||||
heritages => flatMap(heritages, it => it.types),
|
||||
);
|
||||
|
||||
return cleanupEmpty({
|
||||
comment: this.includeComments ? extractComment(classLike) : undefined,
|
||||
name: classLike.name?.escapedText ?? 'ERROR',
|
||||
kind: LightweightDefinitionKind.CLASS_LIKE,
|
||||
modifiers: getModifiers(classLike.getText(), isClassDeclaration(classLike) ? 'class' : 'interface'),
|
||||
extendedDefinitions: heritages[ts.SyntaxKind.ExtendsKeyword]?.map(it => this.lightweightTypeAtNode(it)),
|
||||
implementedDefinitions: heritages[ts.SyntaxKind.ImplementsKeyword]?.map(it =>
|
||||
this.lightweightTypeAtNode(it),
|
||||
),
|
||||
indexSignatures: keyBy(
|
||||
filterNodeTo(
|
||||
classLike.members as NodeArray<ClassElement | TypeElement>,
|
||||
isIndexSignatureDeclaration,
|
||||
).map(indexSignature =>
|
||||
cleanupEmpty({
|
||||
name:
|
||||
this.typeChecker.getSignatureFromDeclaration(indexSignature)?.parameters?.[0]?.escapedName ??
|
||||
'UNRESOLVED_INDEX_SIGNATURE',
|
||||
type: this.lightweightTypeFromType(
|
||||
this.typeChecker.getTypeFromTypeNode(indexSignature.type),
|
||||
indexSignature.type,
|
||||
),
|
||||
indexSignatureType: this.lightweightTypeFromType(
|
||||
this.typeChecker.getTypeFromTypeNode(indexSignature.parameters[0].type!),
|
||||
indexSignature.parameters[0].type!,
|
||||
),
|
||||
}),
|
||||
),
|
||||
it => it.name,
|
||||
),
|
||||
typeParameters: classLike.typeParameters?.map(it => it.name.getText()),
|
||||
properties: this.collectProperties(classLike.members),
|
||||
});
|
||||
}
|
||||
|
||||
collectProperties(members: NodeArray<ClassElement | TypeElement>): Record<string, LightweightProperty> {
|
||||
return keyBy(
|
||||
filterNodeTo(members as NodeArray<ClassElement | TypeElement>, isProperty).map(property =>
|
||||
cleanupEmpty({
|
||||
comment: this.includeComments ? extractComment(property) : undefined,
|
||||
name: resolvePropertyName(property.name) ?? property.getText(),
|
||||
type: this.lightweightTypeAtNode(property),
|
||||
properties: this.collectProperties((property.type as TypeLiteralNode)?.members),
|
||||
optional: isPropertyDeclaration(property)
|
||||
? property.questionToken === undefined
|
||||
? undefined
|
||||
: true
|
||||
: undefined,
|
||||
}),
|
||||
),
|
||||
it => it.name,
|
||||
);
|
||||
}
|
||||
|
||||
private lightweightTypeAtNode(node: ts.Node): LightweightType {
|
||||
const type = this.typeChecker.getTypeAtLocation(node);
|
||||
|
||||
return this.lightweightTypeFromType(type, this.typeChecker.typeToTypeNode(type, node, undefined));
|
||||
}
|
||||
|
||||
private lightweightTypeFromType(type: ts.Type, typeNode?: TypeNode): LightweightType {
|
||||
if (typeNode?.kind === SyntaxKind.ConditionalType) {
|
||||
return {value: 'UNSUPPORTED_CONDITIONAL_TYPE', flags: TypeFlags.Unknown};
|
||||
}
|
||||
if (isArrayLikeType(typeNode)) {
|
||||
const elementType = isArrayTypeNode(typeNode) ? typeNode.elementType : typeNode.typeArguments?.[0]!;
|
||||
const out = this.lightweightTypeFromType(
|
||||
this.typeChecker.getTypeFromTypeNode(elementType),
|
||||
elementType,
|
||||
);
|
||||
out.isArray = true;
|
||||
|
||||
return out;
|
||||
}
|
||||
const isReference = typeNode !== undefined && isTypeReferenceNode(typeNode) && !isEnumLiteralType(type);
|
||||
const isTypeLiteral = typeNode !== undefined && isTypeLiteralNode(typeNode);
|
||||
// @ts-expect-error intrinsic name & value exist
|
||||
const intrinsicName = (type.intrinsicName ?? type.value) as string | undefined;
|
||||
|
||||
return cleanupEmpty({
|
||||
value: intrinsicName,
|
||||
referenceName: isTypeLiteral
|
||||
? undefined
|
||||
: resolveTypeName(typeNode) ?? (type.symbol?.escapedName as string | undefined),
|
||||
flags: type.flags,
|
||||
genericsTypes: isTypeVariable(type)
|
||||
? undefined
|
||||
: this.typeChecker
|
||||
.getApparentType(type)
|
||||
// @ts-expect-error resolvedTypeArguments exits
|
||||
?.resolvedTypeArguments?.filter(it => !it.isThisType)
|
||||
?.map((it: Type) => this.lightweightTypeFromType(it)),
|
||||
specificationTypes:
|
||||
type.isUnionOrIntersection() && !isReference
|
||||
? type.types.map(it =>
|
||||
this.lightweightTypeFromType(it, this.typeChecker.typeToTypeNode(it, undefined, undefined)),
|
||||
)
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the conversion process
|
||||
*/
|
||||
convert(): LightweightProject {
|
||||
return mapValues(
|
||||
keyBy(this.sourceFiles, it => it.fileName),
|
||||
file =>
|
||||
keyBy(
|
||||
[
|
||||
...filterChildrenTo(file, isClassLikeNode).map(it => this.convertClassLike(it)),
|
||||
...filterChildrenTo(file, isEnumLikeNode).map(it => this.convertAliasLike(it)),
|
||||
],
|
||||
it => it.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as conversion, but generates a simple list of all definitions.
|
||||
*/
|
||||
convertToList(): LightweightDefinition[] {
|
||||
return flatMap(this.convert(), it => it.values);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (C) 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.
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {LightweightDefinitionBase} from './lightweight-definition';
|
||||
import {LightweightDefinitionKind} from './lightweight-definition-kind';
|
||||
import {LightweightType} from './lightweight-type';
|
||||
/**
|
||||
* Represents an enum definition
|
||||
*/
|
||||
export interface LightweightAliasDefinition extends LightweightDefinitionBase {
|
||||
/**
|
||||
* Kind
|
||||
*/
|
||||
kind: LightweightDefinitionKind.ALIAS_LIKE;
|
||||
|
||||
/**
|
||||
* Enumeration or union values
|
||||
*/
|
||||
type?: LightweightType;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (C) 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.
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {LightweightDefinitionBase} from './lightweight-definition';
|
||||
import {LightweightDefinitionKind} from './lightweight-definition-kind';
|
||||
import {LightweightIndexSignature, LightweightProperty} from './lightweight-property';
|
||||
import {LightweightType} from './lightweight-type';
|
||||
|
||||
/**
|
||||
* Represents a class definition
|
||||
*/
|
||||
export interface LightweightClassDefinition extends LightweightDefinitionBase {
|
||||
/**
|
||||
* String values of the extended definitions
|
||||
*/
|
||||
extendedDefinitions?: LightweightType[];
|
||||
|
||||
/**
|
||||
* String values of the implemented definitions
|
||||
*/
|
||||
implementedDefinitions?: LightweightType[];
|
||||
|
||||
/**
|
||||
* Index signatures
|
||||
*/
|
||||
indexSignatures?: Record<string, LightweightIndexSignature>;
|
||||
|
||||
/**
|
||||
* Kind
|
||||
*/
|
||||
kind: LightweightDefinitionKind.CLASS_LIKE;
|
||||
|
||||
/**
|
||||
* Properties of the definition
|
||||
*/
|
||||
properties?: Record<string, LightweightProperty>;
|
||||
|
||||
/**
|
||||
* Generic type parameters of this class
|
||||
*/
|
||||
typeParameters?: string[];
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (C) 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.
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
/**
|
||||
* Represents a Comment
|
||||
*/
|
||||
export interface LightweightComment {
|
||||
/**
|
||||
* Description of the comment
|
||||
*/
|
||||
description?: string;
|
||||
|
||||
/**
|
||||
* Short summary of the comment
|
||||
*/
|
||||
shortSummary?: string;
|
||||
|
||||
/**
|
||||
* Tags of the comment
|
||||
*/
|
||||
tags?: LightweightCommentTag[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Lightweight comment tag
|
||||
*/
|
||||
export interface LightweightCommentTag {
|
||||
/**
|
||||
* The name of the tag
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* The parameters of the tag
|
||||
*/
|
||||
parameters?: string[];
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright (C) 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.
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export enum LightweightDefinitionKind {
|
||||
CLASS_LIKE = 'class-like',
|
||||
ALIAS_LIKE = 'alias-like',
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (C) 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.
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {LightweightDefinitionKind} from './lightweight-definition-kind';
|
||||
import {LightweightComment} from './lightweight-comment';
|
||||
import {LightweightClassDefinition} from './lightweight-class-definition';
|
||||
import {LightweightAliasDefinition} from './lightweight-alias-definition';
|
||||
|
||||
export type LightweightDefinition = LightweightClassDefinition | LightweightAliasDefinition;
|
||||
|
||||
/**
|
||||
* Represents any definition without specifics
|
||||
*/
|
||||
export interface LightweightDefinitionBase {
|
||||
/**
|
||||
* The comment of the definition
|
||||
*/
|
||||
comment?: LightweightComment;
|
||||
|
||||
/**
|
||||
* Kind of the definition
|
||||
*/
|
||||
kind: LightweightDefinitionKind;
|
||||
|
||||
/**
|
||||
* The definition type
|
||||
* e.g. [`abstract`, `class`] or [`enum`] or [`export`, `type`]
|
||||
*/
|
||||
modifiers?: string[];
|
||||
|
||||
/**
|
||||
* Name of the definition
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
100
packages/core-tools/src/easy-ast/types/lightweight-project.ts
Normal file
100
packages/core-tools/src/easy-ast/types/lightweight-project.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright (C) 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.
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {assign, cloneDeep, flatMap, fromPairs, trimEnd} from 'lodash';
|
||||
import {mapNotNil} from '../../util/collections';
|
||||
import {definitionsOf, isLightweightClass} from '../ast-util';
|
||||
import {lightweightProjectFromPath} from '../easy-ast';
|
||||
import {LightweightClassDefinition} from './lightweight-class-definition';
|
||||
import {LightweightDefinition} from './lightweight-definition';
|
||||
|
||||
/**
|
||||
* Build an index for a lightweight project
|
||||
*/
|
||||
function buildIndex(project: LightweightProject): Record<string, string> {
|
||||
return fromPairs(
|
||||
flatMap(project, (definitions, file) => Object.keys(definitions).map(definition => [definition, file])),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A lightweight definition class for more advanced use cases
|
||||
*/
|
||||
export class LightweightProjectWithIndex {
|
||||
/**
|
||||
* All definitions
|
||||
*/
|
||||
readonly definitions: Record<string, LightweightDefinition>;
|
||||
|
||||
/**
|
||||
* Project
|
||||
*/
|
||||
readonly files: LightweightProject;
|
||||
|
||||
/**
|
||||
* Index of all definitions to their respective files
|
||||
*/
|
||||
readonly index: {
|
||||
[definitionName: string]: string;
|
||||
};
|
||||
|
||||
constructor(project: LightweightProject | string) {
|
||||
this.files = typeof project === 'string' ? lightweightProjectFromPath(project) : project;
|
||||
this.index = buildIndex(this.files);
|
||||
this.definitions = definitionsOf(this.files);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply inherited classes; default deeply
|
||||
*/
|
||||
applyInheritance(classLike: LightweightClassDefinition, deep?: boolean): LightweightDefinition {
|
||||
return assign(
|
||||
mapNotNil(
|
||||
[...(classLike.implementedDefinitions ?? []), ...(classLike.extendedDefinitions ?? [])],
|
||||
extension => {
|
||||
const object = this.definitions[extension.referenceName ?? ''];
|
||||
|
||||
return (deep ?? true) && isLightweightClass(object)
|
||||
? this.applyInheritance(object)
|
||||
: cloneDeep(object);
|
||||
},
|
||||
),
|
||||
cloneDeep(classLike),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a definition
|
||||
*
|
||||
* Requires the program to be run with `--require ts-node/register`
|
||||
*/
|
||||
async instantiateDefinitionByName<T>(name: string, findCompiledModule = true): Promise<T | undefined> {
|
||||
const fsPath = this.index[name];
|
||||
if (fsPath === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const module = await import(findCompiledModule ? `${trimEnd(fsPath, 'd.ts')}.js` : fsPath);
|
||||
|
||||
return new module[name]() as T;
|
||||
}
|
||||
}
|
||||
|
||||
export interface LightweightFile {
|
||||
[definitionName: string]: LightweightDefinition;
|
||||
}
|
||||
|
||||
export interface LightweightProject {
|
||||
[sourcePath: string]: LightweightFile;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (C) 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.
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {LightweightComment} from './lightweight-comment';
|
||||
import {LightweightType} from './lightweight-type';
|
||||
|
||||
/**
|
||||
* Represents a property definition
|
||||
*/
|
||||
export interface LightweightProperty {
|
||||
/**
|
||||
* The comment of the property
|
||||
*/
|
||||
comment?: LightweightComment;
|
||||
|
||||
/**
|
||||
* Name of the property
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Is the property marked as optional
|
||||
*/
|
||||
optional?: true;
|
||||
|
||||
/**
|
||||
* A record of properties if the property happens to be a type literal
|
||||
*/
|
||||
properties?: Record<string, LightweightProperty>;
|
||||
|
||||
/**
|
||||
* Type of the property
|
||||
*/
|
||||
type: LightweightType;
|
||||
}
|
||||
|
||||
export interface LightweightIndexSignature extends LightweightProperty {
|
||||
/**
|
||||
* Type of the index signature, if it is an index signature
|
||||
*/
|
||||
indexSignatureType: LightweightType;
|
||||
}
|
||||
58
packages/core-tools/src/easy-ast/types/lightweight-type.ts
Normal file
58
packages/core-tools/src/easy-ast/types/lightweight-type.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (C) 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.
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {TypeFlags} from 'typescript';
|
||||
|
||||
/**
|
||||
* Describes an easy to use type definition.
|
||||
*/
|
||||
export interface LightweightType {
|
||||
/**
|
||||
* Type Flags
|
||||
*/
|
||||
flags: TypeFlags;
|
||||
|
||||
/**
|
||||
* Contains all types inside of <> brackets
|
||||
*/
|
||||
genericsTypes?: LightweightType[];
|
||||
|
||||
/**
|
||||
* If it is an array(-like) type
|
||||
*/
|
||||
isArray?: true;
|
||||
|
||||
/**
|
||||
* If it is a type parameter
|
||||
*/
|
||||
isTypeParameter?: true;
|
||||
|
||||
/**
|
||||
* The name of the type that is referenced. Enum members have reference names that lead no where.
|
||||
*/
|
||||
referenceName?: string;
|
||||
|
||||
/**
|
||||
* Type specifications, if the type is combined by either an array, union or a typeOperator
|
||||
*/
|
||||
specificationTypes?: LightweightType[];
|
||||
|
||||
/**
|
||||
* Value of the type
|
||||
*
|
||||
* Literal types have their value here, non-literals their type name (for example 'string')
|
||||
*/
|
||||
value?: string | number | boolean;
|
||||
}
|
||||
464
packages/core-tools/src/pack.ts
Normal file
464
packages/core-tools/src/pack.ts
Normal file
@@ -0,0 +1,464 @@
|
||||
/* eslint-disable unicorn/error-message */
|
||||
/*
|
||||
* Copyright (C) 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.
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {Logger} from '@openstapps/logger';
|
||||
import del from 'del';
|
||||
import {existsSync} from 'fs';
|
||||
import {cwd} from 'process';
|
||||
import {globPromisified, readFilePromisified, unlinkPromisified, writeFilePromisified} from './common';
|
||||
import {JavaScriptModule} from './types/pack';
|
||||
import path from 'path';
|
||||
|
||||
const PACK_IDENTIFIER = '/* PACKED BY @openstapps/pack */';
|
||||
|
||||
/**
|
||||
* Pack cli.js
|
||||
*
|
||||
* This finds all internal requires and replaces the paths with `./index` or internal requires if it has been
|
||||
* required already.
|
||||
*
|
||||
* Furthermore it checks that no shebang line is present and that it does not export anything.
|
||||
*/
|
||||
async function packCliJs(): Promise<void> {
|
||||
const cliPath = path.join(cwd(), 'lib', 'cli.js');
|
||||
|
||||
if (!existsSync(cliPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.info('Adjusting JavaScript CLI...');
|
||||
|
||||
const buffer = await readFilePromisified(cliPath);
|
||||
const content = buffer.toString();
|
||||
|
||||
if (content.indexOf('#!/') === 0) {
|
||||
throw new Error('`cli.js` must not contain a shebang line! It is added by this script.');
|
||||
}
|
||||
|
||||
let internalRequire: string | undefined;
|
||||
|
||||
const adjustedContent = content
|
||||
.split('\n')
|
||||
.map((line, lineNumber) => {
|
||||
// check for exports (cli.js is not allowed to export anything)
|
||||
if (Array.isArray(line.match(/^\s*((exports)|(module\.exports))/))) {
|
||||
throw new TypeError(
|
||||
`Line '${lineNumber}' in 'cli.js' exports something. cli.js is not for exporting. Line was:\n${line}`,
|
||||
);
|
||||
}
|
||||
|
||||
// replace lines with internal requires
|
||||
// extract module name from line
|
||||
const match = line.match(/^(\s*)(const|var) ([a-z0-9_]*) = require\(("[^"]+"|'[^']+')\);$/i);
|
||||
|
||||
if (match !== null) {
|
||||
const importedName = match[3];
|
||||
// eslint-disable-next-line unicorn/prefer-string-slice
|
||||
const moduleName = match[4].substring(1, match[4].length - 1);
|
||||
|
||||
// if it begins with '.' and not ends with json
|
||||
if (/^[.]{1,2}\/(?!.*\.json$).*$/i.test(moduleName)) {
|
||||
// is the first internal require
|
||||
if (internalRequire) {
|
||||
return `const ${importedName} = ${internalRequire};`;
|
||||
}
|
||||
|
||||
// only the first import needs a require
|
||||
internalRequire = importedName;
|
||||
|
||||
return `const ${importedName} = require("./index");`;
|
||||
}
|
||||
}
|
||||
|
||||
return line;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
return writeFilePromisified(cliPath, `#!/usr/bin/env node\n\n${adjustedContent}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list containing the contents of all type definition files
|
||||
*/
|
||||
async function getAllTypeDefinitions(): Promise<string[]> {
|
||||
const fileNames = await globPromisified(path.join(cwd(), '*(lib|src)', '**', '*.d.ts'), {
|
||||
ignore: [
|
||||
path.join(cwd(), 'lib', 'doc', '**', '*.d.ts'),
|
||||
path.join(cwd(), 'lib', 'test', '**', '*.d.ts'),
|
||||
path.join(cwd(), 'lib', 'cli.d.ts'),
|
||||
],
|
||||
});
|
||||
|
||||
const promises = fileNames.map(async (fileName: string) => {
|
||||
return readFilePromisified(fileName, 'utf8');
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pack a list of type definitions into one file
|
||||
*/
|
||||
async function packTypeDefinitions(): Promise<void> {
|
||||
Logger.info('Packing TypeScript definition files...');
|
||||
|
||||
const indexPath = path.join(cwd(), 'lib', 'index.d.ts');
|
||||
|
||||
await deleteFileIfExistingAndPacked(indexPath);
|
||||
|
||||
const typeDefinitions = await getAllTypeDefinitions();
|
||||
|
||||
// pack TypeScript definition files
|
||||
const imports: {[k: string]: string[]} = {};
|
||||
|
||||
const referenceLines: string[] = [];
|
||||
|
||||
let allDefinitions = typeDefinitions
|
||||
// concat them separated by new lines
|
||||
.join('\n\n\n\n\n')
|
||||
// split all lines
|
||||
.split('\n')
|
||||
.map(line => {
|
||||
if (line.includes('export =')) {
|
||||
throw new Error('`export =` is not allowed by pack. Use named imports instead.');
|
||||
}
|
||||
|
||||
if (line.indexOf('/// <reference') === 0) {
|
||||
referenceLines.push(line);
|
||||
|
||||
return '// moved referenced line';
|
||||
}
|
||||
|
||||
// match import lines
|
||||
const match = line.match(/^import {([^}].*)} from '([^'].*)';$/);
|
||||
|
||||
if (match !== null) {
|
||||
const module = match[2];
|
||||
|
||||
// extract imported objects
|
||||
const importedObjects = match[1].split(',').map(object => {
|
||||
return object.trim();
|
||||
});
|
||||
|
||||
// add list of already imported objects for module
|
||||
if (imports[module] === undefined) {
|
||||
imports[module] = [];
|
||||
}
|
||||
|
||||
// count already imported objects and objects to import now
|
||||
const objectsToImport: string[] = [];
|
||||
for (const object of importedObjects) {
|
||||
if (!imports[module].includes(object)) {
|
||||
imports[module].push(object);
|
||||
objectsToImport.push(object);
|
||||
}
|
||||
}
|
||||
|
||||
// replace import line
|
||||
if (objectsToImport.length === 0) {
|
||||
return '// extraneous removed import';
|
||||
}
|
||||
|
||||
return `import {${objectsToImport.join(', ')}} from '${module}';`;
|
||||
}
|
||||
|
||||
return line;
|
||||
})
|
||||
// filter lines which contain "local" imports
|
||||
.filter(line => {
|
||||
return line.match(/^import .* from '\./) === null;
|
||||
})
|
||||
// concat all lines separated by new lines
|
||||
.join('\n');
|
||||
|
||||
if (allDefinitions.length > 0) {
|
||||
if (referenceLines.length > 0) {
|
||||
allDefinitions = `${referenceLines.join('\n')}
|
||||
|
||||
${allDefinitions}`;
|
||||
}
|
||||
|
||||
// write packed TypeScript definition files
|
||||
return writeFilePromisified(
|
||||
indexPath,
|
||||
`${PACK_IDENTIFIER}
|
||||
|
||||
${allDefinitions}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all JavaScript modules
|
||||
*/
|
||||
async function getAllJavaScriptModules(): Promise<JavaScriptModule[]> {
|
||||
const fileNames = await globPromisified(path.join(cwd(), 'lib', '**', '*.js'), {
|
||||
ignore: [
|
||||
path.join(cwd(), 'lib', 'doc', '**', '*.js'),
|
||||
path.join(cwd(), 'lib', 'test', '*.js'),
|
||||
path.join(cwd(), 'lib', 'cli.js'),
|
||||
],
|
||||
});
|
||||
|
||||
const promises = fileNames.map(async (fileName: string) => {
|
||||
const fileContent = await readFilePromisified(fileName, 'utf8');
|
||||
const directory = path.dirname(fileName).replace(new RegExp(`^${path.join(cwd(), 'lib')}`), '');
|
||||
|
||||
return {
|
||||
content: `(function() {
|
||||
${fileContent}
|
||||
})();
|
||||
`,
|
||||
dependencies: getAllInternalDependencies(fileContent),
|
||||
directory: directory,
|
||||
name: path.basename(fileName, '.js'),
|
||||
};
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pack all javascript files
|
||||
*/
|
||||
async function packJavaScriptFiles(): Promise<void> {
|
||||
const indexPath = path.join(cwd(), 'lib', 'index.js');
|
||||
|
||||
Logger.info('Packing JavaScript files...');
|
||||
|
||||
await deleteFileIfExistingAndPacked(indexPath);
|
||||
|
||||
// topologically sort the modules (sort by dependencies)
|
||||
const jsModules = topologicalSort(await getAllJavaScriptModules());
|
||||
|
||||
let wholeCode = jsModules
|
||||
// convert modules to strings
|
||||
.map(module => {
|
||||
module.content = module.content
|
||||
.split('\n')
|
||||
.map(line => {
|
||||
const match = line.match(
|
||||
/^(\s*)(const|var) ([a-z0-9_]*) = ((require\("([^"]+)"\))|(require\('([^']+)'\)));$/i,
|
||||
);
|
||||
|
||||
// replace lines with internal requires
|
||||
if (match !== null) {
|
||||
if (match[6] === undefined) {
|
||||
match[6] = match[8];
|
||||
}
|
||||
|
||||
const whiteSpace = typeof match[1] === 'string' && match[1].length > 0 ? match[1] : '';
|
||||
const importedName = match[3];
|
||||
const modulePath = match[6];
|
||||
|
||||
// leave line unchanged if it is a "global" import
|
||||
if (modulePath.match(/^[.]{1,2}\//) === null) {
|
||||
return line;
|
||||
}
|
||||
|
||||
// replace internal requires with `module.exports`
|
||||
if (existsSync(path.join(cwd(), 'lib', module.directory, `${modulePath}.js`))) {
|
||||
return `${whiteSpace}const ${importedName} = module.exports;`;
|
||||
}
|
||||
|
||||
if (existsSync(path.join(cwd(), 'src', module.directory, modulePath))) {
|
||||
return `${whiteSpace} const ${importedName} = require(../src/${modulePath});`;
|
||||
}
|
||||
|
||||
Logger.warn(`Import ${importedName} could not be found in module.directory ${modulePath}.`);
|
||||
}
|
||||
|
||||
return line;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
return `// Module: ${module.name}
|
||||
${module.content}`;
|
||||
})
|
||||
// concat them separated by new lines
|
||||
.join('\n\n\n\n\n')
|
||||
// split all lines
|
||||
.split('\n')
|
||||
// filter lines
|
||||
.filter(line => {
|
||||
// remove strict usage
|
||||
if (line === '"use strict";') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// remove esModule property
|
||||
if (line === 'Object.defineProperty(exports, "__esModule", { value: true });') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// remove source map references
|
||||
if (line.indexOf('//# sourceMappingURL=') === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// keep all other lines
|
||||
return true;
|
||||
})
|
||||
// concat all lines separated by new lines
|
||||
.join('\n');
|
||||
|
||||
if (wholeCode.length > 0) {
|
||||
// add meta lines to the file
|
||||
wholeCode = `"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
|
||||
${wholeCode}`;
|
||||
|
||||
// write packed JavaScript files
|
||||
return writeFilePromisified(
|
||||
indexPath,
|
||||
`${PACK_IDENTIFIER}
|
||||
|
||||
${wholeCode}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete file if it exists and is packed by this script
|
||||
*
|
||||
* @param path Path to file to check/delete
|
||||
*/
|
||||
async function deleteFileIfExistingAndPacked(path: string): Promise<void> {
|
||||
try {
|
||||
const buffer = await readFilePromisified(path);
|
||||
const content = buffer.toString();
|
||||
|
||||
// check if packed by this script
|
||||
if (content.indexOf(PACK_IDENTIFIER) === 0) {
|
||||
Logger.log(`Found '${path}' which is packed by this script. Deleting it...`);
|
||||
|
||||
return unlinkPromisified(path);
|
||||
}
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all internal dependencies from the content of a module
|
||||
*
|
||||
* @param moduleContent Module content to analyze
|
||||
*/
|
||||
function getAllInternalDependencies(moduleContent: string): string[] {
|
||||
// match all const <name> = require(<moduleName>);
|
||||
const requireLines = moduleContent.match(
|
||||
/^\s*(const|var) [a-z0-9_]* = require\("([^"]+)"\)|require\('([^']+)'\);$/gim,
|
||||
);
|
||||
|
||||
if (Array.isArray(requireLines)) {
|
||||
return requireLines
|
||||
.map(requireLine => {
|
||||
const matches = requireLine.match(/require\("([^"]+)"\)|require\('([^']+)'\);$/i);
|
||||
|
||||
// previously matched require line does not contain a require?!
|
||||
if (matches === null) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
// return only the moduleName
|
||||
return matches[1];
|
||||
})
|
||||
.filter(moduleName => {
|
||||
// filter out internal modules beginning with './' and not ending with '.json'
|
||||
return /^[.]{1,2}\/(?!.*\.json$).*$/i.test(moduleName);
|
||||
})
|
||||
.map(internalModuleName => {
|
||||
// cut './' from the name
|
||||
return internalModuleName.slice('./'.length);
|
||||
});
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort modules by their dependencies
|
||||
*
|
||||
* @param modules Modules to sort
|
||||
*/
|
||||
function topologicalSort(modules: JavaScriptModule[]): JavaScriptModule[] {
|
||||
// eslint-disable-next-line unicorn/prefer-module,@typescript-eslint/no-var-requires
|
||||
const topoSort = require('toposort');
|
||||
|
||||
// vertices are modules, an edge from a to b means that b depends on a
|
||||
const edges: string[][] = [];
|
||||
const nodes: string[] = [];
|
||||
|
||||
// add all edges
|
||||
for (const module of modules) {
|
||||
for (const dependencyPath of module.dependencies) {
|
||||
// add edge from dependency to our module
|
||||
edges.push([path.basename(dependencyPath), module.name]);
|
||||
}
|
||||
|
||||
nodes.push(module.name);
|
||||
}
|
||||
|
||||
// sort graph and return as an array of sorted modules
|
||||
return topoSort.array(nodes, edges).map((moduleName: string) => {
|
||||
return modules.find(module => {
|
||||
return module.name === moduleName;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Pack
|
||||
*/
|
||||
export async function pack() {
|
||||
Logger.log(`Packing project in ${process.cwd()}...`);
|
||||
|
||||
// run all tasks in parallel
|
||||
const promises: Array<Promise<void>> = [packCliJs(), packTypeDefinitions(), packJavaScriptFiles()];
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
// clean up afterwards
|
||||
Logger.info('Deleting extraneous files...');
|
||||
|
||||
await del([
|
||||
// delete all transpiled files
|
||||
'lib/*',
|
||||
|
||||
// keep packed files
|
||||
'!lib/index.d.ts',
|
||||
'!lib/index.js',
|
||||
|
||||
// keep converted schema files
|
||||
'!lib/schema',
|
||||
'!lib/schema/*.json',
|
||||
|
||||
// keep documentation
|
||||
'!lib/doc',
|
||||
'!lib/doc/*',
|
||||
'!lib/doc/**/*',
|
||||
|
||||
// keep cli
|
||||
'!lib/cli.js',
|
||||
|
||||
// keep tests
|
||||
'!lib/test',
|
||||
'!lib/test/*',
|
||||
'!lib/test/**/*',
|
||||
]);
|
||||
}
|
||||
38
packages/core-tools/src/resources/foo.ts
Normal file
38
packages/core-tools/src/resources/foo.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (C) 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.
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This is a simple interface declaration for
|
||||
* testing the schema generation and validation.
|
||||
*
|
||||
* @validatable
|
||||
*/
|
||||
export interface Foo {
|
||||
/**
|
||||
* Dummy parameter
|
||||
*/
|
||||
lorem: 'lorem' | 'ipsum';
|
||||
|
||||
/**
|
||||
* String literal type property
|
||||
*/
|
||||
type: FooType;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a simple type declaration for
|
||||
* usage in the Foo interface.
|
||||
*/
|
||||
export type FooType = 'Foo';
|
||||
41
packages/core-tools/src/resources/openapi-303-template.ts
Normal file
41
packages/core-tools/src/resources/openapi-303-template.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (C) 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.
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {OpenAPIV3} from 'openapi-types';
|
||||
|
||||
export const openapi3Template: OpenAPIV3.Document = {
|
||||
openapi: '3.0.3',
|
||||
info: {
|
||||
title: 'Openstapps Backend',
|
||||
description: `# Introduction
|
||||
This is a human readable documentation of the backend OpenAPI representation.`,
|
||||
contact: {
|
||||
name: 'Openstapps Team',
|
||||
url: 'https://gitlab.com/openstapps/backend',
|
||||
email: 'app@uni-frankfurt.de',
|
||||
},
|
||||
license: {
|
||||
name: 'AGPL 3.0',
|
||||
url: 'https://www.gnu.org/licenses/agpl-3.0.en.html',
|
||||
},
|
||||
version: '2.0.0',
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: 'https://mobile.server.uni-frankfurt.de:3000',
|
||||
description: 'Production server',
|
||||
},
|
||||
],
|
||||
paths: {},
|
||||
};
|
||||
161
packages/core-tools/src/routes.ts
Normal file
161
packages/core-tools/src/routes.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {assign, filter, map} from 'lodash';
|
||||
import {OpenAPIV3} from 'openapi-types';
|
||||
import {isLightweightClass} from './easy-ast/ast-util';
|
||||
import {LightweightProjectWithIndex} from './easy-ast/types/lightweight-project';
|
||||
import {RouteInstanceWithMeta, RouteWithMetaInformation} from './types/routes';
|
||||
import {rejectNil} from './util/collections';
|
||||
import {capitalize} from './util/string';
|
||||
import path from 'path';
|
||||
import {lightweightProjectFromPath} from './easy-ast/easy-ast';
|
||||
|
||||
/**
|
||||
* Gather relevant information of routes
|
||||
*
|
||||
* This gathers the information for all routes that implement the abstract class SCAbstractRoute.
|
||||
* Furthermore it instantiates every route and adds it to the information.
|
||||
*/
|
||||
export async function gatherRouteInformation(path: string): Promise<RouteWithMetaInformation[]> {
|
||||
const project = new LightweightProjectWithIndex(lightweightProjectFromPath(path));
|
||||
|
||||
// find all classes that implement the SCAbstractRoute
|
||||
return rejectNil(
|
||||
await Promise.all(
|
||||
map(filter(project.definitions, isLightweightClass), async node => {
|
||||
if (!node.extendedDefinitions?.some(it => it.referenceName === 'SCAbstractRoute')) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const instantiatedRoute = (await project.instantiateDefinitionByName(
|
||||
node.name,
|
||||
)) as RouteInstanceWithMeta;
|
||||
// instantiate all errors
|
||||
instantiatedRoute.errors = await Promise.all(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
instantiatedRoute.errorNames.map(async (error: any) =>
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
assign((await project.instantiateDefinitionByName(error.name)) as object, {name: error.name}),
|
||||
),
|
||||
);
|
||||
instantiatedRoute.responseBodyDescription =
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
|
||||
project.definitions[instantiatedRoute.responseBodyName]?.comment?.shortSummary!;
|
||||
instantiatedRoute.requestBodyDescription =
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
|
||||
project.definitions[instantiatedRoute.requestBodyName]?.comment?.shortSummary!;
|
||||
|
||||
return {
|
||||
description: {
|
||||
shortText: node.comment?.shortSummary,
|
||||
text: node.comment?.description,
|
||||
},
|
||||
name: node.name!,
|
||||
route: instantiatedRoute,
|
||||
};
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate documentation snippet for one route
|
||||
*
|
||||
* @param routeWithInfo A route instance with its meta information
|
||||
* @param outDirectorySchemasPath Path to directory that will contain relevant schemas for the route
|
||||
* @param schemasToCopy Schemas identified as relevant for this route
|
||||
* @param tagsToKeep Tags / keywords that can be used for grouping routes
|
||||
*/
|
||||
export function generateOpenAPIForRoute(
|
||||
routeWithInfo: RouteWithMetaInformation,
|
||||
outDirectorySchemasPath: string,
|
||||
schemasToCopy: string[],
|
||||
tagsToKeep: string[],
|
||||
): OpenAPIV3.PathItemObject {
|
||||
const route = routeWithInfo.route;
|
||||
const openapiPath: OpenAPIV3.PathItemObject = {};
|
||||
|
||||
schemasToCopy.push(route.requestBodyName, route.responseBodyName);
|
||||
|
||||
openapiPath[route.method.toLowerCase() as OpenAPIV3.HttpMethods] = {
|
||||
summary: capitalize(routeWithInfo.description.shortText?.replace(/(Route to |Route for )/gim, '')),
|
||||
description: routeWithInfo.description.text,
|
||||
requestBody: {
|
||||
description: route.responseBodyDescription ?? undefined,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: path.join(outDirectorySchemasPath, `${route.requestBodyName}.json`),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
parameters: [
|
||||
{
|
||||
name: 'X-StApps-Version',
|
||||
in: 'header',
|
||||
schema: {
|
||||
type: 'string',
|
||||
example: '2.0.0',
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
responses: {},
|
||||
tags: routeWithInfo.tags?.filter(value => tagsToKeep.includes(value)),
|
||||
};
|
||||
|
||||
openapiPath[route.method.toLowerCase() as OpenAPIV3.HttpMethods]!.responses![route.statusCodeSuccess] = {
|
||||
description: route.responseBodyDescription,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: path.join(outDirectorySchemasPath, `${route.responseBodyName}.json`),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
for (const error of route.errors) {
|
||||
schemasToCopy.push(error.name);
|
||||
openapiPath[route.method.toLowerCase() as OpenAPIV3.HttpMethods]!.responses![error.statusCode] = {
|
||||
description: error.message ?? capitalize(error.name.replace(/([A-Z][a-z])/g, ' $1').replace('SC ', '')),
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: path.join(outDirectorySchemasPath, `${error.name}.json`),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof route.obligatoryParameters === 'object') {
|
||||
for (const [parameter, schemaDefinition] of Object.entries(route.obligatoryParameters)) {
|
||||
const openapiParameter: OpenAPIV3.ParameterObject = {
|
||||
in: 'path',
|
||||
name: parameter,
|
||||
required: true,
|
||||
schema: {
|
||||
// TODO make this less of a hack and search copied schemas for the first occurring definition
|
||||
$ref: `schemas/SCSearchResponse.json#/definitions/${schemaDefinition}`,
|
||||
},
|
||||
};
|
||||
openapiPath[route.method.toLowerCase() as OpenAPIV3.HttpMethods]?.parameters?.push(openapiParameter);
|
||||
}
|
||||
}
|
||||
|
||||
return openapiPath;
|
||||
}
|
||||
119
packages/core-tools/src/schema.ts
Normal file
119
packages/core-tools/src/schema.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import Ajv from 'ajv';
|
||||
import {JSONSchema7 as JSONSchema} from 'json-schema';
|
||||
import {chain} from 'lodash';
|
||||
import {Config, DEFAULT_CONFIG, Definition, SchemaGenerator} from 'ts-json-schema-generator';
|
||||
import {createFormatter} from 'ts-json-schema-generator/dist/factory/formatter';
|
||||
import {createParser} from 'ts-json-schema-generator/dist/factory/parser';
|
||||
import {createProgram} from 'ts-json-schema-generator/dist/factory/program';
|
||||
import {getTsconfigPath} from './common';
|
||||
import {definitionsOf} from './easy-ast/ast-util';
|
||||
import {lightweightProjectFromPath} from './easy-ast/easy-ast';
|
||||
import {isSchemaWithDefinitions} from './util/guards';
|
||||
import path from 'path';
|
||||
import re2 from './types/re2';
|
||||
|
||||
/**
|
||||
* StAppsCore converter
|
||||
*
|
||||
* Converts TypeScript source files to JSON schema files
|
||||
*/
|
||||
export class Converter {
|
||||
/**
|
||||
* Generator instance
|
||||
*/
|
||||
private readonly generator: SchemaGenerator;
|
||||
|
||||
/**
|
||||
* Schema validator instance
|
||||
*/
|
||||
private readonly schemaValidator: Ajv;
|
||||
|
||||
/**
|
||||
* Create a new converter
|
||||
*
|
||||
* @param projectPath Path to the project
|
||||
* @param sourcePath Path to optionally point to a different directory of / or single source file
|
||||
*/
|
||||
constructor(projectPath: string, sourcePath?: string) {
|
||||
// set config for schema generator
|
||||
const config: Config = {
|
||||
...DEFAULT_CONFIG,
|
||||
path: sourcePath,
|
||||
sortProps: true,
|
||||
topRef: false,
|
||||
tsconfig: path.join(getTsconfigPath(projectPath), 'tsconfig.json'),
|
||||
type: 'SC',
|
||||
};
|
||||
|
||||
// create TypeScript program from config
|
||||
const program = createProgram(config);
|
||||
|
||||
// create generator
|
||||
this.generator = new SchemaGenerator(program, createParser(program, config), createFormatter(config));
|
||||
|
||||
// create Ajv instance
|
||||
this.schemaValidator = new Ajv({code: {regExp: re2}});
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires,unicorn/prefer-module
|
||||
this.schemaValidator.addMetaSchema(require('ajv/lib/refs/json-schema-draft-06.json'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get schema for specific StAppsCore type
|
||||
*
|
||||
* @param type Type to get the schema for
|
||||
* @param version Version to set for the schema
|
||||
* @returns Generated schema
|
||||
*/
|
||||
getSchema(type: string, version: string): JSONSchema {
|
||||
// generate schema for this file/type
|
||||
const schema: JSONSchema = this.generator.createSchema(type);
|
||||
|
||||
// set id of schema
|
||||
schema.$id = `https://core.stapps.tu-berlin.de/v${version}/lib/schema/${type}.json`;
|
||||
|
||||
if (isSchemaWithDefinitions(schema)) {
|
||||
const selfReference = {
|
||||
...schema,
|
||||
};
|
||||
|
||||
delete selfReference.$schema;
|
||||
delete selfReference.definitions;
|
||||
delete selfReference.$id;
|
||||
|
||||
// add self reference to definitions
|
||||
schema.definitions![`SC${type}`] = {
|
||||
...(selfReference as unknown as Definition),
|
||||
};
|
||||
}
|
||||
|
||||
if (!this.schemaValidator.validateSchema(schema)) {
|
||||
throw new Error(`Generated schema for ${type} is invalid!`);
|
||||
}
|
||||
|
||||
return schema;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of validatable types from an API extractor file
|
||||
*/
|
||||
export function getValidatableTypesInPath(path: string): string[] {
|
||||
return chain(definitionsOf(lightweightProjectFromPath(path)))
|
||||
.filter(type => !!type.comment?.tags?.find(it => it.name === 'validatable'))
|
||||
.map(type => type.name)
|
||||
.value();
|
||||
}
|
||||
39
packages/core-tools/src/types/pack.ts
Normal file
39
packages/core-tools/src/types/pack.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A JavaScript module representation to sort a list of them by dependencies
|
||||
*/
|
||||
export interface JavaScriptModule {
|
||||
/**
|
||||
* Content of the module
|
||||
*/
|
||||
content: string;
|
||||
|
||||
/**
|
||||
* List of names of dependencies
|
||||
*/
|
||||
dependencies: string[];
|
||||
|
||||
/**
|
||||
* Directory the module is in
|
||||
*/
|
||||
directory: string;
|
||||
|
||||
/**
|
||||
* The name of the module
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
6
packages/core-tools/src/types/re2.ts
Normal file
6
packages/core-tools/src/types/re2.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import re2 from 're2';
|
||||
|
||||
type Re2 = typeof re2 & {code: string};
|
||||
(re2 as Re2).code = 'require("lib/types/re2").default';
|
||||
|
||||
export default re2 as Re2;
|
||||
97
packages/core-tools/src/types/routes.ts
Normal file
97
packages/core-tools/src/types/routes.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type SCRoute = any;
|
||||
|
||||
export interface RouteInstanceWithMeta extends SCRoute {
|
||||
/**
|
||||
* Possible errors on a route
|
||||
*/
|
||||
errors: SCErrorResponse[];
|
||||
|
||||
/**
|
||||
* Description of the request body
|
||||
*/
|
||||
requestBodyDescription: string;
|
||||
|
||||
/**
|
||||
* Description of the response body
|
||||
*/
|
||||
responseBodyDescription: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A route instance with its relevant meta information
|
||||
*/
|
||||
export interface RouteWithMetaInformation {
|
||||
/**
|
||||
* Description of the route
|
||||
*/
|
||||
description: {
|
||||
/**
|
||||
* Short text of the description - title
|
||||
*/
|
||||
shortText?: string;
|
||||
/**
|
||||
* Text of the description
|
||||
*/
|
||||
text?: string;
|
||||
};
|
||||
/**
|
||||
* Name of the route
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Instance of the route
|
||||
*/
|
||||
route: RouteInstanceWithMeta;
|
||||
|
||||
/**
|
||||
* Possible tags/keywords the route can be associated with
|
||||
*/
|
||||
tags?: [string];
|
||||
}
|
||||
|
||||
/**
|
||||
* A node with its relevant meta information
|
||||
*/
|
||||
export interface NodeWithMetaInformation {
|
||||
/**
|
||||
* Module the node belongs to
|
||||
*/
|
||||
module: string;
|
||||
|
||||
/**
|
||||
* Type of the node
|
||||
*/
|
||||
type: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A generic error that can be returned by the backend if somethings fails during the processing of a request
|
||||
*/
|
||||
export interface SCErrorResponse extends Error {
|
||||
/**
|
||||
* Additional data that describes the error
|
||||
*/
|
||||
additionalData?: unknown;
|
||||
|
||||
/**
|
||||
* HTTP status code to return this error with
|
||||
*/
|
||||
statusCode: number;
|
||||
}
|
||||
26
packages/core-tools/src/types/schema.ts
Normal file
26
packages/core-tools/src/types/schema.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {JSONSchema7 as JSONSchema} from 'json-schema';
|
||||
import {Definition} from 'ts-json-schema-generator';
|
||||
|
||||
/**
|
||||
* A schema with definitions
|
||||
*/
|
||||
export interface SchemaWithDefinitions extends JSONSchema {
|
||||
/**
|
||||
* Definitions of the schema
|
||||
*/
|
||||
definitions?: {[name: string]: Definition};
|
||||
}
|
||||
87
packages/core-tools/src/types/validator.ts
Normal file
87
packages/core-tools/src/types/validator.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
/**
|
||||
* The validation result
|
||||
*/
|
||||
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
|
||||
*/
|
||||
expected: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A map of files and their expected validation errors
|
||||
*/
|
||||
export interface ExpectedValidationErrors {
|
||||
[fileName: string]: ExpectedValidationError[];
|
||||
}
|
||||
257
packages/core-tools/src/uml/create-diagram.ts
Normal file
257
packages/core-tools/src/uml/create-diagram.ts
Normal file
@@ -0,0 +1,257 @@
|
||||
/*
|
||||
* Copyright (C) 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.
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {Logger} from '@openstapps/logger';
|
||||
import {createWriteStream} from 'fs';
|
||||
import * as request from 'got';
|
||||
import {forEach, map, isEmpty} from 'lodash';
|
||||
import {expandTypeValue, isLightweightClass, isUnionOrIntersectionType} from '../easy-ast/ast-util';
|
||||
import {LightweightAliasDefinition} from '../easy-ast/types/lightweight-alias-definition';
|
||||
import {LightweightClassDefinition} from '../easy-ast/types/lightweight-class-definition';
|
||||
import {LightweightDefinition} from '../easy-ast/types/lightweight-definition';
|
||||
import {LightweightProperty} from '../easy-ast/types/lightweight-property';
|
||||
import {LightweightType} from '../easy-ast/types/lightweight-type';
|
||||
import {UMLConfig} from './uml-config';
|
||||
|
||||
/**
|
||||
* Converts the lightweight class/enum definitions according to the configuration,
|
||||
* to valid PlantUML Code, which will then be encoded, converted by the plantuml server
|
||||
* and saved as a .svg file in directory, in which this method was called
|
||||
*
|
||||
* @param definitions all type definitions of the project
|
||||
* @param config contains information on how the PlantUML should be generated
|
||||
* @param plantUmlBaseURL Hostname of the PlantUML-Server
|
||||
*/
|
||||
export async function createDiagram(
|
||||
definitions: LightweightDefinition[],
|
||||
config: UMLConfig,
|
||||
plantUmlBaseURL: string,
|
||||
): Promise<string> {
|
||||
// when non definitions were specified use all
|
||||
config.definitions = map(definitions, 'name');
|
||||
|
||||
// when providing definitions and either showing associations or inheritance the
|
||||
// inherited definitions will be added automatically
|
||||
if (config.showInheritance) {
|
||||
// TODO: showInheritance
|
||||
/*const inheritedDefinitions = gatherTypeAssociations(
|
||||
definitions,
|
||||
config.definitions,
|
||||
);*/
|
||||
// config.definitions = config.definitions.concat(inheritedDefinitions);
|
||||
}
|
||||
|
||||
// creates a UML definition for every specified definition name
|
||||
// however if no definitions were provided all definitions will be transformed
|
||||
const modelPlantUMLCode = map(
|
||||
definitions.filter(it => !config.definitions.includes(it.name)),
|
||||
definition =>
|
||||
isLightweightClass(definition)
|
||||
? createPlantUMLCodeForClass(config, definition)
|
||||
: createPlantUMLCodeForEnum(config, definition),
|
||||
).join('');
|
||||
|
||||
return createDiagramFromString(modelPlantUMLCode, plantUmlBaseURL, config.outputFileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* This will encode the plantuml code and post the code to the plantuml server
|
||||
* The server will then parse the code and create a corresponding diagram
|
||||
*
|
||||
* @param modelPlantUMLCode raw PlantUML code
|
||||
* @param plantUmlBaseURL PlantUML server address that shall be used
|
||||
* @param outputFile filename of the output file without file extension
|
||||
*/
|
||||
export async function createDiagramFromString(
|
||||
modelPlantUMLCode: string,
|
||||
plantUmlBaseURL: string,
|
||||
outputFile = `Diagram-${new Date().toISOString()}`,
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires,unicorn/prefer-module
|
||||
const plantumlEncoder = require('plantuml-encoder');
|
||||
const plantUMLCode = plantumlEncoder.encode(`@startuml\n${modelPlantUMLCode}\n@enduml`);
|
||||
const url = `${plantUmlBaseURL}/svg/${plantUMLCode}`;
|
||||
let response;
|
||||
try {
|
||||
response = await request.default.get(url);
|
||||
const httpOK = 200;
|
||||
if (response.statusCode !== httpOK) {
|
||||
await Logger.error(`Plantuml Server responded with an error.\n${response.statusMessage}`);
|
||||
throw new Error('Response not okay');
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.log(
|
||||
`Please try using the public plantuml server:\nhttp://www.plantuml.com/plantuml/svg/${plantUMLCode}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
// attach file extension
|
||||
const fileName = `${outputFile}.svg`;
|
||||
try {
|
||||
createWriteStream(fileName).write(response.body);
|
||||
Logger.log(`Writen data to file: ${fileName}`);
|
||||
} catch {
|
||||
throw new Error('Could not write file. Are you missing permissions?');
|
||||
}
|
||||
|
||||
return fileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively iterates over all types, to find implemented generic types and parents
|
||||
*
|
||||
* @param definitions all type definitions of the project
|
||||
* @param abstractionNames currently known string values of inherited classes
|
||||
*/
|
||||
|
||||
/*function gatherTypeAssociations(
|
||||
definitions: LightweightDefinition[],
|
||||
abstractionNames: string[],
|
||||
): string[] {
|
||||
let abstractions: string[] = [];
|
||||
for (const name of abstractionNames) {
|
||||
const declaration = definitions.find(
|
||||
(definition) => definition.name === name,
|
||||
);
|
||||
if (isLightweightClass(declaration)) {
|
||||
const currentAbstractions: string[] = declaration.extendedDefinitions.concat(
|
||||
declaration.implementedDefinitions,
|
||||
);
|
||||
|
||||
abstractions = abstractions.concat(currentAbstractions);
|
||||
abstractions = abstractions.concat(
|
||||
gatherTypeAssociations(definitions, currentAbstractions),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return abstractions;
|
||||
}*/
|
||||
|
||||
/**
|
||||
* Collects all reference information of this type.
|
||||
*
|
||||
* Reference information is everything that is indirectly referencing a type or class by name.
|
||||
*
|
||||
* @param type Type with references to other types
|
||||
*/
|
||||
function getReferenceTypes(type: LightweightType): string[] {
|
||||
const types: string[] = [];
|
||||
if (type.referenceName !== undefined) {
|
||||
types.push(type.referenceName);
|
||||
}
|
||||
|
||||
forEach(type.genericsTypes, specificType => {
|
||||
for (const value of getReferenceTypes(specificType)) {
|
||||
types.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
if ((isUnionOrIntersectionType(type) && isEmpty(type.specificationTypes)) || type.isArray) {
|
||||
forEach(type.specificationTypes, specificType => {
|
||||
for (const value of getReferenceTypes(specificType)) {
|
||||
types.push(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates Plant UML code according to the config for the provided class
|
||||
*
|
||||
* @param config Configuration for how the UML should be tweaked
|
||||
* @param readerClass Class or interface representation
|
||||
*/
|
||||
function createPlantUMLCodeForClass(config: UMLConfig, readerClass: LightweightClassDefinition): string {
|
||||
// create the definition header, what type the definition is, it's name and it's inheritance
|
||||
let model = `${readerClass.modifiers} ${readerClass.name}`;
|
||||
|
||||
if (readerClass.typeParameters?.length ?? 0 > 0) {
|
||||
model += `<${readerClass.typeParameters!.join(', ')}>`;
|
||||
}
|
||||
|
||||
if (config.showInheritance && (readerClass.extendedDefinitions?.length ?? 0 > 0)) {
|
||||
// PlantUML will automatically create links, when using extends
|
||||
model += ` extends ${readerClass.extendedDefinitions!.join(', ')}`;
|
||||
}
|
||||
if (config.showInheritance && (readerClass.implementedDefinitions?.length ?? 0 > 0)) {
|
||||
// PlantUML will automatically create links, when using implements
|
||||
model += ` implements ${readerClass.implementedDefinitions!.join(', ')}`;
|
||||
}
|
||||
model += '{';
|
||||
|
||||
// add the properties to the definition body
|
||||
if (config.showProperties) {
|
||||
forEach(readerClass.properties, property => {
|
||||
if (property.optional && !config.showOptionalProperties) {
|
||||
// don't show optional attributes
|
||||
return;
|
||||
}
|
||||
/*if (property.inherited && !config.showInheritedProperties) {
|
||||
// don't show inherited properties
|
||||
continue;
|
||||
}*/
|
||||
model += `\n\t${createPropertyLine(property)}`;
|
||||
});
|
||||
}
|
||||
|
||||
// close the definition body
|
||||
model += '\n}\n';
|
||||
|
||||
// add associations from properties with references
|
||||
forEach(readerClass.properties, property => {
|
||||
const types: string[] = getReferenceTypes(property.type);
|
||||
for (const type of types) {
|
||||
if (config.showAssociations) {
|
||||
/*if (property.inherited && !config.showInheritedProperties) {
|
||||
continue;
|
||||
}*/
|
||||
model += `${readerClass.name} -up-> ${type} : ${property.name} >\n`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates PlantUML code according to the config for the provided enum/-like definition
|
||||
*
|
||||
* @param config Configuration for how the UML should be tweaked
|
||||
* @param readerEnum Enum/-like representation
|
||||
*/
|
||||
function createPlantUMLCodeForEnum(config: UMLConfig, readerEnum: LightweightAliasDefinition): string {
|
||||
// create enum header
|
||||
let model = `enum ${readerEnum.name} {`;
|
||||
// add values
|
||||
if (config.showEnumValues) {
|
||||
forEach(readerEnum.type?.specificationTypes, value => {
|
||||
model += `\n\t${value.toString()}`;
|
||||
});
|
||||
}
|
||||
model += '\n}\n';
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a property PlantUML Line
|
||||
*/
|
||||
function createPropertyLine(property: LightweightProperty): string {
|
||||
const prefix = `${/*(property.inherited ? '/ ' : */ ''}${property.optional ? '? ' : ''}`;
|
||||
|
||||
return `${prefix}${property.name} : ${expandTypeValue(property.type)}`;
|
||||
}
|
||||
59
packages/core-tools/src/uml/uml-config.ts
Normal file
59
packages/core-tools/src/uml/uml-config.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (C) 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.
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Holds configuration information of how the UML code should be build
|
||||
*/
|
||||
export interface UMLConfig {
|
||||
/**
|
||||
* Defines which definitions are shown
|
||||
*/
|
||||
definitions: string[];
|
||||
|
||||
/**
|
||||
* Defines the output file name without file extension
|
||||
*/
|
||||
outputFileName?: string;
|
||||
|
||||
/**
|
||||
* Should the associations between definitions be shown
|
||||
*/
|
||||
showAssociations: boolean;
|
||||
|
||||
/**
|
||||
* Should enum/-like values be shown
|
||||
*/
|
||||
showEnumValues: boolean;
|
||||
|
||||
/**
|
||||
* Should the inheritance be shown
|
||||
*/
|
||||
showInheritance: boolean;
|
||||
|
||||
/**
|
||||
* Should the inherited properties be shown
|
||||
*/
|
||||
showInheritedProperties: boolean;
|
||||
|
||||
/**
|
||||
* Should optional properties be shown
|
||||
*/
|
||||
showOptionalProperties: boolean;
|
||||
|
||||
/**
|
||||
* Should properties be shown
|
||||
*/
|
||||
showProperties: boolean;
|
||||
}
|
||||
37
packages/core-tools/src/util/collections.ts
Normal file
37
packages/core-tools/src/util/collections.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (C) 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.
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {omitBy, isNil, reject, isEmpty, isArray, isObject} from 'lodash';
|
||||
|
||||
/**
|
||||
* Filters only defined elements
|
||||
*/
|
||||
export function rejectNil<T>(array: Array<T | undefined | null>): T[] {
|
||||
return reject(array, isNil) as T[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Map elements that are not null
|
||||
*/
|
||||
export function mapNotNil<T, S>(array: readonly T[], transform: (element: T) => S | undefined | null): S[] {
|
||||
return rejectNil(array.map(transform));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all properties with the value 'undefined', [] or {}
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
export function cleanupEmpty<T extends object>(object: T): T {
|
||||
return omitBy(object, it => isNil(it) || ((isObject(it) || isArray(it)) && isEmpty(it))) as T;
|
||||
}
|
||||
35
packages/core-tools/src/util/guards.ts
Normal file
35
packages/core-tools/src/util/guards.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (C) 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.
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {JSONSchema7 as JSONSchema} from 'json-schema';
|
||||
import {SchemaWithDefinitions} from '../types/schema';
|
||||
|
||||
/**
|
||||
* Guard for if a JSON schema is in fact a schema with definitions
|
||||
*/
|
||||
export function isSchemaWithDefinitions(schema: JSONSchema): schema is SchemaWithDefinitions {
|
||||
return schema.definitions !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Guard method for determining if an object (a thing) has a type property with a type of string
|
||||
*/
|
||||
export function isThingWithType(thing: unknown): thing is {type: string} {
|
||||
return (
|
||||
typeof thing === 'object' &&
|
||||
thing !== null &&
|
||||
'type' in thing &&
|
||||
typeof (thing as {type: unknown}).type === 'string'
|
||||
);
|
||||
}
|
||||
38
packages/core-tools/src/util/io.ts
Normal file
38
packages/core-tools/src/util/io.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (C) 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.
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {readdirSync, statSync} from 'fs';
|
||||
import {flatMap} from 'lodash';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* Expand a path to a list of all files deeply contained in it
|
||||
*/
|
||||
export function expandPathToFilesSync(sourcePath: string, accept: (fileName: string) => boolean): string[] {
|
||||
const fullPath = path.resolve(sourcePath);
|
||||
const directory = statSync(fullPath);
|
||||
|
||||
return directory.isDirectory()
|
||||
? flatMap(readdirSync(fullPath), fragment =>
|
||||
expandPathToFilesSync(path.resolve(sourcePath, fragment), accept),
|
||||
)
|
||||
: [fullPath].filter(accept);
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a Windows path and make a Unix path out of it
|
||||
*/
|
||||
export function toUnixPath(pathString: string): string {
|
||||
return pathString.replace(/\\/g, '/');
|
||||
}
|
||||
30
packages/core-tools/src/util/posix-path.ts
Normal file
30
packages/core-tools/src/util/posix-path.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (C) 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.
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import path from 'path';
|
||||
import {PathLike} from 'fs';
|
||||
|
||||
/**
|
||||
* Convert a path to a POSIX path
|
||||
*
|
||||
* Replaces the system separator with posix separators
|
||||
* Replaces Windows drive letters with a root '/'
|
||||
*/
|
||||
export function toPosixPath(pathLike: PathLike, separator = path.sep): string {
|
||||
return pathLike
|
||||
.toString()
|
||||
.split(separator)
|
||||
.join(path.posix.sep)
|
||||
.replace(/^[A-z]:\//, '/');
|
||||
}
|
||||
20
packages/core-tools/src/util/string.ts
Normal file
20
packages/core-tools/src/util/string.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright (C) 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.
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
/**
|
||||
* Creates sentence cased string
|
||||
*/
|
||||
export function capitalize(string?: string): string {
|
||||
return `${string?.charAt(0).toUpperCase()}${string?.slice(1).toLowerCase()}`;
|
||||
}
|
||||
311
packages/core-tools/src/validate.ts
Normal file
311
packages/core-tools/src/validate.ts
Normal file
@@ -0,0 +1,311 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {Logger} from '@openstapps/logger';
|
||||
import Ajv from 'ajv';
|
||||
import betterAjvErrors, {IOutputError} from 'better-ajv-errors';
|
||||
import {PathLike} from 'fs';
|
||||
import {JSONSchema7} from 'json-schema';
|
||||
import * as mustache from 'mustache';
|
||||
import {Schema} from 'ts-json-schema-generator';
|
||||
import {globPromisified, readFilePromisified, writeFilePromisified} from './common';
|
||||
import {ExpectedValidationErrors, ValidationError, ValidationResult} from './types/validator';
|
||||
import {isThingWithType} from './util/guards';
|
||||
import path from 'path';
|
||||
import re2 from './types/re2';
|
||||
import {toPosixPath} from './util/posix-path';
|
||||
|
||||
/**
|
||||
* StAppsCore validator
|
||||
*/
|
||||
export class Validator {
|
||||
/**
|
||||
* JSON Schema Validator
|
||||
*/
|
||||
private readonly ajv = new Ajv({
|
||||
verbose: true,
|
||||
code: {regExp: re2},
|
||||
});
|
||||
|
||||
/**
|
||||
* Map of schema names to schemas
|
||||
*/
|
||||
private readonly schemas: {[type: string]: Schema} = {};
|
||||
|
||||
/**
|
||||
* 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 ajvValidateWrapper(schema: JSONSchema7, instance: unknown): ValidationResult {
|
||||
return fromAjvResult(this.ajv.validate(schema, instance), schema, instance, this.ajv);
|
||||
}
|
||||
|
||||
/**
|
||||
* Feed the schema files to the validator
|
||||
*
|
||||
* @param schemaDirectory Path to directory that contains schema files
|
||||
*/
|
||||
public async addSchemas(schemaDirectory: PathLike): Promise<string[]> {
|
||||
const schemaFiles = await globPromisified(path.posix.join(toPosixPath(schemaDirectory), '*.json'));
|
||||
|
||||
if (schemaFiles.length === 0) {
|
||||
throw new Error(`No schema files in ${schemaDirectory.toString()}!`);
|
||||
}
|
||||
|
||||
Logger.log(`Adding schemas from ${schemaDirectory} to validator.`);
|
||||
|
||||
await Promise.all(
|
||||
schemaFiles.map(async (file: string) => {
|
||||
// read schema file
|
||||
const buffer = await readFilePromisified(file);
|
||||
|
||||
// add schema to map
|
||||
this.schemas[path.basename(file, '.json')] = JSON.parse(buffer.toString());
|
||||
|
||||
Logger.info(`Added ${file} to validator.`);
|
||||
}),
|
||||
);
|
||||
|
||||
return schemaFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates anything against a given schema name or infers schema name from object
|
||||
*
|
||||
* @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): ValidationResult {
|
||||
if (schema === undefined) {
|
||||
if (isThingWithType(instance)) {
|
||||
// schema name can be inferred from type string
|
||||
const schemaSuffix = (instance as {type: string}).type
|
||||
.split(' ')
|
||||
.map((part: string) => {
|
||||
return part.slice(0, 1).toUpperCase() + part.slice(1);
|
||||
})
|
||||
.join('');
|
||||
const schemaName = `SC${schemaSuffix}`;
|
||||
|
||||
return this.validate(instance, schemaName);
|
||||
}
|
||||
throw new Error('Instance.type does not exist.');
|
||||
}
|
||||
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 TypeError(`No schema available for ${schema}.`);
|
||||
}
|
||||
|
||||
// schema will be cached
|
||||
return this.ajvValidateWrapper(this.schemas[schema], instance);
|
||||
}
|
||||
|
||||
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,
|
||||
): ValidationResult {
|
||||
const betterErrorObject: 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) => {
|
||||
const error: ValidationError = {
|
||||
dataPath: ajvError.instancePath,
|
||||
instance: instance,
|
||||
message: betterErrorObject?.[index]?.error ?? ajvError.message,
|
||||
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 schemaDirectory The directory where the JSON schema files are
|
||||
* @param resourcesDirectory The directory where the test files are
|
||||
*/
|
||||
export async function validateFiles(
|
||||
schemaDirectory: string,
|
||||
resourcesDirectory: string,
|
||||
): Promise<ExpectedValidationErrors> {
|
||||
// instantiate new validator
|
||||
const v = new Validator();
|
||||
await v.addSchemas(schemaDirectory);
|
||||
|
||||
// get list of files to test
|
||||
const testFiles = await globPromisified(path.join(resourcesDirectory, '*.json'));
|
||||
|
||||
if (testFiles.length === 0) {
|
||||
throw new Error(`No test files in ${resourcesDirectory}!`);
|
||||
}
|
||||
|
||||
Logger.log(`Found ${testFiles.length} file(s) to test.`);
|
||||
|
||||
// map of errors per file
|
||||
const errors: ExpectedValidationErrors = {};
|
||||
|
||||
await Promise.all(
|
||||
testFiles.map(async (testFile: string) => {
|
||||
const testFileName = path.basename(testFile);
|
||||
|
||||
const buffer = await readFilePromisified(path.join(resourcesDirectory, 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[] = [...testDescription.errorNames];
|
||||
|
||||
// number of unexpected errors
|
||||
let unexpectedErrors = 0;
|
||||
|
||||
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;
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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',
|
||||
});
|
||||
}
|
||||
} 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: ExpectedValidationErrors): Promise<void> {
|
||||
// eslint-disable-next-line unicorn/prefer-module
|
||||
let buffer = await readFilePromisified(path.resolve(__dirname, '..', 'resources', 'file.html.mustache'));
|
||||
const fileTemplate = buffer.toString();
|
||||
|
||||
// eslint-disable-next-line unicorn/prefer-module
|
||||
buffer = await readFilePromisified(path.resolve(__dirname, '..', 'resources', 'error.html.mustache'));
|
||||
const errorTemplate = buffer.toString();
|
||||
|
||||
let output = '';
|
||||
|
||||
for (const fileName in errors) {
|
||||
if (!errors.hasOwnProperty(fileName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let fileOutput = '';
|
||||
|
||||
for (const [index, error] of errors[fileName].entries()) {
|
||||
fileOutput += mustache.render(errorTemplate, {
|
||||
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',
|
||||
suggestion: error.suggestion,
|
||||
});
|
||||
}
|
||||
|
||||
output += mustache.render(fileTemplate, {
|
||||
errors: fileOutput,
|
||||
testFile: fileName,
|
||||
});
|
||||
}
|
||||
|
||||
// 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(),
|
||||
}),
|
||||
);
|
||||
|
||||
Logger.ok(`Wrote report to ${reportPath}.`);
|
||||
}
|
||||
Reference in New Issue
Block a user