/* * 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 . */ import {Logger} from '@openstapps/logger'; import * as request from 'got'; import { expandTypeValue, isLightweightClass, LightweightAliasDefinition, LightweightClassDefinition, LightweightDefinition, LightweightProperty, LightweightType, } from '@openstapps/easy-ast'; import {UMLConfig} from './uml-config.js'; import {writeFile} from 'fs/promises'; /** * Converts the lightweight class/enum definitions according to the configuration, * 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 definitions of the project * @param config contains information on how the PlantUML should be generated * @param plantUmlBaseURL Hostname of the PlantUML-Server */ export async function createDiagram( definitions: LightweightDefinition[], config: UMLConfig, plantUmlBaseURL: string, ): Promise { // when non definitions were specified use all config.definitions = definitions.map(it => it.name); // when providing definitions and either showing associations or inheritance the // inherited definitions will be added automatically if (config.showInheritance) { // TODO: showInheritance /*const inheritedDefinitions = gatherTypeAssociations( definitions, config.definitions, );*/ // config.definitions = config.definitions.concat(inheritedDefinitions); } // creates a UML definition for every specified definition name // however if no definitions were provided all definitions will be transformed const modelPlantUMLCode = definitions .filter(it => !config.definitions.includes(it.name)) .map(definition => isLightweightClass(definition) ? createPlantUMLCodeForClass(config, definition) : createPlantUMLCodeForEnum(config, definition), ) .join(''); return createDiagramFromString(modelPlantUMLCode, plantUmlBaseURL, config.outputFileName); } /** * This will encode the plantuml code and post the code to the plantuml server * The server will then parse the code and create a corresponding diagram * @param modelPlantUMLCode raw PlantUML code * @param plantUmlBaseURL PlantUML server address that shall be used * @param outputFile filename of the output file without file extension */ export async function createDiagramFromString( modelPlantUMLCode: string, plantUmlBaseURL: string, outputFile = `Diagram-${new Date().toISOString()}`, ) { // @ts-expect-error no declarations const plantumlEncoder = await import('plantuml-encoder'); const plantUMLCode = plantumlEncoder.encode(`@startuml\n${modelPlantUMLCode}\n@enduml`); const url = `${plantUmlBaseURL}/svg/${plantUMLCode}`; let response; try { response = await request.default.get(url); const httpOK = 200; if (response.statusCode !== httpOK) { await Logger.error(`Plantuml Server responded with an error.\n${response.statusMessage}`); throw new Error('Response not okay'); } } 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.replaceAll(/[^\w-]/g, '_')}.svg`; await writeFile(fileName, response.body); Logger.log(`Writen data to file: ${fileName}`); return fileName; } /** * Recursively iterates over all types, to find implemented generic types and parents * @param definitions all type definitions of the project * @param abstractionNames currently known string values of inherited classes */ /*function gatherTypeAssociations( definitions: LightweightDefinition[], abstractionNames: string[], ): string[] { let abstractions: string[] = []; for (const name of abstractionNames) { const declaration = definitions.find( (definition) => definition.name === name, ); if (isLightweightClass(declaration)) { const currentAbstractions: string[] = declaration.extendedDefinitions.concat( declaration.implementedDefinitions, ); abstractions = abstractions.concat(currentAbstractions); abstractions = abstractions.concat( gatherTypeAssociations(definitions, currentAbstractions), ); } } return abstractions; }*/ /** * Collects all reference information of this type. * * Reference information is everything that is indirectly referencing a type or class by name. * @param type Type with references to other types */ function getReferenceTypes(type: LightweightType): string[] { const types: string[] = []; if (type.referenceName !== undefined) { types.push(type.referenceName); } if (type.genericsTypes) { for (const specificType of type.genericsTypes) { for (const value of getReferenceTypes(specificType)) { types.push(value); } } } if (Array.isArray(type.specificationTypes)) { for (const specificType of type.specificationTypes) { for (const value of getReferenceTypes(specificType)) { types.push(value); } } } return types; } /** * Creates Plant UML code according to the config for the provided class * @param config Configuration for how the UML should be tweaked * @param readerClass Class or interface representation */ 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.modifiers} ${readerClass.name}`; if (readerClass.typeParameters?.length ?? 0 > 0) { model += `<${readerClass.typeParameters!.join(', ')}>`; } if (config.showInheritance && (readerClass.extendedDefinitions?.length ?? 0 > 0)) { // PlantUML will automatically create links, when using extends model += ` extends ${readerClass.extendedDefinitions!.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 && readerClass.properties) { for (const key in readerClass.properties) { const property = readerClass.properties[key]; if (property.optional && !config.showOptionalProperties) { // don't show optional attributes continue; } /*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 if (readerClass.properties) { for (const key in readerClass.properties) { const property = readerClass.properties[key]; const types: string[] = getReferenceTypes(property.type); for (const type of types) { if (config.showAssociations) { /*if (property.inherited && !config.showInheritedProperties) { continue; }*/ model += `${readerClass.name} -up-> ${type} : ${property.name} >\n`; } } } } return model; } /** * Creates PlantUML code according to the config for the provided enum/-like definition * @param config Configuration for how the UML should be tweaked * @param readerEnum Enum/-like representation */ function createPlantUMLCodeForEnum(config: UMLConfig, readerEnum: LightweightAliasDefinition): string { // create enum header let model = `enum ${readerEnum.name} {`; // add values if (config.showEnumValues && readerEnum.type?.specificationTypes) { for (const value of readerEnum.type?.specificationTypes) { model += `\n\t${value.toString()}`; } } model += '\n}\n'; return model; } /** * Creates a property PlantUML Line */ function createPropertyLine(property: LightweightProperty): string { const prefix = `${/*(property.inherited ? '/ ' : */ ''}${property.optional ? '? ' : ''}`; return `${prefix}${property.name} : ${expandTypeValue(property.type)}`; }