/* * 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 . */ import {Logger} from '@openstapps/logger'; import {Command} from 'commander'; import {existsSync, readFileSync, writeFileSync} from 'fs'; import {copy} from 'fs-extra'; import path from 'path'; import {mkdirPromisified, readFilePromisified} from './common'; import {lightweightDefinitionsFromPath, lightweightProjectFromPath} from './easy-ast/easy-ast'; import {pack} from './pack'; import {openapi3Template} from './resources/openapi-303-template'; import {gatherRouteInformation, generateOpenAPIForRoute} from './routes'; import {Converter, getValidatableTypesInPath} from './schema'; import {createDiagram, createDiagramFromString} from './uml/create-diagram'; import {UMLConfig} from './uml/uml-config'; import {capitalize} from './util/string'; import {validateFiles, writeReport} from './validate'; // handle unhandled promise rejections process.on('unhandledRejection', async (reason: unknown) => { if (reason instanceof Error) { await Logger.error(reason.message); Logger.info(reason.stack); } process.exit(1); }); const commander = new Command('openstapps-core-tools'); // eslint-disable-next-line unicorn/prefer-module commander.version(JSON.parse(readFileSync(path.resolve(__dirname, '..', 'package.json')).toString()).version); commander.command('prototype ').action(async (sourcePath, out) => { const files = lightweightProjectFromPath(sourcePath); writeFileSync(path.resolve(out), JSON.stringify(files, undefined, 2)); }); commander .command('openapi ') .action(async (relativeSourceBundlePath, relativeOutDirectoryPath) => { // get absolute paths const sourcePath = path.resolve(relativeSourceBundlePath); const outDirectoryPath = path.resolve(relativeOutDirectoryPath); const outDirectorySchemasPath = path.join(outDirectoryPath, 'schemas'); // get information about routes const routes = await gatherRouteInformation(sourcePath); routes.sort((a, b) => a.route.urlFragment.localeCompare(b.route.urlFragment)); // change url path parameters to openapi notation 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, ); // initialize json output const output = openapi3Template; // names of the schemas to copy const schemasToCopy: string[] = []; // generate documentation for all routes for (const routeWithMetaInformation of routes) { routeWithMetaInformation.tags = [capitalize(routeWithMetaInformation.route.urlFragment.split('/')[1])]; output.paths[routeWithMetaInformation.route.urlFragment] = generateOpenAPIForRoute( routeWithMetaInformation, path.relative(relativeOutDirectoryPath, outDirectorySchemasPath), schemasToCopy, tagsToKeep, ); } // copy schema json schema files try { if (!existsSync(outDirectorySchemasPath)) { await mkdirPromisified(outDirectorySchemasPath, { recursive: true, }); } for (const fileName of schemasToCopy) { await copy( path.join(sourcePath, 'schema', `${fileName}.json`), path.join(outDirectorySchemasPath, `${fileName}.json`), ); } } catch (error) { await Logger.error(error); process.exit(-2); } // write openapi object to file (prettified) writeFileSync(path.join(outDirectoryPath, 'openapi.json'), JSON.stringify(output, undefined, 2)); Logger.ok(`OpenAPI representation resources written to ${outDirectoryPath} .`); }); commander.command('schema ').action(async (relativeSourcePath, relativeSchemaPath) => { // get absolute paths const absoluteSourcePath = path.resolve(relativeSourcePath); const schemaPath = path.resolve(relativeSchemaPath); // initialize new core converter const coreConverter = new Converter(absoluteSourcePath); // get validatable types const validatableTypes = getValidatableTypesInPath(absoluteSourcePath); Logger.info(`Found ${validatableTypes.length} type(s) to generate schemas for.`); await mkdirPromisified(schemaPath, { recursive: true, }); Logger.info(`Trying to find a package.json for ${absoluteSourcePath}.`); let packagePath = absoluteSourcePath; // TODO: this check should be less ugly! --- What is this doing anyway? while (!existsSync(path.join(packagePath, 'package.json')) && packagePath.length > 5) { packagePath = path.resolve(packagePath, '..'); } const corePackageJsonPath = path.join(packagePath, 'package.json'); Logger.info(`Using ${corePackageJsonPath} to determine version for schemas.`); const buffer = await readFilePromisified(corePackageJsonPath); const corePackageJson = JSON.parse(buffer.toString()); const coreVersion = corePackageJson.version; Logger.log(`Using ${coreVersion} as version for schemas.`); // generate and write JSONSchema files for validatable types for (const type of validatableTypes) { const schema = coreConverter.getSchema(type, coreVersion); const stringifiedSchema = JSON.stringify(schema, undefined, 2); const file = path.join(schemaPath, `${type}.json`); // write schema to file writeFileSync(file, stringifiedSchema); Logger.info(`Generated schema for ${type} and saved to ${file}.`); } Logger.ok(`Generated schemas for ${validatableTypes.length} type(s).`); }); commander .command('validate [reportPath]') .action(async (relativeSchemaPath, relativeTestPath, relativeReportPath) => { // get absolute paths const schemaPath = path.resolve(relativeSchemaPath); const testPath = path.resolve(relativeTestPath); const errorsPerFile = await validateFiles(schemaPath, testPath); let unexpected = false; for (const file in errorsPerFile) { if (!errorsPerFile.hasOwnProperty(file)) { continue; } unexpected = unexpected || errorsPerFile[file].some(error => !error.expected); } if (typeof relativeReportPath !== 'undefined') { const reportPath = path.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)', it => it.split(','), ) .option('--showAssociations', 'Shows associations of definitions') .option('--showInheritance', 'Shows extensions and implementations of definitions') .option('--showEnumValues', 'Show enum values') .option('--showProperties', 'Show attributes') .option('--showInheritedProperties', 'Shows inherited attributes, needs --showProperties') .option('--showOptionalProperties', 'Shows optional attributes and relations, needs --showProperties') .option('--excludeExternals', 'Exclude external definitions') .option('--outputFileName ', '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, 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)}`); await createDiagram(lightweightDefinitionsFromPath(relativeSourcePath), plantUmlConfig, plantumlServer); }); commander .command('plantuml-file [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);