/* * Copyright (C) 2018-2019 StApps * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ import {Logger} from '@openstapps/logger'; import {Command} from 'commander'; import {existsSync, readFileSync, writeFileSync} from 'fs'; import * as got from 'got'; import {join, resolve} from 'path'; import {exit} from 'process'; import { getProjectReflection, mkdirPromisified, readFilePromisified, toArray, } from './common'; import {generateTemplate} from './mapping'; import {pack} from './pack'; import { gatherRouteInformation, generateDocumentationForRoute, getNodeMetaInformationMap, } from './routes'; import {Converter, getValidatableTypesFromReflection} from './schema'; import {createDiagram, createDiagramFromString} from './uml/create-diagram'; import {readDefinitions} from './uml/read-definitions'; import {UMLConfig} from './uml/uml-config'; import {validateFiles, writeReport} from './validate'; // handle unhandled promise rejections process.on('unhandledRejection', async (reason: unknown) => { if (reason instanceof Error) { await Logger.error(reason.message); Logger.info(reason.stack); } process.exit(1); }); const commander = new Command('openstapps-core-tools'); commander .version(JSON.parse( readFileSync(resolve(__dirname, '..', 'package.json')) .toString(), ).version); commander .command('routes ') .action(async (relativeSrcPath, relativeMdPath) => { // get absolute paths const srcPath = resolve(relativeSrcPath); const mdPath = resolve(relativeMdPath); // get project reflection const projectReflection = getProjectReflection(srcPath); // get information about routes const routes = await gatherRouteInformation(projectReflection); // initialize markdown output let output = '# Routes\n\n'; // generate documentation for all routes routes.forEach((routeWithMetaInformation) => { output += generateDocumentationForRoute( routeWithMetaInformation, getNodeMetaInformationMap(projectReflection), ); }); // write documentation to file writeFileSync(mdPath, output); Logger.ok(`Route documentation written to ${mdPath}.`); }); commander .command('mapping [ignoredTags]') .action(async (relativeSrcPath, relativeMappingPath, ignoredTags) => { // get absolute paths const srcPath = resolve(relativeSrcPath); const mappingPath = resolve(relativeMappingPath); let ignoredTagsList: string[] = []; if (typeof ignoredTags === 'string') { ignoredTagsList = ignoredTags.split(','); } // get project reflection const projectReflection = getProjectReflection(srcPath); 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!'); } // write documentation to file // tslint:disable-next-line:no-magic-numbers writeFileSync(mappingPath, JSON.stringify(result.mappings, null, 2)); Logger.ok(`Elasticsearch mapping written to ${mappingPath}.`); }); commander .command('put-es-templates [ignoredTags]') .action(async (relativeSrcPath, esAddress, ignoredTags) => { // get absolute paths const srcPath = resolve(relativeSrcPath); let ignoredTagsList: string[] = []; if (typeof ignoredTags === 'string') { ignoredTagsList = ignoredTags.split(','); } // get project reflection const projectReflection = getProjectReflection(srcPath); 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!'); } for (const template in result.mappings) { if (!result.mappings.hasOwnProperty(template)) { continue; } const response = await got.put(`${esAddress}_template/${template}`, { body: result.mappings[template], json: true, }); 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); } } Logger.ok(`Templates accepted by Elasticsearch.`); }); commander .command('schema ') .action(async (relativeSrcPath, relativeSchemaPath) => { // get absolute paths const srcPath = resolve(relativeSrcPath); const schemaPath = resolve(relativeSchemaPath); // initialize new core converter const coreConverter = new Converter(srcPath); // get project reflection const projectReflection = getProjectReflection(srcPath); // 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).`); }); commander .command('validate [reportPath]') .action(async (relativeSchemaPath, relativeTestPath, relativeReportPath) => { // get absolute paths const schemaPath = resolve(relativeSchemaPath); const testPath = resolve(relativeTestPath); const errorsPerFile = await validateFiles(schemaPath, testPath); let unexpected = false; for (const file in errorsPerFile) { if (!errorsPerFile.hasOwnProperty(file)) { continue; } unexpected = unexpected || errorsPerFile[file].some((error) => !error.expected); } if (typeof relativeReportPath !== 'undefined') { const reportPath = resolve(relativeReportPath); await writeReport(reportPath, errorsPerFile); } if (!unexpected) { Logger.ok('Successfully finished validation.'); } else { await Logger.error('Unexpected errors occurred during validation'); process.exit(1); } }); commander .command('pack') .action(async () => { await pack(); }); commander .command('plantuml ') .option( '--definitions ', 'Shows these specific definitions (class, interface or enum)', toArray, ) .option('--showAssociations', 'Shows associations of definitions') .option( '--showInheritance', 'Shows extensions and implementations of definitions', ) .option('--showEnumValues', 'Show enum values') .option('--showProperties', 'Show attributes') .option( '--showInheritedProperties', 'Shows inherited attributes, needs --showProperties', ) .option( '--showOptionalProperties', 'Shows optional attributes and relations, needs --showProperties', ) .option( '--excludeExternals', 'Exclude external definitions', ) .option( '--outputFileName ', 'Defines the filename of the output', ) .action(async (relativeSrcPath, 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, showInheritedProperties: typeof options.showInheritedProperties !== 'undefined' ? options.showInheritedProperties : false, showOptionalProperties: typeof options.showOptionalProperties !== 'undefined' ? options.showOptionalProperties : false, showProperties: typeof options.showProperties !== 'undefined' ? options.showProperties : false, }; if (typeof options.outputFileName !== 'undefined') { plantUmlConfig.outputFileName = options.outputFileName; } 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); }); commander .command('plantuml-file [outputFile]') .action(async (file: string, plantumlserver: string, outputFile: string) => { const fileContent = readFileSync(resolve(file)) .toString(); await createDiagramFromString(fileContent, plantumlserver, outputFile); }); commander.parse(process.argv); if (commander.args.length < 1) { commander.outputHelp(); process.exit(1); }