Skip to content

Commit aabaf2f

Browse files
authored
Merge pull request #167 from engalar/fix/enum-value-context-syntax
fix: emit correct enum value format for XPath vs expression contexts
2 parents 3a39a66 + 9a450c2 commit aabaf2f

File tree

5 files changed

+182
-2
lines changed

5 files changed

+182
-2
lines changed

mdl/executor/cmd_microflows_builder_actions.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ func (fb *flowBuilder) addCreateObjectAction(s *ast.CreateObjectStmt) model.ID {
108108
memberChange := &microflows.MemberChange{
109109
BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())},
110110
Type: microflows.MemberChangeTypeSet,
111-
Value: fb.exprToString(change.Value),
111+
Value: fb.memberExpressionToString(change.Value, entityQN, change.Attribute),
112112
}
113113
fb.resolveMemberChange(memberChange, change.Attribute, entityQN)
114114
action.InitialMembers = append(action.InitialMembers, memberChange)
@@ -257,7 +257,7 @@ func (fb *flowBuilder) addChangeObjectAction(s *ast.ChangeObjectStmt) model.ID {
257257
memberChange := &microflows.MemberChange{
258258
BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())},
259259
Type: microflows.MemberChangeTypeSet,
260-
Value: fb.exprToString(change.Value),
260+
Value: fb.memberExpressionToString(change.Value, entityQN, change.Attribute),
261261
}
262262
fb.resolveMemberChange(memberChange, change.Attribute, entityQN)
263263
action.Changes = append(action.Changes, memberChange)

mdl/executor/cmd_microflows_helpers.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"github.com/mendixlabs/mxcli/mdl/ast"
1212
"github.com/mendixlabs/mxcli/model"
13+
"github.com/mendixlabs/mxcli/sdk/domainmodel"
1314
"github.com/mendixlabs/mxcli/sdk/microflows"
1415
)
1516

@@ -184,12 +185,77 @@ func expressionToXPath(expr ast.Expression) string {
184185
return "empty"
185186
}
186187
return expressionToString(expr)
188+
case *ast.QualifiedNameExpr:
189+
return qualifiedNameToXPath(e)
187190
default:
188191
// For all other expression types, the standard serialization is correct
189192
return expressionToString(expr)
190193
}
191194
}
192195

196+
// qualifiedNameToXPath converts a QualifiedNameExpr to XPath format.
197+
// For enum value references (3-part: Module.EnumName.Value), XPath requires
198+
// just the value name in quotes: 'Value'. For 2-part names (associations,
199+
// entity references), returns the qualified name as-is.
200+
func qualifiedNameToXPath(e *ast.QualifiedNameExpr) string {
201+
// 3-part names (Name contains a dot) are enum references: Module.EnumName.Value
202+
if dotIdx := strings.LastIndex(e.QualifiedName.Name, "."); dotIdx >= 0 {
203+
valueName := e.QualifiedName.Name[dotIdx+1:]
204+
return "'" + valueName + "'"
205+
}
206+
return e.QualifiedName.String()
207+
}
208+
209+
// memberExpressionToString converts an AST Expression to a Mendix expression string,
210+
// resolving enum string literals to qualified enum names when the attribute type is known.
211+
// For example, 'Processing' becomes MyModule.ENUM_Status.Processing when the attribute
212+
// is of type Enumeration(MyModule.ENUM_Status).
213+
func (fb *flowBuilder) memberExpressionToString(expr ast.Expression, entityQN, attrName string) string {
214+
// Only transform string literals for enum attributes
215+
if lit, ok := expr.(*ast.LiteralExpr); ok && lit.Kind == ast.LiteralString {
216+
if enumRef := fb.lookupEnumRef(entityQN, attrName); enumRef != "" {
217+
// Convert 'Value' to Module.EnumName.Value
218+
return enumRef + "." + fmt.Sprintf("%v", lit.Value)
219+
}
220+
}
221+
return fb.exprToString(expr)
222+
}
223+
224+
// lookupEnumRef returns the enumeration qualified name (e.g., "MyModule.ENUM_Status")
225+
// for an attribute if it is an enumeration type. Returns "" if the attribute is not
226+
// an enumeration or if the domain model is not available.
227+
func (fb *flowBuilder) lookupEnumRef(entityQN, attrName string) string {
228+
if fb.reader == nil || entityQN == "" || attrName == "" {
229+
return ""
230+
}
231+
parts := strings.SplitN(entityQN, ".", 2)
232+
if len(parts) != 2 {
233+
return ""
234+
}
235+
mod, err := fb.reader.GetModuleByName(parts[0])
236+
if err != nil || mod == nil {
237+
return ""
238+
}
239+
dm, err := fb.reader.GetDomainModel(mod.ID)
240+
if err != nil || dm == nil {
241+
return ""
242+
}
243+
for _, entity := range dm.Entities {
244+
if entity.Name == parts[1] {
245+
for _, attr := range entity.Attributes {
246+
if attr.Name == attrName {
247+
if enumType, ok := attr.Type.(*domainmodel.EnumerationAttributeType); ok {
248+
return enumType.EnumerationRef
249+
}
250+
return ""
251+
}
252+
}
253+
return ""
254+
}
255+
}
256+
return ""
257+
}
258+
193259
// xpathPathExprToString serializes an XPathPathExpr to an XPath path string.
194260
func xpathPathExprToString(path *ast.XPathPathExpr) string {
195261
var parts []string
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package executor
4+
5+
import (
6+
"testing"
7+
8+
"github.com/mendixlabs/mxcli/mdl/ast"
9+
)
10+
11+
func TestQualifiedNameToXPath_EnumValue(t *testing.T) {
12+
// 3-part names (Module.EnumName.Value) should emit just the value in quotes
13+
expr := &ast.QualifiedNameExpr{
14+
QualifiedName: ast.QualifiedName{Module: "MyModule", Name: "ENUM_Status.Processing"},
15+
}
16+
got := qualifiedNameToXPath(expr)
17+
want := "'Processing'"
18+
if got != want {
19+
t.Errorf("qualifiedNameToXPath(%q) = %q, want %q", expr.QualifiedName.String(), got, want)
20+
}
21+
}
22+
23+
func TestQualifiedNameToXPath_NonEnum(t *testing.T) {
24+
// 2-part names (Module.AssocName) should pass through as-is
25+
expr := &ast.QualifiedNameExpr{
26+
QualifiedName: ast.QualifiedName{Module: "MyModule", Name: "SomeAssoc"},
27+
}
28+
got := qualifiedNameToXPath(expr)
29+
want := "MyModule.SomeAssoc"
30+
if got != want {
31+
t.Errorf("qualifiedNameToXPath(%q) = %q, want %q", expr.QualifiedName.String(), got, want)
32+
}
33+
}
34+
35+
func TestExpressionToXPath_EnumInComparison(t *testing.T) {
36+
// WHERE Status = Module.ENUM.Value should produce: Status = 'Value'
37+
expr := &ast.BinaryExpr{
38+
Left: &ast.IdentifierExpr{Name: "Status"},
39+
Operator: "=",
40+
Right: &ast.QualifiedNameExpr{
41+
QualifiedName: ast.QualifiedName{Module: "BST", Name: "ComplianceStatus.Rectified"},
42+
},
43+
}
44+
got := expressionToXPath(expr)
45+
want := "Status = 'Rectified'"
46+
if got != want {
47+
t.Errorf("expressionToXPath = %q, want %q", got, want)
48+
}
49+
}
50+
51+
func TestExpressionToXPath_StringLiteralPreserved(t *testing.T) {
52+
// WHERE Status = 'Pending' should stay as Status = 'Pending'
53+
expr := &ast.BinaryExpr{
54+
Left: &ast.IdentifierExpr{Name: "Status"},
55+
Operator: "=",
56+
Right: &ast.LiteralExpr{Value: "Pending", Kind: ast.LiteralString},
57+
}
58+
got := expressionToXPath(expr)
59+
want := "Status = 'Pending'"
60+
if got != want {
61+
t.Errorf("expressionToXPath = %q, want %q", got, want)
62+
}
63+
}
64+
65+
func TestExpressionToString_QualifiedNameUnchanged(t *testing.T) {
66+
// In expression context, qualified names should remain as-is (correct for enum refs)
67+
expr := &ast.QualifiedNameExpr{
68+
QualifiedName: ast.QualifiedName{Module: "MyModule", Name: "ENUM_Status.Processing"},
69+
}
70+
got := expressionToString(expr)
71+
want := "MyModule.ENUM_Status.Processing"
72+
if got != want {
73+
t.Errorf("expressionToString = %q, want %q", got, want)
74+
}
75+
}

mdl/visitor/visitor_page_v3.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1163,6 +1163,12 @@ func xpathExprToString(expr ast.Expression) string {
11631163
case *ast.IdentifierExpr:
11641164
return e.Name
11651165
case *ast.QualifiedNameExpr:
1166+
// For enum value references (3-part: Module.EnumName.Value), XPath requires
1167+
// just the value name in quotes: 'Value'.
1168+
if dotIdx := strings.LastIndex(e.QualifiedName.Name, "."); dotIdx >= 0 {
1169+
valueName := e.QualifiedName.Name[dotIdx+1:]
1170+
return "'" + valueName + "'"
1171+
}
11661172
return e.QualifiedName.String()
11671173
default:
11681174
return ""

mdl/visitor/visitor_xpath_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,39 @@ func TestXPath_ComplexExpressions(t *testing.T) {
343343
}
344344
}
345345

346+
func TestXPath_EnumValueReference(t *testing.T) {
347+
tests := []struct {
348+
name string
349+
input string
350+
want string
351+
}{
352+
{
353+
"3-part enum value becomes quoted value",
354+
"[Status = BST.ComplianceStatus.Rectified]",
355+
"[Status = 'Rectified']",
356+
},
357+
{
358+
"2-part qualified name preserved",
359+
"[Module.Association = $object]",
360+
"[Module.Association = $object]",
361+
},
362+
{
363+
"string literal enum preserved",
364+
"[Status = 'Active']",
365+
"[Status = 'Active']",
366+
},
367+
}
368+
369+
for _, tt := range tests {
370+
t.Run(tt.name, func(t *testing.T) {
371+
got := roundTripXPath(tt.input)
372+
if got != tt.want {
373+
t.Errorf("roundTripXPath(%q) = %q, want %q", tt.input, got, tt.want)
374+
}
375+
})
376+
}
377+
}
378+
346379
func TestXPath_ASTTypes(t *testing.T) {
347380
t.Run("bare path creates XPathPathExpr", func(t *testing.T) {
348381
expr := parseXPathConstraint("[Module.Assoc/Module.Entity/Attr = $val]")

0 commit comments

Comments
 (0)