import { appendFileSync, createWriteStream, writeFileSync } from 'fs'; import { generate } from 'node-plantuml'; import { OUTPUT_DIR } from '.'; import { isArrayOfStrings, objectFromEntries, trim } from './util'; export interface DiagramConfiguration { readonly hiddenFields?: string[]; readonly hidePlaintext?: boolean; } export function isValidDiagramConfiguration( toBeValidated: unknown, ): toBeValidated is DiagramConfiguration { if (typeof toBeValidated !== 'object' || toBeValidated === null) { return false; } const diagramConfiguration = toBeValidated as DiagramConfiguration; return ( ['boolean', 'undefined'].includes( typeof diagramConfiguration.hidePlaintext, ) && (typeof diagramConfiguration.hiddenFields === 'undefined' || isArrayOfStrings(diagramConfiguration.hiddenFields)) ); } function getInputFile(scenario: string): string { return `${OUTPUT_DIR()}/_${scenario}.input`; } function getOutputFile(scenario: string): string { return `${OUTPUT_DIR()}/_${scenario}.png`; } const hidingText = '***'; function hideFields(payload: object, hiddenFields: string[]): object { return objectFromEntries( Object.entries(payload).map(([key, value]) => hiddenFields.includes(key) ? [key, hidingText] : [key, value], ), ); } function hideFieldsIfNeeded( payload: unknown, hiddenFields?: string[], ): unknown { return typeof payload === 'object' && payload !== null && hiddenFields !== undefined && hiddenFields.length !== 0 ? hideFields(payload, hiddenFields) : payload; } function hidePlaintextIfNeeded(payload: string, hidePlaintext = false): string { return hidePlaintext ? hidingText : payload; } function formatBinaryPayload(payload: Buffer): string { return `binary data (${(payload as Buffer).length} bytes)`; } function formatPlaintextPayload( payload: string, diagramConfiguration: DiagramConfiguration, ): string { return trim( hidePlaintextIfNeeded(payload, diagramConfiguration.hidePlaintext), 30, ); } function formatObjectPayload( payload: unknown, diagramConfiguration: DiagramConfiguration, ): string { return JSON.stringify( hideFieldsIfNeeded(payload, diagramConfiguration.hiddenFields), null, 1, ); } export function formatPayload( payload: unknown, diagramConfiguration: DiagramConfiguration, ): string { if (Buffer.isBuffer(payload)) { return formatBinaryPayload(payload); } if (typeof payload === 'string') { return formatPlaintextPayload(payload, diagramConfiguration); } return formatObjectPayload(payload, diagramConfiguration); } function currentTimestamp(): string { return new Date().toISOString(); } function enquote(str: string): string { return `"${str}"`; } export const initDiagramCreation = (scenarioId: string): void => { writeFileSync(getInputFile(scenarioId), ''); const initValues = [ '@startuml', 'autonumber', 'skinparam handwritten false', 'control MQTT', 'actor ALT #red\n', ]; appendFileSync(getInputFile(scenarioId), initValues.join('\n')); }; export const addRequest = ( scenarioId: string, target: string, url: string, data: unknown, diagramConfiguration: DiagramConfiguration, ): void => { const enquotedTarget = enquote(target); const request = `ALT -> ${enquotedTarget}: ${url}\nactivate ${enquotedTarget}\n${ data ? `note right\n**${currentTimestamp()}**\n${formatPayload( data, diagramConfiguration, )}\nend note\n` : '' }`; appendFileSync(getInputFile(scenarioId), request); }; export const addSuccessfulResponse = ( scenarioId: string, source: string, status: string, body: unknown, diagramConfiguration: DiagramConfiguration, ): void => { doAddResponse(scenarioId, source, status, 'green'); if (body) { const note = `note left\n**${currentTimestamp()}**\n${formatPayload( body, diagramConfiguration, )}\nend note\n`; appendFileSync(getInputFile(scenarioId), note); } }; export const addFailedResponse = ( scenarioId: string, source: string, status: string, body: string, diagramConfiguration: DiagramConfiguration, ): void => { doAddResponse(scenarioId, source, status, 'red'); appendFileSync( getInputFile(scenarioId), `note right: ${formatPayload( body, diagramConfiguration, )}\n||20||\n`, ); }; const doAddResponse = ( scenarioId: string, source: string, status: string, color: string, ): void => { const enquotedSource = enquote(source); appendFileSync( getInputFile(scenarioId), `${enquotedSource} --> ALT: ${status}\ndeactivate ${enquotedSource}\n`, ); }; export const addDelay = (scenarioId: string, durationInSec: number): void => { appendFileSync( getInputFile(scenarioId), `\n...sleep ${durationInSec} s...\n`, ); }; export const addWsMessage = ( scenarioId: string, source: string, payload: unknown, diagramConfiguration: DiagramConfiguration, ): void => { const enquotedSource = enquote(source); appendFileSync( getInputFile(scenarioId), `${enquotedSource} -[#0000FF]->o ALT : [WS]\n`, ); const note = `note left #aqua\n**${currentTimestamp()}**\n${formatPayload( payload, diagramConfiguration, )}\nend note\n`; appendFileSync(getInputFile(scenarioId), note); }; export const addMqttMessage = ( scenarioId: string, topic: string, payload: unknown, diagramConfiguration: DiagramConfiguration, ): void => { appendFileSync( getInputFile(scenarioId), `MQTT -[#green]->o ALT : ${topic}\n`, ); const note = `note right #99FF99\n**${currentTimestamp()}**\n${formatPayload( payload, diagramConfiguration, )}\nend note\n`; appendFileSync(getInputFile(scenarioId), note); }; export const addMqttPublishMessage = ( scenarioId: string, topic: string, payload: any, diagramConfiguration: DiagramConfiguration, ): void => { appendFileSync( getInputFile(scenarioId), `ALT -[#green]->o MQTT : ${topic}\n`, ); const note = `note left #99FF99\n**${currentTimestamp()}**\n${formatPayload( JSON.parse(payload), diagramConfiguration, )}\nend note\n`; appendFileSync(getInputFile(scenarioId), note); }; export const addAMQPReceivedMessage = ( scenarioId: string, source: string, exchange: string, routingKey: string, payload: unknown, diagramConfiguration: DiagramConfiguration, ): void => { const enquotedSource = enquote(source); appendFileSync( getInputFile(scenarioId), `${enquotedSource} -[#FF6600]->o ALT : ${exchange}/${routingKey}\n`, ); const note = `note left #FF6600\n**${currentTimestamp()}**\n${formatPayload( payload, diagramConfiguration, )}\nend note\n`; appendFileSync(getInputFile(scenarioId), note); }; export const generateSequenceDiagram = (scenarioId: string): Promise => new Promise(resolve => { appendFileSync(getInputFile(scenarioId), '\n@enduml'); const gen = generate(getInputFile(scenarioId)); gen.out.pipe(createWriteStream(getOutputFile(scenarioId))); gen.out.on('end', () => resolve()); });