Skip to content

Commit a5af42d

Browse files
authored
Merge pull request #460 from hjotha/submit/merged-pr-review-followups-2
fix: address merged microflow review follow-ups
2 parents f77980e + 5339db6 commit a5af42d

6 files changed

Lines changed: 167 additions & 8 deletions

File tree

mdl-examples/doctype-tests/02-microflow-examples.mdl

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,28 @@ begin
671671
end;
672672
/
673673

674+
/**
675+
* Example 6.1b: LOOP with custom caption
676+
*
677+
* Demonstrates activity metadata on a loop.
678+
*/
679+
create microflow MfTest.M021_2_LoopWithCaption (
680+
$ProductList: list of MfTest.Product
681+
)
682+
returns integer as $count
683+
begin
684+
declare $count integer = 0;
685+
686+
@caption 'Process products'
687+
loop $Product in $ProductList
688+
begin
689+
set $count = $count + 1;
690+
end loop;
691+
692+
return $count;
693+
end;
694+
/
695+
674696
/**
675697
* Example 6.2: LOOP with object modification
676698
*

mdl/executor/cmd_microflows_builder_actions.go

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -272,9 +272,8 @@ func (fb *flowBuilder) addChangeObjectAction(s *ast.ChangeObjectStmt) model.ID {
272272
}
273273

274274
func (fb *flowBuilder) addEnumSplit(s *ast.EnumSplitStmt) model.ID {
275-
if len(s.Cases) > len(splitCaseOrderAnchors) {
276-
fb.addError("case statement on '$%s' has %d cases; maximum supported is %d",
277-
s.Variable, len(s.Cases), len(splitCaseOrderAnchors))
275+
if count := enumSplitBranchCount(s); count > maxEnumSplitBranches {
276+
fb.addError("enum split has %d branches; at most %d branches are supported", count, maxEnumSplitBranches)
278277
return ""
279278
}
280279

@@ -346,7 +345,9 @@ func (fb *flowBuilder) addEnumSplit(s *ast.EnumSplitStmt) model.ID {
346345

347346
lastID := model.ID("")
348347
pendingCase := ""
348+
var prevAnchor *ast.FlowAnchors
349349
for _, stmt := range br.body {
350+
thisAnchor := stmtOwnAnchor(stmt)
350351
actID := fb.addStatement(stmt)
351352
if actID == "" {
352353
continue
@@ -357,14 +358,26 @@ func (fb *flowBuilder) addEnumSplit(s *ast.EnumSplitStmt) model.ID {
357358
}
358359
if lastID == "" {
359360
fb.addGroupedEnumSplitFlows(splitID, actID, br.values, i, splitX+SplitWidth+HorizontalSpacing/4, branchY)
361+
// The first statement in a case can carry @anchor(from:…,
362+
// to:…) that should apply to the split→firstActivity flow.
363+
// addGroupedEnumSplitFlows appends one flow per case value;
364+
// anchor the last one so `@anchor(to: top)` etc. round-trips
365+
// through describe → exec without silently dropping.
366+
if thisAnchor != nil && len(fb.flows) > 0 {
367+
applyUserAnchors(fb.flows[len(fb.flows)-1], nil, thisAnchor)
368+
}
360369
} else {
370+
var flow *microflows.SequenceFlow
361371
if pendingCase != "" {
362-
fb.flows = append(fb.flows, newHorizontalFlowWithCase(lastID, actID, pendingCase))
372+
flow = newHorizontalFlowWithCase(lastID, actID, pendingCase)
363373
pendingCase = ""
364374
} else {
365-
fb.flows = append(fb.flows, newHorizontalFlow(lastID, actID))
375+
flow = newHorizontalFlow(lastID, actID)
366376
}
377+
applyUserAnchors(flow, prevAnchor, thisAnchor)
378+
fb.flows = append(fb.flows, flow)
367379
}
380+
prevAnchor = thisAnchor
368381
if fb.nextConnectionPoint != "" {
369382
lastID = fb.nextConnectionPoint
370383
fb.nextConnectionPoint = ""
@@ -456,6 +469,8 @@ var splitCaseOrderAnchors = []splitCaseOrderAnchor{
456469
{AnchorLeft, AnchorBottom},
457470
}
458471

472+
var maxEnumSplitBranches = len(splitCaseOrderAnchors)
473+
459474
func applySplitCaseOrder(flow *microflows.SequenceFlow, order int) {
460475
if flow == nil || order < 0 || order >= len(splitCaseOrderAnchors) {
461476
return
@@ -475,6 +490,17 @@ func enumSplitCaseValues(c ast.EnumSplitCase) []string {
475490
return nil
476491
}
477492

493+
func enumSplitBranchCount(s *ast.EnumSplitStmt) int {
494+
if s == nil {
495+
return 0
496+
}
497+
count := len(s.Cases)
498+
if len(s.ElseBody) > 0 {
499+
count++
500+
}
501+
return count
502+
}
503+
478504
func appendEnumBodies(s *ast.EnumSplitStmt) []ast.MicroflowStatement {
479505
var stmts []ast.MicroflowStatement
480506
for _, c := range s.Cases {

mdl/executor/cmd_microflows_builder_enum_split_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
package executor
44

55
import (
6+
"fmt"
7+
"strings"
68
"testing"
79

810
"github.com/mendixlabs/mxcli/mdl/ast"
@@ -186,6 +188,22 @@ func TestEnumSplitAllCasesReturnWithoutElseDoesNotCreateFallthrough(t *testing.T
186188
}
187189
}
188190

191+
func TestEnumSplitBuilderRejectsMoreThanSupportedBranches(t *testing.T) {
192+
fb := &flowBuilder{
193+
spacing: HorizontalSpacing,
194+
measurer: &layoutMeasurer{},
195+
}
196+
197+
if id := fb.addEnumSplit(enumSplitWithBranchCount(maxEnumSplitBranches + 1)); id != "" {
198+
t.Fatalf("unsupported enum split returned split ID %q", id)
199+
}
200+
201+
errors := strings.Join(fb.GetErrors(), "\n")
202+
if !strings.Contains(errors, "enum split has 17 branches; at most 16 branches are supported") {
203+
t.Fatalf("expected unsupported branch count error, got %q", errors)
204+
}
205+
}
206+
189207
func objectByID(objects []microflows.MicroflowObject, id model.ID) microflows.MicroflowObject {
190208
for _, obj := range objects {
191209
if obj.GetID() == id {
@@ -211,3 +229,79 @@ func logActivityHasMessage(obj microflows.MicroflowObject, message string) bool
211229
}
212230
return false
213231
}
232+
233+
func enumSplitWithBranchCount(count int) *ast.EnumSplitStmt {
234+
cases := make([]ast.EnumSplitCase, 0, count)
235+
for i := 0; i < count; i++ {
236+
cases = append(cases, ast.EnumSplitCase{
237+
Value: fmt.Sprintf("Value%d", i+1),
238+
Body: []ast.MicroflowStatement{
239+
&ast.LogStmt{Level: ast.LogInfo, Message: &ast.LiteralExpr{Kind: ast.LiteralString, Value: "branch"}},
240+
},
241+
})
242+
}
243+
return &ast.EnumSplitStmt{
244+
Variable: "SyntheticStatus",
245+
Cases: cases,
246+
}
247+
}
248+
249+
// TestEnumSplitBuilderPreservesFirstStatementAnchor guards against silent
250+
// loss of @anchor(from:..., to:...) on the first statement inside an enum
251+
// split case. Before the fix the enum split builder never read
252+
// stmtOwnAnchor(stmt) for case bodies, so any round-tripped anchor dropped
253+
// on re-exec — describe → exec → describe lost the FlowAnchor entirely.
254+
func TestEnumSplitBuilderPreservesFirstStatementAnchor(t *testing.T) {
255+
fb := &flowBuilder{
256+
spacing: HorizontalSpacing,
257+
measurer: &layoutMeasurer{},
258+
}
259+
260+
// @anchor(to: bottom) on the first case statement — bottom is a
261+
// non-default destination anchor (AnchorSideBottom == 2) so we can
262+
// distinguish it from the layout default.
263+
anchor := &ast.FlowAnchors{
264+
From: ast.AnchorSideUnset,
265+
To: ast.AnchorSideBottom,
266+
}
267+
fb.addEnumSplit(&ast.EnumSplitStmt{
268+
Variable: "Status",
269+
Cases: []ast.EnumSplitCase{
270+
{
271+
Values: []string{"Open"},
272+
Body: []ast.MicroflowStatement{
273+
&ast.LogStmt{
274+
Level: ast.LogInfo,
275+
Message: &ast.LiteralExpr{Kind: ast.LiteralString, Value: "open"},
276+
Annotations: &ast.ActivityAnnotations{Anchor: anchor},
277+
},
278+
},
279+
},
280+
},
281+
})
282+
283+
var split *microflows.ExclusiveSplit
284+
for _, obj := range fb.objects {
285+
if s, ok := obj.(*microflows.ExclusiveSplit); ok {
286+
split = s
287+
break
288+
}
289+
}
290+
if split == nil {
291+
t.Fatal("expected ExclusiveSplit")
292+
}
293+
294+
var firstCaseFlow *microflows.SequenceFlow
295+
for _, f := range fb.flows {
296+
if f.OriginID == split.ID {
297+
firstCaseFlow = f
298+
}
299+
}
300+
if firstCaseFlow == nil {
301+
t.Fatal("expected split→case flow")
302+
}
303+
if firstCaseFlow.DestinationConnectionIndex != int(ast.AnchorSideBottom) {
304+
t.Errorf("DestinationConnectionIndex = %d, want %d — @anchor(to: bottom) was dropped",
305+
firstCaseFlow.DestinationConnectionIndex, int(ast.AnchorSideBottom))
306+
}
307+
}

mdl/executor/cmd_microflows_builder_validate.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ func (fb *flowBuilder) validateStatement(stmt ast.MicroflowStatement) {
101101
}
102102

103103
case *ast.EnumSplitStmt:
104+
if count := enumSplitBranchCount(s); count > maxEnumSplitBranches {
105+
fb.addError("enum split has %d branches; at most %d branches are supported", count, maxEnumSplitBranches)
106+
}
104107
for _, c := range s.Cases {
105108
fb.validateStatements(c.Body)
106109
}

mdl/executor/validate_microflow_enum_split_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package executor
44

55
import (
6+
"strings"
67
"testing"
78

89
"github.com/mendixlabs/mxcli/mdl/ast"
@@ -115,3 +116,17 @@ func TestValidateMicroflow_EnumSplitBranchScopedVariable(t *testing.T) {
115116
}
116117
t.Fatalf("expected MDL005 for variable declared inside ENUM split branch, got %#v", violations)
117118
}
119+
120+
func TestValidateMicroflowBody_EnumSplitRejectsMoreThanSupportedBranches(t *testing.T) {
121+
stmt := &ast.CreateMicroflowStmt{
122+
Name: ast.QualifiedName{Module: "Sample", Name: "Route"},
123+
Body: []ast.MicroflowStatement{
124+
enumSplitWithBranchCount(maxEnumSplitBranches + 1),
125+
},
126+
}
127+
128+
errors := strings.Join(ValidateMicroflowBody(stmt), "\n")
129+
if !strings.Contains(errors, "enum split has 17 branches; at most 16 branches are supported") {
130+
t.Fatalf("expected unsupported branch count error, got %q", errors)
131+
}
132+
}

mdl/grammar/MDLParser.g4

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1512,9 +1512,8 @@ callJavaScriptActionStatement
15121512
;
15131513

15141514
// Legacy SOAP call. The preferred structured form stores service and mapping
1515-
// references in STRING_LITERAL tokens rather than qualifiedName tokens because
1516-
// Structured references prefer qualifiedName tokens. STRING_LITERAL remains as
1517-
// a fallback for dangling raw IDs that cannot be represented as identifiers.
1515+
// references as qualified names. STRING_LITERAL remains as a fallback for
1516+
// dangling raw IDs that cannot be represented as identifiers.
15181517
// Raw BSON remains the escape hatch for unsupported SOAP payload details.
15191518
callWebServiceStatement
15201519
: (VARIABLE EQUALS)? CALL WEB SERVICE

0 commit comments

Comments
 (0)