Skip to content

Commit 90ce9c8

Browse files
committed
merge: submit/microflow-roundtrip-log-node (PR mendixlabs#264)
# Conflicts: # mdl/grammar/parser/MDLParser.interp
2 parents 8c96113 + 497867f commit 90ce9c8

22 files changed

Lines changed: 533 additions & 82 deletions

mdl/ast/ast_microflow.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,10 +261,10 @@ func (p *TemplateParam) IsDataSourceRef() bool {
261261
return p.DataSourceName != ""
262262
}
263263

264-
// LogStmt represents: LOG LEVEL NODE 'node' 'message' [WITH params]
264+
// LogStmt represents: LOG LEVEL [NODE expr] message [WITH params]
265265
type LogStmt struct {
266266
Level LogLevel // Log level (INFO, WARNING, etc.)
267-
Node string // Log node name
267+
Node Expression // Optional log node expression
268268
Message Expression // Message expression
269269
Template []TemplateParam // Optional WITH template params
270270
Annotations *ActivityAnnotations // Optional @position, @caption, @color, @annotation

mdl/executor/bugfix_regression_test.go

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,157 @@ func TestFormatActivity_WhileLoop(t *testing.T) {
6565
}
6666
}
6767

68+
func TestAddLoopStatement_PreservesAnnotatedPosition(t *testing.T) {
69+
fb := &flowBuilder{
70+
posX: 350,
71+
posY: 200,
72+
spacing: HorizontalSpacing,
73+
varTypes: map[string]string{"Items": "List of Test.Item"},
74+
declaredVars: map[string]string{},
75+
measurer: &layoutMeasurer{varTypes: map[string]string{"Items": "List of Test.Item"}},
76+
}
77+
78+
stmt := &ast.LoopStmt{
79+
LoopVariable: "Item",
80+
ListVariable: "Items",
81+
Annotations: &ast.ActivityAnnotations{
82+
Position: &ast.Position{X: 350, Y: 200},
83+
},
84+
}
85+
86+
id := fb.addLoopStatement(stmt)
87+
if id == "" {
88+
t.Fatal("expected loop activity ID")
89+
}
90+
91+
loop, ok := fb.objects[len(fb.objects)-1].(*microflows.LoopedActivity)
92+
if !ok {
93+
t.Fatalf("expected LoopedActivity, got %T", fb.objects[len(fb.objects)-1])
94+
}
95+
if loop.Position.X != 350 || loop.Position.Y != 200 {
96+
t.Fatalf("got loop position (%d, %d), want (350, 200)", loop.Position.X, loop.Position.Y)
97+
}
98+
wantNextX := 350 + loop.Size.Width/2 + HorizontalSpacing
99+
if fb.posX != wantNextX {
100+
t.Fatalf("got next posX %d, want %d", fb.posX, wantNextX)
101+
}
102+
}
103+
104+
func TestAddWhileStatement_PreservesAnnotatedPosition(t *testing.T) {
105+
fb := &flowBuilder{
106+
posX: 420,
107+
posY: 180,
108+
spacing: HorizontalSpacing,
109+
varTypes: map[string]string{},
110+
declaredVars: map[string]string{},
111+
measurer: &layoutMeasurer{varTypes: map[string]string{}},
112+
}
113+
114+
stmt := &ast.WhileStmt{
115+
Condition: &ast.BinaryExpr{
116+
Left: &ast.VariableExpr{Name: "Count"},
117+
Operator: "<",
118+
Right: &ast.LiteralExpr{Kind: ast.LiteralInteger, Value: int64(10)},
119+
},
120+
Annotations: &ast.ActivityAnnotations{
121+
Position: &ast.Position{X: 420, Y: 180},
122+
},
123+
}
124+
125+
id := fb.addWhileStatement(stmt)
126+
if id == "" {
127+
t.Fatal("expected while activity ID")
128+
}
129+
130+
loop, ok := fb.objects[len(fb.objects)-1].(*microflows.LoopedActivity)
131+
if !ok {
132+
t.Fatalf("expected LoopedActivity, got %T", fb.objects[len(fb.objects)-1])
133+
}
134+
if loop.Position.X != 420 || loop.Position.Y != 180 {
135+
t.Fatalf("got while position (%d, %d), want (420, 180)", loop.Position.X, loop.Position.Y)
136+
}
137+
wantNextX := 420 + loop.Size.Width/2 + HorizontalSpacing
138+
if fb.posX != wantNextX {
139+
t.Fatalf("got next posX %d, want %d", fb.posX, wantNextX)
140+
}
141+
}
142+
143+
func TestAddLogMessageAction_PreservesNodeExpression(t *testing.T) {
144+
fb := &flowBuilder{
145+
posX: 100,
146+
posY: 200,
147+
spacing: HorizontalSpacing,
148+
measurer: &layoutMeasurer{},
149+
}
150+
151+
stmt := &ast.LogStmt{
152+
Level: ast.LogInfo,
153+
Node: &ast.ConstantRefExpr{
154+
QualifiedName: ast.QualifiedName{Module: "TestModule", Name: "SecurityLogNode"},
155+
},
156+
Message: &ast.LiteralExpr{Kind: ast.LiteralString, Value: "User added"},
157+
}
158+
159+
id := fb.addLogMessageAction(stmt)
160+
if id == "" {
161+
t.Fatal("expected log activity ID")
162+
}
163+
164+
activity, ok := fb.objects[len(fb.objects)-1].(*microflows.ActionActivity)
165+
if !ok {
166+
t.Fatalf("expected ActionActivity, got %T", fb.objects[len(fb.objects)-1])
167+
}
168+
169+
action, ok := activity.Action.(*microflows.LogMessageAction)
170+
if !ok {
171+
t.Fatalf("expected LogMessageAction, got %T", activity.Action)
172+
}
173+
174+
if action.LogNodeName != "@TestModule.SecurityLogNode" {
175+
t.Fatalf("got log node %q, want %q", action.LogNodeName, "@TestModule.SecurityLogNode")
176+
}
177+
}
178+
179+
func TestAddLogMessageAction_TemplateLiteralDoesNotKeepQuotes(t *testing.T) {
180+
fb := &flowBuilder{
181+
posX: 100,
182+
posY: 200,
183+
spacing: HorizontalSpacing,
184+
measurer: &layoutMeasurer{},
185+
}
186+
187+
stmt := &ast.LogStmt{
188+
Level: ast.LogInfo,
189+
Node: &ast.LiteralExpr{Kind: ast.LiteralString, Value: "App"},
190+
Message: &ast.LiteralExpr{
191+
Kind: ast.LiteralString,
192+
Value: "Order {1}",
193+
},
194+
Template: []ast.TemplateParam{
195+
{Index: 1, Value: &ast.VariableExpr{Name: "OrderNumber"}},
196+
},
197+
}
198+
199+
id := fb.addLogMessageAction(stmt)
200+
if id == "" {
201+
t.Fatal("expected log activity ID")
202+
}
203+
204+
activity, ok := fb.objects[len(fb.objects)-1].(*microflows.ActionActivity)
205+
if !ok {
206+
t.Fatalf("expected ActionActivity, got %T", fb.objects[len(fb.objects)-1])
207+
}
208+
209+
action, ok := activity.Action.(*microflows.LogMessageAction)
210+
if !ok {
211+
t.Fatalf("expected LogMessageAction, got %T", activity.Action)
212+
}
213+
214+
if got := action.MessageTemplate.Translations["en_US"]; got != "Order {1}" {
215+
t.Fatalf("got message template %q, want %q", got, "Order {1}")
216+
}
217+
}
218+
68219
// =============================================================================
69220
// Issue #19: Long type must not be downgraded to Integer
70221
// =============================================================================

mdl/executor/cmd_diff_mdl.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -345,9 +345,9 @@ func microflowStatementToMDL(ctx *ExecContext, stmt ast.MicroflowStatement, inde
345345
lines = append(lines, indentStr+"end loop;")
346346

347347
case *ast.LogStmt:
348-
nodeStr := s.Node
349-
if !strings.HasPrefix(nodeStr, "'") {
350-
nodeStr = "'" + nodeStr + "'"
348+
nodeStr := "'Application'"
349+
if s.Node != nil {
350+
nodeStr = diffExpressionToString(ctx, s.Node)
351351
}
352352
msgStr := diffExpressionToString(ctx, s.Message)
353353
stmt := fmt.Sprintf("%slog %s node %s %s", indentStr, strings.ToLower(s.Level.String()), nodeStr, msgStr)

mdl/executor/cmd_microflows_builder_calls.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ func (fb *flowBuilder) addLogMessageAction(s *ast.LogStmt) model.ID {
3838

3939
if len(s.Template) > 0 {
4040
// Use provided template parameters
41-
templateText = fb.exprToString(s.Message)
41+
if lit, ok := s.Message.(*ast.LiteralExpr); ok && lit.Kind == ast.LiteralString {
42+
templateText = fmt.Sprintf("%v", lit.Value)
43+
} else {
44+
templateText = fb.exprToString(s.Message)
45+
}
4246
// Sort parameters by index to ensure correct order
4347
maxIndex := 0
4448
for _, p := range s.Template {
@@ -61,10 +65,15 @@ func (fb *flowBuilder) addLogMessageAction(s *ast.LogStmt) model.ID {
6165
templateParams = []string{fb.exprToString(s.Message)}
6266
}
6367

68+
logNodeName := "'Application'"
69+
if s.Node != nil {
70+
logNodeName = fb.exprToString(s.Node)
71+
}
72+
6473
action := &microflows.LogMessageAction{
6574
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
6675
LogLevel: logLevel,
67-
LogNodeName: "'" + s.Node + "'", // Store as expression (e.g., 'TEST')
76+
LogNodeName: logNodeName,
6877
MessageTemplate: &model.Text{
6978
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
7079
Translations: map[string]string{

mdl/executor/cmd_microflows_builder_control.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,13 @@ func (fb *flowBuilder) addLoopStatement(s *ast.LoopStmt) model.ID {
266266
innerStartX := LoopPadding + iteratorSpace // Extra offset for iterator icon and label
267267
innerStartY := LoopPadding + ActivityHeight/2 // Center activities vertically with some padding
268268

269+
loopLeftX := fb.posX
270+
loopCenterX := loopLeftX + loopWidth/2
271+
if s.Annotations != nil && s.Annotations.Position != nil {
272+
loopCenterX = s.Annotations.Position.X
273+
loopLeftX = loopCenterX - loopWidth/2
274+
}
275+
269276
// Add loop variable to varTypes with element type derived from list type
270277
// If $ProductList is "List of MfTest.Product", then $Product is "MfTest.Product"
271278
if fb.varTypes != nil {
@@ -313,7 +320,7 @@ func (fb *flowBuilder) addLoopStatement(s *ast.LoopStmt) model.ID {
313320
loop := &microflows.LoopedActivity{
314321
BaseMicroflowObject: microflows.BaseMicroflowObject{
315322
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
316-
Position: model.Point{X: fb.posX + loopWidth/2, Y: fb.posY},
323+
Position: model.Point{X: loopCenterX, Y: fb.posY},
317324
Size: model.Size{Width: loopWidth, Height: loopHeight},
318325
},
319326
LoopSource: &microflows.IterableList{
@@ -335,7 +342,7 @@ func (fb *flowBuilder) addLoopStatement(s *ast.LoopStmt) model.ID {
335342
// This is how Mendix stores them - all flows at the microflow level
336343
fb.flows = append(fb.flows, loopBuilder.flows...)
337344

338-
fb.posX += loopWidth + HorizontalSpacing
345+
fb.posX = loopLeftX + loopWidth + HorizontalSpacing
339346

340347
return loop.ID
341348
}
@@ -351,6 +358,13 @@ func (fb *flowBuilder) addWhileStatement(s *ast.WhileStmt) model.ID {
351358
innerStartX := LoopPadding
352359
innerStartY := LoopPadding + ActivityHeight/2
353360

361+
loopLeftX := fb.posX
362+
loopCenterX := loopLeftX + loopWidth/2
363+
if s.Annotations != nil && s.Annotations.Position != nil {
364+
loopCenterX = s.Annotations.Position.X
365+
loopLeftX = loopCenterX - loopWidth/2
366+
}
367+
354368
loopBuilder := &flowBuilder{
355369
posX: innerStartX,
356370
posY: innerStartY,
@@ -385,7 +399,7 @@ func (fb *flowBuilder) addWhileStatement(s *ast.WhileStmt) model.ID {
385399
loop := &microflows.LoopedActivity{
386400
BaseMicroflowObject: microflows.BaseMicroflowObject{
387401
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
388-
Position: model.Point{X: fb.posX + loopWidth/2, Y: fb.posY},
402+
Position: model.Point{X: loopCenterX, Y: fb.posY},
389403
Size: model.Size{Width: loopWidth, Height: loopHeight},
390404
},
391405
LoopSource: &microflows.WhileLoopCondition{
@@ -402,7 +416,7 @@ func (fb *flowBuilder) addWhileStatement(s *ast.WhileStmt) model.ID {
402416

403417
fb.objects = append(fb.objects, loop)
404418
fb.flows = append(fb.flows, loopBuilder.flows...)
405-
fb.posX += loopWidth + HorizontalSpacing
419+
fb.posX = loopLeftX + loopWidth + HorizontalSpacing
406420

407421
return loop.ID
408422
}

mdl/executor/cmd_microflows_format_action.go

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -377,8 +377,7 @@ func formatAction(
377377
if text, ok := a.MessageTemplate.Translations["en_US"]; ok {
378378
message = text
379379
}
380-
// Wrap message in quotes for MDL syntax (escape any existing single quotes)
381-
message = "'" + strings.ReplaceAll(message, "'", "''") + "'"
380+
message = mdlQuote(message)
382381
}
383382

384383
// Build WITH clause if there are template parameters
@@ -453,7 +452,7 @@ func formatAction(
453452
}
454453
case *microflows.EntityTypeCodeActionParameterValue:
455454
if v.Entity != "" {
456-
valueStr = "'" + v.Entity + "'"
455+
valueStr = mdlQuote(v.Entity)
457456
}
458457
}
459458
params = append(params, fmt.Sprintf("%s = %s", paramName, valueStr))
@@ -554,8 +553,7 @@ func formatAction(
554553
if text, ok := a.Template.Translations["en_US"]; ok {
555554
message = text
556555
}
557-
// Wrap message in quotes for MDL syntax (escape any existing single quotes)
558-
message = "'" + strings.ReplaceAll(message, "'", "''") + "'"
556+
message = mdlQuote(message)
559557
}
560558
result := fmt.Sprintf("show message %s type %s", message, msgType)
561559
if len(a.TemplateParameters) > 0 {
@@ -574,8 +572,7 @@ func formatAction(
574572
if text, ok := a.Template.Translations["en_US"]; ok {
575573
msgText = text
576574
}
577-
// Wrap message in quotes for MDL syntax (escape any existing single quotes)
578-
msgText = "'" + strings.ReplaceAll(msgText, "'", "''") + "'"
575+
msgText = mdlQuote(msgText)
579576
}
580577
// Build attribute path from variable and attribute name
581578
// AttributeName format: Module.Entity.Attribute
@@ -641,7 +638,7 @@ func formatAction(
641638
return formatWorkflowOperationAction(ctx, a)
642639

643640
case *microflows.SetTaskOutcomeAction:
644-
return fmt.Sprintf("set task outcome $%s '%s';", a.WorkflowTaskVariable, a.OutcomeValue)
641+
return fmt.Sprintf("set task outcome $%s %s;", a.WorkflowTaskVariable, mdlQuote(a.OutcomeValue))
645642

646643
case *microflows.OpenUserTaskAction:
647644
return fmt.Sprintf("open user task $%s;", a.UserTaskVariable)
@@ -689,7 +686,7 @@ func formatWorkflowOperationAction(ctx *ExecContext, a *microflows.WorkflowOpera
689686
switch op := a.Operation.(type) {
690687
case *microflows.AbortOperation:
691688
if op.Reason != "" {
692-
return fmt.Sprintf("workflow operation abort $%s reason '%s';", op.WorkflowVariable, strings.ReplaceAll(op.Reason, "'", "''"))
689+
return fmt.Sprintf("workflow operation abort $%s reason %s;", op.WorkflowVariable, mdlQuote(op.Reason))
693690
}
694691
return fmt.Sprintf("workflow operation abort $%s;", op.WorkflowVariable)
695692
case *microflows.ContinueOperation:
@@ -848,7 +845,7 @@ func formatRestCallAction(ctx *ExecContext, a *microflows.RestCallAction) string
848845
// URL
849846
url := "''"
850847
if a.HttpConfiguration != nil && a.HttpConfiguration.LocationTemplate != "" {
851-
url = "'" + strings.ReplaceAll(a.HttpConfiguration.LocationTemplate, "'", "''") + "'"
848+
url = mdlQuote(a.HttpConfiguration.LocationTemplate)
852849
}
853850
sb.WriteString(url)
854851

@@ -867,9 +864,9 @@ func formatRestCallAction(ctx *ExecContext, a *microflows.RestCallAction) string
867864
// Headers
868865
if a.HttpConfiguration != nil && len(a.HttpConfiguration.CustomHeaders) > 0 {
869866
for _, h := range a.HttpConfiguration.CustomHeaders {
870-
sb.WriteString("\n header '")
871-
sb.WriteString(strings.ReplaceAll(h.Name, "'", "''"))
872-
sb.WriteString("' = ")
867+
sb.WriteString("\n header ")
868+
sb.WriteString(mdlQuote(h.Name))
869+
sb.WriteString(" = ")
873870
sb.WriteString(h.Value)
874871
}
875872
}
@@ -887,9 +884,8 @@ func formatRestCallAction(ctx *ExecContext, a *microflows.RestCallAction) string
887884
switch rh := a.RequestHandling.(type) {
888885
case *microflows.CustomRequestHandling:
889886
if rh.Template != "" {
890-
sb.WriteString("\n body '")
891-
sb.WriteString(strings.ReplaceAll(rh.Template, "'", "''"))
892-
sb.WriteString("'")
887+
sb.WriteString("\n body ")
888+
sb.WriteString(mdlQuote(rh.Template))
893889
// Add template parameters if present
894890
if len(rh.TemplateParams) > 0 {
895891
sb.WriteString(" with (")

0 commit comments

Comments
 (0)