Files
openstapps/packages/core-tools/src/uml/create-diagram.ts

258 lines
8.7 KiB
TypeScript

/*
* 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 {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<string> {
// 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)}`;
}