feat: modernize core-tools

This commit is contained in:
Wieland Schöbl
2021-08-25 09:47:36 +00:00
parent 106dd26f89
commit fe59204b42
106 changed files with 4131 additions and 6216 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2018-2019 StApps
* Copyright (C) 2018-2021 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
@@ -16,27 +16,16 @@ import {Logger} from '@openstapps/logger';
import {Command} from 'commander';
import {existsSync, readFileSync, writeFileSync} from 'fs';
import {copy} from 'fs-extra';
import got from 'got';
import {join, relative, resolve} from 'path';
import {exit} from 'process';
import {
capitalize,
getProjectReflection,
mkdirPromisified,
readFilePromisified,
toArray,
} from './common';
import {generateTemplate} from './mapping';
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, getValidatableTypesFromReflection} from './schema';
import {gatherRouteInformation, generateOpenAPIForRoute} from './routes';
import {Converter, getValidatableTypesInPath} from './schema';
import {createDiagram, createDiagramFromString} from './uml/create-diagram';
import {readDefinitions} from './uml/read-definitions';
import {UMLConfig} from './uml/uml-config';
import {capitalize} from './util/string';
import {validateFiles, writeReport} from './validate';
// handle unhandled promise rejections
@@ -50,35 +39,41 @@ process.on('unhandledRejection', async (reason: unknown) => {
const commander = new Command('openstapps-core-tools');
commander
.version(JSON.parse(
readFileSync(resolve(__dirname, '..', 'package.json'))
.toString(),
).version);
// 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 (relativeSrcPath, relativeOutDirPath) => {
.action(async (relativeSourceBundlePath, relativeOutDirectoryPath) => {
// get absolute paths
const srcPath = resolve(relativeSrcPath);
const outDirPath = resolve(relativeOutDirPath);
const outDirSchemasPath = join(outDirPath, 'schemas');
// get project reflection
const projectReflection = getProjectReflection(srcPath);
const sourcePath = path.resolve(relativeSourceBundlePath);
const outDirectoryPath = path.resolve(relativeOutDirectoryPath);
const outDirectorySchemasPath = path.join(outDirectoryPath, 'schemas');
// get information about routes
const routes = await gatherRouteInformation(projectReflection);
const routes = await gatherRouteInformation(sourcePath);
routes.sort((a, b) => a.route.urlFragment.localeCompare(b.route.urlFragment));
// change url path parameters to openapi notation
routes.forEach((routeWithMetaInformation) => {
routeWithMetaInformation.route.urlFragment = routeWithMetaInformation.route.urlFragment.replace(/:\w+/g, (match) => `{${match.replace(':','')}}`);
});
for (const routeWithMetaInformation of routes) {
routeWithMetaInformation.route.urlFragment = routeWithMetaInformation.route.urlFragment.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.urlFragment.split('/')[1]));
tagsToKeep = tagsToKeep.filter((element, i, array) => array.indexOf(element) === i && array.lastIndexOf(element) !== i);
let tagsToKeep = routes.map(routeWithMetaInformation =>
capitalize(routeWithMetaInformation.route.urlFragment.split('/')[1]),
);
tagsToKeep = tagsToKeep.filter(
(element, i, array) => array.indexOf(element) === i && array.lastIndexOf(element) !== i,
);
// initialize json output
const output = openapi3Template;
@@ -87,194 +82,99 @@ commander
const schemasToCopy: string[] = [];
// generate documentation for all routes
routes.forEach((routeWithMetaInformation) => {
for (const routeWithMetaInformation of routes) {
routeWithMetaInformation.tags = [capitalize(routeWithMetaInformation.route.urlFragment.split('/')[1])];
output.paths[routeWithMetaInformation.route.urlFragment] = generateOpenAPIForRoute(
routeWithMetaInformation,
relative(relativeOutDirPath,outDirSchemasPath),
path.relative(relativeOutDirectoryPath, outDirectorySchemasPath),
schemasToCopy,
tagsToKeep,
);
});
}
// copy schema json schema files
try {
if (!existsSync(outDirSchemasPath)){
await mkdirPromisified(outDirSchemasPath, {
if (!existsSync(outDirectorySchemasPath)) {
await mkdirPromisified(outDirectorySchemasPath, {
recursive: true,
});
}
for (const fileName of schemasToCopy) {
await copy(join(srcPath, 'schema', `${fileName}.json`), join(outDirSchemasPath, `${fileName}.json`));
await copy(
path.join(sourcePath, 'schema', `${fileName}.json`),
path.join(outDirectorySchemasPath, `${fileName}.json`),
);
}
} catch (error) {
await Logger.error(error);
// tslint:disable-next-line: no-magic-numbers
process.exit(-2);
}
// write openapi object to file (prettified)
// tslint:disable-next-line: no-magic-numbers
writeFileSync(join(outDirPath, 'openapi.json'), JSON.stringify(output, null, 2));
writeFileSync(path.join(outDirectoryPath, 'openapi.json'), JSON.stringify(output, undefined, 2));
Logger.ok(`OpenAPI representation resources written to ${outDirPath} .`);
Logger.ok(`OpenAPI representation resources written to ${outDirectoryPath} .`);
});
commander
.command('mapping <relativeSrcPath>')
.option('-m, --mappingPath <relativeMappingPath>', 'Mapping Path')
.option('-i, --ignoredTags <ignoredTags>', 'Ignored Tags (comma-separated)')
.option('-a, --aggPath <relativeAggregationPath>', 'Aggregations Path')
.option('-e, --errorPath <relativeErrorPath>', 'Error Path')
.action(async (relativeSrcPath, options) => {
// get absolute paths
const srcPath = resolve(relativeSrcPath);
commander.command('schema <srcPath> <schemaPath>').action(async (relativeSourcePath, relativeSchemaPath) => {
// get absolute paths
const absoluteSourcePath = path.resolve(relativeSourcePath);
const schemaPath = path.resolve(relativeSchemaPath);
let ignoredTagsList: string[] = [];
if (typeof options.ignoredTags === 'string') {
ignoredTagsList = options.ignoredTags.split(',');
}
// initialize new core converter
const coreConverter = new Converter(absoluteSourcePath);
// get project reflection
const projectReflection = getProjectReflection(srcPath);
// get validatable types
const validatableTypes = getValidatableTypesInPath(absoluteSourcePath);
const result = generateTemplate(projectReflection, ignoredTagsList, true);
if (result.errors.length !== 0) {
await Logger.error('Mapping generated with errors!');
} else {
Logger.ok('Mapping generated without errors!');
}
Logger.info(`Found ${validatableTypes.length} type(s) to generate schemas for.`);
// write documentation to file
if (typeof options.aggPath !== 'undefined') {
const aggPath = resolve(options.aggPath);
// tslint:disable-next-line:no-magic-numbers
writeFileSync(aggPath, JSON.stringify(result.aggregations, null, 2));
Logger.ok(`Elasticsearch aggregations written to ${aggPath}.`);
}
if (typeof options.mappingPath !== 'undefined') {
const mappingPath = resolve(options.mappingPath);
// tslint:disable-next-line:no-magic-numbers
writeFileSync(mappingPath, JSON.stringify(result.mappings, null, 2));
Logger.ok(`Elasticsearch mappings written to ${mappingPath}.`);
}
if (typeof options.errorPath !== 'undefined') {
const errPath = resolve(options.errorPath);
// tslint:disable-next-line:no-magic-numbers
writeFileSync(errPath, JSON.stringify(result.errors, null, 2));
Logger.ok(`Mapping errors written to ${errPath}.`);
}
await mkdirPromisified(schemaPath, {
recursive: true,
});
commander
.command('put-es-templates <srcPath> <esAddress> [ignoredTags]')
.action(async (relativeSrcPath, esAddress, ignoredTags) => {
// get absolute paths
const srcPath = resolve(relativeSrcPath);
Logger.info(`Trying to find a package.json for ${absoluteSourcePath}.`);
let ignoredTagsList: string[] = [];
if (typeof ignoredTags === 'string') {
ignoredTagsList = ignoredTags.split(',');
}
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, '..');
}
// get project reflection
const projectReflection = getProjectReflection(srcPath);
const corePackageJsonPath = path.join(packagePath, 'package.json');
const result = generateTemplate(projectReflection, ignoredTagsList, true);
if (result.errors.length !== 0) {
await Logger.error(`Mapping generated with errors:\n${JSON.stringify(result.errors)}`);
exit(-1);
} else {
Logger.ok('Mapping generated without errors!');
}
Logger.info(`Using ${corePackageJsonPath} to determine version for schemas.`);
for (const template in result.mappings) {
if (!result.mappings.hasOwnProperty(template)) {
continue;
}
const buffer = await readFilePromisified(corePackageJsonPath);
const corePackageJson = JSON.parse(buffer.toString());
const coreVersion = corePackageJson.version;
const response = await got.put(`${esAddress}_template/${template}`, {
json: result.mappings[template],
});
Logger.log(`Using ${coreVersion} as version for schemas.`);
const HTTP_STATUS_OK = 200;
if (response.statusCode !== HTTP_STATUS_OK) {
await Logger.error(`Template for "${template}" failed in Elasticsearch:\n${JSON.stringify(response.body)}`);
exit(-1);
}
}
// generate and write JSONSchema files for validatable types
for (const type of validatableTypes) {
const schema = coreConverter.getSchema(type, coreVersion);
Logger.ok(`Templates accepted by Elasticsearch.`);
});
const stringifiedSchema = JSON.stringify(schema, undefined, 2);
commander
.command('schema <srcPath> <schemaPath>')
.action(async (relativeSrcPath, relativeSchemaPath) => {
// get absolute paths
const srcPath = resolve(relativeSrcPath);
const schemaPath = resolve(relativeSchemaPath);
const file = path.join(schemaPath, `${type}.json`);
// initialize new core converter
const coreConverter = new Converter(srcPath);
// write schema to file
writeFileSync(file, stringifiedSchema);
// get project reflection
const projectReflection = getProjectReflection(srcPath);
Logger.info(`Generated schema for ${type} and saved to ${file}.`);
}
// get validatable types
const validatableTypes = getValidatableTypesFromReflection(
projectReflection,
);
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 ${srcPath}.`);
let path = srcPath;
// TODO: this check should be less ugly! --- What is this doing anyway?
// tslint:disable-next-line:no-magic-numbers
while (!existsSync(join(path, 'package.json')) && path.length > 5) {
path = resolve(path, '..');
}
const corePackageJsonPath = join(path, '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
validatableTypes.forEach((type) => {
const schema = coreConverter.getSchema(type, coreVersion);
// tslint:disable-next-line:no-magic-numbers
const stringifiedSchema = JSON.stringify(schema, null, 2);
const file = 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).`);
});
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 = resolve(relativeSchemaPath);
const testPath = resolve(relativeTestPath);
const schemaPath = path.resolve(relativeSchemaPath);
const testPath = path.resolve(relativeTestPath);
const errorsPerFile = await validateFiles(schemaPath, testPath);
@@ -284,11 +184,11 @@ commander
continue;
}
unexpected = unexpected || errorsPerFile[file].some((error) => !error.expected);
unexpected = unexpected || errorsPerFile[file].some(error => !error.expected);
}
if (typeof relativeReportPath !== 'undefined') {
const reportPath = resolve(relativeReportPath);
const reportPath = path.resolve(relativeReportPath);
await writeReport(reportPath, errorsPerFile);
}
@@ -300,70 +200,34 @@ commander
}
});
commander
.command('pack')
.action(async () => {
await pack();
});
commander.command('pack').action(async () => {
await pack();
});
commander
.command('plantuml <srcPath> <plantumlserver>')
.option(
'--definitions <definitions>',
'Shows these specific definitions (class, interface or enum)',
toArray,
.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('--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 (relativeSrcPath, plantumlserver, options) => {
.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:
typeof options.definitions !== 'undefined' ? options.definitions : [],
showAssociations:
typeof options.showAssociations !== 'undefined'
? options.showAssociations
: false,
showEnumValues:
typeof options.showEnumValues !== 'undefined'
? options.showEnumValues
: false,
showInheritance:
typeof options.showInheritance !== 'undefined'
? options.showInheritance
: false,
definitions: typeof options.definitions !== 'undefined' ? options.definitions : [],
showAssociations: typeof options.showAssociations !== 'undefined' ? options.showAssociations : false,
showEnumValues: typeof options.showEnumValues !== 'undefined' ? options.showEnumValues : false,
showInheritance: typeof options.showInheritance !== 'undefined' ? options.showInheritance : false,
showInheritedProperties:
typeof options.showInheritedProperties !== 'undefined'
? options.showInheritedProperties
: false,
typeof options.showInheritedProperties !== 'undefined' ? options.showInheritedProperties : false,
showOptionalProperties:
typeof options.showOptionalProperties !== 'undefined'
? options.showOptionalProperties
: false,
showProperties:
typeof options.showProperties !== 'undefined'
? options.showProperties
: false,
typeof options.showOptionalProperties !== 'undefined' ? options.showOptionalProperties : false,
showProperties: typeof options.showProperties !== 'undefined' ? options.showProperties : false,
};
if (typeof options.outputFileName !== 'undefined') {
plantUmlConfig.outputFileName = options.outputFileName;
@@ -371,21 +235,14 @@ commander
Logger.log(`PlantUML options: ${JSON.stringify(plantUmlConfig)}`);
const srcPath = resolve(relativeSrcPath);
const projectReflection = getProjectReflection(srcPath, !options.excludeExternals ? false : true);
const definitions = readDefinitions(projectReflection);
await createDiagram(definitions, plantUmlConfig, plantumlserver);
await createDiagram(lightweightDefinitionsFromPath(relativeSourcePath), plantUmlConfig, plantumlServer);
});
commander
.command('plantuml-file <inputFile> <plantumlserver> [outputFile]')
.action(async (file: string, plantumlserver: string, outputFile: string) => {
const fileContent = readFileSync(resolve(file))
.toString();
await createDiagramFromString(fileContent, 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);

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2018-2019 StApps
* Copyright (C) 2018-2021 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
@@ -13,16 +13,11 @@
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Logger} from '@openstapps/logger';
import {existsSync, mkdir, PathLike, readFile, unlink, writeFile} from 'fs';
import {existsSync, mkdir, readFile, unlink, writeFile} from 'fs';
import {Glob} from 'glob';
import {JSONSchema7 as JSONSchema} from 'json-schema';
import {platform} from 'os';
import {join, sep} from 'path';
import {Definition} from 'ts-json-schema-generator';
import {Application, ProjectReflection} from 'typedoc';
import {ModuleKind, ScriptTarget} from 'typescript';
import {promisify} from 'util';
import {LightweightType} from './uml/model/lightweight-type';
import path from 'path';
export const globPromisified = promisify(Glob);
export const mkdirPromisified = promisify(mkdir);
@@ -30,267 +25,6 @@ export const readFilePromisified = promisify(readFile);
export const writeFilePromisified = promisify(writeFile);
export const unlinkPromisified = promisify(unlink);
/**
* 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: {
/**
* Possible errors on a route
*/
errors: SCErrorResponse[];
/**
* Method of the route
*/
method: string;
/**
* Obligatory parameters of the route
*/
obligatoryParameters: {
[k: string]: string;
};
/**
* Description of the request body
*/
requestBodyDescription: string;
/**
* Name of the request body
*/
requestBodyName: string;
/**
* Description of the response body
*/
responseBodyDescription: string;
/**
* Name of the response body
*/
responseBodyName: string;
/**
* Status code on success
*/
statusCodeSuccess: number;
/**
* URL fragment
*/
urlFragment: string;
};
/**
* 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;
}
/**
* A map of nodes indexed by their name
*/
export interface NodesWithMetaInformation {
/**
* Index signature
*/
[k: string]: NodeWithMetaInformation;
}
/**
* A schema with definitions
*/
interface SchemaWithDefinitions extends JSONSchema {
/**
* Definitions of the schema
*/
definitions: { [name: string]: Definition; };
}
/**
* 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[];
}
/**
* Get a project reflection from a path
*
* @param srcPath Path to get reflection from
* @param excludeExternals Exclude external dependencies
*/
export function getProjectReflection(srcPath: PathLike, excludeExternals = true): ProjectReflection {
Logger.info(`Generating project reflection for ${srcPath.toString()}.`);
const tsconfigPath = getTsconfigPath(srcPath.toString());
// initialize new Typedoc application
const app = new Application();
app.options.setValues({
excludeExternals: excludeExternals,
ignoreCompilerErrors: false, // TODO: true
includeDeclarations: true,
module: ModuleKind.CommonJS,
target: ScriptTarget.Latest,
tsconfig: join(tsconfigPath, 'tsconfig.json'),
});
let inputFilePath = srcPath;
if (inputFilePath === tsconfigPath) {
inputFilePath = join(tsconfigPath, 'src');
}
// get input files
const inputFiles = app.expandInputFiles([inputFilePath.toString()]);
// get project reflection from input files
const result = app.convert(inputFiles);
if (typeof result === 'undefined') {
throw new Error('Project reflection could not be generated.');
}
return result;
}
/**
* Guard method for checking if a schema has definitions
*
* @param schema Schema to check
*/
export function isSchemaWithDefinitions(
schema: JSONSchema,
): schema is SchemaWithDefinitions {
return typeof schema.definitions !== 'undefined';
}
// tslint:disable: completed-docs
/**
* Guard method for determining if an object (a thing) has a type property with a type of string
*
* @param thing An object (thing)
*/
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';
}
// tslint:enable: completed-docs
/**
* Get path that contains a tsconfig.json
*
@@ -300,12 +34,10 @@ 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(sep)[0] : '/';
const root = platform() === 'win32' ? process.cwd().split(path.sep)[0] : '/';
// repeat until a tsconfig.json is found
while (!existsSync(join(tsconfigPath, 'tsconfig.json'))) {
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}!`,
@@ -313,72 +45,12 @@ export function getTsconfigPath(startPath: string): string {
}
// pop last directory
const tsconfigPathParts = tsconfigPath.split(sep);
const tsconfigPathParts = tsconfigPath.split(path.sep);
tsconfigPathParts.pop();
tsconfigPath = tsconfigPathParts.join(sep);
tsconfigPath = tsconfigPathParts.join(path.sep);
}
Logger.info(`Using 'tsconfig.json' from ${tsconfigPath}.`);
return tsconfigPath;
}
/**
* Converts a comma separated string into a string array
*
* @param val Comma separated string
*/
export function toArray(val: string): string[] {
return val.split(',');
}
/**
* Creates the full name of a lightweight type recursively
*
* @param type Type to get the full name of
*/
export function getFullTypeName(type: LightweightType): string {
// init name
let fullName: string = type.name;
if (type.isTypeParameter) {
// type parameters are a sink
return fullName;
}
if (type.isLiteral) {
// literals are a sink
return `'${fullName}'`;
}
if (type.isUnion && type.specificationTypes.length > 0) {
const tempNames: string[] = [];
for (const easyType of type.specificationTypes) {
tempNames.push(getFullTypeName(easyType));
}
// since unions can't be applied to other types, it is a sink.
return tempNames.join(' | ');
}
// check if type is generic and has a type attached
if (type.isTyped && type.genericsTypes.length > 0) {
const tempNames: string[] = [];
for (const easyType of type.genericsTypes) {
tempNames.push(getFullTypeName(easyType));
}
fullName = `${fullName}<${tempNames.join(', ')}>`;
}
// check if type is array
if (type.isArray) {
fullName += '[]';
}
return fullName;
}
/**
* Creates sentence cased string
*
* @param str The string to capitalize
*/
export function capitalize(str?: string): string {
// tslint:disable-next-line: newline-per-chained-call
return `${str?.charAt(0).toUpperCase()}${str?.slice(1).toLowerCase()}`;
}

View 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 typeof 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 typeof name !== 'undefined'
? isComputedPropertyName(name)
? 'UNSUPPORTED_IDENTIFIER_TYPE'
: name.getText()
: undefined;
}
/** @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 typeof 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
src/easy-ast/ast-util.ts Normal file
View 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;
}

274
src/easy-ast/easy-ast.ts Normal file
View File

@@ -0,0 +1,274 @@
/* 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)
? typeof property.questionToken !== 'undefined'
? true
: undefined
: 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 =
typeof typeNode !== 'undefined' && isTypeReferenceNode(typeNode) && !isEnumLiteralType(type);
const isTypeLiteral = typeof 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);
}
}

View File

@@ -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;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2019 StApps
* 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.
@@ -13,44 +13,42 @@
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {LightweightDefinition} from './lightweight-definition';
import {LightweightProperty} from './lightweight-property';
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 class LightweightClassDefinition extends LightweightDefinition {
export interface LightweightClassDefinition extends LightweightDefinitionBase {
/**
* String values of the extended definitions
*/
public extendedDefinitions: string[];
extendedDefinitions?: LightweightType[];
/**
* String values of the implemented definitions
*/
public implementedDefinitions: string[];
implementedDefinitions?: LightweightType[];
/**
* Index signatures
*/
indexSignatures?: Record<string, LightweightIndexSignature>;
/**
* Kind
*/
kind: LightweightDefinitionKind.CLASS_LIKE;
/**
* Properties of the definition
*/
public properties: LightweightProperty[];
/**
* The definition type
* e.g. `interface`/[`abstract`] `class`
*/
public type: string;
properties?: Record<string, LightweightProperty>;
/**
* Generic type parameters of this class
*/
public typeParameters: string[];
constructor(name: string, type: string) {
super(name);
this.type = type;
this.properties = [];
this.extendedDefinitions = [];
this.implementedDefinitions = [];
this.typeParameters = [];
}
typeParameters?: string[];
}

View File

@@ -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[];
}

View File

@@ -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',
}

View File

@@ -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;
}

View 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 (typeof 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;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2019 StApps
* 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.
@@ -12,43 +12,42 @@
* 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 class LightweightProperty {
export interface LightweightProperty {
/**
* Is the property inherited from another definition
* The comment of the property
*/
public inherited: boolean;
comment?: LightweightComment;
/**
* Name of the property
*/
public name: string;
name: string;
/**
* Is the property marked as optional
*/
public optional: boolean;
optional?: true;
/**
* A record of properties if the property happens to be a type literal
*/
properties?: Record<string, LightweightProperty>;
/**
* Type of the property
*/
public type: LightweightType;
/**
* Constructor for LightweightProperty
*
* @param name Name of the property
* @param type Type of the property
* @param optional Is the property optional
*/
constructor(name: string, type: LightweightType, optional = true) {
this.name = name;
this.optional = optional;
this.inherited = false;
this.type = type;
}
type: LightweightType;
}
export interface LightweightIndexSignature extends LightweightProperty {
/**
* Type of the index signature, if it is an index signature
*/
indexSignatureType: LightweightType;
}

View 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;
}

View File

@@ -1,812 +0,0 @@
/*
* Copyright (C) 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 {Logger} from '@openstapps/logger';
import merge from 'deepmerge';
import {stringify} from 'flatted';
import {DeclarationReflection, ProjectReflection, SignatureReflection} from 'typedoc';
import {
ArrayType,
Comment,
CommentTag,
IntrinsicType,
ReferenceType,
ReflectionType,
StringLiteralType,
Type,
TypeParameterType,
UnionType,
} from 'typedoc/dist/lib/models';
import {AggregationSchema, ESNestedAggregation} from './mappings/aggregation-definitions';
import {fieldmap, filterableMap, filterableTagName} from './mappings/definitions/fieldmap';
import {premaps} from './mappings/definitions/premap';
import {settings} from './mappings/definitions/settings';
import {dynamicTypes, ElasticsearchDataType, typemap} from './mappings/definitions/typemap';
import {
ElasticsearchDynamicTemplate,
ElasticsearchObject,
ElasticsearchTemplateCollection,
ElasticsearchType,
ElasticsearchValue,
} from './mappings/mapping-definitions';
let dynamicTemplates: ElasticsearchDynamicTemplate[] = [];
let errors: string[] = [];
let showErrors = true;
let aggregations: AggregationSchema = {};
const indexableTag = 'indexable';
const aggregatableTag = 'aggregatable';
const aggregatableTagParameterGlobal = 'global';
const inheritTagsName = 'inherittags';
// clamp printed object to 1000 chars to keep error messages readable
const maxErrorObjectChars = 1000;
let ignoredTagsList = ['indexable', 'validatable', inheritTagsName];
let inheritTagsMap: { [path: string]: CommentTag[]; } = {};
/**
* Gets all interfaces that have an @indexable tag
*
* @param projectReflection the project reflection from which to extract the indexable interfaces
*/
export function getAllIndexableInterfaces(projectReflection: ProjectReflection): DeclarationReflection[] {
let indexableInterfaces: DeclarationReflection[] = [];
if (!Array.isArray(projectReflection.children) || projectReflection.children.length === 0) {
throw new Error('No DeclarationReflections found. Please check your input path');
}
// push all declaration reflections into one array
projectReflection.children.forEach((declarationReflection) => {
if (Array.isArray(declarationReflection.children)) {
indexableInterfaces = indexableInterfaces.concat(declarationReflection.children);
}
});
// filter all declaration reflections with an @indexable tag
indexableInterfaces = indexableInterfaces.filter((declarationReflection) => {
if (
typeof declarationReflection.comment === 'undefined' ||
typeof declarationReflection.comment.tags === 'undefined'
) {
return false;
}
return typeof declarationReflection.comment.tags.find((commentTag) => {
return commentTag.tagName === indexableTag;
}) !== 'undefined';
});
return indexableInterfaces;
}
/**
* Composes error messages, that are readable and contain a certain minimum of information
*
* @param path the path where the error took place
* @param topTypeName the name of the SCThingType
* @param typeName the name of the object, with which something went wrong
* @param object the object or name
* @param message the error message
*/
function composeErrorMessage(path: string, topTypeName: string, typeName: string, object: string, message: string) {
const error = `At "${topTypeName}::${path.substr(0, path.length - 1)}" for ${typeName} "${trimString(object, maxErrorObjectChars)}": ${message}`;
errors.push(error);
if (showErrors) {
// tslint:disable-next-line:no-floating-promises
void Logger.error(error);
}
}
/**
* Trims a string to a readable size and appends "..."
*
* @param value the string to trim
* @param maxLength the maximum allowed length before it is clamped
*/
function trimString(value: string, maxLength: number): string {
return value.length > maxLength ?
`${value.substring(0, maxLength)}...` :
value;
}
/**
* Gets the Reflections and names for Generics in a ReferenceType of a DeclarationReflection
*
* Warning to future maintainers: The code for generics doesn't account for depth. when there is a new generic, it will
* override the previous one, if there isn't, it will just continue passing it down.
*
* @param type the ReferenceType of a DeclarationReflection
* @param out the previous reflection, it then overrides all parameters or keeps old ones
* @param topTypeName the name of the object, with which something went wrong
* @param path the current path to the object we are in
* @param tags any tags attached to the type
*/
function getReflectionGeneric(type: ReferenceType,
out: Map<string, ElasticsearchValue>,
topTypeName: string,
path: string,
tags: CommentTag[]): Map<string, ElasticsearchValue> {
if (typeof type.typeArguments !== 'undefined'
&& type.reflection instanceof DeclarationReflection
&& typeof type.reflection.typeParameters !== 'undefined') {
for (let i = 0; i < type.reflection.typeParameters.length; i++) {
if (i < type.typeArguments.length) {
out
.set(type.reflection.typeParameters[i].name, handleType(type.typeArguments[i], out, topTypeName, path, tags));
} else {
// this can happen due to a bug in TypeDoc https://github.com/TypeStrong/typedoc/issues/1061
// we have no way to know the type here, so we have to use this.
out.set(type.reflection.typeParameters[i].name, {
dynamic: true,
properties: {},
});
Logger.warn(`Type "${type.name}": Defaults of generics (Foo<T = any>) currently don't work due to a bug` +
` in TypeDoc. It has been replaced by a dynamic type.`);
}
}
}
return out;
}
/**
* Handles a ReferenceType that has no value
*
* Most of the times that is an external type.
*
* @param ref the ReferenceType
* @param generics the generics from levels above, so we can use them without having access to the parent
* @param path the current path to the object we are in
* @param topTypeName the name of the SCThingType
* @param tags any tags attached to the type
*/
function handleExternalType(ref: ReferenceType, generics: Map<string, ElasticsearchValue>,
path: string, topTypeName: string, tags: CommentTag[]): ElasticsearchValue {
for (const premap of Object.keys(premaps)) {
if (premap === ref.name) {
return readFieldTags(premaps[premap], path, topTypeName, tags);
}
}
if (ref.name === 'Array') { // basically an external type, but Array is quite common, especially with generics
if (typeof ref.typeArguments === 'undefined' || typeof ref.typeArguments[0] === 'undefined') {
composeErrorMessage(path, topTypeName, 'Array with generics', 'array', 'Failed to parse');
return {type: ElasticsearchDataType.parse_error};
}
return readFieldTags(
handleType(
ref.typeArguments[0], getReflectionGeneric(
ref, new Map(generics), path, topTypeName, tags),
path, topTypeName, tags),
path, topTypeName, tags);
}
if (ref.name === '__type') { // empty object
return {
dynamic: 'strict',
properties: {},
};
}
composeErrorMessage(path, topTypeName, 'external type', ref.name, 'Missing pre-map');
return readFieldTags({type: ElasticsearchDataType.missing_premap}, path, topTypeName, tags);
}
/**
* Handles an object
*
* @param decl the DeclarationReflection of the object
* @param generics the generics from levels above, so we can use them without having access to the parent
* @param path the current path to the object we are in
* @param topTypeName the name of the SCThingType
* @param inheritedTags the inherited tags
*/
function handleDeclarationReflection(decl: DeclarationReflection,
generics: Map<string, ElasticsearchValue>,
path: string,
topTypeName: string,
inheritedTags?: CommentTag[]):
ElasticsearchValue {
// check if we have an object referencing a generic
if (generics.has(decl.name)) { // if the object name is the same as the generic name
return readFieldTags(generics.get(decl.name) as ElasticsearchObject | ElasticsearchType, path, topTypeName,
decl.comment?.tags ?? []);
// use the value defined by the generic
}
// start the actual handling process
const out: ElasticsearchObject = {
dynamic: 'strict',
properties: {},
};
let empty = true;
// first check if there are any index signatures, so for example `[name: string]: Foo`
if (typeof decl.indexSignature !== 'undefined') {
out.dynamic = true;
if (typeof decl.indexSignature.type !== 'undefined') {
empty = false;
const template: ElasticsearchDynamicTemplate = {};
template[decl.name] = {
mapping: handleType(
decl.indexSignature.type,
new Map(generics), path, topTypeName,
getCommentTags(decl.indexSignature, path, topTypeName),
),
match: '*',
match_mapping_type: '*',
path_match: `${path}*`,
};
dynamicTemplates.push(template);
}
}
if (decl.kindString === 'Enumeration') {
return readTypeTags('string', path, topTypeName, getCommentTags(decl, path, topTypeName, inheritedTags));
}
// check all the children, so in this case we are dealing with an OBJECT
if (typeof decl.children !== 'undefined' && decl.children.length > 0) {
for (const child of decl.children) {
empty = false;
out.properties[child.name] =
handleDeclarationReflection(child, new Map(generics), `${path}${child.name}.`, topTypeName);
}
} else if (decl.type instanceof Type) { // if the object is a type, so we are dealing with a PROPERTY
// get inherited tags
const tags = (inheritedTags ?? []).length > 0 ? inheritedTags! : getCommentTags(decl, path, topTypeName);
return handleType(decl.type, new Map(generics), path, topTypeName, tags);
} else if (decl.kindString === 'Enumeration member') {
return readTypeTags(typeof decl.defaultValue, path, topTypeName,
getCommentTags(decl, path, topTypeName, inheritedTags));
}
if (empty) {
composeErrorMessage(path, topTypeName, 'object', decl.name, 'Empty object');
}
return readFieldTags(out, path, topTypeName, getCommentTags(decl, path, topTypeName));
}
/**
* Reads all comment tags, including inherited ones
*
* @param decl the DeclarationReflection to read the tags from
* @param path the path on which the comments lie
* @param topTypeName the name of the SCThingType
* @param inheritedTags any tags that might have been inherited by a parent
* @param breakId the id of the previous reflection to prevent infinite recursion in some cases
*/
function getCommentTags(
decl: DeclarationReflection | SignatureReflection,
path: string,
topTypeName: string,
inheritedTags: CommentTag[] = [],
// tslint:disable-next-line:no-unnecessary-initializer
breakId: number | undefined = undefined,
): CommentTag[] {
if (decl.id === breakId) {
return [];
}
let out: CommentTag[] = decl.comment instanceof Comment ?
typeof decl.comment.tags !== 'undefined' ? decl.comment.tags : inheritedTags : inheritedTags;
if (decl.overwrites instanceof ReferenceType && decl.overwrites.reflection instanceof DeclarationReflection) {
out = arrayPriorityJoin(
getCommentTags(decl.overwrites.reflection, path, topTypeName, inheritedTags, decl.id), out);
}
if (decl.inheritedFrom instanceof ReferenceType && decl.inheritedFrom.reflection instanceof DeclarationReflection) {
out = arrayPriorityJoin(
getCommentTags(decl.inheritedFrom.reflection, path, topTypeName, inheritedTags, decl.id), out);
}
saveCommentTags(out, path, topTypeName);
const inheritTag = out.find(((value) => value.tagName === inheritTagsName));
if (typeof inheritTag !== 'undefined') {
out = arrayPriorityJoin(out, retrieveCommentTags(inheritTag.text.trim(), path, topTypeName));
}
return out;
}
/**
* Saves all comment tags to the map
*
* @param tags all tags to be saved (@see and @[inheritTags] will be stripped)
* @param path the path of field
* @param topTypeName the name of the SCThingType
*/
function saveCommentTags(tags: CommentTag[], path: string, topTypeName: string) {
inheritTagsMap[`${topTypeName}::${path.substr(0, path.length - 1)}`] =
tags.filter(((value) => value.tagName !== 'see' && value.tagName !== inheritTagsName));
}
/**
* Retrieves any saved tags
*
* @param path the path to the original field
* @param currentPath the current path to the object we are in
* @param topTypeName the name of the SCThingType
*/
function retrieveCommentTags(path: string, currentPath: string, topTypeName: string): CommentTag[] {
if (!(path in inheritTagsMap)) {
composeErrorMessage(currentPath, topTypeName, path, 'Comment', 'Referenced path to tags does not exist!');
return [];
}
return inheritTagsMap[path];
}
/**
* Joins two arrays of CommentTags, but overrides all original CommentTags with the same tagName
*
* @param originals the original array
* @param overrider the array that should be appended and provide the override values
*/
function arrayPriorityJoin(originals: CommentTag[], overrider: CommentTag[]): CommentTag[] {
const out: CommentTag[] = overrider;
originals.forEach((original) => {
const result = overrider.find((element) => original.tagName === element.tagName);
// no support for multiple tags with the same name
if (!(result instanceof CommentTag)) {
out.push(original);
}
});
return out;
}
/**
* Handles UnionTypes
*
* Put into a separate function as it is a little bit more complex
* Works fairly reliable, although there are issues with primitive union types, which don't work at all (And never will)
*
* @param type the type object
* @param generics the generics from levels above, so we can use them without having access to the parent
* @param path the current path to the object we are in
* @param topTypeName the name of the SCThingType
* @param tags any tags attached to the type
*/
function handleUnionType(type: UnionType,
generics: Map<string, ElasticsearchValue>,
path: string,
topTypeName: string,
tags: CommentTag[]): ElasticsearchValue {
const list: ElasticsearchValue[] = [];
for (const subType of type.types) {
if (subType instanceof IntrinsicType && subType.name === 'undefined') {
continue;
}
list.push(handleType(subType, new Map(generics), path, topTypeName, tags));
}
if (list.length > 0) {
let out = list[0];
for (const item of list) {
out = merge<ElasticsearchValue>(out, item);
}
return out;
}
composeErrorMessage(path, topTypeName, 'Union Type', stringify(list),
'Empty union type. This is likely not a user error.');
return {type: ElasticsearchDataType.parse_error};
}
/**
* Serves as a kind of distributor for the different types, should not contain any specific code
*
* @param type the type object
* @param generics the generics from levels above, so we can use them without having access to the parent
* @param path the current path to the object we are in
* @param topTypeName the name of the SCThingType
* @param tags any tags attached to the type
*/
function handleType(type: Type, generics: Map<string, ElasticsearchValue>, path: string, topTypeName: string,
tags: CommentTag[]):
ElasticsearchValue {
// logger.log((type as any).name);
if (type instanceof ArrayType) { // array is irrelevant in Elasticsearch, so just go with the element type
const esType = handleType(type.elementType, new Map(generics), path, topTypeName, tags);
// also merge tags of the array to the element type
// filter out the type tags lazily, this can lead to double messages for "Not implemented tag"
let newTags = tags;
if ('type' in esType) {
newTags = tags.filter((tag) => {
return !(tag.tagName === esType.type);
});
}
return readFieldTags(esType, path, topTypeName, newTags);
}
if (type.type === 'stringLiteral') { // a string literal, usually for type
return readTypeTags(type.type, path, topTypeName, tags);
}
if (type instanceof IntrinsicType) { // the absolute default type, like strings
return readTypeTags(type.name, path, topTypeName, tags);
}
if (type instanceof UnionType) { // the union type...
return handleUnionType(type, new Map(generics), path, topTypeName, tags);
}
if (type instanceof ReferenceType) {
if (typeof premaps[type.name] === 'undefined' && typeof type.reflection !== 'undefined') {
// there is really no way to make this typesafe, every element in DeclarationReflection is optional.
return handleDeclarationReflection(type.reflection as DeclarationReflection,
getReflectionGeneric(type, new Map(generics), path, topTypeName, tags), path, topTypeName, tags);
}
return handleExternalType(type, new Map(generics), path, topTypeName, tags);
}
if (type instanceof TypeParameterType) {
// check if we have an object referencing a generic
if (generics.has(type.name)) {
return generics.get(type.name) as ElasticsearchObject | ElasticsearchType;
}
composeErrorMessage(path, topTypeName, 'Generic', type.name, 'Missing reflection, please report!');
return {type: ElasticsearchDataType.parse_error};
}
if (type instanceof ReflectionType) {
return readFieldTags(handleDeclarationReflection(type.declaration, new Map(generics), path, topTypeName),
path, topTypeName, tags);
}
composeErrorMessage(path, topTypeName, 'type', stringify(type), 'Not implemented type');
return {type: ElasticsearchDataType.parse_error};
}
/**
* Adds an aggregatable to the aggregations list
*
* @param path the current path
* @param topTypeName the name of the top type
* @param global whether the topTypeName will be used
*/
function addAggregatable(path: string, topTypeName: string, global: boolean) {
// push type.path and remove the '.' at the end of the path
if (global) {
const prop = path.slice(0, -1)
.split('.')
.pop() as string; // cannot be undefined
return (aggregations['@all'] as ESNestedAggregation).aggs[prop.split('.')
.pop() as string] = {
terms: {
field: `${prop}.raw`,
size: 1000,
},
};
}
const property = path.slice(0, -1);
return (aggregations[topTypeName] as ESNestedAggregation).aggs[property] = {
terms: {
field: `${property}.raw`,
size: 1000,
},
};
}
/**
* Reads all tags related to Elasticsearch fields from the fieldMap
*
* @param prev the previous ElasticsearchValue, for example and object
* @param path the current path to the object we are in
* @param topTypeName the name of the SCThingType
* @param tags tags attached to the value
* @param dataType the ElasticsearchDataType, for checking if a tag is a type tag
*/
function readFieldTags(prev: ElasticsearchValue,
path: string,
topTypeName: string,
tags: CommentTag[],
dataType?: string): ElasticsearchValue {
for (const tag of tags) {
if (tag.tagName === aggregatableTag) {
addAggregatable(path, topTypeName, tag.text.trim() === aggregatableTagParameterGlobal);
}
if (!ignoredTagsList.includes(tag.tagName)) {
if (typeof fieldmap[tag.tagName] !== 'undefined') {
if (typeof prev.fields === 'undefined') {
// create in case it doesn't exist
prev.fields = {};
}
if (tag.text.trim() === '') {
// merge fields
prev.fields = {...prev.fields, ...fieldmap[tag.tagName].default};
} else if (typeof fieldmap[tag.tagName][tag.text.trim()] !== 'undefined') {
// merge fields
prev.fields = {...prev.fields, ...fieldmap[tag.tagName][tag.text.trim()]};
} else if (!fieldmap[tag.tagName].ignore.includes(tag.text.trim())) {
// when there is an unidentified tag
composeErrorMessage(path, topTypeName, 'tag', tag.tagName, `Not implemented tag param "${tag.text.trim()}"`);
}
} else if (tag.tagName === filterableTagName) {
if (typeof prev.fields === 'undefined') {
prev.fields = {};
}
if ('type' in prev) {
const type = filterableMap[prev.type];
if (typeof type !== 'undefined') {
// merge fields
prev.fields = {...prev.fields, ...{raw: {type: type}}};
} else {
composeErrorMessage(path, topTypeName, 'tag', tag.tagName, `Not implemented for ${prev.type}`);
}
} else {
composeErrorMessage(path, topTypeName, 'tag', tag.tagName, 'Not applicable for object types');
}
} else if (typeof dataType === 'undefined' || typeof typemap[dataType][tag.tagName] === 'undefined') {
composeErrorMessage(path, topTypeName, 'tag', tag.tagName, `Not implemented tag`);
}
}
}
return prev;
}
/**
* Reads all types related to Elasticsearch fields from the fieldMap
*
* @param type the type of the value
* @param path the current path to the object we are in
* @param topTypeName the name of the SCThingType
* @param tags tags attached to the value
*/
function readTypeTags(type: string, path: string, topTypeName: string, tags: CommentTag[]): ElasticsearchValue {
let out: ElasticsearchValue = {type: ElasticsearchDataType.parse_error};
if (typeof typemap[type] !== 'undefined') { // first look if the value has a definition in the typemap
for (let i = tags.length - 1; i >= 0; i--) {
if (!ignoredTagsList.includes(tags[i].tagName) && typeof typemap[type][tags[i].tagName] !== 'undefined') {
// if we have a tag that indicates a type
if (out.type !== ElasticsearchDataType.parse_error) {
composeErrorMessage(path, topTypeName, 'type', type,
`Type conflict; "${typemap[type][tags[i].tagName]}" would override "${out.type}"`);
out.type = ElasticsearchDataType.type_conflict;
continue;
}
out.type = typemap[type][tags[i].tagName];
}
}
if (out.type === ElasticsearchDataType.parse_error) {
out.type = typemap[type].default;
}
out = readFieldTags(out, path, topTypeName, tags, type);
return out;
}
if (dynamicTypes.includes(type)) { // Elasticsearch dynamic type TODO: doesn't work for direct types
return {
dynamic: true,
properties: {},
};
}
composeErrorMessage(path, topTypeName, 'type', type, 'Not implemented type');
return out;
}
/**
* Reset the state
*
* This is kind of a suboptimal solution and should be changed in the future.
* https://gitlab.com/openstapps/core-tools/-/issues/49
*
* @param resetInheritTags whether inherited tags should be reset as well
*/
function reset(resetInheritTags = true) {
errors = [];
dynamicTemplates = [];
aggregations = {
'@all': {
aggs: {},
filter: {
match_all: {},
},
},
};
if (resetInheritTags) {
inheritTagsMap = {};
}
}
/**
* Takes a project reflection and generates an ElasticsearchTemplate from it
*
* Serves as the entry point for getting the mapping, so if you just want to get the mapping files for Elasticsearch,
* you can do so by calling this function, `RETURNED_VALUE.template` contains the mapping in a fashion that is directly
* readable by Elasticsearch.
*
* @param projectReflection a reflection of the project you want to get the ES Mappings from
* @param ignoredTags the tag names for which the error output should be suppressed
* @param showErrorOutput whether to print all errors in the command line or not
* @param interfaceFilter only parse specific interfaces, this is for testing purposes
*/
export function generateTemplate(projectReflection: ProjectReflection,
ignoredTags: string[],
showErrorOutput = true,
interfaceFilter: string[] = []):
// tslint:disable-next-line:completed-docs
{ aggregations: AggregationSchema; errors: string[]; mappings: ElasticsearchTemplateCollection; } {
reset();
showErrors = showErrorOutput;
ignoredTagsList = ['indexable', 'validatable', inheritTagsName];
ignoredTagsList.push.apply(ignoredTagsList, ignoredTags);
const indexableInterfaces = getAllIndexableInterfaces(projectReflection);
const out: ElasticsearchTemplateCollection = {};
for (const _interface of indexableInterfaces) {
// TODO: lots of duplicate code, this all needs to be changed https://gitlab.com/openstapps/core-tools/-/issues/49
if (!Array.isArray(_interface.children) || _interface.children.length === 0) {
throw new Error('Interface needs at least some properties to be indexable');
}
const typeObject = _interface.children.find((declarationReflection) => {
return declarationReflection.name === 'type';
});
if (typeof typeObject === 'undefined' || typeof typeObject.type === 'undefined') {
throw new Error('Interface needs a type to be indexable');
}
let typeName = 'INVALID_TYPE';
if (typeObject.type instanceof ReferenceType) {
if (typeObject.type.reflection instanceof DeclarationReflection
&& typeof typeObject.type.reflection.defaultValue === 'string') {
typeName = typeObject.type.reflection.defaultValue.replace('"', '')
.replace('"', '');
} else {
// tslint:disable-next-line:no-floating-promises
void Logger.error('Your input files seem to be incorrect, or there is a major bug in the mapping generator.');
}
} else if (typeObject.type instanceof StringLiteralType) {
Logger.warn(`The interface ${_interface.name} uses a string literal as type, please use SCThingType.`);
typeName = typeObject.type.value;
} else {
// tslint:disable-next-line:no-floating-promises
void Logger.error(`The interface ${_interface.name} is required to use an SCThingType as a type, please do so.`);
}
// init aggregation schema for type
aggregations[typeName] = {
aggs: {},
filter: {
type: {
value: typeName,
},
},
};
handleDeclarationReflection(_interface, new Map(), '', typeName);
}
// second traversal
reset(false);
for (const _interface of indexableInterfaces) {
if (!Array.isArray(_interface.children) || _interface.children.length === 0) {
throw new Error('Interface needs at least some properties to be indexable');
}
const typeObject = _interface.children.find((declarationReflection) => {
return declarationReflection.name === 'type';
});
if (typeof typeObject === 'undefined' || typeof typeObject.type === 'undefined') {
throw new Error('Interface needs a type to be indexable');
}
let typeName = 'INVALID_TYPE';
if (typeObject.type instanceof ReferenceType) {
if (typeObject.type.reflection instanceof DeclarationReflection
&& typeof typeObject.type.reflection.defaultValue === 'string') {
typeName = typeObject.type.reflection.defaultValue.replace('"', '')
.replace('"', '');
} else {
// tslint:disable-next-line:no-floating-promises
void Logger.error('Your input files seem to be incorrect, or there is a major bug in the mapping generator.');
}
} else if (typeObject.type instanceof StringLiteralType) {
Logger.warn(`The interface ${_interface.name} uses a string literal as type, please use SCThingType.`);
typeName = typeObject.type.value;
} else {
// tslint:disable-next-line:no-floating-promises
void Logger.error(`The interface ${_interface.name} is required to use an SCThingType as a type, please do so.`);
}
// filter out
if (interfaceFilter.length !== 0) {
if (typeof interfaceFilter.find((it) => it === typeName) === 'undefined') {
continue;
}
}
// init aggregation schema for type
aggregations[typeName] = {
aggs: {},
filter: {
type: {
value: typeName,
},
},
};
let typeNameWithoutSpaces = typeName.toLowerCase();
while (typeNameWithoutSpaces.includes(' ')) {
typeNameWithoutSpaces = typeNameWithoutSpaces.replace(' ', '_');
}
const templateName = `template_${typeNameWithoutSpaces}`;
out[templateName] = {
mappings: {
[typeName]: handleDeclarationReflection(_interface, new Map(), '', typeName) as ElasticsearchObject,
},
settings: settings,
template: `stapps_${typeNameWithoutSpaces}*`,
}
;
out[templateName].mappings[typeName].properties.creation_date = {
type: ElasticsearchDataType.date,
};
out[templateName].mappings[typeName].dynamic_templates = dynamicTemplates;
// Set some properties
out[templateName].mappings[typeName]._source = {
excludes: [
'creation_date',
],
};
out[templateName].mappings[typeName].date_detection = false;
dynamicTemplates = [];
if (Object.keys((aggregations[typeName] as ESNestedAggregation).aggs).length === 0) {
delete aggregations[typeName];
}
}
return {aggregations, mappings: out, errors};
}

View File

@@ -1,81 +0,0 @@
/*
* Copyright (C) 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/>.
*/
/**
* An elasticsearch bucket aggregation
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/search-aggregations-bucket.html
*/
export interface AggregationSchema {
[aggregationName: string]: ESTermsFilter | ESNestedAggregation;
}
/**
* An elasticsearch terms filter
*/
export interface ESTermsFilter {
/**
* Terms filter definition
*/
terms: {
/**
* Field to apply filter to
*/
field: string;
/**
* Number of results
*/
size?: number;
};
}
/**
* Filter that filters by name of the the field type
*/
export interface ESAggTypeFilter {
/**
* The type of the object to find
*/
type: {
/**
* The name of the type
*/
value: string;
};
}
/**
* Filter that matches everything
*/
export interface ESAggMatchAllFilter {
/**
* Filter that matches everything
*/
match_all: {};
}
/**
* For nested aggregations
*/
export interface ESNestedAggregation {
/**
* Possible nested Aggregations
*/
aggs: AggregationSchema;
/**
* Possible filter for types
*/
filter: ESAggTypeFilter | ESAggMatchAllFilter;
}

View File

@@ -1,59 +0,0 @@
/*
* Copyright (C) 2019-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 {ElasticsearchFieldmap, ElasticsearchFilterableMap} from '../mapping-definitions';
import {ElasticsearchDataType} from './typemap';
export enum analyzers {
ducet_sort = 'ducet_sort',
search_german = 'search_german',
}
export const fieldmap: ElasticsearchFieldmap = {
aggregatable: {
default: {
raw: {
ignore_above: 10000,
type: ElasticsearchDataType.keyword,
},
},
ignore: ['global'],
},
sortable: {
default: {
sort: {
analyzer: analyzers.ducet_sort,
fielddata: true,
type: ElasticsearchDataType.text,
},
},
ducet: {
sort: {
analyzer: analyzers.ducet_sort,
fielddata: true,
type: ElasticsearchDataType.text,
},
},
ignore: ['price'],
},
};
export const filterableTagName = 'filterable';
export const filterableMap: ElasticsearchFilterableMap = {
date: ElasticsearchDataType.keyword,
keyword: ElasticsearchDataType.keyword,
text: ElasticsearchDataType.keyword,
integer: ElasticsearchDataType.integer,
};

View File

@@ -1,69 +0,0 @@
/*
* Copyright (C) 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 {ElasticsearchPremap} from '../mapping-definitions';
import {ElasticsearchDataType} from './typemap';
export const premaps: ElasticsearchPremap = {
CoordinateReferenceSystem: {
dynamic: true,
properties: {
type: {
type: ElasticsearchDataType.keyword,
},
},
},
LineString: {
precision: '1m',
tree: 'quadtree',
type: ElasticsearchDataType.geo_shape,
},
Point: {
properties: {
type: {
type: ElasticsearchDataType.keyword,
},
coordinates: {
type: ElasticsearchDataType.geo_point,
},
},
dynamic: 'strict',
},
Polygon: {
precision: '1m',
tree: 'quadtree',
type: ElasticsearchDataType.geo_shape,
},
SCISO8601DateRange: {
type: ElasticsearchDataType.date_range,
},
'jsonpatch.OpPatch': {
dynamic: 'strict',
properties: {
from: {
type: ElasticsearchDataType.keyword,
},
op: {
type: ElasticsearchDataType.keyword,
},
path: {
type: ElasticsearchDataType.keyword,
},
value: {
// this is actually an 'any' type, however ES does not really support that.
type: ElasticsearchDataType.keyword,
},
},
},
};

View File

@@ -1,65 +0,0 @@
/*
* Copyright (C) 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 {ElasticsearchSettings} from '../mapping-definitions';
export const settings: ElasticsearchSettings = {
analysis: {
analyzer: {
ducet_sort: {
filter: [
'german_phonebook',
],
tokenizer: 'keyword',
type: 'custom',
},
search_german: {
filter: [
'lowercase',
'german_stop',
'german_stemmer',
],
tokenizer: 'stapps_ngram',
type: 'custom',
},
},
filter: {
german_phonebook: {
country: 'DE',
language: 'de',
type: 'icu_collation',
variant: '@collation=phonebook',
},
german_stemmer: {
language: 'german',
type: 'stemmer',
},
german_stop: {
stopwords: '_german_',
type: 'stop',
},
},
tokenizer: {
stapps_ngram: {
max_gram: 7,
min_gram: 4,
type: 'ngram',
},
},
},
'mapping.total_fields.limit': 10000,
max_result_window: 30000,
number_of_replicas: 0,
number_of_shards: 1,
};

View File

@@ -1,70 +0,0 @@
/*
* Copyright (C) 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 {ElasticsearchTypemap} from '../mapping-definitions';
export enum ElasticsearchDataType {
missing_premap = 'MISSING_PREMAP',
parse_error = 'PARSE_ERROR',
type_conflict = 'TYPE_CONFLICT',
text = 'text',
keyword = 'keyword',
date = 'date',
// long = 'long',
// double = 'double',
float = 'float',
boolean = 'boolean',
ip = 'ip',
integer = 'integer',
object = 'object',
nested = 'nested',
geo_point = 'geo_point',
geo_shape = 'geo_shape',
completion = 'completion',
date_range = 'date_range',
// integer_range = 'integer_range',
// float_range = 'float_range',
// long_range = 'long_range',
// double_range = 'double_range',
// ip_range = 'ip_range',
}
export const typemap: ElasticsearchTypemap = {
boolean: {
default: ElasticsearchDataType.boolean,
},
false: {
default: ElasticsearchDataType.boolean,
},
number: {
default: ElasticsearchDataType.integer,
float: ElasticsearchDataType.float,
integer: ElasticsearchDataType.integer,
date: ElasticsearchDataType.date,
},
string: {
default: ElasticsearchDataType.text,
keyword: ElasticsearchDataType.keyword,
text: ElasticsearchDataType.text,
date: ElasticsearchDataType.date,
},
stringLiteral: {
default: ElasticsearchDataType.keyword,
},
true: {
default: ElasticsearchDataType.boolean,
},
};
export const dynamicTypes = ['any', 'unknown'];

View File

@@ -1,324 +0,0 @@
/*
* Copyright (C) 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 {ElasticsearchDataType} from './definitions/typemap';
// tslint:disable:no-any
/**
* ElasticsearchValue can be either a type or an object.
*
* Both are composed similarly, and can be the value of a propery
* of an Elasticsearch Object.
*/
export type ElasticsearchValue = ElasticsearchType | ElasticsearchObject | ElasticsearchGeoShape;
/**
* The Typemap is used to get the corresponding ElasicsearchDataType for a name provided by the ProjectReflection
*/
export interface ElasticsearchTypemap {
/**
* The `stringLiteral` type must always be provided
*/
stringLiteral: {
/**
* The default can be chosen freely, but must be provided
*/
default: ElasticsearchDataType;
};
/**
* The name of the JS type, so for `number` it would be number
*/
[name: string]: {
/**
* The default ElasticsearchDataType that should be used, if no tag or only not implemented tags are found
*/
default: ElasticsearchDataType;
/**
* The name of the tag, so for `@integer` it would be `integer`
*/
[name: string]: ElasticsearchDataType;
};
}
/**
* The representation of a `DynamicTemplate` in Elasticsearch
*
* https://www.elastic.co/guide/en/elasticsearch/reference/5.6/dynamic-templates.html
*/
export interface ElasticsearchDynamicTemplate {
/**
* The name of the dynamicTemplate
*/
[name: string]: {
/**
* The mapping of the template
*/
mapping: ElasticsearchValue;
/**
* With automatic mapping, we use `path_match` more or less out of convenience and because it is least error-prone
*
* This also means that match should match all ("*") interface names (because we provide the exact path of the
* interface)
*/
match: '*';
/**
* With automatic mapping, we use `path_match` more or less out of convenience and because it is least error-prone
*
* This also means that match_mapping_type should match all ("*") names (because we provide the exact path of the
* interface)
*/
match_mapping_type: '*';
/**
* With automatic mapping, we use `path_match` more or less out of convenience and because it is least error-prone
*/
path_match: string;
};
}
export interface ElasticsearchFilterableMap {
[name: string]: ElasticsearchDataType;
}
/**
* The Fieldmap contains all tag names for fields and the corresponding fields
*
* The Fieldmap works in a similar fashion to the Typemap
*/
export interface ElasticsearchFieldmap {
/**
* The name of the tag, so for `@sortable` it would be `sortable`
*/
[name: string]: {
/**
* The default value if no parameter is provided
*/
default: {
/**
* To allow the usage of `prev.fields = {...prev.fields, ...fieldmap[tag.tagName].default}`
*
* We could also have used `default: any`, but this adds slightly more improved type-safety.
*/
[name: string]: any;
};
/**
* The tag parameters that will be ignored
*
* Some tag parameters might not be important for your implementation, so you can add their names here to not get
* any errors. The `default` will be used in that case.
*/
ignore: string[];
/**
* The parameters of the tag, so for `@sortable ducet` it would be `ducet`
*/
[name: string]: {
/**
* To allow the usage of `prev.fields = {...prev.fields, ...fieldmap[tag.tagName][tag.text.trim()]}`
*
* We could also have used `default: any`, but this adds slightly more improved type-safety.
*/
[name: string]: any;
};
};
}
/**
* A primitive data type
*
* https://www.elastic.co/guide/en/elasticsearch/reference/5.6/mapping-types.html
*/
export interface ElasticsearchType {
/**
* Fields for a type
*
* The fields are optional, they are used for things like sorting, which is not needed for every single type.
*/
fields?: {
[name: string]: any;
};
/**
* The type as an ElasticsearchDataType
*/
type: ElasticsearchDataType;
}
/**
* A GeoShape data type
*
* https://www.elastic.co/guide/en/elasticsearch/reference/5.6/geo-shape.html
*/
export interface ElasticsearchGeoShape {
/**
* Does not exist; here for TypeScript compiler
*/
fields?: undefined;
/**
* This parameter may be used instead of tree_levels to set an appropriate value for the tree_levels parameter.
*
* The value specifies the desired precision and Elasticsearch will calculate the best tree_levels value to honor
* this precision. The value should be a number followed by an optional distance unit. Valid distance units include:
* in, inch, yd, yard, mi, miles, km, kilometers, m,meters, cm,centimeters, mm, millimeters.
*/
precision: string;
/**
* Name of the PrefixTree implementation to be used: geohash for GeohashPrefixTree and quadtree for QuadPrefixTree.
*/
tree: 'quadtree' | 'geohash';
/**
* The type of the object, obviously geo_shape
*/
type: ElasticsearchDataType.geo_shape;
}
/**
* An object data type
*
* https://www.elastic.co/guide/en/elasticsearch/reference/5.6/object.html
*/
export interface ElasticsearchObject {
/**
* Only for the top type
*/
_source?: {
/**
* Fields that should be excluded in the _source field
*/
excludes: [
'creation_date'
];
};
/**
* Whether the creation date should be set automatically
*/
date_detection?: boolean;
/**
* If the object is a dynamic
*
* https://www.elastic.co/guide/en/elasticsearch/reference/5.6/dynamic.html
* The default should be `'strict'`
*/
dynamic: true | false | 'strict';
/**
* dynamic_templates for an object
*
* https://www.elastic.co/guide/en/elasticsearch/reference/5.6/dynamic-templates.html
* This is a more complex topic, before touching this you should really know what you are doing.
*/
dynamic_templates?: ElasticsearchDynamicTemplate[];
/**
* Fields for a type
*
* The fields are optional, they are used for things like sorting, which is not needed for every single type.
*/
fields?: {
[name: string]: any;
};
/**
* Any properties of the object
*
* https://www.elastic.co/guide/en/elasticsearch/reference/5.6/properties.html
*/
properties: {
/**
* Each property can be any Elasticsearch value
*
* https://www.elastic.co/guide/en/elasticsearch/reference/5.6/mapping-types.html
*/
[name: string]: ElasticsearchValue;
};
}
/**
* A collection of Elasticsearch Templates
*/
export interface ElasticsearchTemplateCollection {
[indexName: string]: ElasticsearchTemplate;
}
/**
* An Elasticsearch template
*
* https://www.elastic.co/guide/en/elasticsearch/reference/5.6/mapping.html
* This is what you pass to Elasticsearch
*/
export interface ElasticsearchTemplate {
/**
* This is a pre-defined structure you should use for your mapping
*/
mappings: {
[typeName: string]: ElasticsearchObject;
};
/**
* The settings for Elasticsearch
*/
settings: ElasticsearchSettings;
/**
* The name of the template, for referencing in Elasticsearch
*/
template: string;
}
/**
* A representation of ElasticsearchSettings used in Mappings
*/
export interface ElasticsearchSettings {
/**
* The settings
*/
[name: string]: any;
/**
* This is where any analyzers go
*
* https://www.elastic.co/guide/en/elasticsearch/reference/5.6/analysis-analyzers.html
*/
analysis: {
[name: string]: any;
};
}
/**
* A premap for a specific value in a ProjectReflection
*
* This is meant to be used for external types. To aid performance, you usually should not include external libs in the
* ProjectReflection. This means that there is no way the generator can generate a mapping for it, so you can use the
* premaps to map out a type manually.
*/
export interface ElasticsearchPremap {
/**
* The name of the type with the corresponding map
*
* So for `const a: B` the name would be `B`
*/
[name: string]: ElasticsearchValue;
}

View File

@@ -1,5 +1,6 @@
/* eslint-disable unicorn/error-message */
/*
* Copyright (C) 2019 StApps
* 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.
@@ -15,37 +16,13 @@
import {Logger} from '@openstapps/logger';
import del from 'del';
import {existsSync} from 'fs';
import {basename, dirname, join} from 'path';
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 */';
/**
* A JavaScript module representation to sort a list of them by dependencies
*/
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;
}
/**
* Pack cli.js
*
@@ -55,32 +32,30 @@ interface JavaScriptModule {
* Furthermore it checks that no shebang line is present and that it does not export anything.
*/
async function packCliJs(): Promise<void> {
const path = join(cwd(), 'lib', 'cli.js');
const cliPath = path.join(cwd(), 'lib', 'cli.js');
if (!existsSync(path)) {
if (!existsSync(cliPath)) {
return;
}
Logger.info('Adjusting JavaScript CLI...');
const buffer = await readFilePromisified(path);
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 | null = null;
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 Error(
`Line '${lineNumber}' in 'cli.js' exports something. cli.js is not for exporting. Line was:
${line}`,
throw new TypeError(
`Line '${lineNumber}' in 'cli.js' exports something. cli.js is not for exporting. Line was:\n${line}`,
);
}
@@ -89,16 +64,14 @@ ${line}`,
const match = line.match(/^(\s*)(const|var) ([a-z0-9_]*) = require\(("[^"]+"|'[^']+')\);$/i);
if (match !== null) {
// tslint:disable-next-line:no-magic-numbers
const importedName = match[3];
// tslint:disable-next-line:no-magic-numbers
// 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 !== null) {
if (internalRequire) {
return `const ${importedName} = ${internalRequire};`;
}
@@ -113,20 +86,18 @@ ${line}`,
})
.join('\n');
return writeFilePromisified(path, `#!/usr/bin/env node
${adjustedContent}`);
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(join(cwd(), '*(lib|src)', '**', '*.d.ts'), {
const fileNames = await globPromisified(path.join(cwd(), '*(lib|src)', '**', '*.d.ts'), {
ignore: [
join(cwd(), 'lib', 'doc', '**', '*.d.ts'),
join(cwd(), 'lib', 'test', '**', '*.d.ts'),
join(cwd(), 'lib', 'cli.d.ts'),
path.join(cwd(), 'lib', 'doc', '**', '*.d.ts'),
path.join(cwd(), 'lib', 'test', '**', '*.d.ts'),
path.join(cwd(), 'lib', 'cli.d.ts'),
],
});
@@ -143,24 +114,24 @@ async function getAllTypeDefinitions(): Promise<string[]> {
async function packTypeDefinitions(): Promise<void> {
Logger.info('Packing TypeScript definition files...');
const path = join(cwd(), 'lib', 'index.d.ts');
const indexPath = path.join(cwd(), 'lib', 'index.d.ts');
await deleteFileIfExistingAndPacked(path);
await deleteFileIfExistingAndPacked(indexPath);
const typeDefinitions = await getAllTypeDefinitions();
// pack TypeScript definition files
const imports: { [k: string]: string[]; } = {};
const imports: {[k: string]: string[]} = {};
const referenceLines: string[] = [];
let allDefinitions = typeDefinitions
// concat them separated by new lines
// concat them separated by new lines
.join('\n\n\n\n\n')
// split all lines
.split('\n')
.map((line) => {
if (line.indexOf('export =') !== -1) {
.map(line => {
if (line.includes('export =')) {
throw new Error('`export =` is not allowed by pack. Use named imports instead.');
}
@@ -174,15 +145,12 @@ async function packTypeDefinitions(): Promise<void> {
const match = line.match(/^import {([^}].*)} from '([^'].*)';$/);
if (match !== null) {
// tslint:disable-next-line:no-magic-numbers - extract module
const module = match[2];
// extract imported objects
const importedObjects = match[1]
.split(',')
.map((object) => {
return object.trim();
});
const importedObjects = match[1].split(',').map(object => {
return object.trim();
});
// add list of already imported objects for module
if (typeof imports[module] === 'undefined') {
@@ -191,12 +159,12 @@ async function packTypeDefinitions(): Promise<void> {
// count already imported objects and objects to import now
const objectsToImport: string[] = [];
importedObjects.forEach((object) => {
if (imports[module].indexOf(object) === -1) {
for (const object of importedObjects) {
if (!imports[module].includes(object)) {
imports[module].push(object);
objectsToImport.push(object);
}
});
}
// replace import line
if (objectsToImport.length === 0) {
@@ -209,7 +177,7 @@ async function packTypeDefinitions(): Promise<void> {
return line;
})
// filter lines which contain "local" imports
.filter((line) => {
.filter(line => {
return line.match(/^import .* from '\./) === null;
})
// concat all lines separated by new lines
@@ -223,9 +191,12 @@ ${allDefinitions}`;
}
// write packed TypeScript definition files
return writeFilePromisified(path, `${PACK_IDENTIFIER}
return writeFilePromisified(
indexPath,
`${PACK_IDENTIFIER}
${allDefinitions}`);
${allDefinitions}`,
);
}
}
@@ -233,18 +204,17 @@ ${allDefinitions}`);
* Get all JavaScript modules
*/
async function getAllJavaScriptModules(): Promise<JavaScriptModule[]> {
const fileNames = await globPromisified(join(cwd(), 'lib', '**', '*.js'), {
const fileNames = await globPromisified(path.join(cwd(), 'lib', '**', '*.js'), {
ignore: [
join(cwd(), 'lib', 'doc', '**', '*.js'),
join(cwd(), 'lib', 'test', '*.js'),
join(cwd(), 'lib', 'cli.js'),
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 = dirname(fileName)
.replace(new RegExp(`^${join(cwd(), 'lib')}`), '');
const directory = path.dirname(fileName).replace(new RegExp(`^${path.join(cwd(), 'lib')}`), '');
return {
content: `(function() {
@@ -253,7 +223,7 @@ ${fileContent}
`,
dependencies: getAllInternalDependencies(fileContent),
directory: directory,
name: basename(fileName, '.js'),
name: path.basename(fileName, '.js'),
};
});
@@ -264,37 +234,33 @@ ${fileContent}
* Pack all javascript files
*/
async function packJavaScriptFiles(): Promise<void> {
const path = join(cwd(), 'lib', 'index.js');
const indexPath = path.join(cwd(), 'lib', 'index.js');
Logger.info('Packing JavaScript files...');
await deleteFileIfExistingAndPacked(path);
await deleteFileIfExistingAndPacked(indexPath);
// topologically sort the modules (sort by dependencies)
const jsModules = topologicalSort(await getAllJavaScriptModules());
let wholeCode = jsModules
// convert modules to strings
.map((module) => {
// convert modules to strings
.map(module => {
module.content = module.content
.split('\n')
.map((line) => {
.map(line => {
const match = line.match(
/^(\s*)(const|var) ([a-z0-9_]*) = ((require\("([^"]+)"\))|(require\('([^']+)'\)));$/i,
);
// replace lines with internal requires
if (match !== null) {
// tslint:disable-next-line:no-magic-numbers - match[6] or match[8] contain the modulePath
if (typeof match[6] === 'undefined') {
// tslint:disable-next-line:no-magic-numbers
match[6] = match[8];
}
const whiteSpace = (typeof match[1] === 'string' && match[1].length > 0) ? match[1] : '';
// tslint:disable-next-line:no-magic-numbers
const whiteSpace = typeof match[1] === 'string' && match[1].length > 0 ? match[1] : '';
const importedName = match[3];
// tslint:disable-next-line:no-magic-numbers
const modulePath = match[6];
// leave line unchanged if it is a "global" import
@@ -303,11 +269,11 @@ async function packJavaScriptFiles(): Promise<void> {
}
// replace internal requires with `module.exports`
if (existsSync(join(cwd(), 'lib', module.directory, `${modulePath}.js`))) {
if (existsSync(path.join(cwd(), 'lib', module.directory, `${modulePath}.js`))) {
return `${whiteSpace}const ${importedName} = module.exports;`;
}
if (existsSync(join(cwd(), 'src', module.directory, modulePath))) {
if (existsSync(path.join(cwd(), 'src', module.directory, modulePath))) {
return `${whiteSpace} const ${importedName} = require(../src/${modulePath});`;
}
@@ -326,7 +292,7 @@ ${module.content}`;
// split all lines
.split('\n')
// filter lines
.filter((line) => {
.filter(line => {
// remove strict usage
if (line === '"use strict";') {
return false;
@@ -356,9 +322,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
${wholeCode}`;
// write packed JavaScript files
return writeFilePromisified(path, `${PACK_IDENTIFIER}
return writeFilePromisified(
indexPath,
`${PACK_IDENTIFIER}
${wholeCode}`);
${wholeCode}`,
);
}
}
@@ -378,8 +347,8 @@ async function deleteFileIfExistingAndPacked(path: string): Promise<void> {
return unlinkPromisified(path);
}
} catch (err) {
if (err.code === 'ENOENT') {
} catch (error) {
if (error.code === 'ENOENT') {
return;
}
}
@@ -392,12 +361,13 @@ async function deleteFileIfExistingAndPacked(path: string): Promise<void> {
*/
function getAllInternalDependencies(moduleContent: string): string[] {
// match all const <name> = require(<moduleName>);
const requireLines =
moduleContent.match(/^\s*(const|var) [a-z0-9_]* = require\("([^"]+)"\)|require\('([^']+)'\);$/gmi);
const requireLines = moduleContent.match(
/^\s*(const|var) [a-z0-9_]* = require\("([^"]+)"\)|require\('([^']+)'\);$/gim,
);
if (Array.isArray(requireLines)) {
return requireLines
.map((requireLine) => {
.map(requireLine => {
const matches = requireLine.match(/require\("([^"]+)"\)|require\('([^']+)'\);$/i);
// previously matched require line does not contain a require?!
@@ -408,13 +378,13 @@ function getAllInternalDependencies(moduleContent: string): string[] {
// return only the moduleName
return matches[1];
})
.filter((moduleName) => {
.filter(moduleName => {
// filter out internal modules beginning with './' and not ending with '.json'
return /^[.]{1,2}\/(?!.*\.json$).*$/i.test(moduleName);
})
.map((internalModuleName) => {
.map(internalModuleName => {
// cut './' from the name
return internalModuleName.substring('./'.length);
return internalModuleName.slice('./'.length);
});
}
@@ -427,6 +397,7 @@ function getAllInternalDependencies(moduleContent: string): string[] {
* @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
@@ -434,23 +405,21 @@ function topologicalSort(modules: JavaScriptModule[]): JavaScriptModule[] {
const nodes: string[] = [];
// add all edges
modules.forEach((module) => {
module.dependencies.forEach((dependencyPath) => {
for (const module of modules) {
for (const dependencyPath of module.dependencies) {
// add edge from dependency to our module
edges.push([basename(dependencyPath), module.name]);
});
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;
});
return topoSort.array(nodes, edges).map((moduleName: string) => {
return modules.find(module => {
return module.name === moduleName;
});
});
}
/**
@@ -460,11 +429,7 @@ export async function pack() {
Logger.log(`Packing project in ${process.cwd()}...`);
// run all tasks in parallel
const promises: Array<Promise<void>> = [
packCliJs(),
packTypeDefinitions(),
packJavaScriptFiles(),
];
const promises: Array<Promise<void>> = [packCliJs(), packTypeDefinitions(), packJavaScriptFiles()];
await Promise.all(promises);
@@ -476,18 +441,24 @@ export async function pack() {
'lib/*',
// keep packed files
'!lib/index.d.ts', '!lib/index.js',
'!lib/index.d.ts',
'!lib/index.js',
// keep converted schema files
'!lib/schema', '!lib/schema/*.json',
'!lib/schema',
'!lib/schema/*.json',
// keep documentation
'!lib/doc', '!lib/doc/*', '!lib/doc/**/*',
'!lib/doc',
'!lib/doc/*',
'!lib/doc/**/*',
// keep cli
'!lib/cli.js',
// keep tests
'!lib/test', '!lib/test/*', '!lib/test/**/*',
'!lib/test',
'!lib/test/*',
'!lib/test/**/*',
]);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2019 StApps
* 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.
@@ -16,14 +16,14 @@
/**
* This is a simple interface declaration for
* testing the schema generation and validation.
*
*
* @validatable
*/
export interface Foo {
/**
* Dummy parameter
*/
lorem: 'ipsum';
lorem: 'lorem' | 'ipsum';
/**
* String literal type property
@@ -33,6 +33,6 @@ export interface Foo {
/**
* This is a simple type declaration for
* usage in the Foo interace.
* usage in the Foo interface.
*/
export type FooType = 'Foo';

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2018-2019 StApps
* Copyright (C) 2018-2021 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
@@ -12,182 +12,139 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {asyncPool} from '@krlwlfrt/async-pool/lib/async-pool';
import {Logger} from '@openstapps/logger';
import {assign, filter, map} from 'lodash';
import {OpenAPIV3} from 'openapi-types';
import {basename, dirname, join} from 'path';
import {ProjectReflection} from 'typedoc';
import {Type} from 'typedoc/dist/lib/models';
import {capitalize, NodeWithMetaInformation, RouteWithMetaInformation} from './common';
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.
*
* @param reflection Contents of the JSON representation which Typedoc generates
*/
export async function gatherRouteInformation(reflection: ProjectReflection): Promise<RouteWithMetaInformation[]> {
const routes: RouteWithMetaInformation[] = [];
export async function gatherRouteInformation(path: string): Promise<RouteWithMetaInformation[]> {
const project = new LightweightProjectWithIndex(lightweightProjectFromPath(path));
if (!Array.isArray(reflection.children)) {
throw new Error('Project reflection doesn\'t contain any modules.');
}
// tslint:disable-next-line:no-magic-numbers
await asyncPool(2, reflection.children, async (module) => {
if (Array.isArray(module.children) && module.children.length > 0) {
// tslint:disable-next-line:no-magic-numbers
await asyncPool(2, module.children, (async (node) => {
if (Array.isArray(node.extendedTypes) && node.extendedTypes.length > 0) {
if (node.extendedTypes.some((extendedType) => {
// tslint:disable-next-line:completed-docs
return (extendedType as (Type & { name: string; })).name === 'SCAbstractRoute';
})) {
Logger.info(`Found ${node.name} in ${module.originalName}.`);
if (Array.isArray(module.originalName.match(/\.d\.ts$/))) {
module.originalName = join(dirname(module.originalName), basename(module.originalName, '.d.ts'));
Logger.info(`Using compiled version of module in ${module.originalName}.`);
}
const importedModule = await import(module.originalName);
const route = new importedModule[node.name]();
// tslint:disable-next-line: no-any
const errors = route.errorNames.map((error: any) => {
const scError = new importedModule[error.name]();
scError.name = error.name;
return scError;
});
route.responseBodyDescription = module.children!.find(element => element.name === route.responseBodyName)?.comment?.shortText;
route.requestBodyDescription = module.children!.find(element => element.name === route.requestBodyName)?.comment?.shortText;
route.errors = errors;
routes.push({description: node.comment!, name: node.name, route});
}
// 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;
}
}));
}
});
if (routes.length === 0) {
throw new Error('No route information found.');
}
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 routes;
}
/**
* Get link for a node
*
* @param name Name of the node
* @param node Node itself
*/
export function getLinkForNode(name: string, node: NodeWithMetaInformation): string {
let link = 'https://openstapps.gitlab.io/core/';
const module = node.module
.toLowerCase()
.split('/')
.join('_');
if (node.type === 'Type alias') {
link += 'modules/';
link += `_${module}_`;
link += `.html#${name.toLowerCase()}`;
return link;
}
let type = 'classes';
if (node.type !== 'Class') {
type = `${node.type.toLowerCase()}s`;
}
link += `${type}/`;
link += `_${module}_`;
link += `.${name.toLowerCase()}.html`;
return link;
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 outDirSchemasPath Path to directory that will contain relevant schemas for the route
* @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,
outDirSchemasPath: string,
schemasToCopy: string[],
tagsToKeep: string[]): OpenAPIV3.PathItemObject {
export function generateOpenAPIForRoute(
routeWithInfo: RouteWithMetaInformation,
outDirectorySchemasPath: string,
schemasToCopy: string[],
tagsToKeep: string[],
): OpenAPIV3.PathItemObject {
const route = routeWithInfo.route;
const path: OpenAPIV3.PathItemObject = {};
const openapiPath: OpenAPIV3.PathItemObject = {};
schemasToCopy.push(route.requestBodyName, route.responseBodyName);
path[(route.method.toLowerCase() as OpenAPIV3.HttpMethods)] = {
summary: capitalize(routeWithInfo.description.shortText?.replace(/(Route to |Route for )/gmi, '')),
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: join(outDirSchemasPath, `${route.requestBodyName}.json`),
$ref: path.join(outDirectorySchemasPath, `${route.requestBodyName}.json`),
},
},
},
},
parameters: [{
name: 'X-StApps-Version',
in: 'header',
schema: {
type: 'string',
example: '2.0.0',
parameters: [
{
name: 'X-StApps-Version',
in: 'header',
schema: {
type: 'string',
example: '2.0.0',
},
required: true,
},
required: true,
}],
],
responses: {},
tags: routeWithInfo.tags?.filter(value => tagsToKeep.includes(value)),
};
path[(route.method.toLowerCase() as OpenAPIV3.HttpMethods)]!.responses![route.statusCodeSuccess] = {
openapiPath[route.method.toLowerCase() as OpenAPIV3.HttpMethods]!.responses![route.statusCodeSuccess] = {
description: route.responseBodyDescription,
content: {
'application/json': {
schema: {
$ref: join(outDirSchemasPath, `${route.responseBodyName}.json`),
$ref: path.join(outDirectorySchemasPath, `${route.responseBodyName}.json`),
},
},
},
};
route.errors.forEach(error => {
for (const error of route.errors) {
schemasToCopy.push(error.name);
path[(route.method.toLowerCase() as OpenAPIV3.HttpMethods)]!.responses![error.statusCode] = {
description: error.message ?? capitalize(error.name.replace(/([A-Z][a-z])/g,' $1')
.replace('SC ', '')),
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: join(outDirSchemasPath, `${error.name}.json`),
$ref: path.join(outDirectorySchemasPath, `${error.name}.json`),
},
},
},
};
});
}
if (typeof route.obligatoryParameters === 'object') {
for (const [parameter, schemaDefinition] of Object.entries(route.obligatoryParameters)) {
const openapiParam: OpenAPIV3.ParameterObject = {
const openapiParameter: OpenAPIV3.ParameterObject = {
in: 'path',
name: parameter,
required: true,
@@ -196,9 +153,9 @@ export function getLinkForNode(name: string, node: NodeWithMetaInformation): str
$ref: `schemas/SCSearchResponse.json#/definitions/${schemaDefinition}`,
},
};
path[(route.method.toLowerCase() as OpenAPIV3.HttpMethods)]?.parameters?.push(openapiParam);
openapiPath[route.method.toLowerCase() as OpenAPIV3.HttpMethods]?.parameters?.push(openapiParameter);
}
}
return path;
return openapiPath;
}

View File

@@ -14,13 +14,16 @@
*/
import Ajv from 'ajv';
import {JSONSchema7 as JSONSchema} from 'json-schema';
import {join} from 'path';
import {filter} 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 {ProjectReflection} from 'typedoc';
import {getTsconfigPath, isSchemaWithDefinitions} from './common';
import {getTsconfigPath} from './common';
import {definitionsOf, isLightweightClass} from './easy-ast/ast-util';
import {lightweightProjectFromPath} from './easy-ast/easy-ast';
import {isSchemaWithDefinitions} from './util/guards';
import path from 'path';
/**
* StAppsCore converter
@@ -41,15 +44,15 @@ export class Converter {
/**
* Create a new converter
*
* @param path Path to the project
* @param projectPath Path to the project
*/
constructor(path: string) {
constructor(projectPath: string) {
// set config for schema generator
const config: Config = {
...DEFAULT_CONFIG,
sortProps: true,
topRef: false,
tsconfig: join(getTsconfigPath(path), 'tsconfig.json'),
tsconfig: path.join(getTsconfigPath(projectPath), 'tsconfig.json'),
type: 'SC',
};
@@ -57,14 +60,11 @@ export class Converter {
const program = createProgram(config);
// create generator
this.generator = new SchemaGenerator(
program,
createParser(program, config),
createFormatter(config),
);
this.generator = new SchemaGenerator(program, createParser(program, config), createFormatter(config));
// create Ajv instance
this.schemaValidator = new Ajv();
// eslint-disable-next-line @typescript-eslint/no-var-requires,unicorn/prefer-module
this.schemaValidator.addMetaSchema(require('ajv/lib/refs/json-schema-draft-06.json'));
}
@@ -93,9 +93,9 @@ export class Converter {
delete selfReference.$id;
// add self reference to definitions
schema.definitions[`SC${type}`] = {
schema.definitions![`SC${type}`] = {
...{},
...selfReference as unknown as Definition,
...(selfReference as unknown as Definition),
};
}
@@ -108,32 +108,10 @@ export class Converter {
}
/**
* Get a list of validatable types from a reflection
*
* @param projectReflection Reflection to get validatable types from
* Get a list of validatable types from an API extractor file
*/
export function getValidatableTypesFromReflection(projectReflection: ProjectReflection): string[] {
const validatableTypes: string[] = [];
if (typeof projectReflection.children === 'undefined') {
throw new Error('Project reflection doesn\'t contain any modules.');
}
// iterate over modules
projectReflection.children.forEach((module) => {
if (Array.isArray(module.children) && module.children.length > 0) {
// iterate over types
module.children.forEach((type) => {
// check if type has annotation @validatable
if (typeof type.comment === 'object'
&& Array.isArray(type.comment.tags)
&& type.comment.tags.findIndex((tag) => tag.tagName === 'validatable') >= 0) {
// add type to list
validatableTypes.push(type.name);
}
});
}
});
return validatableTypes;
export function getValidatableTypesInPath(path: string): string[] {
return filter(definitionsOf(lightweightProjectFromPath(path)), isLightweightClass)
.filter(type => type.comment?.tags?.find(it => it.name === 'validatable'))
.map(type => type.name);
}

39
src/types/pack.d.ts vendored Normal file
View 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;
}

97
src/types/routes.d.ts vendored Normal file
View 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;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2019 StApps
* Copyright (C) 2018-2021 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
@@ -12,19 +12,15 @@
* 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';
import {LightweightDefinition} from './lightweight-definition';
/**
* Represents an enum definition
* A schema with definitions
*/
export class LightweightEnumDefinition extends LightweightDefinition {
interface SchemaWithDefinitions extends JSONSchema {
/**
* Enumeration or union values
* Definitions of the schema
*/
public values: string[];
constructor(name: string) {
super(name);
this.values = [];
}
definitions?: {[name: string]: Definition};
}

87
src/types/validator.d.ts vendored Normal file
View 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[];
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2019 StApps
* 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.
@@ -15,12 +15,13 @@
import {Logger} from '@openstapps/logger';
import {createWriteStream} from 'fs';
import * as request from 'got';
import {getFullTypeName} from '../common';
import {LightweightClassDefinition} from './model/lightweight-class-definition';
import {LightweightDefinition} from './model/lightweight-definition';
import {LightweightEnumDefinition} from './model/lightweight-enum-definition';
import {LightweightProperty} from './model/lightweight-property';
import {LightweightType} from './model/lightweight-type';
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';
/**
@@ -28,7 +29,7 @@ import {UMLConfig} from './uml-config';
* 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 definitons of the project
* @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
*/
@@ -37,49 +38,29 @@ export async function createDiagram(
config: UMLConfig,
plantUmlBaseURL: string,
): Promise<string> {
// when non definitions were specified use all
if (config.definitions.length === 0) {
config.definitions = [];
definitions.forEach((definition) => {
config.definitions.push(definition.name);
});
}
config.definitions = map(definitions, 'name');
// when providing definitions and either showing associations or inheritance the
// inherited definitions will be added automatically
if (config.showInheritance) {
const inheritedDefinitions = gatherTypeAssociations(
// TODO: showInheritance
/*const inheritedDefinitions = gatherTypeAssociations(
definitions,
config.definitions,
);
config.definitions = config.definitions.concat(inheritedDefinitions);
);*/
// config.definitions = config.definitions.concat(inheritedDefinitions);
}
let modelPlantUMLCode = '';
// creates a UML definition for every specified definition name
// however if no definitions were provided all definitions will be transformed
for (const definition of definitions) {
if (
config.definitions.length > 0 &&
!config.definitions.includes(definition.name)
) {
// current definition not specified
continue;
}
// either the definitions are empty or the definition was specified, proceed
let definitionPlantUMLCode = '';
if (definition instanceof LightweightClassDefinition) {
definitionPlantUMLCode = createPlantUMLCodeForClass(config, definition);
} else if (definition instanceof LightweightEnumDefinition) {
definitionPlantUMLCode = createPlantUMLCodeForEnum(config, definition);
} else {
continue;
}
modelPlantUMLCode += definitionPlantUMLCode;
}
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);
}
@@ -97,6 +78,7 @@ export async function createDiagramFromString(
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}`;
@@ -108,17 +90,18 @@ export async function createDiagramFromString(
await Logger.error(`Plantuml Server responded with an error.\n${response.statusMessage}`);
throw new Error('Response not okay');
}
} catch (e) {
Logger.log(`Please try using the public plantuml server:\nhttp://www.plantuml.com/plantuml/svg/${plantUMLCode}`);
throw e;
} 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);
createWriteStream(fileName).write(response.body);
Logger.log(`Writen data to file: ${fileName}`);
} catch (e) {
} catch {
throw new Error('Could not write file. Are you missing permissions?');
}
@@ -126,12 +109,13 @@ export async function createDiagramFromString(
}
/**
* Recursivly iterates over all types, to find implemented generic types and parents
* Recursively iterates over all types, to find implemented generic types and parents
*
* @param definitions all type definitons of the project
* @param definitions all type definitions of the project
* @param abstractionNames currently known string values of inherited classes
*/
function gatherTypeAssociations(
/*function gatherTypeAssociations(
definitions: LightweightDefinition[],
abstractionNames: string[],
): string[] {
@@ -140,7 +124,7 @@ function gatherTypeAssociations(
const declaration = definitions.find(
(definition) => definition.name === name,
);
if (declaration instanceof LightweightClassDefinition) {
if (isLightweightClass(declaration)) {
const currentAbstractions: string[] = declaration.extendedDefinitions.concat(
declaration.implementedDefinitions,
);
@@ -153,7 +137,7 @@ function gatherTypeAssociations(
}
return abstractions;
}
}*/
/**
* Collects all reference information of this type.
@@ -164,25 +148,22 @@ function gatherTypeAssociations(
*/
function getReferenceTypes(type: LightweightType): string[] {
const types: string[] = [];
if (type.isReference) {
types.push(type.name);
if (typeof type.referenceName !== 'undefined') {
types.push(type.referenceName);
}
if (type.isTyped && type.genericsTypes.length > 0) {
for (const specificType of type.genericsTypes) {
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);
}
}
}
if (
(type.isUnion && type.specificationTypes.length > 0) ||
(type.isArray && type.specificationTypes.length > 0)
) {
for (const specificType of type.specificationTypes) {
for (const value of getReferenceTypes(specificType)) {
types.push(value);
}
}
});
}
return types;
@@ -194,57 +175,54 @@ function getReferenceTypes(type: LightweightType): string[] {
* @param config Configuration for how the UML should be tweaked
* @param readerClass Class or interface representation
*/
function createPlantUMLCodeForClass(
config: UMLConfig,
readerClass: LightweightClassDefinition,
): string {
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.type} ${readerClass.name}`;
let model = `${readerClass.modifiers} ${readerClass.name}`;
if (readerClass.typeParameters.length > 0) {
model += `<${readerClass.typeParameters.join(', ')}>`;
if (readerClass.typeParameters?.length ?? 0 > 0) {
model += `<${readerClass.typeParameters!.join(', ')}>`;
}
if (config.showInheritance && readerClass.extendedDefinitions.length > 0) {
if (config.showInheritance && (readerClass.extendedDefinitions?.length ?? 0 > 0)) {
// PlantUML will automatically create links, when using extends
model += ` extends ${readerClass.extendedDefinitions.join(', ')}`;
model += ` extends ${readerClass.extendedDefinitions!.join(', ')}`;
}
if (config.showInheritance && readerClass.implementedDefinitions.length > 0) {
// PlantUML will automatically create links, when using implenents
model += ` implements ${readerClass.implementedDefinitions.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) {
for (const property of readerClass.properties) {
forEach(readerClass.properties, property => {
if (property.optional && !config.showOptionalProperties) {
// don't show optional attributes
continue;
return;
}
if (property.inherited && !config.showInheritedProperties) {
/*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
for (const property of readerClass.properties) {
forEach(readerClass.properties, property => {
const types: string[] = getReferenceTypes(property.type);
for (const type of types) {
if ( config.showAssociations) {
if (property.inherited && !config.showInheritedProperties) {
if (config.showAssociations) {
/*if (property.inherited && !config.showInheritedProperties) {
continue;
}
}*/
model += `${readerClass.name} -up-> ${type} : ${property.name} >\n`;
}
}
}
});
return model;
}
@@ -255,17 +233,14 @@ function createPlantUMLCodeForClass(
* @param config Configuration for how the UML should be tweaked
* @param readerEnum Enum/-like representation
*/
function createPlantUMLCodeForEnum(
config: UMLConfig,
readerEnum: LightweightEnumDefinition,
): string {
function createPlantUMLCodeForEnum(config: UMLConfig, readerEnum: LightweightAliasDefinition): string {
// create enum header
let model = `enum ${readerEnum.name} {`;
// add values
if (config.showEnumValues) {
for (const value of readerEnum.values) {
forEach(readerEnum.type?.specificationTypes, value => {
model += `\n\t${value.toString()}`;
}
});
}
model += '\n}\n';
@@ -276,7 +251,7 @@ function createPlantUMLCodeForEnum(
* Creates a property PlantUML Line
*/
function createPropertyLine(property: LightweightProperty): string {
const prefix = `${(property.inherited ? '/ ' : '')}${(property.optional ? '? ' : '')}`;
const prefix = `${/*(property.inherited ? '/ ' : */ ''}${property.optional ? '? ' : ''}`;
return `${prefix}${property.name} : ${getFullTypeName(property.type)}`;
return `${prefix}${property.name} : ${expandTypeValue(property.type)}`;
}

View File

@@ -1,85 +0,0 @@
/*
* Copyright (C) 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/>.
*/
/**
* Describes an easy to use type definition.
*/
export class LightweightType {
/**
* Contains all types inside of <> brackets
*/
genericsTypes: LightweightType[];
/**
* Does the type have generic-parameters
*/
hasTypeInformation = false;
/**
* Does the type represent an array type
*/
isArray = false;
/**
* Does the type represent a literal type
*/
isLiteral = false;
/**
* Does the type represent a primitive type
*/
isPrimitive = false;
/**
* Does the type contain a reference to
*/
isReference = false;
/**
* Is the type a reflection and not avaiblabe at compile time
*/
isReflection = false;
/**
* Does the type have type parameters
*/
isTyped = false;
/**
* Is the type a typed parameter
*/
isTypeParameter = false;
/**
* Is the type a union type
*/
isUnion = false;
/**
* Name of the type
*/
name: string;
/**
* Type specifications, if the type is combined by either an array, union or a typeOperator
*/
specificationTypes: LightweightType[];
constructor() {
this.specificationTypes = [];
this.genericsTypes = [];
this.name = '';
}
}

View File

@@ -1,471 +0,0 @@
/*
* Copyright (C) 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 {Logger} from '@openstapps/logger';
import {
ArrayType,
ConditionalType,
DeclarationReflection,
IntrinsicType,
ProjectReflection,
QueryType,
ReferenceType,
ReflectionKind,
ReflectionType,
StringLiteralType,
Type,
TypeOperatorType,
TypeParameterType,
UnionType,
} from 'typedoc/dist/lib/models';
import {getFullTypeName} from '../common';
import {LightweightClassDefinition} from './model/lightweight-class-definition';
import {LightweightDefinition} from './model/lightweight-definition';
import {LightweightEnumDefinition} from './model/lightweight-enum-definition';
import {LightweightProperty} from './model/lightweight-property';
import {LightweightType} from './model/lightweight-type';
/**
* Reads the reflection model from typedoc and converts it into a flatter, easier to handle model
*
* @param srcPath Path to source file directory
*/
export function readDefinitions(projectReflection: ProjectReflection): LightweightDefinition[] {
const definitions: LightweightDefinition[] = [];
// define known types and categorize them
const enumLike: string[] = ['Type alias', 'Enumeration'];
const classLike: string[] = ['Class', 'Interface'];
const unused: string[] = ['Function', 'Object literal', 'Variable'];
// children need to be not undefined, if they are return empty
if (typeof projectReflection.children === 'undefined') {
return [];
}
for (const module of projectReflection.children) {
if (Array.isArray(module.children) && module.children.length > 0) {
// iterate over class and enum declarations
for (const type of module.children) {
// only if kindString is set
if (typeof type.kindString !== 'undefined') {
// check if declaration is enum
if (classLike.includes(type.kindString)) {
definitions.push(readAsClassDefinition(type));
} else if (enumLike.includes(type.kindString)) {
definitions.push(readAsEnumDefinition(type));
} else if (unused.includes(type.kindString)) {
Logger.info(`Unconverted ${type.kindString} : ${type.name}`);
} else {
Logger.log(
`Uncaught declaration type (${type.kindString}) : ${type.name}`,
);
}
}
}
}
}
return definitions;
}
/**
* Transforms the declaration into a `LightweightClassDefinition`
*
* @param declaration declaration
*/
export function readAsEnumDefinition(
declaration: DeclarationReflection,
): LightweightEnumDefinition {
// init enum definition
const enumDefinition: LightweightEnumDefinition = new LightweightEnumDefinition(
declaration.name,
);
// get enum values according to type
if (declaration.kindString === 'Enumeration' && typeof declaration.children !== 'undefined') {
// standard enumeration
for (const child of declaration.children) {
if (child.kindString === 'Enumeration member') {
let value = child.name;
if (typeof child.defaultValue !== 'undefined') {
value = `${value} = ${child.defaultValue}`;
}
enumDefinition.values.push(value);
} else {
Logger.log(
"Every enumeration member should be an 'EnumerationMemberType'",
);
}
}
} else if (
declaration.kindString === 'Type alias' &&
typeof declaration.type !== 'undefined'
) {
// enum like declaration
try {
const a = readTypeInformation(declaration.type);
enumDefinition.values = enumDefinition.values.concat(
getTypeInformation(a),
);
} catch (e) {
Logger.warn(
`Could not read the light type for ${declaration.name}. ${e}`,
);
}
}
return enumDefinition;
}
/**
* Used for enumrations to get the type value
*/
function getTypeInformation(type: LightweightType): string[] {
const values: string[] = [];
if (!type.hasTypeInformation) {
for (const specificType of type.specificationTypes) {
for (const value of getTypeInformation(specificType)) {
values.push(value);
}
}
} else {
values.push(type.name);
}
return values;
}
/**
* Transforms the declaration into a `LightweightClassDefinition`
*
* @param declaration declaration
*/
export function readAsClassDefinition(
declaration: DeclarationReflection,
): LightweightClassDefinition {
let type = typeof declaration.kindString !== 'undefined' ? declaration.kindString.toLowerCase() : '';
type = (declaration.flags.isAbstract ? 'abstract ' : '') + type;
const classDefinition: LightweightClassDefinition = new LightweightClassDefinition(
declaration.name,
type,
);
// get generic types
if (typeof declaration.typeParameters !== 'undefined') {
const typeParameters: string[] = [];
declaration.typeParameters.forEach((typeParameter) =>
typeParameters.push(typeParameter.name),
);
classDefinition.typeParameters = typeParameters;
}
// extracts extended types of the declaration
if (typeof declaration.extendedTypes !== 'undefined') {
for (const extType of declaration.extendedTypes) {
classDefinition.extendedDefinitions.push((extType as ReferenceType).name);
}
}
// extracts implemented types of the declaration
// HINT: typedoc automatically adds inherited interfaces to the declaration directly
if (typeof declaration.implementedTypes !== 'undefined') {
for (const implType of declaration.implementedTypes) {
classDefinition.implementedDefinitions.push(
(implType as ReferenceType).name,
);
}
}
if (typeof declaration.children !== 'undefined') {
for (const child of declaration.getChildrenByKind(
ReflectionKind.Property,
)) {
try {
if (typeof child.type === 'undefined') {
throw new Error();
}
const myType: LightweightType = readTypeInformation(child.type);
const property = new LightweightProperty(child.name, myType);
const flags = child.flags;
if (flags.isOptional !== undefined) {
property.optional = flags.isOptional as boolean;
property.inherited = !(
child.inheritedFrom === undefined || child.inheritedFrom === null
);
}
classDefinition.properties.push(property);
} catch (e) {
Logger.warn(e);
}
}
}
return classDefinition;
}
/**
* The structure of reflection type has a huge overhead
* This method and all submethods will convert these types in easier to process Types
*
* @param declarationType Type to be converted
*/
function readTypeInformation(declarationType: Type): LightweightType {
if (declarationType instanceof ReflectionType) {
return readAsReflectionType(declarationType);
}
if (declarationType instanceof TypeOperatorType) {
return readAsTypeOperatorType(declarationType);
}
if (declarationType instanceof TypeParameterType) {
return readAsTypeParameterType(declarationType);
}
if (declarationType instanceof IntrinsicType) {
return readAsIntrinsicType(declarationType);
}
if (declarationType instanceof StringLiteralType) {
return readAsStringLiteralType(declarationType);
}
if (declarationType instanceof ReferenceType) {
return readAsReferenceType(declarationType);
}
if (declarationType instanceof ArrayType) {
return readAsArrayType(declarationType);
}
if (declarationType instanceof UnionType) {
return readAsUnionType(declarationType);
}
if (declarationType instanceof QueryType) {
return readAsQueryType(declarationType);
}
if (declarationType instanceof ConditionalType) {
return readAsConditionalType(declarationType);
}
throw new Error(`Could not read type ${declarationType.type}`);
}
/**
* Conversion method for ConditionalTypes
*
* @param _type Type to be converted
*/
function readAsConditionalType(_type: ConditionalType): LightweightType {
const returnType: LightweightType = new LightweightType();
returnType.specificationTypes = [];
returnType.name = getFullTypeName(returnType);
returnType.isUnion = true;
return returnType;
}
/**
* Conversion method for QueryTypes
*
* @param type Type to be converted
*/
function readAsQueryType(type: QueryType): LightweightType {
const out = readAsReferenceType(type.queryType);
out.isReference = true;
return out;
}
/**
* Conversion method for IntrinsicType's
*
* e.g. remainingAttendeeCapacity?: number;
*
* @param type Type to be converted
*/
function readAsIntrinsicType(type: IntrinsicType): LightweightType {
const easyType: LightweightType = new LightweightType();
easyType.name = type.name;
easyType.isPrimitive = true;
easyType.hasTypeInformation = true;
return easyType;
}
/**
* Conversion method for StringLiteralType's
*
* e.g. inputType: 'multipleChoice';
*
* @param type Type to be converted
*/
function readAsStringLiteralType(type: StringLiteralType): LightweightType {
const returnType: LightweightType = new LightweightType();
returnType.name = type.value;
returnType.isLiteral = true;
returnType.hasTypeInformation = true;
return returnType;
}
/**
* Conversion method for ReferenceType's
*
* Everything that is a user or API designed definition and not a primitive type or core-language feature.
*
* e.g. publishers?: Array<SCPersonWithoutReferences | SCOrganizationWithoutReferences>;
*
* Array, SCPersonWithoutReferences and SCOrganizationWithoutReferences will be recognized as reference types!
*
* @param type Type to be converted
*/
function readAsReferenceType(type: ReferenceType): LightweightType {
const returnType: LightweightType = new LightweightType();
returnType.name = type.name;
if (type.typeArguments !== undefined && type.typeArguments.length > 0) {
const typeArguments: LightweightType[] = [];
for (const value of type.typeArguments) {
typeArguments.push(readTypeInformation(value));
}
returnType.isTyped = true;
returnType.genericsTypes = typeArguments;
}
if (type.reflection !== undefined && type.reflection !== null) {
const tempTypeReflection = type.reflection as DeclarationReflection;
// interfaces and classes in a type are a sink, since their declaration are defined elsewhere
if (
typeof tempTypeReflection.kindString !== 'undefined' &&
['Interface', 'Class', 'Enumeration', 'Type alias'].includes(
tempTypeReflection.kindString)) {
returnType.isReference = true;
}
}
returnType.hasTypeInformation = true;
return returnType;
}
/**
* Conversion method for ArrayType's
*
* The actual type of the array is stored in the first element of specificationTypes.
*
* e.g. articleBody?: string[];
*
* @param type Type to be converted
*/
function readAsArrayType(type: ArrayType): LightweightType {
const returnType: LightweightType = new LightweightType();
const typeOfArray: LightweightType = readTypeInformation(type.elementType);
returnType.name = getFullTypeName(typeOfArray);
returnType.specificationTypes = [typeOfArray];
returnType.isArray = true;
return returnType;
}
/**
* Conversion method for UnionType's
*
* The Union-LightType store the single types of the union inside a
* separate LightType inside specificationTypes.
*
* e.g. maintainer?: SCPerson | SCOrganization;
*
* @param type Type to be converted
*/
function readAsUnionType(type: UnionType): LightweightType {
const returnType: LightweightType = new LightweightType();
const typesOfUnion: LightweightType[] = [];
for (const value of type.types) {
typesOfUnion.push(readTypeInformation(value));
}
returnType.specificationTypes = typesOfUnion;
returnType.name = getFullTypeName(returnType);
returnType.isUnion = true;
return returnType;
}
/**
* Conversion method for ReflectionType's
*
* The explicit type is not contained in reflection!
* It might be possible to get the structure of type by reading tempType.decoration.children,
* but this structure is currently not supported in the data-model.
*
* e.g. categorySpecificValues?: { [s: string]: U };
*
* @param type Type to be converted
*/
function readAsReflectionType(type: ReflectionType): LightweightType {
const returnType: LightweightType = new LightweightType();
if (typeof type.declaration.sources !== 'undefined') {
const src = type.declaration.sources[0];
Logger.warn(
`${src.line} : ${src.fileName}: Reflection Type not recognized. Refactoring to explicit class is advised.`,
);
}
returnType.name = 'object';
returnType.isReflection = true;
return returnType;
}
/**
* Conversion method for TypeOperatorType's
*
* This type is similar to reflection, that the actual type can only be evaluated at runtime.
*
* e.g. universityRole: keyof SCSportCoursePriceGroup;
*
* @param type Type to be converted
*/
function readAsTypeOperatorType(type: TypeOperatorType): LightweightType {
const returnType: LightweightType = new LightweightType();
const typeOf: LightweightType = readTypeInformation(type.target);
returnType.name = `keyof ${getFullTypeName(typeOf)}`;
returnType.specificationTypes = [typeOf];
// can't be traced deeper! so might as well be a primitive
returnType.isPrimitive = true;
returnType.hasTypeInformation = true;
return returnType;
}
/**
* Conversion method for TypeParameterType's
*
* Should only be called in generic classes/interfaces, when a property is
* referencing the generic-type.
*
* e.g. prices?: T;
*
* Does not match on Arrays of the generic type. Those will be matched with ArrayType.
*
* @param type Needs to be a TypeParameterType
*/
function readAsTypeParameterType(type: TypeParameterType): LightweightType {
const returnType: LightweightType = new LightweightType();
returnType.name = type.name;
returnType.isTypeParameter = true;
returnType.hasTypeInformation = true;
return returnType;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2019 StApps
* 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.

37
src/util/collections.ts Normal file
View 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
src/util/guards.ts Normal file
View 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 typeof 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
src/util/io.ts Normal file
View 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, '/');
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2019 StApps
* 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.
@@ -12,17 +12,9 @@
* 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 any definition without specifics
* Creates sentence cased string
*/
export abstract class LightweightDefinition {
/**
* Name of the definiton
*/
public name: string;
constructor(name: string) {
this.name = name;
}
export function capitalize(string?: string): string {
return `${string?.charAt(0).toUpperCase()}${string?.slice(1).toLowerCase()}`;
}

View File

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