/* * 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 path from 'path'; import {lightweightDefinitionsFromPath, lightweightProjectFromPath} from '@openstapps/easy-ast'; import {openapi3Template} from './resources/openapi-303-template.js'; import {gatherRouteInformation, generateOpenAPIForRoute} from './routes.js'; import {Converter, getValidatableTypesInPath} from './schema.js'; import {createDiagram, createDiagramFromString} from './uml/create-diagram.js'; import {UMLConfig} from './uml/uml-config.js'; import {capitalize} from './util/string.js'; import {validateFiles, writeReport} from './validate.js'; import {fileURLToPath} from 'url'; import {mkdir, readFile} from 'fs/promises'; // 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.join(path.dirname(fileURLToPath(import.meta.url)), '..', '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, 'schema'); // get information about routes const routes = await gatherRouteInformation(sourcePath); routes.sort((a, b) => a.route.urlPath.localeCompare(b.route.urlPath)); // change url path parameters to openapi notation for (const routeWithMetaInformation of routes) { routeWithMetaInformation.route.urlPath = routeWithMetaInformation.route.urlPath.replaceAll( /:\w+/g, (match: string) => `{${match.replace(':', '')}}`, ); } // keep openapi tags for routes that actually share url fragments let tagsToKeep = routes.map(routeWithMetaInformation => capitalize(routeWithMetaInformation.route.urlPath.split('/')[1]), ); tagsToKeep = tagsToKeep.filter( (element, i, array) => array.indexOf(element) === i && array.lastIndexOf(element) !== i, ); // initialize json output const output = openapi3Template; // generate documentation for all routes for (const routeWithMetaInformation of routes) { routeWithMetaInformation.tags = [capitalize(routeWithMetaInformation.route.urlPath.split('/')[1])]; output.paths[routeWithMetaInformation.route.urlPath] = generateOpenAPIForRoute( routeWithMetaInformation, path.relative(relativeOutDirectoryPath, outDirectorySchemasPath), tagsToKeep, ); } // 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 mkdir(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 readFile(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 (relativeReportPath !== undefined) { const reportPath = path.resolve(relativeReportPath); await writeReport(reportPath, errorsPerFile); } if (unexpected) { await Logger.error('Unexpected errors occurred during validation'); process.exit(1); } else { Logger.ok('Successfully finished validation.'); } }); commander .command('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: options.definitions === undefined ? [] : options.definitions, showAssociations: options.showAssociations === undefined ? false : options.showAssociations, showEnumValues: options.showEnumValues === undefined ? false : options.showEnumValues, showInheritance: options.showInheritance === undefined ? false : options.showInheritance, showInheritedProperties: options.showInheritedProperties === undefined ? false : options.showInheritedProperties, showOptionalProperties: options.showOptionalProperties === undefined ? false : options.showOptionalProperties, showProperties: options.showProperties === undefined ? false : options.showProperties, }; if (options.outputFileName !== undefined) { plantUmlConfig.outputFileName = options.outputFileName; } Logger.log(`PlantUML options: ${JSON.stringify(plantUmlConfig)}`); await createDiagram(lightweightDefinitionsFromPath(relativeSourcePath), plantUmlConfig, plantumlServer); }); commander .command('plantuml-file [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);