Skip to content

Commit 6afde3c

Browse files
authored
Merge pull request #266 from hjotha/submit/microflow-describe-rule-split-and-free-annotations
fix: preserve rule split describer output and free annotations
2 parents 907184b + 27c0dc9 commit 6afde3c

5 files changed

Lines changed: 83 additions & 12 deletions

File tree

mdl/executor/cmd_microflows_builder_control.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,10 @@ func (fb *flowBuilder) addIfStatement(s *ast.IfStmt) model.ID {
127127
}
128128
}
129129

130-
// Connect THEN body to merge only if it doesn't end with RETURN
131-
if !thenReturns {
130+
// Connect THEN body to merge only if it doesn't end with RETURN and a merge exists.
131+
// When needMerge=false, the continuing branch is wired up by the parent via
132+
// nextConnectionPoint/nextFlowCase, so we must not emit a dangling flow here.
133+
if !thenReturns && needMerge {
132134
if lastThenID != "" {
133135
fb.flows = append(fb.flows, newHorizontalFlow(lastThenID, mergeID))
134136
} else {
@@ -163,8 +165,10 @@ func (fb *flowBuilder) addIfStatement(s *ast.IfStmt) model.ID {
163165
}
164166
}
165167

166-
// Connect ELSE body to merge only if it doesn't end with RETURN
167-
if !elseReturns {
168+
// Connect ELSE body to merge only if it doesn't end with RETURN and a merge exists.
169+
// When needMerge=false, the continuing branch is handled by the parent; emitting
170+
// a flow with an empty mergeID would create an orphan SequenceFlow.
171+
if !elseReturns && needMerge {
168172
if lastElseID != "" {
169173
fb.flows = append(fb.flows, newUpwardFlow(lastElseID, mergeID))
170174
}
@@ -206,8 +210,10 @@ func (fb *flowBuilder) addIfStatement(s *ast.IfStmt) model.ID {
206210
}
207211
}
208212

209-
// Connect THEN body to merge only if it doesn't end with RETURN
210-
if !thenReturns {
213+
// Connect THEN body to merge only if it doesn't end with RETURN and a merge exists.
214+
// With no ELSE + thenReturns, needMerge=false and the FALSE path is threaded through
215+
// the parent — any flow emitted here would dangle with mergeID="".
216+
if !thenReturns && needMerge {
211217
if lastThenID != "" {
212218
fb.flows = append(fb.flows, newUpwardFlow(lastThenID, mergeID))
213219
} else {

mdl/executor/cmd_microflows_format_action.go

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,7 @@ func formatActivity(
3939
return formatAction(ctx, activity.Action, entityNames, microflowNames)
4040

4141
case *microflows.ExclusiveSplit:
42-
condition := "true"
43-
if activity.SplitCondition != nil {
44-
if exprCond, ok := activity.SplitCondition.(*microflows.ExpressionSplitCondition); ok {
45-
condition = exprCond.Expression
46-
}
47-
}
42+
condition := formatSplitCondition(activity.SplitCondition)
4843
return fmt.Sprintf("if %s then", condition)
4944

5045
case *microflows.ExclusiveMerge:
@@ -1159,3 +1154,42 @@ func (e *Executor) formatListOperation(op microflows.ListOperation, outputVar st
11591154
func (e *Executor) formatRestCallAction(a *microflows.RestCallAction) string {
11601155
return formatRestCallAction(e.newExecContext(context.Background()), a)
11611156
}
1157+
1158+
// formatSplitCondition renders an ExclusiveSplit's condition as an MDL expression.
1159+
// ExpressionSplitCondition is emitted verbatim. RuleSplitCondition is rendered as
1160+
// a rule call expression using the rule's qualified name — Mendix expressions
1161+
// allow calling a rule the same way as a microflow, so this is re-parseable.
1162+
// Unknown or nil conditions fall back to "true" so the describer still produces
1163+
// valid MDL; callers should rely on the original Caption (emitted via @caption)
1164+
// to preserve human-readable intent.
1165+
func formatSplitCondition(cond microflows.SplitCondition) string {
1166+
switch c := cond.(type) {
1167+
case *microflows.ExpressionSplitCondition:
1168+
expr := strings.TrimRight(c.Expression, " \t\n\r")
1169+
if expr == "" {
1170+
return "true"
1171+
}
1172+
return expr
1173+
case *microflows.RuleSplitCondition:
1174+
name := c.RuleQualifiedName
1175+
if name == "" {
1176+
return "true"
1177+
}
1178+
args := make([]string, 0, len(c.ParameterMappings))
1179+
for _, pm := range c.ParameterMappings {
1180+
paramName := pm.ParameterName
1181+
if idx := strings.LastIndex(paramName, "."); idx >= 0 {
1182+
paramName = paramName[idx+1:]
1183+
}
1184+
arg := strings.TrimRight(pm.Argument, " \t\n\r")
1185+
if paramName != "" {
1186+
args = append(args, fmt.Sprintf("%s = %s", paramName, arg))
1187+
} else {
1188+
args = append(args, arg)
1189+
}
1190+
}
1191+
return fmt.Sprintf("%s(%s)", name, strings.Join(args, ", "))
1192+
default:
1193+
return "true"
1194+
}
1195+
}

mdl/executor/cmd_microflows_show.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,15 @@ func describeMicroflow(ctx *ExecContext, name ast.QualifiedName) error {
276276
// Generate activities
277277
if targetMf.ObjectCollection != nil && len(targetMf.ObjectCollection.Objects) > 0 {
278278
activityLines := formatMicroflowActivities(ctx, targetMf, entityNames, microflowNames)
279+
freeAnnots := collectFreeAnnotations(targetMf.ObjectCollection)
280+
if len(freeAnnots) > 0 && len(activityLines) > 0 {
281+
prefix := make([]string, 0, len(freeAnnots))
282+
for _, text := range freeAnnots {
283+
escaped := strings.ReplaceAll(text, "'", "''")
284+
prefix = append(prefix, fmt.Sprintf("@annotation '%s'", escaped))
285+
}
286+
activityLines = append(prefix, activityLines...)
287+
}
279288
for _, line := range activityLines {
280289
lines = append(lines, " "+line)
281290
}
@@ -533,6 +542,15 @@ func renderMicroflowMDL(
533542
} else {
534543
activityLines = formatMicroflowActivities(ctx, mf, entityNames, microflowNames)
535544
}
545+
freeAnnots := collectFreeAnnotations(mf.ObjectCollection)
546+
if len(freeAnnots) > 0 && len(activityLines) > 0 {
547+
prefix := make([]string, 0, len(freeAnnots))
548+
for _, text := range freeAnnots {
549+
escaped := strings.ReplaceAll(text, "'", "''")
550+
prefix = append(prefix, fmt.Sprintf("@annotation '%s'", escaped))
551+
}
552+
activityLines = append(prefix, activityLines...)
553+
}
536554
for _, line := range activityLines {
537555
lines = append(lines, " "+line)
538556
}

mdl/executor/cmd_microflows_show_helpers.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,15 @@ func emitObjectAnnotations(obj microflows.MicroflowObject, lines *[]string, inde
8484
}
8585
}
8686

87+
if split, ok := obj.(*microflows.ExclusiveSplit); ok && split.Caption != "" {
88+
escapedCaption := strings.ReplaceAll(split.Caption, "'", "''")
89+
*lines = append(*lines, indentStr+fmt.Sprintf("@caption '%s'", escapedCaption))
90+
}
91+
if split, ok := obj.(*microflows.InheritanceSplit); ok && split.Caption != "" {
92+
escapedCaption := strings.ReplaceAll(split.Caption, "'", "''")
93+
*lines = append(*lines, indentStr+fmt.Sprintf("@caption '%s'", escapedCaption))
94+
}
95+
8796
// @annotation (attached Annotation objects)
8897
if annotationsByTarget != nil {
8998
for _, caption := range annotationsByTarget[currentID] {

sdk/mpr/writer_core.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ import (
1515
// idToBsonBinary converts a UUID string to BSON Binary format.
1616
// For invalid or empty UUIDs (e.g. test placeholders), generates a random ID
1717
// to maintain backward compatibility with existing serialization paths.
18+
//
19+
// WARNING: an empty id here is almost always a bug (e.g. an unset pointer on a
20+
// SequenceFlow) and produces a random UUID that references nothing — which
21+
// Studio Pro surfaces as "KeyNotFoundException". Fix callers to pass a real ID.
1822
func idToBsonBinary(id string) primitive.Binary {
1923
blob := types.UUIDToBlob(id)
2024
if blob == nil || len(blob) != 16 {

0 commit comments

Comments
 (0)