Skip to content

Commit 6ee1200

Browse files
akoclaude
andcommitted
fix: DESCRIBE ODATA SERVICE expose roundtrip broken by KEY token (issue #400)
The grammar's exposeMemberOptions rule used IDENTIFIER, but 'Key' is a reserved token (KEY) in the lexer so it was rejected by the parser. Fixes: - Change exposeMember and exposeMemberOptions grammar rules to use identifierOrKeyword instead of IDENTIFIER so reserved tokens like Key are accepted. - Regenerate ANTLR parser. - Update visitor to use IdentifierOrKeyword()/AllIdentifierOrKeyword() and handle both "key" and "ispartofkey" as IsPartOfKey. - Formatter now emits IsPartOfKey (a plain identifier) instead of Key to use the canonical form accepted by the visitor. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ebae4b2 commit 6ee1200

6 files changed

Lines changed: 129 additions & 39 deletions

File tree

mdl/executor/cmd_odata.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,7 @@ func outputPublishedODataServiceMDL(ctx *ExecContext, svc *model.PublishedODataS
425425
modifiers = append(modifiers, "Sortable")
426426
}
427427
if m.IsPartOfKey {
428-
modifiers = append(modifiers, "Key")
428+
modifiers = append(modifiers, "IsPartOfKey")
429429
}
430430

431431
line := fmt.Sprintf(" %s as '%s'", m.Name, m.ExposedName)

mdl/executor/cmd_odata_mock_test.go

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

88
"github.com/mendixlabs/mxcli/mdl/ast"
99
"github.com/mendixlabs/mxcli/mdl/backend/mock"
10+
"github.com/mendixlabs/mxcli/mdl/visitor"
1011
"github.com/mendixlabs/mxcli/model"
1112
)
1213

@@ -201,6 +202,62 @@ func TestDescribeODataService_NotFound(t *testing.T) {
201202
assertError(t, describeODataService(ctx, ast.QualifiedName{Module: "X", Name: "NoSuch"}))
202203
}
203204

205+
// TestDescribeODataService_ExposeRoundtrip verifies that DESCRIBE ODATA SERVICE
206+
// output for entities with key/filterable/sortable members is valid MDL that
207+
// the parser can re-parse (issue #400).
208+
func TestDescribeODataService_ExposeRoundtrip(t *testing.T) {
209+
mod := mkModule("MyModule")
210+
svc := &model.PublishedODataService{
211+
BaseElement: model.BaseElement{ID: nextID("pos")},
212+
ContainerID: mod.ID,
213+
Name: "CatalogService",
214+
Path: "/odata/v1",
215+
Version: "1.0",
216+
ODataVersion: "4.0",
217+
EntityTypes: []*model.PublishedEntityType{
218+
{
219+
Entity: "MyModule.Order",
220+
ExposedName: "Orders",
221+
Members: []*model.PublishedMember{
222+
{Name: "Id", ExposedName: "Id", IsPartOfKey: true},
223+
{Name: "Name", ExposedName: "Name", Filterable: true, Sortable: true},
224+
},
225+
},
226+
},
227+
EntitySets: []*model.PublishedEntitySet{
228+
{
229+
ExposedName: "Orders",
230+
EntityTypeName: "MyModule.Order",
231+
ReadMode: "Readable",
232+
InsertMode: "NotSupported",
233+
},
234+
},
235+
}
236+
h := mkHierarchy(mod)
237+
withContainer(h, svc.ContainerID, mod.ID)
238+
239+
mb := &mock.MockBackend{
240+
IsConnectedFunc: func() bool { return true },
241+
ListPublishedODataServicesFunc: func() ([]*model.PublishedODataService, error) {
242+
return []*model.PublishedODataService{svc}, nil
243+
},
244+
}
245+
246+
ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h))
247+
assertNoError(t, describeODataService(ctx, ast.QualifiedName{Module: "MyModule", Name: "CatalogService"}))
248+
249+
out := buf.String()
250+
assertContainsStr(t, out, "IsPartOfKey")
251+
252+
_, errs := visitor.Build(out)
253+
if len(errs) > 0 {
254+
t.Errorf("DESCRIBE output failed to parse (roundtrip broken):\n%s\nErrors:", out)
255+
for _, e := range errs {
256+
t.Errorf(" %v", e)
257+
}
258+
}
259+
}
260+
204261
func TestCreateODataClient_InvalidMetadataURL(t *testing.T) {
205262
mb := &mock.MockBackend{
206263
IsConnectedFunc: func() bool { return true },

mdl/grammar/MDLParser.g4

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2808,11 +2808,11 @@ exposeClause
28082808
;
28092809

28102810
exposeMember
2811-
: IDENTIFIER (AS STRING_LITERAL)? exposeMemberOptions?
2811+
: identifierOrKeyword (AS STRING_LITERAL)? exposeMemberOptions?
28122812
;
28132813

28142814
exposeMemberOptions
2815-
: LPAREN IDENTIFIER (COMMA IDENTIFIER)* RPAREN
2815+
: LPAREN identifierOrKeyword (COMMA identifierOrKeyword)* RPAREN
28162816
;
28172817

28182818
/**

mdl/grammar/parser/MDLParser.interp

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

mdl/grammar/parser/mdl_parser.go

Lines changed: 64 additions & 31 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mdl/visitor/visitor_odata.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -387,12 +387,12 @@ func parseExposeMembers(ctx parser.IExposeClauseContext) []*ast.PublishedMemberD
387387
member := memberCtx.(*parser.ExposeMemberContext)
388388

389389
// Guard against incomplete parse (e.g., user typing in LSP)
390-
if member.IDENTIFIER() == nil {
390+
if member.IdentifierOrKeyword() == nil {
391391
continue
392392
}
393393

394394
m := &ast.PublishedMemberDef{
395-
Name: member.IDENTIFIER().GetText(),
395+
Name: member.IdentifierOrKeyword().GetText(),
396396
}
397397

398398
// Optional AS 'ExposedName'
@@ -403,13 +403,13 @@ func parseExposeMembers(ctx parser.IExposeClauseContext) []*ast.PublishedMemberD
403403
// Optional options (Filterable, Sortable, IsPartOfKey)
404404
if opts := member.ExposeMemberOptions(); opts != nil {
405405
optsCtx := opts.(*parser.ExposeMemberOptionsContext)
406-
for _, id := range optsCtx.AllIDENTIFIER() {
406+
for _, id := range optsCtx.AllIdentifierOrKeyword() {
407407
switch strings.ToLower(id.GetText()) {
408408
case "filterable":
409409
m.Filterable = true
410410
case "sortable":
411411
m.Sortable = true
412-
case "ispartofkey":
412+
case "key", "ispartofkey":
413413
m.IsPartOfKey = true
414414
}
415415
}

0 commit comments

Comments
 (0)