1 | import { appendFileSync, createWriteStream, writeFileSync } from 'fs';
|
2 | import { generate } from 'node-plantuml';
|
3 | import { OUTPUT_DIR } from '.';
|
4 | import { isArrayOfStrings, objectFromEntries, trim } from './util';
|
5 |
|
6 | export interface DiagramConfiguration {
|
7 | readonly hiddenFields?: string[];
|
8 | readonly hidePlaintext?: boolean;
|
9 | }
|
10 |
|
11 | export function isValidDiagramConfiguration(
|
12 | toBeValidated: unknown,
|
13 | ): toBeValidated is DiagramConfiguration {
|
14 | if (typeof toBeValidated !== 'object' || toBeValidated === null) {
|
15 | return false;
|
16 | }
|
17 | const diagramConfiguration = toBeValidated as DiagramConfiguration;
|
18 |
|
19 | return (
|
20 | ['boolean', 'undefined'].includes(
|
21 | typeof diagramConfiguration.hidePlaintext,
|
22 | ) &&
|
23 | (typeof diagramConfiguration.hiddenFields === 'undefined' ||
|
24 | isArrayOfStrings(diagramConfiguration.hiddenFields))
|
25 | );
|
26 | }
|
27 |
|
28 | function getInputFile(scenario: string): string {
|
29 | return `${OUTPUT_DIR()}/_${scenario}.input`;
|
30 | }
|
31 |
|
32 | function getOutputFile(scenario: string): string {
|
33 | return `${OUTPUT_DIR()}/_${scenario}.png`;
|
34 | }
|
35 |
|
36 | const hidingText = '***';
|
37 |
|
38 | function hideFields(payload: object, hiddenFields: string[]): object {
|
39 | return objectFromEntries(
|
40 | Object.entries(payload).map(([key, value]) =>
|
41 | hiddenFields.includes(key) ? [key, hidingText] : [key, value],
|
42 | ),
|
43 | );
|
44 | }
|
45 |
|
46 | function hideFieldsIfNeeded(
|
47 | payload: unknown,
|
48 | hiddenFields?: string[],
|
49 | ): unknown {
|
50 | return typeof payload === 'object' &&
|
51 | payload !== null &&
|
52 | hiddenFields !== undefined &&
|
53 | hiddenFields.length !== 0
|
54 | ? hideFields(payload, hiddenFields)
|
55 | : payload;
|
56 | }
|
57 |
|
58 | function hidePlaintextIfNeeded(payload: string, hidePlaintext = false): string {
|
59 | return hidePlaintext ? hidingText : payload;
|
60 | }
|
61 |
|
62 | function formatBinaryPayload(payload: Buffer): string {
|
63 | return `binary data (${(payload as Buffer).length} bytes)`;
|
64 | }
|
65 |
|
66 | function formatPlaintextPayload(
|
67 | payload: string,
|
68 | diagramConfiguration: DiagramConfiguration,
|
69 | ): string {
|
70 | return trim(
|
71 | hidePlaintextIfNeeded(payload, diagramConfiguration.hidePlaintext),
|
72 | 30,
|
73 | );
|
74 | }
|
75 |
|
76 | function formatObjectPayload(
|
77 | payload: unknown,
|
78 | diagramConfiguration: DiagramConfiguration,
|
79 | ): string {
|
80 | return JSON.stringify(
|
81 | hideFieldsIfNeeded(payload, diagramConfiguration.hiddenFields),
|
82 | null,
|
83 | 1,
|
84 | );
|
85 | }
|
86 |
|
87 | export function formatPayload(
|
88 | payload: unknown,
|
89 | diagramConfiguration: DiagramConfiguration,
|
90 | ): string {
|
91 | if (Buffer.isBuffer(payload)) {
|
92 | return formatBinaryPayload(payload);
|
93 | }
|
94 | if (typeof payload === 'string') {
|
95 | return formatPlaintextPayload(payload, diagramConfiguration);
|
96 | }
|
97 | return formatObjectPayload(payload, diagramConfiguration);
|
98 | }
|
99 |
|
100 | function currentTimestamp(): string {
|
101 | return new Date().toISOString();
|
102 | }
|
103 |
|
104 | function enquote(str: string): string {
|
105 | return `"${str}"`;
|
106 | }
|
107 |
|
108 | export const initDiagramCreation = (scenarioId: string): void => {
|
109 | writeFileSync(getInputFile(scenarioId), '');
|
110 | const initValues = [
|
111 | '@startuml',
|
112 | 'autonumber',
|
113 | 'skinparam handwritten false',
|
114 | 'control MQTT',
|
115 | 'actor ALT #red\n',
|
116 | ];
|
117 | appendFileSync(getInputFile(scenarioId), initValues.join('\n'));
|
118 | };
|
119 |
|
120 | export const addRequest = (
|
121 | scenarioId: string,
|
122 | target: string,
|
123 | url: string,
|
124 | data: unknown,
|
125 | diagramConfiguration: DiagramConfiguration,
|
126 | ): void => {
|
127 | const enquotedTarget = enquote(target);
|
128 | const request = `ALT -> ${enquotedTarget}: ${url}\nactivate ${enquotedTarget}\n${
|
129 | data
|
130 | ? `note right\n**${currentTimestamp()}**\n${formatPayload(
|
131 | data,
|
132 | diagramConfiguration,
|
133 | )}\nend note\n`
|
134 | : ''
|
135 | }`;
|
136 |
|
137 | appendFileSync(getInputFile(scenarioId), request);
|
138 | };
|
139 |
|
140 | export const addSuccessfulResponse = (
|
141 | scenarioId: string,
|
142 | source: string,
|
143 | status: string,
|
144 | body: unknown,
|
145 | diagramConfiguration: DiagramConfiguration,
|
146 | ): void => {
|
147 | doAddResponse(scenarioId, source, status, 'green');
|
148 | if (body) {
|
149 | const note = `note left\n**${currentTimestamp()}**\n${formatPayload(
|
150 | body,
|
151 | diagramConfiguration,
|
152 | )}\nend note\n`;
|
153 | appendFileSync(getInputFile(scenarioId), note);
|
154 | }
|
155 | };
|
156 |
|
157 | export const addFailedResponse = (
|
158 | scenarioId: string,
|
159 | source: string,
|
160 | status: string,
|
161 | body: string,
|
162 | diagramConfiguration: DiagramConfiguration,
|
163 | ): void => {
|
164 | doAddResponse(scenarioId, source, status, 'red');
|
165 | appendFileSync(
|
166 | getInputFile(scenarioId),
|
167 | `note right: <color red>${formatPayload(
|
168 | body,
|
169 | diagramConfiguration,
|
170 | )}</color>\n||20||\n`,
|
171 | );
|
172 | };
|
173 |
|
174 | const doAddResponse = (
|
175 | scenarioId: string,
|
176 | source: string,
|
177 | status: string,
|
178 | color: string,
|
179 | ): void => {
|
180 | const enquotedSource = enquote(source);
|
181 | appendFileSync(
|
182 | getInputFile(scenarioId),
|
183 | `${enquotedSource} --> ALT: <color ${color}>${status}</color>\ndeactivate ${enquotedSource}\n`,
|
184 | );
|
185 | };
|
186 |
|
187 | export const addDelay = (scenarioId: string, durationInSec: number): void => {
|
188 | appendFileSync(
|
189 | getInputFile(scenarioId),
|
190 | `\n...sleep ${durationInSec} s...\n`,
|
191 | );
|
192 | };
|
193 |
|
194 | export const addWsMessage = (
|
195 | scenarioId: string,
|
196 | source: string,
|
197 | payload: unknown,
|
198 | diagramConfiguration: DiagramConfiguration,
|
199 | ): void => {
|
200 | const enquotedSource = enquote(source);
|
201 | appendFileSync(
|
202 | getInputFile(scenarioId),
|
203 | `${enquotedSource} -[#0000FF]->o ALT : [WS]\n`,
|
204 | );
|
205 | const note = `note left #aqua\n**${currentTimestamp()}**\n${formatPayload(
|
206 | payload,
|
207 | diagramConfiguration,
|
208 | )}\nend note\n`;
|
209 | appendFileSync(getInputFile(scenarioId), note);
|
210 | };
|
211 |
|
212 | export const addMqttMessage = (
|
213 | scenarioId: string,
|
214 | topic: string,
|
215 | payload: unknown,
|
216 | diagramConfiguration: DiagramConfiguration,
|
217 | ): void => {
|
218 | appendFileSync(
|
219 | getInputFile(scenarioId),
|
220 | `MQTT -[#green]->o ALT : ${topic}\n`,
|
221 | );
|
222 | const note = `note right #99FF99\n**${currentTimestamp()}**\n${formatPayload(
|
223 | payload,
|
224 | diagramConfiguration,
|
225 | )}\nend note\n`;
|
226 | appendFileSync(getInputFile(scenarioId), note);
|
227 | };
|
228 |
|
229 | export const addMqttPublishMessage = (
|
230 | scenarioId: string,
|
231 | topic: string,
|
232 | payload: any,
|
233 | diagramConfiguration: DiagramConfiguration,
|
234 | ): void => {
|
235 | appendFileSync(
|
236 | getInputFile(scenarioId),
|
237 | `ALT -[#green]->o MQTT : ${topic}\n`,
|
238 | );
|
239 | const note = `note left #99FF99\n**${currentTimestamp()}**\n${formatPayload(
|
240 | JSON.parse(payload),
|
241 | diagramConfiguration,
|
242 | )}\nend note\n`;
|
243 | appendFileSync(getInputFile(scenarioId), note);
|
244 | };
|
245 |
|
246 | export const addAMQPReceivedMessage = (
|
247 | scenarioId: string,
|
248 | source: string,
|
249 | exchange: string,
|
250 | routingKey: string,
|
251 | payload: unknown,
|
252 | diagramConfiguration: DiagramConfiguration,
|
253 | ): void => {
|
254 | const enquotedSource = enquote(source);
|
255 | appendFileSync(
|
256 | getInputFile(scenarioId),
|
257 | `${enquotedSource} -[#FF6600]->o ALT : ${exchange}/${routingKey}\n`,
|
258 | );
|
259 | const note = `note left #FF6600\n**${currentTimestamp()}**\n${formatPayload(
|
260 | payload,
|
261 | diagramConfiguration,
|
262 | )}\nend note\n`;
|
263 | appendFileSync(getInputFile(scenarioId), note);
|
264 | };
|
265 |
|
266 | export const generateSequenceDiagram = (scenarioId: string): Promise<void> =>
|
267 | new Promise<void>(resolve => {
|
268 | appendFileSync(getInputFile(scenarioId), '\n@enduml');
|
269 | const gen = generate(getInputFile(scenarioId));
|
270 | gen.out.pipe(createWriteStream(getOutputFile(scenarioId)));
|
271 | gen.out.on('end', () => resolve());
|
272 | });
|