Skip to content

Commit a5faffb

Browse files
committed
feat(export): enrich littlehorse scaffold
1 parent de684aa commit a5faffb

File tree

2 files changed

+194
-17
lines changed

2 files changed

+194
-17
lines changed

src/export-littlehorse.js

Lines changed: 193 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,29 @@ function toLittleHorseSkeleton(model) {
1313

1414
processes.forEach((processNode) => {
1515
const className = sanitizeClassName(processNode.name);
16-
lines.push(`public class ${className}Workflow {`);
16+
const workflowName = toKebabName(processNode.name);
17+
lines.push(`public final class ${className}Workflow {`);
1718
lines.push(" // TODO: adapt this skeleton to your LittleHorse SDK version.");
18-
lines.push(" public void buildWorkflow() {");
19-
lines.push(` // Process: ${processNode.name}`);
20-
lines.push(" // TODO: declare variables and task definitions");
19+
lines.push(" public static Workflow getWorkflow() {");
20+
lines.push(` return new WorkflowImpl("${workflowName}", wf -> {`);
21+
lines.push(` // Process: ${processNode.name}`);
22+
const declarations = renderDeclarations(processNode.body || [], 6);
23+
if (declarations.length > 0) {
24+
lines.push(" // TODO: declare variables and task definitions");
25+
lines.push(...declarations);
26+
lines.push("");
27+
} else {
28+
lines.push(" // TODO: declare variables and task definitions");
29+
}
2130

22-
const bodyLines = renderStatements(processNode.body || [], 4);
31+
const bodyLines = renderStatements(processNode.body || [], 6);
2332
if (bodyLines.length > 0) {
2433
lines.push(...bodyLines);
2534
} else {
26-
lines.push(" // TODO: add workflow steps");
35+
lines.push(" // TODO: add workflow steps");
2736
}
2837

38+
lines.push(" });");
2939
lines.push(" }");
3040
lines.push("}");
3141
lines.push("");
@@ -34,6 +44,134 @@ function toLittleHorseSkeleton(model) {
3444
return `${lines.join("\n").trimEnd()}\n`;
3545
}
3646

47+
function renderDeclarations(statements, indentSize) {
48+
const indent = " ".repeat(indentSize);
49+
const entries = collectFieldUsage(statements);
50+
const lines = [];
51+
52+
for (const entry of entries) {
53+
const decl = renderDeclaration(entry);
54+
if (decl) {
55+
lines.push(`${indent}${decl}`);
56+
}
57+
}
58+
59+
return lines;
60+
}
61+
62+
function collectFieldUsage(statements, usage = new Map()) {
63+
for (const statement of statements) {
64+
if (statement.type === "when") {
65+
continue;
66+
}
67+
68+
if (statement.type === "if") {
69+
collectConditionUsage(statement.condition, usage);
70+
collectFieldUsage(statement.then || [], usage);
71+
for (const branch of statement.elseIf || []) {
72+
collectConditionUsage(branch.condition, usage);
73+
collectFieldUsage(branch.then || [], usage);
74+
}
75+
if (statement.else && Array.isArray(statement.else.body)) {
76+
collectFieldUsage(statement.else.body, usage);
77+
}
78+
continue;
79+
}
80+
81+
if (statement.type === "assign" || statement.type === "transition" || statement.type === "update") {
82+
recordField(usage, statement.target, statement.value);
83+
continue;
84+
}
85+
86+
if (statement.type === "notify") {
87+
continue;
88+
}
89+
90+
if (statement.type === "create") {
91+
continue;
92+
}
93+
94+
if (statement.type === "require") {
95+
continue;
96+
}
97+
}
98+
99+
return Array.from(usage.values());
100+
}
101+
102+
function collectConditionUsage(condition, usage) {
103+
if (!condition) {
104+
return;
105+
}
106+
107+
if (condition.type === "logical") {
108+
for (const child of condition.conditions || []) {
109+
collectConditionUsage(child, usage);
110+
}
111+
return;
112+
}
113+
114+
if (condition.left && condition.left.type === "field") {
115+
recordField(usage, condition.left.path, condition.right);
116+
}
117+
}
118+
119+
function recordField(usage, path, value) {
120+
if (!path) {
121+
return;
122+
}
123+
const key = String(path);
124+
const existing = usage.get(key);
125+
const inferred = inferType(value);
126+
if (!existing) {
127+
usage.set(key, {
128+
path: key,
129+
type: inferred,
130+
});
131+
return;
132+
}
133+
existing.type = mergeTypes(existing.type, inferred);
134+
}
135+
136+
function inferType(value) {
137+
if (!value) {
138+
return "string";
139+
}
140+
if (value.type === "number") {
141+
return "number";
142+
}
143+
if (value.type === "boolean") {
144+
return "boolean";
145+
}
146+
if (value.type === "string") {
147+
return "string";
148+
}
149+
return "string";
150+
}
151+
152+
function mergeTypes(existing, incoming) {
153+
if (existing === incoming) {
154+
return existing;
155+
}
156+
if (existing === "number" || incoming === "number") {
157+
return "number";
158+
}
159+
if (existing === "boolean" || incoming === "boolean") {
160+
return "boolean";
161+
}
162+
return "string";
163+
}
164+
165+
function renderDeclaration(entry) {
166+
const variableName = toVariableName(entry.path);
167+
if (!variableName) {
168+
return null;
169+
}
170+
171+
const typeSuffix = entry.type === "number" ? "Double" : entry.type === "boolean" ? "Boolean" : "Str";
172+
return `var ${variableName} = wf.declare${typeSuffix}("${variableName}");`;
173+
}
174+
37175
function renderStatements(statements, indentSize) {
38176
const lines = [];
39177
const indent = " ".repeat(indentSize);
@@ -45,16 +183,17 @@ function renderStatements(statements, indentSize) {
45183
}
46184

47185
if (statement.type === "if") {
48-
lines.push(`${indent}// if ${formatCondition(statement.condition)}:`);
186+
lines.push(`${indent}wf.doIf(/* ${formatCondition(statement.condition)} */, ifBody -> {`);
49187
lines.push(...renderStatements(statement.then || [], indentSize + 2));
188+
lines.push(`${indent}});`);
50189

51190
for (const branch of statement.elseIf || []) {
52-
lines.push(`${indent}// else if ${formatCondition(branch.condition)}:`);
191+
lines.push(`${indent}// else if ${formatCondition(branch.condition)} (map to doElseIf)`);
53192
lines.push(...renderStatements(branch.then || [], indentSize + 2));
54193
}
55194

56195
if (statement.else && (statement.else.body || []).length > 0) {
57-
lines.push(`${indent}// else:`);
196+
lines.push(`${indent}// else (map to doElse)`);
58197
lines.push(...renderStatements(statement.else.body || [], indentSize + 2));
59198
}
60199
continue;
@@ -65,38 +204,48 @@ function renderStatements(statements, indentSize) {
65204
continue;
66205
}
67206

68-
lines.push(`${indent}// ${formatAction(statement)}`);
207+
const action = formatAction(statement);
208+
if (action) {
209+
lines.push(`${indent}${action}`);
210+
continue;
211+
}
212+
213+
lines.push(`${indent}// ${statement.type}`);
69214
}
70215

71216
return lines;
72217
}
73218

74219
function formatAction(statement) {
75220
if (statement.type === "assign") {
76-
return `assign ${statement.target || "?"} = ${formatExpression(statement.value)}`;
221+
return `// assign ${statement.target || "?"} = ${formatExpression(statement.value)}`;
77222
}
78223

79224
if (statement.type === "transition") {
80-
return `transition ${statement.target || "?"} -> ${formatExpression(statement.value)}`;
225+
return `// transition ${statement.target || "?"} -> ${formatExpression(statement.value)}`;
81226
}
82227

83228
if (statement.type === "notify") {
84-
return `notify ${statement.target} "${statement.message}"`;
229+
const target = statement.target || "target";
230+
const message = statement.message ? `"${statement.message}"` : "\"message\"";
231+
return `wf.execute("notify", "${target}", ${message});`;
85232
}
86233

87234
if (statement.type === "create") {
88-
return `create ${statement.entity}`;
235+
const entity = statement.entity || "entity";
236+
return `wf.execute("create", "${entity}");`;
89237
}
90238

91239
if (statement.type === "update") {
92-
return `update ${statement.target || "?"} = ${formatExpression(statement.value)}`;
240+
return `wf.execute("update", "${statement.target || "target"}", ${formatExpression(statement.value)});`;
93241
}
94242

95243
if (statement.type === "require") {
96-
return `require ${statement.requirement}`;
244+
const requirement = statement.requirement || "requirement";
245+
return `wf.execute("require", "${requirement}");`;
97246
}
98247

99-
return statement.type;
248+
return null;
100249
}
101250

102251
function formatCondition(condition) {
@@ -146,6 +295,33 @@ function sanitizeClassName(value) {
146295
return safe.charAt(0).toUpperCase() + safe.slice(1);
147296
}
148297

298+
function toKebabName(value) {
299+
const raw = String(value || "workflow").trim();
300+
if (!raw) {
301+
return "workflow";
302+
}
303+
const spaced = raw.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
304+
return spaced
305+
.replace(/[^A-Za-z0-9]+/g, "-")
306+
.replace(/^-+|-+$/g, "")
307+
.toLowerCase();
308+
}
309+
310+
function toVariableName(path) {
311+
const value = String(path || "").trim();
312+
if (!value) {
313+
return null;
314+
}
315+
const sanitized = value.replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
316+
if (!sanitized) {
317+
return null;
318+
}
319+
if (/^[A-Za-z_]/.test(sanitized)) {
320+
return sanitized;
321+
}
322+
return `var_${sanitized}`;
323+
}
324+
149325
module.exports = {
150326
toLittleHorseSkeleton,
151327
};

tests/run.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -995,6 +995,7 @@ function testSkeletonExporters() {
995995

996996
const littleHorse = toLittleHorseSkeleton(model);
997997
assert.ok(littleHorse.includes("OrderApprovalWorkflow"), "Expected LittleHorse class name");
998+
assert.ok(littleHorse.includes("WorkflowImpl(\"order-approval\""), "Expected workflow name");
998999
assert.ok(littleHorse.includes("when order.submitted"), "Expected trigger comment");
9991000

10001001
const graph = JSON.parse(toGraphJson(model));

0 commit comments

Comments
 (0)