Skip to content

Commit fa3fdb4

Browse files
akoclaude
andcommitted
feat(asyncapi): add SHOW/DESCRIBE CONTRACT CHANNELS/MESSAGES for AsyncAPI contracts
Parse the Document YAML field stored on business event client services to let users browse available channels, messages, and payload schemas from cached AsyncAPI contracts without network access. New commands: - SHOW CONTRACT CHANNELS FROM Module.Service - SHOW CONTRACT MESSAGES FROM Module.Service - DESCRIBE CONTRACT MESSAGE Module.Service.MessageName Implementation: - Add Document field to BusinessEventService model and parser - Add AsyncAPI 2.x YAML parser (sdk/mpr/asyncapi.go) with $ref resolution - Add CHANNELS and MESSAGES tokens to lexer - Wire grammar, AST, visitor, executor for all 3 commands - Update help topics, quick reference, docs-site, CLAUDE.md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ebcfb04 commit fa3fdb4

File tree

21 files changed

+6964
-6146
lines changed

21 files changed

+6964
-6146
lines changed

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,7 @@ Full syntax tables for all MDL statements (microflows, pages, security, navigati
406406
- CALCULATED BY microflow syntax for calculated attributes
407407
- Image collections (SHOW/DESCRIBE/CREATE/DROP)
408408
- OData contract browsing (SHOW/DESCRIBE CONTRACT ENTITIES/ACTIONS FROM cached $metadata)
409+
- AsyncAPI contract browsing (SHOW/DESCRIBE CONTRACT CHANNELS/MESSAGES FROM cached AsyncAPI)
409410
- SHOW EXTERNAL ACTIONS, SHOW PUBLISHED REST SERVICES
410411
- Integration catalog tables (rest_clients, rest_operations, published_rest_services, external_entities, external_actions, business_events)
411412

cmd/mxcli/help_topics/odata.txt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,21 @@ SHOW COMMANDS
1313
SHOW EXTERNAL ACTIONS; -- List external actions used in microflows
1414
SHOW EXTERNAL ACTIONS IN MyModule; -- Filter by module
1515

16-
CONTRACT COMMANDS (browse cached $metadata)
17-
--------------------------------------------
16+
CONTRACT COMMANDS (browse cached contracts)
17+
-------------------------------------------
1818

19+
OData ($metadata):
1920
SHOW CONTRACT ENTITIES FROM MyModule.MyClient; -- List entity types from $metadata
2021
SHOW CONTRACT ACTIONS FROM MyModule.MyClient; -- List actions/functions from $metadata
21-
2222
DESCRIBE CONTRACT ENTITY MyModule.MyClient.Product; -- Properties and nav props
2323
DESCRIBE CONTRACT ENTITY MyModule.MyClient.Product FORMAT mdl; -- Generate CREATE EXTERNAL ENTITY
2424
DESCRIBE CONTRACT ACTION MyModule.MyClient.CreateOrder; -- Parameters and return type
2525

26+
AsyncAPI (business events):
27+
SHOW CONTRACT CHANNELS FROM MyModule.MyEventClient; -- List channels
28+
SHOW CONTRACT MESSAGES FROM MyModule.MyEventClient; -- List messages with schemas
29+
DESCRIBE CONTRACT MESSAGE MyModule.MyEventClient.OrderChanged; -- Message properties
30+
2631
DESCRIBE COMMANDS
2732
-----------------
2833

cmd/mxcli/lsp_completions_gen.go

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

docs-site/src/appendixes/quick-reference.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ CREATE CONSTANT MyModule.EnableLogging TYPE Boolean DEFAULT true;
9999
| Show contract actions | `SHOW CONTRACT ACTIONS FROM Module.Client;` | Browse cached $metadata |
100100
| Describe contract entity | `DESCRIBE CONTRACT ENTITY Module.Client.Entity [FORMAT mdl];` | Properties, types, keys |
101101
| Describe contract action | `DESCRIBE CONTRACT ACTION Module.Client.Action [FORMAT mdl];` | Parameters, return type |
102+
| Show contract channels | `SHOW CONTRACT CHANNELS FROM Module.Service;` | Browse cached AsyncAPI |
103+
| Show contract messages | `SHOW CONTRACT MESSAGES FROM Module.Service;` | Browse cached AsyncAPI |
104+
| Describe contract message | `DESCRIBE CONTRACT MESSAGE Module.Service.Message;` | Message payload properties |
102105

103106
**OData Client Example:**
104107
```sql

docs/01-project/MDL_QUICK_REFERENCE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ CREATE CONSTANT MyModule.EnableLogging TYPE Boolean DEFAULT true;
102102
| Show contract actions | `SHOW CONTRACT ACTIONS FROM Module.Client;` | Browse cached $metadata |
103103
| Describe contract entity | `DESCRIBE CONTRACT ENTITY Module.Client.Entity [FORMAT mdl];` | Properties, types, keys |
104104
| Describe contract action | `DESCRIBE CONTRACT ACTION Module.Client.Action [FORMAT mdl];` | Parameters, return type |
105+
| Show contract channels | `SHOW CONTRACT CHANNELS FROM Module.Service;` | Browse cached AsyncAPI |
106+
| Show contract messages | `SHOW CONTRACT MESSAGES FROM Module.Service;` | Browse cached AsyncAPI |
107+
| Describe contract message | `DESCRIBE CONTRACT MESSAGE Module.Service.Message;` | Message payload properties |
105108

106109
**OData Client Example:**
107110
```sql

mdl/ast/ast_query.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ const (
8282
ShowConstantValues // SHOW CONSTANT VALUES [IN module]
8383
ShowContractEntities // SHOW CONTRACT ENTITIES FROM Module.Service
8484
ShowContractActions // SHOW CONTRACT ACTIONS FROM Module.Service
85+
ShowContractChannels // SHOW CONTRACT CHANNELS FROM Module.Service (AsyncAPI)
86+
ShowContractMessages // SHOW CONTRACT MESSAGES FROM Module.Service (AsyncAPI)
8587
)
8688

8789
// String returns the human-readable name of the show object type.
@@ -193,6 +195,10 @@ func (t ShowObjectType) String() string {
193195
return "CONTRACT ENTITIES"
194196
case ShowContractActions:
195197
return "CONTRACT ACTIONS"
198+
case ShowContractChannels:
199+
return "CONTRACT CHANNELS"
200+
case ShowContractMessages:
201+
return "CONTRACT MESSAGES"
196202
default:
197203
return "UNKNOWN"
198204
}
@@ -247,6 +253,7 @@ const (
247253
DescribePublishedRestService // DESCRIBE PUBLISHED REST SERVICE Module.Name
248254
DescribeContractEntity // DESCRIBE CONTRACT ENTITY Service.EntityName [FORMAT mdl]
249255
DescribeContractAction // DESCRIBE CONTRACT ACTION Service.ActionName [FORMAT mdl]
256+
DescribeContractMessage // DESCRIBE CONTRACT MESSAGE Service.MessageName
250257
)
251258

252259
// String returns the human-readable name of the describe object type.
@@ -308,6 +315,8 @@ func (t DescribeObjectType) String() string {
308315
return "CONTRACT ENTITY"
309316
case DescribeContractAction:
310317
return "CONTRACT ACTION"
318+
case DescribeContractMessage:
319+
return "CONTRACT MESSAGE"
311320
default:
312321
return "UNKNOWN"
313322
}

mdl/executor/cmd_contract.go

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,3 +467,224 @@ func edmToMendixType(p *mpr.EdmProperty) string {
467467
return "String(200)"
468468
}
469469
}
470+
471+
// ============================================================================
472+
// AsyncAPI Contract Commands
473+
// ============================================================================
474+
475+
// showContractChannels handles SHOW CONTRACT CHANNELS FROM Module.Service.
476+
func (e *Executor) showContractChannels(name *ast.QualifiedName) error {
477+
if name == nil {
478+
return fmt.Errorf("service name required: SHOW CONTRACT CHANNELS FROM Module.Service")
479+
}
480+
481+
doc, svcQN, err := e.parseAsyncAPIContract(*name)
482+
if err != nil {
483+
return err
484+
}
485+
486+
if len(doc.Channels) == 0 {
487+
fmt.Fprintf(e.output, "No channels found in contract for %s.\n", svcQN)
488+
return nil
489+
}
490+
491+
type row struct {
492+
channel string
493+
operation string
494+
opID string
495+
message string
496+
}
497+
498+
var rows []row
499+
chWidth := len("Channel")
500+
opWidth := len("Operation")
501+
opIDWidth := len("OperationID")
502+
msgWidth := len("Message")
503+
504+
for _, ch := range doc.Channels {
505+
rows = append(rows, row{ch.Name, ch.OperationType, ch.OperationID, ch.MessageRef})
506+
if len(ch.Name) > chWidth {
507+
chWidth = len(ch.Name)
508+
}
509+
if len(ch.OperationType) > opWidth {
510+
opWidth = len(ch.OperationType)
511+
}
512+
if len(ch.OperationID) > opIDWidth {
513+
opIDWidth = len(ch.OperationID)
514+
}
515+
if len(ch.MessageRef) > msgWidth {
516+
msgWidth = len(ch.MessageRef)
517+
}
518+
}
519+
520+
fmt.Fprintf(e.output, "| %-*s | %-*s | %-*s | %-*s |\n",
521+
chWidth, "Channel", opWidth, "Operation", opIDWidth, "OperationID", msgWidth, "Message")
522+
fmt.Fprintf(e.output, "|-%s-|-%s-|-%s-|-%s-|\n",
523+
strings.Repeat("-", chWidth), strings.Repeat("-", opWidth),
524+
strings.Repeat("-", opIDWidth), strings.Repeat("-", msgWidth))
525+
for _, r := range rows {
526+
fmt.Fprintf(e.output, "| %-*s | %-*s | %-*s | %-*s |\n",
527+
chWidth, r.channel, opWidth, r.operation, opIDWidth, r.opID, msgWidth, r.message)
528+
}
529+
fmt.Fprintf(e.output, "\n(%d channels in %s contract)\n", len(rows), svcQN)
530+
531+
return nil
532+
}
533+
534+
// showContractMessages handles SHOW CONTRACT MESSAGES FROM Module.Service.
535+
func (e *Executor) showContractMessages(name *ast.QualifiedName) error {
536+
if name == nil {
537+
return fmt.Errorf("service name required: SHOW CONTRACT MESSAGES FROM Module.Service")
538+
}
539+
540+
doc, svcQN, err := e.parseAsyncAPIContract(*name)
541+
if err != nil {
542+
return err
543+
}
544+
545+
if len(doc.Messages) == 0 {
546+
fmt.Fprintf(e.output, "No messages found in contract for %s.\n", svcQN)
547+
return nil
548+
}
549+
550+
type row struct {
551+
name string
552+
title string
553+
contentType string
554+
props int
555+
}
556+
557+
var rows []row
558+
nameWidth := len("Message")
559+
titleWidth := len("Title")
560+
ctWidth := len("ContentType")
561+
562+
for _, msg := range doc.Messages {
563+
rows = append(rows, row{msg.Name, msg.Title, msg.ContentType, len(msg.Properties)})
564+
if len(msg.Name) > nameWidth {
565+
nameWidth = len(msg.Name)
566+
}
567+
if len(msg.Title) > titleWidth {
568+
titleWidth = len(msg.Title)
569+
}
570+
if len(msg.ContentType) > ctWidth {
571+
ctWidth = len(msg.ContentType)
572+
}
573+
}
574+
575+
sort.Slice(rows, func(i, j int) bool {
576+
return strings.ToLower(rows[i].name) < strings.ToLower(rows[j].name)
577+
})
578+
579+
propsWidth := len("Props")
580+
581+
fmt.Fprintf(e.output, "| %-*s | %-*s | %-*s | %-*s |\n",
582+
nameWidth, "Message", titleWidth, "Title", ctWidth, "ContentType", propsWidth, "Props")
583+
fmt.Fprintf(e.output, "|-%s-|-%s-|-%s-|-%s-|\n",
584+
strings.Repeat("-", nameWidth), strings.Repeat("-", titleWidth),
585+
strings.Repeat("-", ctWidth), strings.Repeat("-", propsWidth))
586+
for _, r := range rows {
587+
fmt.Fprintf(e.output, "| %-*s | %-*s | %-*s | %-*d |\n",
588+
nameWidth, r.name, titleWidth, r.title, ctWidth, r.contentType, propsWidth, r.props)
589+
}
590+
fmt.Fprintf(e.output, "\n(%d messages in %s contract)\n", len(rows), svcQN)
591+
592+
return nil
593+
}
594+
595+
// describeContractMessage handles DESCRIBE CONTRACT MESSAGE Module.Service.MessageName.
596+
func (e *Executor) describeContractMessage(name ast.QualifiedName) error {
597+
svcName, msgName, err := splitContractRef(name)
598+
if err != nil {
599+
return err
600+
}
601+
602+
doc, svcQN, err := e.parseAsyncAPIContract(svcName)
603+
if err != nil {
604+
return err
605+
}
606+
607+
msg := doc.FindMessage(msgName)
608+
if msg == nil {
609+
return fmt.Errorf("message %q not found in contract for %s", msgName, svcQN)
610+
}
611+
612+
fmt.Fprintf(e.output, "%s\n", msg.Name)
613+
if msg.Title != "" {
614+
fmt.Fprintf(e.output, " Title: %s\n", msg.Title)
615+
}
616+
if msg.Description != "" {
617+
fmt.Fprintf(e.output, " Description: %s\n", msg.Description)
618+
}
619+
if msg.ContentType != "" {
620+
fmt.Fprintf(e.output, " ContentType: %s\n", msg.ContentType)
621+
}
622+
623+
if len(msg.Properties) > 0 {
624+
fmt.Fprintln(e.output)
625+
nameWidth := len("Property")
626+
typeWidth := len("Type")
627+
for _, p := range msg.Properties {
628+
if len(p.Name) > nameWidth {
629+
nameWidth = len(p.Name)
630+
}
631+
t := asyncTypeString(p)
632+
if len(t) > typeWidth {
633+
typeWidth = len(t)
634+
}
635+
}
636+
637+
fmt.Fprintf(e.output, " %-*s %-*s\n", nameWidth, "Property", typeWidth, "Type")
638+
fmt.Fprintf(e.output, " %s %s\n", strings.Repeat("-", nameWidth), strings.Repeat("-", typeWidth))
639+
for _, p := range msg.Properties {
640+
fmt.Fprintf(e.output, " %-*s %-*s\n", nameWidth, p.Name, typeWidth, asyncTypeString(p))
641+
}
642+
}
643+
644+
return nil
645+
}
646+
647+
// parseAsyncAPIContract finds a business event service by name and parses its cached AsyncAPI document.
648+
func (e *Executor) parseAsyncAPIContract(name ast.QualifiedName) (*mpr.AsyncAPIDocument, string, error) {
649+
services, err := e.reader.ListBusinessEventServices()
650+
if err != nil {
651+
return nil, "", fmt.Errorf("failed to list business event services: %w", err)
652+
}
653+
654+
h, err := e.getHierarchy()
655+
if err != nil {
656+
return nil, "", fmt.Errorf("failed to build hierarchy: %w", err)
657+
}
658+
659+
for _, svc := range services {
660+
modID := h.FindModuleID(svc.ContainerID)
661+
modName := h.GetModuleName(modID)
662+
663+
if !strings.EqualFold(modName, name.Module) || !strings.EqualFold(svc.Name, name.Name) {
664+
continue
665+
}
666+
667+
svcQN := modName + "." + svc.Name
668+
669+
if svc.Document == "" {
670+
return nil, svcQN, fmt.Errorf("no cached AsyncAPI contract for %s. This service has no Document field (it may be a publisher, not a consumer)", svcQN)
671+
}
672+
673+
doc, err := mpr.ParseAsyncAPI(svc.Document)
674+
if err != nil {
675+
return nil, svcQN, fmt.Errorf("failed to parse AsyncAPI contract for %s: %w", svcQN, err)
676+
}
677+
678+
return doc, svcQN, nil
679+
}
680+
681+
return nil, "", fmt.Errorf("business event service not found: %s.%s", name.Module, name.Name)
682+
}
683+
684+
// asyncTypeString formats an AsyncAPI property type for display.
685+
func asyncTypeString(p *mpr.AsyncAPIProperty) string {
686+
if p.Format != "" {
687+
return p.Type + " (" + p.Format + ")"
688+
}
689+
return p.Type
690+
}

mdl/executor/executor.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,10 @@ func (e *Executor) execShow(s *ast.ShowStmt) error {
782782
return e.showContractEntities(s.Name)
783783
case ast.ShowContractActions:
784784
return e.showContractActions(s.Name)
785+
case ast.ShowContractChannels:
786+
return e.showContractChannels(s.Name)
787+
case ast.ShowContractMessages:
788+
return e.showContractMessages(s.Name)
785789
default:
786790
return fmt.Errorf("unknown show object type")
787791
}
@@ -849,6 +853,8 @@ func (e *Executor) execDescribe(s *ast.DescribeStmt) error {
849853
return e.describeContractEntity(s.Name, s.Format)
850854
case ast.DescribeContractAction:
851855
return e.describeContractAction(s.Name, s.Format)
856+
case ast.DescribeContractMessage:
857+
return e.describeContractMessage(s.Name)
852858
default:
853859
return fmt.Errorf("unknown describe object type")
854860
}

mdl/grammar/MDLLexer.g4

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,8 @@ RULES: R U L E S;
527527
TEXT: T E X T;
528528
SARIF: S A R I F;
529529
MESSAGE: M E S S A G E;
530+
MESSAGES: M E S S A G E S;
531+
CHANNELS: C H A N N E L S;
530532
COMMENT: C O M M E N T;
531533
CATALOG: C A T A L O G;
532534
FORCE: F O R C E;

mdl/grammar/MDLParser.g4

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2440,6 +2440,8 @@ showStatement
24402440
: SHOW MODULES
24412441
| SHOW CONTRACT ENTITIES FROM qualifiedName // SHOW CONTRACT ENTITIES FROM Module.Service (must precede SHOW ENTITIES)
24422442
| SHOW CONTRACT ACTIONS FROM qualifiedName // SHOW CONTRACT ACTIONS FROM Module.Service
2443+
| SHOW CONTRACT CHANNELS FROM qualifiedName // SHOW CONTRACT CHANNELS FROM Module.Service (AsyncAPI)
2444+
| SHOW CONTRACT MESSAGES FROM qualifiedName // SHOW CONTRACT MESSAGES FROM Module.Service (AsyncAPI)
24432445
| SHOW ENTITIES (IN (qualifiedName | IDENTIFIER))?
24442446
| SHOW ASSOCIATIONS (IN (qualifiedName | IDENTIFIER))?
24452447
| SHOW MICROFLOWS (IN (qualifiedName | IDENTIFIER))?
@@ -2552,6 +2554,7 @@ widgetPropertyValue
25522554
describeStatement
25532555
: DESCRIBE CONTRACT ENTITY qualifiedName (FORMAT IDENTIFIER)? // DESCRIBE CONTRACT ENTITY Service.Entity [FORMAT mdl] (must precede DESCRIBE ENTITY)
25542556
| DESCRIBE CONTRACT ACTION qualifiedName (FORMAT IDENTIFIER)? // DESCRIBE CONTRACT ACTION Service.Action [FORMAT mdl]
2557+
| DESCRIBE CONTRACT MESSAGE qualifiedName // DESCRIBE CONTRACT MESSAGE Module.Service.MessageName
25552558
| DESCRIBE ENTITY qualifiedName
25562559
| DESCRIBE ASSOCIATION qualifiedName
25572560
| DESCRIBE MICROFLOW qualifiedName
@@ -3184,7 +3187,7 @@ keyword
31843187
| NAVIGATIONLIST | RADIOBUTTONS | SEARCHBAR | SNIPPETCALL | TEXTAREA | TEXTBOX
31853188
| IMAGE | STATICIMAGE | DYNAMICIMAGE | CUSTOMCONTAINER | GROUPBOX
31863189
| HEADER | FOOTER | IMAGEINPUT
3187-
| VERSION | TIMEOUT | PATH | PUBLISH | PUBLISHED | EXPOSE | NAMESPACE_KW | SOURCE_KW | CONTRACT // OData keywords
3190+
| VERSION | TIMEOUT | PATH | PUBLISH | PUBLISHED | EXPOSE | NAMESPACE_KW | SOURCE_KW | CONTRACT | CHANNELS | MESSAGES // OData/AsyncAPI keywords
31883191
| SESSION | GUEST | BASIC | AUTHENTICATION | ODATA | SERVICE | CLIENT | CLIENTS | SERVICES
31893192
| REST | PAGING | OPERATION | METHOD | BODY | RESPONSE | PARAMETER | PARAMETERS | HEADERS
31903193
| API | BASE | AUTH | OAUTH | JSON | XML | EXTERNAL | MAP | MAPPING | IMPORT | EXPORT

0 commit comments

Comments
 (0)