mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-03-08 07:42:34 +00:00
feat: modernize core-tools
This commit is contained in:
347
src/cli.ts
347
src/cli.ts
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2018-2019 StApps
|
||||
* Copyright (C) 2018-2021 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
@@ -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);
|
||||
|
||||
342
src/common.ts
342
src/common.ts
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2018-2019 StApps
|
||||
* Copyright (C) 2018-2021 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
@@ -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()}`;
|
||||
}
|
||||
|
||||
143
src/easy-ast/ast-internal-util.ts
Normal file
143
src/easy-ast/ast-internal-util.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright (C) 2021 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {first, last, tail, filter} from 'lodash';
|
||||
import {
|
||||
ArrayTypeNode,
|
||||
ClassDeclaration,
|
||||
ClassElement,
|
||||
EnumDeclaration,
|
||||
Identifier,
|
||||
InterfaceDeclaration,
|
||||
isArrayTypeNode,
|
||||
isClassDeclaration,
|
||||
isComputedPropertyName,
|
||||
isEnumDeclaration,
|
||||
isInterfaceDeclaration,
|
||||
isPropertyDeclaration,
|
||||
isPropertySignature,
|
||||
isTypeAliasDeclaration,
|
||||
isTypeReferenceNode,
|
||||
NodeArray,
|
||||
PropertyDeclaration,
|
||||
PropertyName,
|
||||
PropertySignature,
|
||||
TypeAliasDeclaration,
|
||||
TypeElement,
|
||||
TypeNode,
|
||||
TypeReferenceNode,
|
||||
} from 'typescript';
|
||||
import * as ts from 'typescript';
|
||||
import {cleanupEmpty} from '../util/collections';
|
||||
import {LightweightComment} from './types/lightweight-comment';
|
||||
|
||||
/** @internal */
|
||||
export function extractComment(node: ts.Node): LightweightComment | undefined {
|
||||
const jsDocument = last(
|
||||
// @ts-expect-error jsDoc exists in reality
|
||||
node.jsDoc as
|
||||
| Array<{
|
||||
comment?: string;
|
||||
tags?: Array<{comment?: string; tagName?: {escapedText?: string}}>;
|
||||
}>
|
||||
| undefined,
|
||||
);
|
||||
const comment = jsDocument?.comment?.split('\n\n');
|
||||
|
||||
return 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
83
src/easy-ast/ast-util.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
/*
|
||||
* Copyright (C) 2021 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {flatMap, keyBy, isEmpty} from 'lodash';
|
||||
import {TypeFlags} from 'typescript';
|
||||
import {LightweightAliasDefinition} from './types/lightweight-alias-definition';
|
||||
import {LightweightClassDefinition} from './types/lightweight-class-definition';
|
||||
import {LightweightDefinition} from './types/lightweight-definition';
|
||||
import {LightweightDefinitionKind} from './types/lightweight-definition-kind';
|
||||
import {LightweightProject} from './types/lightweight-project';
|
||||
import {LightweightType} from './types/lightweight-type';
|
||||
|
||||
/**
|
||||
* Creates a printable name of a type
|
||||
*/
|
||||
export function expandTypeValue(type: LightweightType): string | undefined {
|
||||
if (type.isArray) {
|
||||
return `${type.value}[]`;
|
||||
}
|
||||
if (isStringLiteralType(type)) {
|
||||
return `'${type.value}'`;
|
||||
}
|
||||
if (isUnionOrIntersectionType(type)) {
|
||||
return type.specificationTypes?.map(expandTypeValue).join(isUnionType(type) ? ' | ' : ' & ');
|
||||
}
|
||||
if (isEmpty(type.genericsTypes)) {
|
||||
return `${type.value}<${type.genericsTypes?.map(expandTypeValue).join(', ')}>`;
|
||||
}
|
||||
|
||||
return type.value?.toString();
|
||||
}
|
||||
|
||||
export function definitionsOf(project: LightweightProject): Record<string, LightweightDefinition> {
|
||||
return keyBy(flatMap(project, Object.values), 'name');
|
||||
}
|
||||
|
||||
export function isPrimitiveType(type: {flags: TypeFlags}): boolean {
|
||||
return (type.flags & TypeFlags.NonPrimitive) === 0;
|
||||
}
|
||||
|
||||
export function isLiteralType(type: {flags: TypeFlags}): boolean {
|
||||
return (type.flags & TypeFlags.Literal) !== 0;
|
||||
}
|
||||
|
||||
export function isEnumLiteralType(type: {flags: TypeFlags}): boolean {
|
||||
return (type.flags & TypeFlags.EnumLiteral) !== 0;
|
||||
}
|
||||
|
||||
export function isStringLiteralType(type: {flags: TypeFlags}): boolean {
|
||||
return (type.flags & TypeFlags.StringLiteral) !== 0;
|
||||
}
|
||||
|
||||
export function isUnionOrIntersectionType(type: {flags: TypeFlags}): boolean {
|
||||
return (type.flags & TypeFlags.UnionOrIntersection) !== 0;
|
||||
}
|
||||
|
||||
export function isUnionType(type: {flags: TypeFlags}): boolean {
|
||||
return (type.flags & TypeFlags.Union) !== 0;
|
||||
}
|
||||
|
||||
export function isLightweightClass(node?: LightweightDefinition): node is LightweightClassDefinition {
|
||||
return node?.kind === LightweightDefinitionKind.CLASS_LIKE;
|
||||
}
|
||||
|
||||
export function isLightweightEnum(node?: LightweightDefinition): node is LightweightAliasDefinition {
|
||||
return node?.kind === LightweightDefinitionKind.ALIAS_LIKE;
|
||||
}
|
||||
|
||||
export function isTypeVariable(type: {flags: TypeFlags}): boolean {
|
||||
return (type.flags & TypeFlags.TypeVariable) !== 0;
|
||||
}
|
||||
274
src/easy-ast/easy-ast.ts
Normal file
274
src/easy-ast/easy-ast.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
32
src/easy-ast/types/lightweight-alias-definition.d.ts
vendored
Normal file
32
src/easy-ast/types/lightweight-alias-definition.d.ts
vendored
Normal 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;
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
48
src/easy-ast/types/lightweight-comment.d.ts
vendored
Normal file
48
src/easy-ast/types/lightweight-comment.d.ts
vendored
Normal 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[];
|
||||
}
|
||||
19
src/easy-ast/types/lightweight-definition-kind.ts
Normal file
19
src/easy-ast/types/lightweight-definition-kind.ts
Normal 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',
|
||||
}
|
||||
46
src/easy-ast/types/lightweight-definition.d.ts
vendored
Normal file
46
src/easy-ast/types/lightweight-definition.d.ts
vendored
Normal 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;
|
||||
}
|
||||
100
src/easy-ast/types/lightweight-project.ts
Normal file
100
src/easy-ast/types/lightweight-project.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright (C) 2021 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {assign, cloneDeep, flatMap, fromPairs, trimEnd} from 'lodash';
|
||||
import {mapNotNil} from '../../util/collections';
|
||||
import {definitionsOf, isLightweightClass} from '../ast-util';
|
||||
import {lightweightProjectFromPath} from '../easy-ast';
|
||||
import {LightweightClassDefinition} from './lightweight-class-definition';
|
||||
import {LightweightDefinition} from './lightweight-definition';
|
||||
|
||||
/**
|
||||
* Build an index for a lightweight project
|
||||
*/
|
||||
function buildIndex(project: LightweightProject): Record<string, string> {
|
||||
return fromPairs(
|
||||
flatMap(project, (definitions, file) => Object.keys(definitions).map(definition => [definition, file])),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A lightweight definition class for more advanced use cases
|
||||
*/
|
||||
export class LightweightProjectWithIndex {
|
||||
/**
|
||||
* All definitions
|
||||
*/
|
||||
readonly definitions: Record<string, LightweightDefinition>;
|
||||
|
||||
/**
|
||||
* Project
|
||||
*/
|
||||
readonly files: LightweightProject;
|
||||
|
||||
/**
|
||||
* Index of all definitions to their respective files
|
||||
*/
|
||||
readonly index: {
|
||||
[definitionName: string]: string;
|
||||
};
|
||||
|
||||
constructor(project: LightweightProject | string) {
|
||||
this.files = typeof project === 'string' ? lightweightProjectFromPath(project) : project;
|
||||
this.index = buildIndex(this.files);
|
||||
this.definitions = definitionsOf(this.files);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply inherited classes; default deeply
|
||||
*/
|
||||
applyInheritance(classLike: LightweightClassDefinition, deep?: boolean): LightweightDefinition {
|
||||
return assign(
|
||||
mapNotNil(
|
||||
[...(classLike.implementedDefinitions ?? []), ...(classLike.extendedDefinitions ?? [])],
|
||||
extension => {
|
||||
const object = this.definitions[extension.referenceName ?? ''];
|
||||
|
||||
return (deep ?? true) && isLightweightClass(object)
|
||||
? this.applyInheritance(object)
|
||||
: cloneDeep(object);
|
||||
},
|
||||
),
|
||||
cloneDeep(classLike),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a definition
|
||||
*
|
||||
* Requires the program to be run with `--require ts-node/register`
|
||||
*/
|
||||
async instantiateDefinitionByName<T>(name: string, findCompiledModule = true): Promise<T | undefined> {
|
||||
const fsPath = this.index[name];
|
||||
if (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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
58
src/easy-ast/types/lightweight-type.d.ts
vendored
Normal file
58
src/easy-ast/types/lightweight-type.d.ts
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (C) 2021 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {TypeFlags} from 'typescript';
|
||||
|
||||
/**
|
||||
* Describes an easy to use type definition.
|
||||
*/
|
||||
export interface LightweightType {
|
||||
/**
|
||||
* Type Flags
|
||||
*/
|
||||
flags: TypeFlags;
|
||||
|
||||
/**
|
||||
* Contains all types inside of <> brackets
|
||||
*/
|
||||
genericsTypes?: LightweightType[];
|
||||
|
||||
/**
|
||||
* If it is an array(-like) type
|
||||
*/
|
||||
isArray?: true;
|
||||
|
||||
/**
|
||||
* If it is a type parameter
|
||||
*/
|
||||
isTypeParameter?: true;
|
||||
|
||||
/**
|
||||
* The name of the type that is referenced. Enum members have reference names that lead no where.
|
||||
*/
|
||||
referenceName?: string;
|
||||
|
||||
/**
|
||||
* Type specifications, if the type is combined by either an array, union or a typeOperator
|
||||
*/
|
||||
specificationTypes?: LightweightType[];
|
||||
|
||||
/**
|
||||
* Value of the type
|
||||
*
|
||||
* Literal types have their value here, non-literals their type name (for example 'string')
|
||||
*/
|
||||
value?: string | number | boolean;
|
||||
}
|
||||
812
src/mapping.ts
812
src/mapping.ts
@@ -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};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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'];
|
||||
@@ -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;
|
||||
}
|
||||
199
src/pack.ts
199
src/pack.ts
@@ -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/**/*',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
195
src/routes.ts
195
src/routes.ts
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2018-2019 StApps
|
||||
* Copyright (C) 2018-2021 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
@@ -12,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;
|
||||
}
|
||||
|
||||
@@ -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
39
src/types/pack.d.ts
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (C) 2018-2021 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A JavaScript module representation to sort a list of them by dependencies
|
||||
*/
|
||||
export interface JavaScriptModule {
|
||||
/**
|
||||
* Content of the module
|
||||
*/
|
||||
content: string;
|
||||
|
||||
/**
|
||||
* List of names of dependencies
|
||||
*/
|
||||
dependencies: string[];
|
||||
|
||||
/**
|
||||
* Directory the module is in
|
||||
*/
|
||||
directory: string;
|
||||
|
||||
/**
|
||||
* The name of the module
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
97
src/types/routes.d.ts
vendored
Normal file
97
src/types/routes.d.ts
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright (C) 2018-2021 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type SCRoute = any;
|
||||
|
||||
export interface RouteInstanceWithMeta extends SCRoute {
|
||||
/**
|
||||
* Possible errors on a route
|
||||
*/
|
||||
errors: SCErrorResponse[];
|
||||
|
||||
/**
|
||||
* Description of the request body
|
||||
*/
|
||||
requestBodyDescription: string;
|
||||
|
||||
/**
|
||||
* Description of the response body
|
||||
*/
|
||||
responseBodyDescription: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A route instance with its relevant meta information
|
||||
*/
|
||||
export interface RouteWithMetaInformation {
|
||||
/**
|
||||
* Description of the route
|
||||
*/
|
||||
description: {
|
||||
/**
|
||||
* Short text of the description - title
|
||||
*/
|
||||
shortText?: string;
|
||||
/**
|
||||
* Text of the description
|
||||
*/
|
||||
text?: string;
|
||||
};
|
||||
/**
|
||||
* Name of the route
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Instance of the route
|
||||
*/
|
||||
route: RouteInstanceWithMeta;
|
||||
|
||||
/**
|
||||
* Possible tags/keywords the route can be associated with
|
||||
*/
|
||||
tags?: [string];
|
||||
}
|
||||
|
||||
/**
|
||||
* A node with its relevant meta information
|
||||
*/
|
||||
export interface NodeWithMetaInformation {
|
||||
/**
|
||||
* Module the node belongs to
|
||||
*/
|
||||
module: string;
|
||||
|
||||
/**
|
||||
* Type of the node
|
||||
*/
|
||||
type: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A generic error that can be returned by the backend if somethings fails during the processing of a request
|
||||
*/
|
||||
export interface SCErrorResponse extends Error {
|
||||
/**
|
||||
* Additional data that describes the error
|
||||
*/
|
||||
additionalData?: unknown;
|
||||
|
||||
/**
|
||||
* HTTP status code to return this error with
|
||||
*/
|
||||
statusCode: number;
|
||||
}
|
||||
@@ -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
87
src/types/validator.d.ts
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (C) 2018-2021 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
/**
|
||||
* The validation result
|
||||
*/
|
||||
export interface ValidationResult {
|
||||
/**
|
||||
* A list of errors that occurred
|
||||
*/
|
||||
errors: ValidationError[];
|
||||
|
||||
/**
|
||||
* whether the validation was successful
|
||||
*/
|
||||
valid: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* An error that occurred while validating
|
||||
*
|
||||
* This is a duplicate of the ValidationError in core/protocol/errors/validation because of incompatibilities
|
||||
* between TypeDoc and TypeScript
|
||||
*/
|
||||
export interface ValidationError {
|
||||
/**
|
||||
* JSON schema path
|
||||
*/
|
||||
dataPath: string;
|
||||
|
||||
/**
|
||||
* The instance
|
||||
*/
|
||||
instance: unknown;
|
||||
|
||||
/**
|
||||
* The message
|
||||
*
|
||||
* Provided by https://www.npmjs.com/package/better-ajv-errors
|
||||
*/
|
||||
message: string;
|
||||
|
||||
/**
|
||||
* Name of the error
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Path within the Schema
|
||||
*/
|
||||
schemaPath: string;
|
||||
|
||||
/**
|
||||
* Suggestion to fix the occurring error
|
||||
*
|
||||
* Provided by https://www.npmjs.com/package/better-ajv-errors
|
||||
*/
|
||||
suggestion?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* An expected error
|
||||
*/
|
||||
export interface ExpectedValidationError extends ValidationError {
|
||||
/**
|
||||
* Whether or not the error is expected
|
||||
*/
|
||||
expected: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A map of files and their expected validation errors
|
||||
*/
|
||||
export interface ExpectedValidationErrors {
|
||||
[fileName: string]: ExpectedValidationError[];
|
||||
}
|
||||
@@ -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)}`;
|
||||
}
|
||||
|
||||
@@ -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 = '';
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
37
src/util/collections.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (C) 2021 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {omitBy, isNil, reject, isEmpty, isArray, isObject} from 'lodash';
|
||||
|
||||
/**
|
||||
* Filters only defined elements
|
||||
*/
|
||||
export function rejectNil<T>(array: Array<T | undefined | null>): T[] {
|
||||
return reject(array, isNil) as T[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Map elements that are not null
|
||||
*/
|
||||
export function mapNotNil<T, S>(array: readonly T[], transform: (element: T) => S | undefined | null): S[] {
|
||||
return rejectNil(array.map(transform));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all properties with the value 'undefined', [] or {}
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
export function cleanupEmpty<T extends object>(object: T): T {
|
||||
return omitBy(object, it => isNil(it) || ((isObject(it) || isArray(it)) && isEmpty(it))) as T;
|
||||
}
|
||||
35
src/util/guards.ts
Normal file
35
src/util/guards.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (C) 2021 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {JSONSchema7 as JSONSchema} from 'json-schema';
|
||||
import {SchemaWithDefinitions} from '../types/schema';
|
||||
|
||||
/**
|
||||
* Guard for if a JSON schema is in fact a schema with definitions
|
||||
*/
|
||||
export function isSchemaWithDefinitions(schema: JSONSchema): schema is SchemaWithDefinitions {
|
||||
return 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
38
src/util/io.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (C) 2021 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {readdirSync, statSync} from 'fs';
|
||||
import {flatMap} from 'lodash';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* Expand a path to a list of all files deeply contained in it
|
||||
*/
|
||||
export function expandPathToFilesSync(sourcePath: string, accept: (fileName: string) => boolean): string[] {
|
||||
const fullPath = path.resolve(sourcePath);
|
||||
const directory = statSync(fullPath);
|
||||
|
||||
return directory.isDirectory()
|
||||
? flatMap(readdirSync(fullPath), fragment =>
|
||||
expandPathToFilesSync(path.resolve(sourcePath, fragment), accept),
|
||||
)
|
||||
: [fullPath].filter(accept);
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a Windows path and make a Unix path out of it
|
||||
*/
|
||||
export function toUnixPath(pathString: string): string {
|
||||
return pathString.replace(/\\/g, '/');
|
||||
}
|
||||
@@ -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()}`;
|
||||
}
|
||||
241
src/validate.ts
241
src/validate.ts
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2018-2019 StApps
|
||||
* Copyright (C) 2018-2021 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
@@ -12,38 +12,31 @@
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {asyncPool} from '@krlwlfrt/async-pool/lib/async-pool';
|
||||
import {Logger} from '@openstapps/logger';
|
||||
import Ajv from 'ajv';
|
||||
import betterAjvErrors from 'better-ajv-errors';
|
||||
import {PathLike} from 'fs';
|
||||
import {JSONSchema7} from 'json-schema';
|
||||
import * as mustache from 'mustache';
|
||||
import {basename, join, resolve} from 'path';
|
||||
import {Schema} from 'ts-json-schema-generator';
|
||||
import {
|
||||
ExpectedValidationErrors,
|
||||
globPromisified,
|
||||
isThingWithType,
|
||||
readFilePromisified,
|
||||
ValidationError,
|
||||
ValidationResult,
|
||||
writeFilePromisified,
|
||||
} from './common';
|
||||
import {globPromisified, readFilePromisified, writeFilePromisified} from './common';
|
||||
import {ExpectedValidationErrors, ValidationError, ValidationResult} from './types/validator';
|
||||
import {isThingWithType} from './util/guards';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* StAppsCore validator
|
||||
*/
|
||||
export class Validator {
|
||||
|
||||
/**
|
||||
* JSON Schema Validator
|
||||
*/
|
||||
private readonly ajv = Ajv({verbose: true, jsonPointers: true, extendRefs: true});
|
||||
|
||||
/**
|
||||
* Map of schema names to schemas
|
||||
*/
|
||||
private readonly schemas: { [type: string]: Schema; } = {};
|
||||
private readonly schemas: {[type: string]: Schema} = {};
|
||||
|
||||
/**
|
||||
* A wrapper function for Ajv that transforms the error into the compatible old error
|
||||
@@ -58,27 +51,28 @@ export class Validator {
|
||||
/**
|
||||
* Feed the schema files to the validator
|
||||
*
|
||||
* @param schemaDir Path to directory that contains schema files
|
||||
* @param schemaDirectory Path to directory that contains schema files
|
||||
*/
|
||||
public async addSchemas(schemaDir: PathLike): Promise<string[]> {
|
||||
const schemaFiles = await globPromisified(join(schemaDir.toString(), '*.json'));
|
||||
public async addSchemas(schemaDirectory: PathLike): Promise<string[]> {
|
||||
const schemaFiles = await globPromisified(path.join(schemaDirectory.toString(), '*.json'));
|
||||
|
||||
if (schemaFiles.length === 0) {
|
||||
throw new Error(`No schema files in ${schemaDir.toString()}!`);
|
||||
throw new Error(`No schema files in ${schemaDirectory.toString()}!`);
|
||||
}
|
||||
|
||||
Logger.log(`Adding schemas from ${schemaDir} to validator.`);
|
||||
Logger.log(`Adding schemas from ${schemaDirectory} to validator.`);
|
||||
|
||||
// tslint:disable-next-line:no-magic-numbers - iterate over schema files
|
||||
await asyncPool(2, schemaFiles, async (file: string) => {
|
||||
// read schema file
|
||||
const buffer = await readFilePromisified(file);
|
||||
await Promise.all(
|
||||
schemaFiles.map(async (file: string) => {
|
||||
// read schema file
|
||||
const buffer = await readFilePromisified(file);
|
||||
|
||||
// add schema to map
|
||||
this.schemas[basename(file, '.json')] = JSON.parse(buffer.toString());
|
||||
// add schema to map
|
||||
this.schemas[path.basename(file, '.json')] = JSON.parse(buffer.toString());
|
||||
|
||||
Logger.info(`Added ${file} to validator.`);
|
||||
});
|
||||
Logger.info(`Added ${file} to validator.`);
|
||||
}),
|
||||
);
|
||||
|
||||
return schemaFiles;
|
||||
}
|
||||
@@ -93,11 +87,10 @@ export class Validator {
|
||||
if (typeof schema === 'undefined') {
|
||||
if (isThingWithType(instance)) {
|
||||
// schema name can be inferred from type string
|
||||
// tslint:disable-next-line: completed-docs
|
||||
const schemaSuffix = (instance as { type: string; }).type.split(' ')
|
||||
const schemaSuffix = (instance as {type: string}).type
|
||||
.split(' ')
|
||||
.map((part: string) => {
|
||||
return part.substr(0, 1)
|
||||
.toUpperCase() + part.substr(1);
|
||||
return part.slice(0, 1).toUpperCase() + part.slice(1);
|
||||
})
|
||||
.join('');
|
||||
const schemaName = `SC${schemaSuffix}`;
|
||||
@@ -109,7 +102,7 @@ export class Validator {
|
||||
if (typeof schema === 'string') {
|
||||
// if you want to access a schema that is contained in the validator object
|
||||
if (typeof this.schemas[schema] !== 'object') {
|
||||
throw new Error(`No schema available for ${schema}.`);
|
||||
throw new TypeError(`No schema available for ${schema}.`);
|
||||
}
|
||||
|
||||
// schema will be cached
|
||||
@@ -136,26 +129,31 @@ function fromAjvResult(
|
||||
instance: unknown,
|
||||
ajvInstance: Ajv.Ajv,
|
||||
): ValidationResult {
|
||||
// tslint:disable-next-line
|
||||
// @ts-ignore function can return void, which at runtime will be undefined. TS doesn't allow to assign void to undefined
|
||||
const betterErrorObject: betterAjvErrors.IOutputError[] | undefined =
|
||||
betterAjvErrors(schema, instance, ajvInstance.errors, {format: 'js', indent: null});
|
||||
// @ts-expect-error function can return void, which at runtime will be undefined. TS doesn't allow to assign void to undefined
|
||||
const betterErrorObject: betterAjvErrors.IOutputError[] | undefined = betterAjvErrors(
|
||||
schema,
|
||||
instance,
|
||||
ajvInstance.errors,
|
||||
// eslint-disable-next-line unicorn/no-null
|
||||
{format: 'js', indent: null},
|
||||
);
|
||||
|
||||
return {
|
||||
errors: ajvInstance.errors?.map((ajvError, index) => {
|
||||
errors:
|
||||
ajvInstance.errors?.map((ajvError, index) => {
|
||||
const error: ValidationError = {
|
||||
dataPath: ajvError.dataPath,
|
||||
instance: instance,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
|
||||
message: betterErrorObject?.[index].error!,
|
||||
name: ajvError.keyword,
|
||||
schemaPath: ajvError.schemaPath,
|
||||
suggestion: betterErrorObject?.[index].suggestion,
|
||||
};
|
||||
// (validationError as ValidationError).humanReadableError = betterErrorCLI?.[index] as unknown as string;
|
||||
|
||||
const error: ValidationError = {
|
||||
dataPath: ajvError.dataPath,
|
||||
instance: instance,
|
||||
message: betterErrorObject?.[index].error!,
|
||||
name: ajvError.keyword,
|
||||
schemaPath: ajvError.schemaPath,
|
||||
suggestion: betterErrorObject?.[index].suggestion,
|
||||
};
|
||||
// (validationError as ValidationError).humanReadableError = betterErrorCLI?.[index] as unknown as string;
|
||||
|
||||
return error;
|
||||
}) ?? [],
|
||||
return error;
|
||||
}) ?? [],
|
||||
valid: typeof result === 'boolean' ? result : false,
|
||||
};
|
||||
}
|
||||
@@ -163,19 +161,22 @@ function fromAjvResult(
|
||||
/**
|
||||
* Validate all test files in the given resources directory against schema files in the given (schema) directory
|
||||
*
|
||||
* @param schemaDir The directory where the JSON schema files are
|
||||
* @param resourcesDir The directory where the test files are
|
||||
* @param schemaDirectory The directory where the JSON schema files are
|
||||
* @param resourcesDirectory The directory where the test files are
|
||||
*/
|
||||
export async function validateFiles(schemaDir: string, resourcesDir: string): Promise<ExpectedValidationErrors> {
|
||||
export async function validateFiles(
|
||||
schemaDirectory: string,
|
||||
resourcesDirectory: string,
|
||||
): Promise<ExpectedValidationErrors> {
|
||||
// instantiate new validator
|
||||
const v = new Validator();
|
||||
await v.addSchemas(schemaDir);
|
||||
await v.addSchemas(schemaDirectory);
|
||||
|
||||
// get list of files to test
|
||||
const testFiles = await globPromisified(join(resourcesDir, '*.json'));
|
||||
const testFiles = await globPromisified(path.join(resourcesDirectory, '*.json'));
|
||||
|
||||
if (testFiles.length === 0) {
|
||||
throw new Error(`No test files in ${resourcesDir}!`);
|
||||
throw new Error(`No test files in ${resourcesDirectory}!`);
|
||||
}
|
||||
|
||||
Logger.log(`Found ${testFiles.length} file(s) to test.`);
|
||||
@@ -183,68 +184,68 @@ export async function validateFiles(schemaDir: string, resourcesDir: string): Pr
|
||||
// map of errors per file
|
||||
const errors: ExpectedValidationErrors = {};
|
||||
|
||||
// tslint:disable-next-line:no-magic-numbers - iterate over files to test
|
||||
await asyncPool(2, testFiles, async (testFile: string) => {
|
||||
const testFileName = basename(testFile);
|
||||
await Promise.all(
|
||||
testFiles.map(async (testFile: string) => {
|
||||
const testFileName = path.basename(testFile);
|
||||
|
||||
const buffer = await readFilePromisified(join(resourcesDir, testFileName));
|
||||
const buffer = await readFilePromisified(path.join(resourcesDirectory, testFileName));
|
||||
|
||||
// read test description from file
|
||||
const testDescription = JSON.parse(buffer.toString());
|
||||
// read test description from file
|
||||
const testDescription = JSON.parse(buffer.toString());
|
||||
|
||||
// validate instance from test description
|
||||
const result = v.validate(testDescription.instance, testDescription.schema);
|
||||
// validate instance from test description
|
||||
const result = v.validate(testDescription.instance, testDescription.schema);
|
||||
|
||||
// list of expected errors for this test description
|
||||
const expectedErrors: string[] = [];
|
||||
expectedErrors.push.apply(expectedErrors, testDescription.errorNames);
|
||||
// list of expected errors for this test description
|
||||
const expectedErrors: string[] = [...testDescription.errorNames];
|
||||
|
||||
// number of unexpected errors
|
||||
let unexpectedErrors = 0;
|
||||
// number of unexpected errors
|
||||
let unexpectedErrors = 0;
|
||||
|
||||
if (result.errors.length > 0) {
|
||||
errors[testFileName] = [];
|
||||
if (result.errors.length > 0) {
|
||||
errors[testFileName] = [];
|
||||
|
||||
// iterate over errors
|
||||
for (const error of result.errors) {
|
||||
const errorIndex = expectedErrors.indexOf(error.name);
|
||||
let expected = false;
|
||||
// iterate over errors
|
||||
for (const error of result.errors) {
|
||||
const errorIndex = expectedErrors.indexOf(error.name);
|
||||
let expected = false;
|
||||
|
||||
if (errorIndex >= 0) {
|
||||
expectedErrors.splice(errorIndex, 1);
|
||||
expected = true;
|
||||
} else {
|
||||
unexpectedErrors++;
|
||||
await Logger.error(`Unexpected error ${error.name} in ${testFile}`);
|
||||
if (errorIndex >= 0) {
|
||||
expectedErrors.splice(errorIndex, 1);
|
||||
expected = true;
|
||||
} else {
|
||||
unexpectedErrors++;
|
||||
await Logger.error(`Unexpected error ${error.name} in ${testFile}`);
|
||||
}
|
||||
|
||||
// add error to list of errors
|
||||
errors[testFileName].push({
|
||||
...error,
|
||||
expected,
|
||||
});
|
||||
}
|
||||
|
||||
// add error to list of errors
|
||||
errors[testFileName].push({
|
||||
...error,
|
||||
expected,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (expectedErrors.length > 0) {
|
||||
for (const error of expectedErrors) {
|
||||
await Logger.error(`Extraneous expected error '${error}' in ${testFile}.`);
|
||||
if (expectedErrors.length > 0) {
|
||||
for (const error of expectedErrors) {
|
||||
await Logger.error(`Extraneous expected error '${error}' in ${testFile}.`);
|
||||
|
||||
errors[testFileName].push({
|
||||
dataPath: 'undefined',
|
||||
expected: false,
|
||||
instance: undefined,
|
||||
// instance: testDescription.instance,
|
||||
message: 'undefined',
|
||||
name: `expected ${error}`,
|
||||
schemaPath: 'undefined',
|
||||
suggestion: 'undefined',
|
||||
});
|
||||
errors[testFileName].push({
|
||||
dataPath: 'undefined',
|
||||
expected: false,
|
||||
instance: undefined,
|
||||
// instance: testDescription.instance,
|
||||
message: 'undefined',
|
||||
name: `expected ${error}`,
|
||||
schemaPath: 'undefined',
|
||||
suggestion: 'undefined',
|
||||
});
|
||||
}
|
||||
} else if (unexpectedErrors === 0) {
|
||||
Logger.info(`Successfully validated ${testFile}.`);
|
||||
}
|
||||
} else if (unexpectedErrors === 0) {
|
||||
Logger.info(`Successfully validated ${testFile}.`);
|
||||
}
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return errors;
|
||||
}
|
||||
@@ -256,10 +257,12 @@ export async function validateFiles(schemaDir: string, resourcesDir: string): Pr
|
||||
* @param errors Errors that occurred in validation
|
||||
*/
|
||||
export async function writeReport(reportPath: PathLike, errors: ExpectedValidationErrors): Promise<void> {
|
||||
let buffer = await readFilePromisified(resolve(__dirname, '..', 'resources', 'file.html.mustache'));
|
||||
// eslint-disable-next-line unicorn/prefer-module
|
||||
let buffer = await readFilePromisified(path.resolve(__dirname, '..', 'resources', 'file.html.mustache'));
|
||||
const fileTemplate = buffer.toString();
|
||||
|
||||
buffer = await readFilePromisified(resolve(__dirname, '..', 'resources', 'error.html.mustache'));
|
||||
// eslint-disable-next-line unicorn/prefer-module
|
||||
buffer = await readFilePromisified(path.resolve(__dirname, '..', 'resources', 'error.html.mustache'));
|
||||
const errorTemplate = buffer.toString();
|
||||
|
||||
let output = '';
|
||||
@@ -271,19 +274,17 @@ export async function writeReport(reportPath: PathLike, errors: ExpectedValidati
|
||||
|
||||
let fileOutput = '';
|
||||
|
||||
errors[fileName].forEach((error, idx) => {
|
||||
|
||||
for (const [index, error] of errors[fileName].entries()) {
|
||||
fileOutput += mustache.render(errorTemplate, {
|
||||
idx: idx + 1,
|
||||
// tslint:disable-next-line:no-magic-numbers
|
||||
instance: JSON.stringify(error.instance, null, 2),
|
||||
idx: index + 1,
|
||||
instance: JSON.stringify(error.instance, undefined, 2),
|
||||
message: error.message,
|
||||
name: error.name,
|
||||
schemaPath: error.schemaPath,
|
||||
status: (error.expected) ? 'alert-success' : 'alert-danger',
|
||||
status: error.expected ? 'alert-success' : 'alert-danger',
|
||||
suggestion: error.suggestion,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
output += mustache.render(fileTemplate, {
|
||||
errors: fileOutput,
|
||||
@@ -291,13 +292,17 @@ export async function writeReport(reportPath: PathLike, errors: ExpectedValidati
|
||||
});
|
||||
}
|
||||
|
||||
buffer = await readFilePromisified(resolve(__dirname, '..', 'resources', 'report.html.mustache'));
|
||||
// eslint-disable-next-line unicorn/prefer-module
|
||||
buffer = await readFilePromisified(path.resolve(__dirname, '..', 'resources', 'report.html.mustache'));
|
||||
const reportTemplate = buffer.toString();
|
||||
|
||||
await writeFilePromisified(reportPath, mustache.render(reportTemplate, {
|
||||
report: output,
|
||||
timestamp: (new Date()).toISOString(),
|
||||
}));
|
||||
await writeFilePromisified(
|
||||
reportPath,
|
||||
mustache.render(reportTemplate, {
|
||||
report: output,
|
||||
timestamp: new Date().toISOString(),
|
||||
}),
|
||||
);
|
||||
|
||||
Logger.ok(`Wrote report to ${reportPath}.`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user