Skip to content

Commit af7a343

Browse files
authoredFeb 29, 2024
Merge pull request #184 from neuroglia-io/fix-183-support-dashes-in-names
fix-183: Dash character in names breaks the Mermaid diagram output
2 parents 67c5f99 + affc9bf commit af7a343

File tree

5 files changed

+679
-254
lines changed

5 files changed

+679
-254
lines changed
 

‎package-lock.json

+516-213
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/lib/diagram/mermaidState.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616
import { Specification } from '../definitions';
17-
import { isObject } from '../utils';
17+
import { humanCase, isObject, pascalCase } from '../utils';
1818

1919
export class MermaidState {
2020
constructor(
@@ -65,7 +65,7 @@ export class MermaidState {
6565
}
6666

6767
private stateKeyDiagram(name: string | undefined) {
68-
return name?.replace(/ /g, '_');
68+
return pascalCase(name || '');
6969
}
7070

7171
private startTransition() {
@@ -373,7 +373,9 @@ export class MermaidState {
373373
}
374374

375375
private definitionName() {
376-
return this.stateKeyDiagram(this.state.name) + ' : ' + this.state.name;
376+
const key = this.stateKeyDiagram(this.state.name);
377+
const label = humanCase(key, true);
378+
return key + ' : ' + label;
377379
}
378380

379381
private transitionDescription(

‎src/lib/utils.ts

+91
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,94 @@ export const isObject = (value: any): boolean => {
4949
const type = typeof value;
5050
return type === 'object';
5151
};
52+
53+
/**
54+
* Represents the options used to convert string to pascal case or camel case
55+
*/
56+
export interface CaseConvertionOptions {
57+
/** Keep dashes (-) characters */
58+
keepDashes: boolean;
59+
/** Capitalize after dashes (-) characters, if kept */
60+
capitalizeAfterDashes: boolean;
61+
/** Keep underscores (_) characters */
62+
keepUnderscores: boolean;
63+
/** Capitalize after underscores (_) characters, if kept */
64+
capitalizeAfterUnderscores: boolean;
65+
/** Keep dots (.) characters */
66+
keepDots: boolean;
67+
/** Capitalize after dots (.) characters, if kept */
68+
capitalizeAfterDots: boolean;
69+
}
70+
71+
/**
72+
* Holds default convertion options
73+
*/
74+
export const defaultConvertionOptions = {
75+
keepDashes: false,
76+
capitalizeAfterDashes: false,
77+
keepUnderscores: false,
78+
capitalizeAfterUnderscores: false,
79+
keepDots: true,
80+
capitalizeAfterDots: true,
81+
} as CaseConvertionOptions;
82+
83+
/**
84+
* Converts a string to pascal case (PascalCase)
85+
* @param source string The string to convert to pascal case
86+
* @param convertionOptions CaseConvertionOptions Defaults: keepDashes: false, capitalizeAfterDashes: false, keepUnderscores: false, capitalizeAfterUnderscores: false, keepDots: true, capitalizeAfterDots: true
87+
* @returns string The pascal case string
88+
*/
89+
export const pascalCase = (
90+
source: string,
91+
convertionOptions: CaseConvertionOptions = defaultConvertionOptions
92+
): string => {
93+
if (!source) return '';
94+
let delimiter = '';
95+
if (!convertionOptions.keepDashes) {
96+
source = source.replace(/-+/g, ' ');
97+
} else if (convertionOptions.capitalizeAfterDashes) {
98+
delimiter += '-';
99+
}
100+
if (!convertionOptions.keepUnderscores) {
101+
source = source.replace(/_+/g, ' ');
102+
} else if (convertionOptions.capitalizeAfterUnderscores) {
103+
delimiter += '_';
104+
}
105+
if (!convertionOptions.keepDots) {
106+
source = source.replace(/\.+/g, ' ');
107+
} else if (convertionOptions.capitalizeAfterDots) {
108+
delimiter += '\\.';
109+
}
110+
if (delimiter) {
111+
source = source.replace(
112+
new RegExp('([' + delimiter + '])+(.)(\\w+)', 'g'),
113+
($1, $2, $3, $4) => `${$2}${$3.toUpperCase()}${$4.toLowerCase()}`
114+
);
115+
}
116+
return source
117+
.replace(/\s+(.)(\w+)/g, ($1, $2, $3) => `${$2.toUpperCase()}${$3.toLowerCase()}`)
118+
.replace(/\s/g, '')
119+
.replace(/\w/, (s) => s.toUpperCase());
120+
};
121+
122+
/**
123+
* Converts a PasalCase/camelCase string into a human readable string
124+
* @param source string The string to convert
125+
* @param keepCapitalLetters boolean If capital letters should be kept
126+
* @returns string The converted string
127+
*/
128+
export const humanCase = (source: string, keepCapitalLetters: boolean = false): string => {
129+
if (!source) return '';
130+
let transformable = source.trim();
131+
transformable =
132+
transformable[0].toUpperCase() +
133+
transformable
134+
.slice(1)
135+
.replace(/([A-Z])/g, ' $1')
136+
.replace(/\s+/g, ' ');
137+
if (keepCapitalLetters) {
138+
return transformable;
139+
} else {
140+
return transformable.toLowerCase();
141+
}
142+
};

‎tests/lib/diagram/mermaidDiagram.spec.ts

+21-21
Original file line numberDiff line numberDiff line change
@@ -22,38 +22,38 @@ describe('MermaidDiagram', () => {
2222
const jsonSource = fs.readFileSync('./tests/examples/jobmonitoring.json', 'utf8');
2323
const actual = new MermaidDiagram(Specification.Workflow.fromSource(jsonSource)).sourceCode();
2424
expect(actual).toBe(`stateDiagram-v2
25-
SubmitJob : SubmitJob
25+
SubmitJob : Submit Job
2626
SubmitJob : type = Operation State
2727
SubmitJob : Action mode = sequential
2828
SubmitJob : Num. of actions = 1
2929
[*] --> SubmitJob
3030
SubmitJob --> WaitForCompletion
3131
32-
WaitForCompletion : WaitForCompletion
32+
WaitForCompletion : Wait For Completion
3333
WaitForCompletion : type = Sleep State
3434
WaitForCompletion : Duration = PT5S
3535
WaitForCompletion --> GetJobStatus
3636
37-
GetJobStatus : GetJobStatus
37+
GetJobStatus : Get Job Status
3838
GetJobStatus : type = Operation State
3939
GetJobStatus : Action mode = sequential
4040
GetJobStatus : Num. of actions = 1
4141
GetJobStatus --> DetermineCompletion
4242
43-
DetermineCompletion : DetermineCompletion
43+
DetermineCompletion : Determine Completion
4444
DetermineCompletion : type = Switch State
4545
DetermineCompletion : Condition type = data-based
4646
DetermineCompletion --> JobSucceeded : \${ .jobStatus == "SUCCEEDED" }
4747
DetermineCompletion --> JobFailed : \${ .jobStatus == "FAILED" }
4848
DetermineCompletion --> WaitForCompletion : default
4949
50-
JobSucceeded : JobSucceeded
50+
JobSucceeded : Job Succeeded
5151
JobSucceeded : type = Operation State
5252
JobSucceeded : Action mode = sequential
5353
JobSucceeded : Num. of actions = 1
5454
JobSucceeded --> [*]
5555
56-
JobFailed : JobFailed
56+
JobFailed : Job Failed
5757
JobFailed : type = Operation State
5858
JobFailed : Action mode = sequential
5959
JobFailed : Num. of actions = 1
@@ -65,22 +65,22 @@ JobFailed --> [*]`);
6565
const actual = new MermaidDiagram(Specification.Workflow.fromSource(jsonSource)).sourceCode();
6666

6767
expect(actual).toBe(`stateDiagram-v2
68-
Item_Purchase : Item Purchase
69-
Item_Purchase : type = Event State
70-
[*] --> Item_Purchase
71-
Item_Purchase --> Cancel_Purchase : compensated by
72-
Item_Purchase --> [*]
68+
ItemPurchase : Item Purchase
69+
ItemPurchase : type = Event State
70+
[*] --> ItemPurchase
71+
ItemPurchase --> CancelPurchase : compensated by
72+
ItemPurchase --> [*]
7373
74-
Cancel_Purchase : Cancel Purchase
75-
Cancel_Purchase : type = Operation State
76-
Cancel_Purchase : usedForCompensation
77-
Cancel_Purchase : Action mode = sequential
78-
Cancel_Purchase : Num. of actions = 1
79-
Cancel_Purchase --> Send_confirmation_purchase_cancelled
74+
CancelPurchase : Cancel Purchase
75+
CancelPurchase : type = Operation State
76+
CancelPurchase : usedForCompensation
77+
CancelPurchase : Action mode = sequential
78+
CancelPurchase : Num. of actions = 1
79+
CancelPurchase --> SendConfirmationPurchaseCancelled
8080
81-
Send_confirmation_purchase_cancelled : Send confirmation purchase cancelled
82-
Send_confirmation_purchase_cancelled : type = Operation State
83-
Send_confirmation_purchase_cancelled : Action mode = sequential
84-
Send_confirmation_purchase_cancelled : Num. of actions = 1`);
81+
SendConfirmationPurchaseCancelled : Send Confirmation Purchase Cancelled
82+
SendConfirmationPurchaseCancelled : type = Operation State
83+
SendConfirmationPurchaseCancelled : Action mode = sequential
84+
SendConfirmationPurchaseCancelled : Num. of actions = 1`);
8585
});
8686
});

‎tests/lib/diagram/mermaidState.spec.ts

+46-17
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ describe('mermaidState', () => {
4343
}`)
4444
)
4545
).sourceCode()
46-
).toBe(`ParallelExec : ParallelExec
46+
).toBe(`ParallelExec : Parallel Exec
4747
ParallelExec : type = Parallel State
4848
ParallelExec : Completion type = allOf
4949
ParallelExec : Num. of branches = 2
@@ -72,7 +72,7 @@ ParallelExec --> [*]`);
7272
}`)
7373
);
7474
const mermaidState = new MermaidState(eventbasedswitch);
75-
expect(mermaidState.sourceCode()).toBe(`CheckVisaStatus : CheckVisaStatus
75+
expect(mermaidState.sourceCode()).toBe(`CheckVisaStatus : Check Visa Status
7676
CheckVisaStatus : type = Switch State
7777
CheckVisaStatus : Condition type = event-based
7878
CheckVisaStatus --> HandleApprovedVisa : visaApprovedEvent
@@ -101,7 +101,7 @@ CheckVisaStatus --> HandleNoVisaDecision : default`);
101101
}`)
102102
);
103103
const mermaidState = new MermaidState(databasedswitch);
104-
expect(mermaidState.sourceCode()).toBe(`CheckApplication : CheckApplication
104+
expect(mermaidState.sourceCode()).toBe(`CheckApplication : Check Application
105105
CheckApplication : type = Switch State
106106
CheckApplication : Condition type = data-based
107107
CheckApplication --> StartApplication : \${ .applicants | .age >= 18 }
@@ -130,7 +130,7 @@ CheckApplication --> RejectApplication : default`);
130130
}`)
131131
);
132132
const mermaidState = new MermaidState(databasedswitch);
133-
expect(mermaidState.sourceCode()).toBe(`CheckApplication : CheckApplication
133+
expect(mermaidState.sourceCode()).toBe(`CheckApplication : Check Application
134134
CheckApplication : type = Switch State
135135
CheckApplication : Condition type = data-based
136136
CheckApplication --> StartApplication : \${ .applicants | .age >= 18 }
@@ -160,7 +160,7 @@ CheckApplication --> StartApplication : default`);
160160
}`)
161161
);
162162
const mermaidState = new MermaidState(databasedswitch);
163-
expect(mermaidState.sourceCode()).toBe(`GreetPerson : GreetPerson
163+
expect(mermaidState.sourceCode()).toBe(`GreetPerson : Greet Person
164164
GreetPerson : type = Operation State
165165
GreetPerson : Num. of actions = 1
166166
GreetPerson --> [*]`);
@@ -191,7 +191,7 @@ GreetPerson --> [*]`);
191191
}`)
192192
);
193193
const mermaidState = new MermaidState(states);
194-
expect(mermaidState.sourceCode()).toBe(`SubmitJob : SubmitJob
194+
expect(mermaidState.sourceCode()).toBe(`SubmitJob : Submit Job
195195
SubmitJob : type = Operation State
196196
SubmitJob : Action mode = sequential
197197
SubmitJob : Num. of actions = 1
@@ -208,7 +208,7 @@ SubmitJob --> WaitForCompletion`);
208208
}`)
209209
);
210210
const mermaidState = new MermaidState(states);
211-
expect(mermaidState.sourceCode()).toBe(`WaitForCompletion : WaitForCompletion
211+
expect(mermaidState.sourceCode()).toBe(`WaitForCompletion : Wait For Completion
212212
WaitForCompletion : type = Sleep State
213213
WaitForCompletion : Duration = PT5S
214214
WaitForCompletion --> GetJobStatus`);
@@ -241,7 +241,7 @@ WaitForCompletion --> GetJobStatus`);
241241
}`)
242242
);
243243
const mermaidState = new MermaidState(states, true);
244-
expect(mermaidState.sourceCode()).toBe(`ProvisionOrdersState : ProvisionOrdersState
244+
expect(mermaidState.sourceCode()).toBe(`ProvisionOrdersState : Provision Orders State
245245
ProvisionOrdersState : type = Foreach State
246246
ProvisionOrdersState : Input collection = \${ .orders }
247247
ProvisionOrdersState : Num. of actions = 1
@@ -270,7 +270,7 @@ ProvisionOrdersState --> [*] : Produced event = [provisioningCompleteEvent]`);
270270
}`)
271271
);
272272
const mermaidState = new MermaidState(states, true);
273-
expect(mermaidState.sourceCode()).toBe(`CheckCredit : CheckCredit
273+
expect(mermaidState.sourceCode()).toBe(`CheckCredit : Check Credit
274274
CheckCredit : type = Callback State
275275
CheckCredit : Callback function = callCreditCheckMicroservice
276276
CheckCredit : Callback event = CreditCheckCompletedEvent
@@ -287,12 +287,12 @@ CheckCredit --> EvaluateDecision`);
287287
}`)
288288
);
289289
const mermaidState = new MermaidState(states);
290-
expect(mermaidState.sourceCode()).toBe(`CheckCredit : CheckCredit
290+
expect(mermaidState.sourceCode()).toBe(`CheckCredit : Check Credit
291291
CheckCredit : type = Callback State
292292
CheckCredit --> EvaluateDecision`);
293293
});
294294

295-
it(`should convert white spaces with underscore to create the state key`, () => {
295+
it(`should remove white spaces when creating the state key`, () => {
296296
const databasedswitch = new Specification.Databasedswitchstate(
297297
JSON.parse(`{
298298
"type":"switch",
@@ -313,11 +313,40 @@ CheckCredit --> EvaluateDecision`);
313313
}`)
314314
);
315315
const mermaidState = new MermaidState(databasedswitch);
316-
expect(mermaidState.sourceCode()).toBe(`Check_Application : Check Application
317-
Check_Application : type = Switch State
318-
Check_Application : Condition type = data-based
319-
Check_Application --> Start_Application : \${ .applicants | .age >= 18 }
320-
Check_Application --> [*] : \${ .applicants | .age < 18 }
321-
Check_Application --> Start_Application : default`);
316+
expect(mermaidState.sourceCode()).toBe(`CheckApplication : Check Application
317+
CheckApplication : type = Switch State
318+
CheckApplication : Condition type = data-based
319+
CheckApplication --> StartApplication : \${ .applicants | .age >= 18 }
320+
CheckApplication --> [*] : \${ .applicants | .age < 18 }
321+
CheckApplication --> StartApplication : default`);
322+
});
323+
324+
it(`should remove dashes when creating the state key`, () => {
325+
const databasedswitch = new Specification.Databasedswitchstate(
326+
JSON.parse(`{
327+
"type":"switch",
328+
"name":"check-application",
329+
"dataConditions": [
330+
{
331+
"condition": "\${ .applicants | .age >= 18 }",
332+
"transition": "start-application"
333+
},
334+
{
335+
"condition": "\${ .applicants | .age < 18 }",
336+
"end": true
337+
}
338+
],
339+
"defaultCondition": {
340+
"transition": "start-application"
341+
}
342+
}`)
343+
);
344+
const mermaidState = new MermaidState(databasedswitch);
345+
expect(mermaidState.sourceCode()).toBe(`CheckApplication : Check Application
346+
CheckApplication : type = Switch State
347+
CheckApplication : Condition type = data-based
348+
CheckApplication --> StartApplication : \${ .applicants | .age >= 18 }
349+
CheckApplication --> [*] : \${ .applicants | .age < 18 }
350+
CheckApplication --> StartApplication : default`);
322351
});
323352
});

0 commit comments

Comments
 (0)
Please sign in to comment.