diff --git a/Makefile b/Makefile
index 18e10a2..4fb894c 100644
--- a/Makefile
+++ b/Makefile
@@ -3,7 +3,7 @@ BUILD_PATH=./bin
GOLANGCI_LINT=$(BUILD_PATH)/golangci-lint
GOLANGCI_LINT_VERSION=v2.1.6
-.PHONY: build clean test lint examples help docker
+.PHONY: build clean test lint examples single-page-mermaid-examples help docker
build: ## build app
$(GO) build -o $(BUILD_PATH)/messageflow ./cmd/messageflow
@@ -32,6 +32,12 @@ examples: ## create examples
--asyncapi-files pkg/schema/source/asyncapi/testdata/analytics_ver2.yaml,pkg/schema/source/asyncapi/testdata/campaign.yaml,pkg/schema/source/asyncapi/testdata/notification.yaml,pkg/schema/source/asyncapi/testdata/user.yaml \
--output examples/docs
+single-page-mermaid-examples: ## create single page mermaid examples
+ $(GO) run cmd/messageflow/main.go gen-docs \
+ --asyncapi-files pkg/schema/source/asyncapi/testdata/analytics.yaml,pkg/schema/source/asyncapi/testdata/campaign.yaml,pkg/schema/source/asyncapi/testdata/notification.yaml,pkg/schema/source/asyncapi/testdata/user.yaml \
+ --output examples/single-page-mermaid-docs \
+ --format mermaid
+
$(GOLANGCI_LINT): ## install local golangci-lint
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/$(GOLANGCI_LINT_VERSION)/install.sh | sh -s -- -b $(BUILD_PATH) $(GOLANGCI_LINT_VERSION)
diff --git a/cmd/messageflow/commands/docs/docs.go b/cmd/messageflow/commands/docs/docs.go
index bac5104..027978a 100644
--- a/cmd/messageflow/commands/docs/docs.go
+++ b/cmd/messageflow/commands/docs/docs.go
@@ -9,8 +9,10 @@ import (
"strings"
"github.com/holydocs/messageflow/internal/docs"
+ "github.com/holydocs/messageflow/pkg/messageflow"
"github.com/holydocs/messageflow/pkg/schema"
"github.com/holydocs/messageflow/pkg/schema/target/d2"
+ "github.com/holydocs/messageflow/pkg/schema/target/mermaid"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)
@@ -36,6 +38,7 @@ Example:
c.cmd.Flags().String("asyncapi-files", "", "Paths to asyncapi files separated by comma")
c.cmd.Flags().String("output", ".", "Output directory for generated documentation")
c.cmd.Flags().String("title", "Message Flow", "Title of the documentation")
+ c.cmd.Flags().String("format", "d2", "Diagram format: d2 or mermaid (default: d2)")
return c
}
@@ -65,6 +68,15 @@ func (c *Command) run(cmd *cobra.Command, _ []string) error {
return fmt.Errorf("error creating output directory %s: %w", outputDir, err)
}
+ format, err := cmd.Flags().GetString("format")
+ if err != nil {
+ return fmt.Errorf("error getting format flag: %w", err)
+ }
+
+ if format != "d2" && format != "mermaid" {
+ return fmt.Errorf("invalid format: %s (must be 'd2' or 'mermaid')", format)
+ }
+
ctx := context.Background()
s, err := schema.Load(ctx, asyncAPIFilesPaths)
@@ -72,12 +84,23 @@ func (c *Command) run(cmd *cobra.Command, _ []string) error {
return fmt.Errorf("error loading schema from files: %w", err)
}
- d2Target, err := d2.NewTarget()
- if err != nil {
- return fmt.Errorf("error creating D2 target: %w", err)
+ var target messageflow.Target
+ switch format {
+ case "d2":
+ d2Target, err := d2.NewTarget()
+ if err != nil {
+ return fmt.Errorf("error creating D2 target: %w", err)
+ }
+ target = d2Target
+ case "mermaid":
+ mermaidTarget, err := mermaid.NewTarget()
+ if err != nil {
+ return fmt.Errorf("error creating Mermaid target: %w", err)
+ }
+ target = mermaidTarget
}
- newChangelog, err := docs.Generate(ctx, s, d2Target, title, outputDir)
+ newChangelog, err := docs.Generate(ctx, s, target, title, outputDir)
if err != nil {
return fmt.Errorf("error generating documentation: %w", err)
}
diff --git a/examples/single-page-mermaid-docs/README.md b/examples/single-page-mermaid-docs/README.md
new file mode 100644
index 0000000..853214e
--- /dev/null
+++ b/examples/single-page-mermaid-docs/README.md
@@ -0,0 +1,762 @@
+# Message Flow
+
+## Table of Contents
+
+- [Context](#context)
+- [Services](#services)
+ - [Analytics Service](#analytics-service)
+ - [Campaign Service](#campaign-service)
+ - [Notification Service](#notification-service)
+ - [User Service](#user-service)
+- [Channels](#channels)
+ - [analytics.alert](#analyticsalert)
+ - [analytics.insights](#analyticsinsights)
+ - [analytics.report.request](#analyticsreportrequest)
+ - [campaign.analytics](#campaignanalytics)
+ - [campaign.create](#campaigncreate)
+ - [campaign.execute](#campaignexecute)
+ - [campaign.status](#campaignstatus)
+ - [notification.analytics](#notificationanalytics)
+ - [notification.preferences.get](#notificationpreferencesget)
+ - [notification.preferences.update](#notificationpreferencesupdate)
+ - [notification.user.{user_id}.push](#notificationuseruser_idpush)
+ - [user.analytics](#useranalytics)
+ - [user.info.request](#userinforequest)
+ - [user.info.update](#userinfoupdate)
+
+## Context
+```mermaid
+flowchart TD
+ Analytics_Service["Analytics Service
A centralized analytics service that receives and processes analytics events from all other services.
+Provides insights, reporting, and analytics data aggregation for user behavior, notification performance,
+campaign effectiveness, and system-wide metrics.
+"]
+ Campaign_Service["Campaign Service
A service that manages notification campaigns, user targeting, and campaign execution.
+Handles campaign creation, user segmentation, scheduling, and personalized notification delivery.
+Uses user data for targeting and personalization of campaign messages.
+"]
+ Notification_Service["Notification Service
A service that handles user notifications, preferences, and interactions.
+Supports real-time notifications, user preferences management.
+"]
+ User_Service["User Service
A service that manages user information, profiles, and authentication.
+Handles user data requests, profile updates, and user lifecycle events.
+"]
+ Campaign_Service -->|"Pub"| Analytics_Service
+ Campaign_Service -->|"Pub"| Notification_Service
+ Campaign_Service -->|"Req"| User_Service
+ Notification_Service -->|"Pub"| Analytics_Service
+ Notification_Service <-->|"Pub/Req"| User_Service
+ User_Service -->|"Pub"| Analytics_Service
+ style Analytics_Service fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+ style Campaign_Service fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+ style Notification_Service fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+ style User_Service fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+
+```
+
+## Services
+
+### Analytics Service
+
+A centralized analytics service that receives and processes analytics events from all other services.
+Provides insights, reporting, and analytics data aggregation for user behavior, notification performance,
+campaign effectiveness, and system-wide metrics.
+
+```mermaid
+flowchart TD
+ Analytics_Service["Analytics Service"]
+ analyticsreportrequest@{ shape: das, label: "analytics.report.request" }
+ analyticsreportrequest -->|"Reply"| Analytics_Service
+ campaignanalytics@{ shape: das, label: "campaign.analytics" }
+ campaignanalytics -->|"Receive"| Analytics_Service
+ notificationanalytics@{ shape: das, label: "notification.analytics" }
+ notificationanalytics -->|"Receive"| Analytics_Service
+ useranalytics@{ shape: das, label: "user.analytics" }
+ useranalytics -->|"Receive"| Analytics_Service
+ analyticsalert@{ shape: das, label: "analytics.alert" }
+ Analytics_Service -->|"Send"| analyticsalert
+ analyticsinsights@{ shape: das, label: "analytics.insights" }
+ Analytics_Service -->|"Send"| analyticsinsights
+ Campaign_Service["Campaign Service"]
+ Campaign_Service --> campaignanalytics
+ Notification_Service["Notification Service"]
+ Notification_Service --> notificationanalytics
+ User_Service["User Service"]
+ User_Service --> useranalytics
+
+ style Analytics_Service fill:#3498db,stroke:#2980b9,stroke-width:2px,color:#fff
+ style Campaign_Service fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+ style Notification_Service fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+ style User_Service fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+
+```
+
+### Campaign Service
+
+A service that manages notification campaigns, user targeting, and campaign execution.
+Handles campaign creation, user segmentation, scheduling, and personalized notification delivery.
+Uses user data for targeting and personalization of campaign messages.
+
+```mermaid
+flowchart TD
+ Campaign_Service["Campaign Service"]
+ campaigncreate@{ shape: das, label: "campaign.create" }
+ campaigncreate -->|"Receive"| Campaign_Service
+ campaignexecute@{ shape: das, label: "campaign.execute" }
+ campaignexecute -->|"Receive"| Campaign_Service
+ campaignanalytics@{ shape: das, label: "campaign.analytics" }
+ Campaign_Service -->|"Send"| campaignanalytics
+ campaignstatus@{ shape: das, label: "campaign.status" }
+ Campaign_Service -->|"Send"| campaignstatus
+ notificationuseruser_idpush@{ shape: das, label: "notification.user.{user_id}.push" }
+ Campaign_Service -->|"Send"| notificationuseruser_idpush
+ userinforequest@{ shape: das, label: "user.info.request" }
+ Campaign_Service -->|"Request"| userinforequest
+ Analytics_Service["Analytics Service"]
+ campaignanalytics --> Analytics_Service
+ Notification_Service["Notification Service"]
+ notificationuseruser_idpush --> Notification_Service
+ User_Service["User Service"]
+ userinforequest --> User_Service
+
+ style Campaign_Service fill:#3498db,stroke:#2980b9,stroke-width:2px,color:#fff
+ style Analytics_Service fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+ style Notification_Service fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+ style User_Service fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+
+```
+
+### Notification Service
+
+A service that handles user notifications, preferences, and interactions.
+Supports real-time notifications, user preferences management.
+
+```mermaid
+flowchart TD
+ Notification_Service["Notification Service"]
+ notificationpreferencesget@{ shape: das, label: "notification.preferences.get" }
+ notificationpreferencesget -->|"Reply"| Notification_Service
+ notificationpreferencesupdate@{ shape: das, label: "notification.preferences.update" }
+ notificationpreferencesupdate -->|"Receive"| Notification_Service
+ notificationuseruser_idpush@{ shape: das, label: "notification.user.{user_id}.push" }
+ notificationuseruser_idpush -->|"Receive"| Notification_Service
+ notificationanalytics@{ shape: das, label: "notification.analytics" }
+ Notification_Service -->|"Send"| notificationanalytics
+ userinforequest@{ shape: das, label: "user.info.request" }
+ Notification_Service -->|"Request"| userinforequest
+ Analytics_Service["Analytics Service"]
+ notificationanalytics --> Analytics_Service
+ Campaign_Service["Campaign Service"]
+ Campaign_Service --> notificationuseruser_idpush
+ User_Service["User Service"]
+ userinforequest --> User_Service
+ User_Service --> notificationpreferencesupdate
+
+ style Notification_Service fill:#3498db,stroke:#2980b9,stroke-width:2px,color:#fff
+ style Analytics_Service fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+ style Campaign_Service fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+ style User_Service fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+
+```
+
+### User Service
+
+A service that manages user information, profiles, and authentication.
+Handles user data requests, profile updates, and user lifecycle events.
+
+```mermaid
+flowchart TD
+ User_Service["User Service"]
+ userinforequest@{ shape: das, label: "user.info.request" }
+ userinforequest -->|"Reply"| User_Service
+ notificationpreferencesupdate@{ shape: das, label: "notification.preferences.update" }
+ User_Service -->|"Send"| notificationpreferencesupdate
+ useranalytics@{ shape: das, label: "user.analytics" }
+ User_Service -->|"Send"| useranalytics
+ userinfoupdate@{ shape: das, label: "user.info.update" }
+ User_Service -->|"Send"| userinfoupdate
+ Analytics_Service["Analytics Service"]
+ useranalytics --> Analytics_Service
+ Campaign_Service["Campaign Service"]
+ Campaign_Service --> userinforequest
+ Notification_Service["Notification Service"]
+ notificationpreferencesupdate --> Notification_Service
+ Notification_Service --> userinforequest
+
+ style User_Service fill:#3498db,stroke:#2980b9,stroke-width:2px,color:#fff
+ style Analytics_Service fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+ style Campaign_Service fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+ style Notification_Service fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+
+```
+
+## Channels
+
+### analytics.alert
+```mermaid
+flowchart LR
+ analyticsalert@{ shape: das, label: "analytics.alert" }
+ Analytics_Service["Analytics Service"]
+ Analytics_Service -->|"Send"| analyticsalert
+ style Analytics_Service fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+
+```
+
+#### Messages
+**AnalyticsAlertMessage**
+```json
+{
+ "actions": [
+ "string"
+ ],
+ "affected_services": [
+ "string[enum:user_service,notification_service,campaign_service]"
+ ],
+ "alert_id": "string[uuid]",
+ "alert_type": "string[enum:anomaly_detected,threshold_exceeded,trend_change,system_issue]",
+ "created_at": "string[date-time]",
+ "current_value": "number",
+ "description": "string",
+ "metadata": {
+ "environment": "string[enum:development,staging,production]",
+ "platform": "string[enum:ios,android,web]",
+ "source": "string[enum:mobile,web,api]",
+ "version": "string"
+ },
+ "metric": "string",
+ "severity": "string[enum:low,medium,high,critical]",
+ "threshold": "number",
+ "time_window": "string",
+ "title": "string"
+}
+```
+
+### analytics.insights
+```mermaid
+flowchart LR
+ analyticsinsights@{ shape: das, label: "analytics.insights" }
+ Analytics_Service["Analytics Service"]
+ Analytics_Service -->|"Send"| analyticsinsights
+ style Analytics_Service fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+
+```
+
+#### Messages
+**AnalyticsInsightMessage**
+```json
+{
+ "category": "string[enum:user_behavior,notification_performance,campaign_effectiveness,system_health]",
+ "confidence": "number[float]",
+ "created_at": "string[date-time]",
+ "data_points": [
+ "object"
+ ],
+ "description": "string",
+ "insight_id": "string[uuid]",
+ "insight_type": "string[enum:trend,anomaly,recommendation,alert]",
+ "metadata": {
+ "environment": "string[enum:development,staging,production]",
+ "platform": "string[enum:ios,android,web]",
+ "source": "string[enum:mobile,web,api]",
+ "version": "string"
+ },
+ "recommendations": [
+ "string"
+ ],
+ "severity": "string[enum:low,medium,high,critical]",
+ "title": "string"
+}
+```
+
+### analytics.report.request
+```mermaid
+flowchart LR
+ analyticsreportrequest@{ shape: das, label: "analytics.report.request" }
+ Analytics_Service["Analytics Service"]
+ analyticsreportrequest -->|"Reply"| Analytics_Service
+ style Analytics_Service fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+
+```
+
+#### Messages
+**request**: AnalyticsReportRequestMessage
+```json
+{
+ "created_at": "string[date-time]",
+ "filters": {
+ "campaign_ids": [
+ "string[uuid]"
+ ],
+ "event_types": [
+ "string"
+ ],
+ "user_ids": [
+ "string[uuid]"
+ ],
+ "user_segments": [
+ "string[enum:all_users,new_users,active_users,inactive_users,premium_users,free_users]"
+ ]
+ },
+ "format": "string[enum:json,csv,pdf]",
+ "metrics": [
+ "string[enum:event_count,user_count,conversion_rate,engagement_rate,response_time,error_rate]"
+ ],
+ "report_id": "string[uuid]",
+ "report_type": "string[enum:user_activity,notification_performance,campaign_effectiveness,system_health,custom]",
+ "time_range": {
+ "end": "string[date-time]",
+ "granularity": "string[enum:minute,hour,day,week,month]",
+ "start": "string[date-time]"
+ }
+}
+```
+**reply**: AnalyticsReportReplyMessage
+```json
+{
+ "data": "object",
+ "error": {
+ "code": "string",
+ "message": "string"
+ },
+ "generated_at": "string[date-time]",
+ "insights": [
+ {
+ "confidence": "number[float]",
+ "data_points": [
+ "object"
+ ],
+ "description": "string",
+ "impact": "string[enum:low,medium,high]",
+ "title": "string",
+ "type": "string[enum:trend,anomaly,correlation,recommendation]"
+ }
+ ],
+ "report_id": "string[uuid]",
+ "report_type": "string[enum:user_activity,notification_performance,campaign_effectiveness,system_health,custom]",
+ "summary": {
+ "event_types": "object",
+ "top_metrics": {
+ "conversion_rate": "number[float]",
+ "engagement_rate": "number[float]",
+ "error_rate": "number[float]",
+ "response_time_avg": "number[float]"
+ },
+ "total_events": "integer",
+ "unique_users": "integer"
+ },
+ "time_range": {
+ "end": "string[date-time]",
+ "granularity": "string[enum:minute,hour,day,week,month]",
+ "start": "string[date-time]"
+ }
+}
+```
+
+### campaign.analytics
+```mermaid
+flowchart LR
+ campaignanalytics@{ shape: das, label: "campaign.analytics" }
+ Campaign_Service["Campaign Service"]
+ Campaign_Service -->|"Send"| campaignanalytics
+ Analytics_Service["Analytics Service"]
+ campaignanalytics -->|"Receive"| Analytics_Service
+ style Campaign_Service fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+ style Analytics_Service fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+
+```
+
+#### Messages
+**CampaignAnalyticsEventMessage**
+```json
+{
+ "campaign_id": "string[uuid]",
+ "event_id": "string[uuid]",
+ "event_type": "string[enum:campaign_created,campaign_executed,notification_sent,notification_opened,notification_clicked,campaign_completed,campaign_failed]",
+ "execution_id": "string[uuid]",
+ "metadata": {
+ "environment": "string[enum:development,staging,production]",
+ "platform": "string[enum:ios,android,web]",
+ "source": "string[enum:mobile,web,api]",
+ "version": "string"
+ },
+ "notification_id": "string[uuid]",
+ "timestamp": "string[date-time]",
+ "user_id": "string[uuid]"
+}
+```
+
+### campaign.create
+```mermaid
+flowchart LR
+ campaigncreate@{ shape: das, label: "campaign.create" }
+ Campaign_Service["Campaign Service"]
+ campaigncreate -->|"Receive"| Campaign_Service
+ style Campaign_Service fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+
+```
+
+#### Messages
+**CampaignCreateMessage**
+```json
+{
+ "campaign_id": "string[uuid]",
+ "created_at": "string[date-time]",
+ "description": "string",
+ "metadata": {
+ "environment": "string[enum:development,staging,production]",
+ "platform": "string[enum:ios,android,web]",
+ "source": "string[enum:mobile,web,api]",
+ "version": "string"
+ },
+ "name": "string",
+ "notification_template": {
+ "body_template": "string",
+ "data": "object",
+ "localization": "object",
+ "priority": "string[enum:low,normal,high]",
+ "title_template": "string"
+ },
+ "schedule": {
+ "recurring": {
+ "end_date": "string[date]",
+ "frequency": "string[enum:daily,weekly,monthly]",
+ "interval": "integer",
+ "start_date": "string[date]"
+ },
+ "scheduled_at": "string[date-time]",
+ "timezone": "string",
+ "type": "string[enum:immediate,scheduled,recurring]"
+ },
+ "settings": {
+ "a_b_testing": {
+ "enabled": "boolean",
+ "traffic_split": [
+ "number"
+ ],
+ "variants": [
+ {
+ "body_template": "string",
+ "data": "object",
+ "localization": "object",
+ "priority": "string[enum:low,normal,high]",
+ "title_template": "string"
+ }
+ ]
+ },
+ "batch_size": "integer",
+ "max_retries": "integer",
+ "rate_limit": "integer",
+ "respect_quiet_hours": "boolean"
+ },
+ "target_audience": {
+ "estimated_reach": "integer",
+ "user_filters": {
+ "language": [
+ "string"
+ ],
+ "last_activity": {
+ "from": "string[date-time]",
+ "to": "string[date-time]"
+ },
+ "registration_date": {
+ "from": "string[date]",
+ "to": "string[date]"
+ },
+ "timezone": [
+ "string"
+ ]
+ },
+ "user_segments": [
+ "string[enum:all_users,new_users,active_users,inactive_users,premium_users,free_users]"
+ ]
+ }
+}
+```
+
+### campaign.execute
+```mermaid
+flowchart LR
+ campaignexecute@{ shape: das, label: "campaign.execute" }
+ Campaign_Service["Campaign Service"]
+ campaignexecute -->|"Receive"| Campaign_Service
+ style Campaign_Service fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+
+```
+
+#### Messages
+**CampaignExecuteMessage**
+```json
+{
+ "batch_size": "integer",
+ "campaign_id": "string[uuid]",
+ "created_at": "string[date-time]",
+ "execution_id": "string[uuid]",
+ "execution_type": "string[enum:immediate,scheduled,batch]",
+ "metadata": {
+ "environment": "string[enum:development,staging,production]",
+ "platform": "string[enum:ios,android,web]",
+ "source": "string[enum:mobile,web,api]",
+ "version": "string"
+ },
+ "priority": "string[enum:low,normal,high]"
+}
+```
+
+### campaign.status
+```mermaid
+flowchart LR
+ campaignstatus@{ shape: das, label: "campaign.status" }
+ Campaign_Service["Campaign Service"]
+ Campaign_Service -->|"Send"| campaignstatus
+ style Campaign_Service fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+
+```
+
+#### Messages
+**CampaignStatusUpdateMessage**
+```json
+{
+ "campaign_id": "string[uuid]",
+ "error": {
+ "code": "string",
+ "message": "string"
+ },
+ "execution_id": "string[uuid]",
+ "progress": {
+ "failed": "integer",
+ "sent": "integer",
+ "success_rate": "number[float]",
+ "total_targets": "integer"
+ },
+ "status": "string[enum:pending,running,completed,failed,paused,cancelled]",
+ "updated_at": "string[date-time]"
+}
+```
+
+### notification.analytics
+```mermaid
+flowchart LR
+ notificationanalytics@{ shape: das, label: "notification.analytics" }
+ Notification_Service["Notification Service"]
+ Notification_Service -->|"Send"| notificationanalytics
+ Analytics_Service["Analytics Service"]
+ notificationanalytics -->|"Receive"| Analytics_Service
+ style Notification_Service fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+ style Analytics_Service fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+
+```
+
+#### Messages
+**NotificationAnalyticsEventMessage**
+```json
+{
+ "event_id": "string[uuid]",
+ "event_type": "string[enum:notification_sent,notification_opened,notification_clicked]",
+ "metadata": {
+ "environment": "string[enum:development,staging,production]",
+ "platform": "string[enum:ios,android,web]",
+ "source": "string[enum:mobile,web,api]",
+ "version": "string"
+ },
+ "notification_id": "string[uuid]",
+ "timestamp": "string[date-time]",
+ "user_id": "string[uuid]"
+}
+```
+
+### notification.preferences.get
+```mermaid
+flowchart LR
+ notificationpreferencesget@{ shape: das, label: "notification.preferences.get" }
+ Notification_Service["Notification Service"]
+ notificationpreferencesget -->|"Reply"| Notification_Service
+ style Notification_Service fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+
+```
+
+#### Messages
+**request**: PreferencesRequestMessage
+```json
+{
+ "user_id": "string[uuid]"
+}
+```
+**reply**: PreferencesReplyMessage
+```json
+{
+ "preferences": {
+ "categories": {
+ "marketing": "boolean",
+ "security": "boolean",
+ "updates": "boolean"
+ },
+ "email_enabled": "boolean",
+ "push_enabled": "boolean",
+ "quiet_hours": {
+ "enabled": "boolean",
+ "end": "string[time]",
+ "start": "string[time]"
+ },
+ "sms_enabled": "boolean"
+ },
+ "updated_at": "string[date-time]"
+}
+```
+
+### notification.preferences.update
+```mermaid
+flowchart LR
+ notificationpreferencesupdate@{ shape: das, label: "notification.preferences.update" }
+ User_Service["User Service"]
+ User_Service -->|"Send"| notificationpreferencesupdate
+ Notification_Service["Notification Service"]
+ notificationpreferencesupdate -->|"Receive"| Notification_Service
+ style User_Service fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+ style Notification_Service fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+
+```
+
+#### Messages
+**PreferencesUpdateMessage**
+```json
+{
+ "preferences": {
+ "categories": {
+ "marketing": "boolean",
+ "security": "boolean",
+ "updates": "boolean"
+ },
+ "email_enabled": "boolean",
+ "push_enabled": "boolean",
+ "quiet_hours": {
+ "enabled": "boolean",
+ "end": "string[time]",
+ "start": "string[time]"
+ },
+ "sms_enabled": "boolean"
+ },
+ "updated_at": "string[date-time]",
+ "user_id": "string[uuid]"
+}
+```
+
+### notification.user.{user_id}.push
+```mermaid
+flowchart LR
+ notificationuseruser_idpush@{ shape: das, label: "notification.user.{user_id}.push" }
+ Campaign_Service["Campaign Service"]
+ Campaign_Service -->|"Send"| notificationuseruser_idpush
+ Notification_Service["Notification Service"]
+ notificationuseruser_idpush -->|"Receive"| Notification_Service
+ style Campaign_Service fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+ style Notification_Service fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+
+```
+
+#### Messages
+**PushNotificationMessage**
+```json
+{
+ "body": "string",
+ "created_at": "string[date-time]",
+ "data": "object",
+ "notification_id": "string[uuid]",
+ "priority": "string[enum:low,normal,high]",
+ "title": "string",
+ "user_id": "string[uuid]"
+}
+```
+
+### user.analytics
+```mermaid
+flowchart LR
+ useranalytics@{ shape: das, label: "user.analytics" }
+ User_Service["User Service"]
+ User_Service -->|"Send"| useranalytics
+ Analytics_Service["Analytics Service"]
+ useranalytics -->|"Receive"| Analytics_Service
+ style User_Service fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+ style Analytics_Service fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+
+```
+
+#### Messages
+**UserAnalyticsEventMessage**
+```json
+{
+ "event_id": "string[uuid]",
+ "event_type": "string[enum:user_registered,user_logged_in,profile_updated,preferences_changed,account_deleted]",
+ "metadata": {
+ "environment": "string[enum:development,staging,production]",
+ "platform": "string[enum:ios,android,web]",
+ "source": "string[enum:mobile,web,api]",
+ "version": "string"
+ },
+ "timestamp": "string[date-time]",
+ "user_id": "string[uuid]"
+}
+```
+
+### user.info.request
+```mermaid
+flowchart LR
+ userinforequest@{ shape: das, label: "user.info.request" }
+ Campaign_Service["Campaign Service"]
+ Campaign_Service -->|"Request"| userinforequest
+ Notification_Service["Notification Service"]
+ Notification_Service -->|"Request"| userinforequest
+ User_Service["User Service"]
+ userinforequest -->|"Reply"| User_Service
+ style Campaign_Service fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+ style Notification_Service fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+ style User_Service fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+
+```
+
+#### Messages
+**request**: UserInfoRequestMessage
+```json
+{
+ "user_id": "string[uuid]"
+}
+```
+**reply**: UserInfoReplyMessage
+```json
+{
+ "email": "string[email]",
+ "error": {
+ "code": "string",
+ "message": "string"
+ },
+ "language": "string",
+ "name": "string",
+ "timezone": "string",
+ "user_id": "string[uuid]"
+}
+```
+
+### user.info.update
+```mermaid
+flowchart LR
+ userinfoupdate@{ shape: das, label: "user.info.update" }
+ User_Service["User Service"]
+ User_Service -->|"Send"| userinfoupdate
+ style User_Service fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+
+```
+
+#### Messages
+**UserInfoUpdateMessage**
+```json
+{
+ "changes": "object",
+ "metadata": {
+ "environment": "string[enum:development,staging,production]",
+ "platform": "string[enum:ios,android,web]",
+ "source": "string[enum:mobile,web,api]",
+ "version": "string"
+ },
+ "updated_at": "string[date-time]",
+ "user_id": "string[uuid]"
+}
+```
diff --git a/examples/single-page-mermaid-docs/messageflow.json b/examples/single-page-mermaid-docs/messageflow.json
new file mode 100644
index 0000000..6ee57a2
--- /dev/null
+++ b/examples/single-page-mermaid-docs/messageflow.json
@@ -0,0 +1,328 @@
+{
+ "schema": {
+ "services": [
+ {
+ "name": "Analytics Service",
+ "description": "A centralized analytics service that receives and processes analytics events from all other services.\nProvides insights, reporting, and analytics data aggregation for user behavior, notification performance,\ncampaign effectiveness, and system-wide metrics.\n",
+ "operations": [
+ {
+ "action": "receive",
+ "channel": {
+ "name": "analytics.report.request",
+ "messages": [
+ {
+ "name": "AnalyticsReportRequestMessage",
+ "payload": "{\n \"created_at\": \"string[date-time]\",\n \"filters\": {\n \"campaign_ids\": [\n \"string[uuid]\"\n ],\n \"event_types\": [\n \"string\"\n ],\n \"user_ids\": [\n \"string[uuid]\"\n ],\n \"user_segments\": [\n \"string[enum:all_users,new_users,active_users,inactive_users,premium_users,free_users]\"\n ]\n },\n \"format\": \"string[enum:json,csv,pdf]\",\n \"metrics\": [\n \"string[enum:event_count,user_count,conversion_rate,engagement_rate,response_time,error_rate]\"\n ],\n \"report_id\": \"string[uuid]\",\n \"report_type\": \"string[enum:user_activity,notification_performance,campaign_effectiveness,system_health,custom]\",\n \"time_range\": {\n \"end\": \"string[date-time]\",\n \"granularity\": \"string[enum:minute,hour,day,week,month]\",\n \"start\": \"string[date-time]\"\n }\n}"
+ }
+ ]
+ },
+ "reply": {
+ "name": "analytics.report.request",
+ "messages": [
+ {
+ "name": "AnalyticsReportReplyMessage",
+ "payload": "{\n \"data\": \"object\",\n \"error\": {\n \"code\": \"string\",\n \"message\": \"string\"\n },\n \"generated_at\": \"string[date-time]\",\n \"insights\": [\n {\n \"confidence\": \"number[float]\",\n \"data_points\": [\n \"object\"\n ],\n \"description\": \"string\",\n \"impact\": \"string[enum:low,medium,high]\",\n \"title\": \"string\",\n \"type\": \"string[enum:trend,anomaly,correlation,recommendation]\"\n }\n ],\n \"report_id\": \"string[uuid]\",\n \"report_type\": \"string[enum:user_activity,notification_performance,campaign_effectiveness,system_health,custom]\",\n \"summary\": {\n \"event_types\": \"object\",\n \"top_metrics\": {\n \"conversion_rate\": \"number[float]\",\n \"engagement_rate\": \"number[float]\",\n \"error_rate\": \"number[float]\",\n \"response_time_avg\": \"number[float]\"\n },\n \"total_events\": \"integer\",\n \"unique_users\": \"integer\"\n },\n \"time_range\": {\n \"end\": \"string[date-time]\",\n \"granularity\": \"string[enum:minute,hour,day,week,month]\",\n \"start\": \"string[date-time]\"\n }\n}"
+ }
+ ]
+ }
+ },
+ {
+ "action": "receive",
+ "channel": {
+ "name": "campaign.analytics",
+ "messages": [
+ {
+ "name": "CampaignAnalyticsEventMessage",
+ "payload": "{\n \"campaign_id\": \"string[uuid]\",\n \"event_id\": \"string[uuid]\",\n \"event_type\": \"string[enum:campaign_created,campaign_executed,notification_sent,notification_opened,notification_clicked,campaign_completed,campaign_failed]\",\n \"execution_id\": \"string[uuid]\",\n \"metadata\": {\n \"environment\": \"string[enum:development,staging,production]\",\n \"platform\": \"string[enum:ios,android,web]\",\n \"source\": \"string[enum:mobile,web,api]\",\n \"version\": \"string\"\n },\n \"notification_id\": \"string[uuid]\",\n \"timestamp\": \"string[date-time]\",\n \"user_id\": \"string[uuid]\"\n}"
+ }
+ ]
+ }
+ },
+ {
+ "action": "receive",
+ "channel": {
+ "name": "notification.analytics",
+ "messages": [
+ {
+ "name": "NotificationAnalyticsEventMessage",
+ "payload": "{\n \"event_id\": \"string[uuid]\",\n \"event_type\": \"string[enum:notification_sent,notification_opened,notification_clicked]\",\n \"metadata\": {\n \"environment\": \"string[enum:development,staging,production]\",\n \"platform\": \"string[enum:ios,android,web]\",\n \"source\": \"string[enum:mobile,web,api]\",\n \"version\": \"string\"\n },\n \"notification_id\": \"string[uuid]\",\n \"timestamp\": \"string[date-time]\",\n \"user_id\": \"string[uuid]\"\n}"
+ }
+ ]
+ }
+ },
+ {
+ "action": "receive",
+ "channel": {
+ "name": "user.analytics",
+ "messages": [
+ {
+ "name": "UserAnalyticsEventMessage",
+ "payload": "{\n \"event_id\": \"string[uuid]\",\n \"event_type\": \"string[enum:user_registered,user_logged_in,profile_updated,preferences_changed,account_deleted]\",\n \"metadata\": {\n \"environment\": \"string[enum:development,staging,production]\",\n \"platform\": \"string[enum:ios,android,web]\",\n \"source\": \"string[enum:mobile,web,api]\",\n \"version\": \"string\"\n },\n \"timestamp\": \"string[date-time]\",\n \"user_id\": \"string[uuid]\"\n}"
+ }
+ ]
+ }
+ },
+ {
+ "action": "send",
+ "channel": {
+ "name": "analytics.alert",
+ "messages": [
+ {
+ "name": "AnalyticsAlertMessage",
+ "payload": "{\n \"actions\": [\n \"string\"\n ],\n \"affected_services\": [\n \"string[enum:user_service,notification_service,campaign_service]\"\n ],\n \"alert_id\": \"string[uuid]\",\n \"alert_type\": \"string[enum:anomaly_detected,threshold_exceeded,trend_change,system_issue]\",\n \"created_at\": \"string[date-time]\",\n \"current_value\": \"number\",\n \"description\": \"string\",\n \"metadata\": {\n \"environment\": \"string[enum:development,staging,production]\",\n \"platform\": \"string[enum:ios,android,web]\",\n \"source\": \"string[enum:mobile,web,api]\",\n \"version\": \"string\"\n },\n \"metric\": \"string\",\n \"severity\": \"string[enum:low,medium,high,critical]\",\n \"threshold\": \"number\",\n \"time_window\": \"string\",\n \"title\": \"string\"\n}"
+ }
+ ]
+ }
+ },
+ {
+ "action": "send",
+ "channel": {
+ "name": "analytics.insights",
+ "messages": [
+ {
+ "name": "AnalyticsInsightMessage",
+ "payload": "{\n \"category\": \"string[enum:user_behavior,notification_performance,campaign_effectiveness,system_health]\",\n \"confidence\": \"number[float]\",\n \"created_at\": \"string[date-time]\",\n \"data_points\": [\n \"object\"\n ],\n \"description\": \"string\",\n \"insight_id\": \"string[uuid]\",\n \"insight_type\": \"string[enum:trend,anomaly,recommendation,alert]\",\n \"metadata\": {\n \"environment\": \"string[enum:development,staging,production]\",\n \"platform\": \"string[enum:ios,android,web]\",\n \"source\": \"string[enum:mobile,web,api]\",\n \"version\": \"string\"\n },\n \"recommendations\": [\n \"string\"\n ],\n \"severity\": \"string[enum:low,medium,high,critical]\",\n \"title\": \"string\"\n}"
+ }
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "name": "Campaign Service",
+ "description": "A service that manages notification campaigns, user targeting, and campaign execution.\nHandles campaign creation, user segmentation, scheduling, and personalized notification delivery.\nUses user data for targeting and personalization of campaign messages.\n",
+ "operations": [
+ {
+ "action": "receive",
+ "channel": {
+ "name": "campaign.create",
+ "messages": [
+ {
+ "name": "CampaignCreateMessage",
+ "payload": "{\n \"campaign_id\": \"string[uuid]\",\n \"created_at\": \"string[date-time]\",\n \"description\": \"string\",\n \"metadata\": {\n \"environment\": \"string[enum:development,staging,production]\",\n \"platform\": \"string[enum:ios,android,web]\",\n \"source\": \"string[enum:mobile,web,api]\",\n \"version\": \"string\"\n },\n \"name\": \"string\",\n \"notification_template\": {\n \"body_template\": \"string\",\n \"data\": \"object\",\n \"localization\": \"object\",\n \"priority\": \"string[enum:low,normal,high]\",\n \"title_template\": \"string\"\n },\n \"schedule\": {\n \"recurring\": {\n \"end_date\": \"string[date]\",\n \"frequency\": \"string[enum:daily,weekly,monthly]\",\n \"interval\": \"integer\",\n \"start_date\": \"string[date]\"\n },\n \"scheduled_at\": \"string[date-time]\",\n \"timezone\": \"string\",\n \"type\": \"string[enum:immediate,scheduled,recurring]\"\n },\n \"settings\": {\n \"a_b_testing\": {\n \"enabled\": \"boolean\",\n \"traffic_split\": [\n \"number\"\n ],\n \"variants\": [\n {\n \"body_template\": \"string\",\n \"data\": \"object\",\n \"localization\": \"object\",\n \"priority\": \"string[enum:low,normal,high]\",\n \"title_template\": \"string\"\n }\n ]\n },\n \"batch_size\": \"integer\",\n \"max_retries\": \"integer\",\n \"rate_limit\": \"integer\",\n \"respect_quiet_hours\": \"boolean\"\n },\n \"target_audience\": {\n \"estimated_reach\": \"integer\",\n \"user_filters\": {\n \"language\": [\n \"string\"\n ],\n \"last_activity\": {\n \"from\": \"string[date-time]\",\n \"to\": \"string[date-time]\"\n },\n \"registration_date\": {\n \"from\": \"string[date]\",\n \"to\": \"string[date]\"\n },\n \"timezone\": [\n \"string\"\n ]\n },\n \"user_segments\": [\n \"string[enum:all_users,new_users,active_users,inactive_users,premium_users,free_users]\"\n ]\n }\n}"
+ }
+ ]
+ }
+ },
+ {
+ "action": "receive",
+ "channel": {
+ "name": "campaign.execute",
+ "messages": [
+ {
+ "name": "CampaignExecuteMessage",
+ "payload": "{\n \"batch_size\": \"integer\",\n \"campaign_id\": \"string[uuid]\",\n \"created_at\": \"string[date-time]\",\n \"execution_id\": \"string[uuid]\",\n \"execution_type\": \"string[enum:immediate,scheduled,batch]\",\n \"metadata\": {\n \"environment\": \"string[enum:development,staging,production]\",\n \"platform\": \"string[enum:ios,android,web]\",\n \"source\": \"string[enum:mobile,web,api]\",\n \"version\": \"string\"\n },\n \"priority\": \"string[enum:low,normal,high]\"\n}"
+ }
+ ]
+ }
+ },
+ {
+ "action": "send",
+ "channel": {
+ "name": "campaign.analytics",
+ "messages": [
+ {
+ "name": "CampaignAnalyticsEventMessage",
+ "payload": "{\n \"campaign_id\": \"string[uuid]\",\n \"event_id\": \"string[uuid]\",\n \"event_type\": \"string[enum:campaign_created,campaign_executed,notification_sent,notification_opened,notification_clicked,campaign_completed,campaign_failed]\",\n \"execution_id\": \"string[uuid]\",\n \"metadata\": {\n \"environment\": \"string[enum:development,staging,production]\",\n \"platform\": \"string[enum:ios,android,web]\",\n \"source\": \"string[enum:mobile,web,api]\",\n \"version\": \"string\"\n },\n \"notification_id\": \"string[uuid]\",\n \"timestamp\": \"string[date-time]\",\n \"user_id\": \"string[uuid]\"\n}"
+ }
+ ]
+ }
+ },
+ {
+ "action": "send",
+ "channel": {
+ "name": "campaign.status",
+ "messages": [
+ {
+ "name": "CampaignStatusUpdateMessage",
+ "payload": "{\n \"campaign_id\": \"string[uuid]\",\n \"error\": {\n \"code\": \"string\",\n \"message\": \"string\"\n },\n \"execution_id\": \"string[uuid]\",\n \"progress\": {\n \"failed\": \"integer\",\n \"sent\": \"integer\",\n \"success_rate\": \"number[float]\",\n \"total_targets\": \"integer\"\n },\n \"status\": \"string[enum:pending,running,completed,failed,paused,cancelled]\",\n \"updated_at\": \"string[date-time]\"\n}"
+ }
+ ]
+ }
+ },
+ {
+ "action": "send",
+ "channel": {
+ "name": "notification.user.{user_id}.push",
+ "messages": [
+ {
+ "name": "PushNotificationMessage",
+ "payload": "{\n \"body\": \"string\",\n \"created_at\": \"string[date-time]\",\n \"data\": \"object\",\n \"notification_id\": \"string[uuid]\",\n \"priority\": \"string[enum:low,normal,high]\",\n \"title\": \"string\",\n \"user_id\": \"string[uuid]\"\n}"
+ }
+ ]
+ }
+ },
+ {
+ "action": "send",
+ "channel": {
+ "name": "user.info.request",
+ "messages": [
+ {
+ "name": "UserInfoRequestMessage",
+ "payload": "{\n \"user_id\": \"string[uuid]\"\n}"
+ }
+ ]
+ },
+ "reply": {
+ "name": "user.info.request",
+ "messages": [
+ {
+ "name": "UserInfoReplyMessage",
+ "payload": "{\n \"email\": \"string[email]\",\n \"error\": {\n \"code\": \"string\",\n \"message\": \"string\"\n },\n \"language\": \"string\",\n \"name\": \"string\",\n \"timezone\": \"string\",\n \"user_id\": \"string[uuid]\"\n}"
+ }
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "name": "Notification Service",
+ "description": "A service that handles user notifications, preferences, and interactions.\nSupports real-time notifications, user preferences management.\n",
+ "operations": [
+ {
+ "action": "receive",
+ "channel": {
+ "name": "notification.preferences.get",
+ "messages": [
+ {
+ "name": "PreferencesRequestMessage",
+ "payload": "{\n \"user_id\": \"string[uuid]\"\n}"
+ }
+ ]
+ },
+ "reply": {
+ "name": "notification.preferences.get",
+ "messages": [
+ {
+ "name": "PreferencesReplyMessage",
+ "payload": "{\n \"preferences\": {\n \"categories\": {\n \"marketing\": \"boolean\",\n \"security\": \"boolean\",\n \"updates\": \"boolean\"\n },\n \"email_enabled\": \"boolean\",\n \"push_enabled\": \"boolean\",\n \"quiet_hours\": {\n \"enabled\": \"boolean\",\n \"end\": \"string[time]\",\n \"start\": \"string[time]\"\n },\n \"sms_enabled\": \"boolean\"\n },\n \"updated_at\": \"string[date-time]\"\n}"
+ }
+ ]
+ }
+ },
+ {
+ "action": "receive",
+ "channel": {
+ "name": "notification.preferences.update",
+ "messages": [
+ {
+ "name": "PreferencesUpdateMessage",
+ "payload": "{\n \"preferences\": {\n \"categories\": {\n \"marketing\": \"boolean\",\n \"security\": \"boolean\",\n \"updates\": \"boolean\"\n },\n \"email_enabled\": \"boolean\",\n \"push_enabled\": \"boolean\",\n \"quiet_hours\": {\n \"enabled\": \"boolean\",\n \"end\": \"string[time]\",\n \"start\": \"string[time]\"\n },\n \"sms_enabled\": \"boolean\"\n },\n \"updated_at\": \"string[date-time]\",\n \"user_id\": \"string[uuid]\"\n}"
+ }
+ ]
+ }
+ },
+ {
+ "action": "receive",
+ "channel": {
+ "name": "notification.user.{user_id}.push",
+ "messages": [
+ {
+ "name": "PushNotificationMessage",
+ "payload": "{\n \"body\": \"string\",\n \"created_at\": \"string[date-time]\",\n \"data\": \"object\",\n \"notification_id\": \"string[uuid]\",\n \"priority\": \"string[enum:low,normal,high]\",\n \"title\": \"string\",\n \"user_id\": \"string[uuid]\"\n}"
+ }
+ ]
+ }
+ },
+ {
+ "action": "send",
+ "channel": {
+ "name": "notification.analytics",
+ "messages": [
+ {
+ "name": "AnalyticsEventMessage",
+ "payload": "{\n \"event_id\": \"string[uuid]\",\n \"event_type\": \"string[enum:notification_sent,notification_opened,notification_clicked]\",\n \"metadata\": {\n \"environment\": \"string[enum:development,staging,production]\",\n \"platform\": \"string[enum:ios,android,web]\",\n \"source\": \"string[enum:mobile,web,api]\",\n \"version\": \"string\"\n },\n \"notification_id\": \"string[uuid]\",\n \"timestamp\": \"string[date-time]\",\n \"user_id\": \"string[uuid]\"\n}"
+ }
+ ]
+ }
+ },
+ {
+ "action": "send",
+ "channel": {
+ "name": "user.info.request",
+ "messages": [
+ {
+ "name": "UserInfoRequestMessage",
+ "payload": "{\n \"user_id\": \"string[uuid]\"\n}"
+ }
+ ]
+ },
+ "reply": {
+ "name": "user.info.request",
+ "messages": [
+ {
+ "name": "UserInfoReplyMessage",
+ "payload": "{\n \"email\": \"string[email]\",\n \"error\": {\n \"code\": \"string\",\n \"message\": \"string\"\n },\n \"language\": \"string\",\n \"name\": \"string\",\n \"timezone\": \"string\",\n \"user_id\": \"string[uuid]\"\n}"
+ }
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "name": "User Service",
+ "description": "A service that manages user information, profiles, and authentication.\nHandles user data requests, profile updates, and user lifecycle events.\n",
+ "operations": [
+ {
+ "action": "receive",
+ "channel": {
+ "name": "user.info.request",
+ "messages": [
+ {
+ "name": "UserInfoRequestMessage",
+ "payload": "{\n \"user_id\": \"string[uuid]\"\n}"
+ }
+ ]
+ },
+ "reply": {
+ "name": "user.info.request",
+ "messages": [
+ {
+ "name": "UserInfoReplyMessage",
+ "payload": "{\n \"email\": \"string[email]\",\n \"error\": {\n \"code\": \"string\",\n \"message\": \"string\"\n },\n \"language\": \"string\",\n \"name\": \"string\",\n \"timezone\": \"string\",\n \"user_id\": \"string[uuid]\"\n}"
+ }
+ ]
+ }
+ },
+ {
+ "action": "send",
+ "channel": {
+ "name": "notification.preferences.update",
+ "messages": [
+ {
+ "name": "PreferencesUpdateMessage",
+ "payload": "{\n \"preferences\": {\n \"categories\": {\n \"marketing\": \"boolean\",\n \"security\": \"boolean\",\n \"updates\": \"boolean\"\n },\n \"email_enabled\": \"boolean\",\n \"push_enabled\": \"boolean\",\n \"quiet_hours\": {\n \"enabled\": \"boolean\",\n \"end\": \"string[time]\",\n \"start\": \"string[time]\"\n },\n \"sms_enabled\": \"boolean\"\n },\n \"updated_at\": \"string[date-time]\",\n \"user_id\": \"string[uuid]\"\n}"
+ }
+ ]
+ }
+ },
+ {
+ "action": "send",
+ "channel": {
+ "name": "user.analytics",
+ "messages": [
+ {
+ "name": "UserAnalyticsEventMessage",
+ "payload": "{\n \"event_id\": \"string[uuid]\",\n \"event_type\": \"string[enum:user_registered,user_logged_in,profile_updated,preferences_changed,account_deleted]\",\n \"metadata\": {\n \"environment\": \"string[enum:development,staging,production]\",\n \"platform\": \"string[enum:ios,android,web]\",\n \"source\": \"string[enum:mobile,web,api]\",\n \"version\": \"string\"\n },\n \"timestamp\": \"string[date-time]\",\n \"user_id\": \"string[uuid]\"\n}"
+ }
+ ]
+ }
+ },
+ {
+ "action": "send",
+ "channel": {
+ "name": "user.info.update",
+ "messages": [
+ {
+ "name": "UserInfoUpdateMessage",
+ "payload": "{\n \"changes\": \"object\",\n \"metadata\": {\n \"environment\": \"string[enum:development,staging,production]\",\n \"platform\": \"string[enum:ios,android,web]\",\n \"source\": \"string[enum:mobile,web,api]\",\n \"version\": \"string\"\n },\n \"updated_at\": \"string[date-time]\",\n \"user_id\": \"string[uuid]\"\n}"
+ }
+ ]
+ }
+ }
+ ]
+ }
+ ]
+ },
+ "changelogs": null
+}
\ No newline at end of file
diff --git a/internal/docs/docs.go b/internal/docs/docs.go
index fd5c11f..458c377 100644
--- a/internal/docs/docs.go
+++ b/internal/docs/docs.go
@@ -10,6 +10,7 @@ import (
"regexp"
"sort"
"strings"
+ "sync"
"text/template"
"unicode"
@@ -25,6 +26,20 @@ type Metadata struct {
Changelogs []messageflow.Changelog `json:"changelogs"`
}
+// DiagramData holds either an SVG file path (for D2) or Mermaid code (for Mermaid)
+type DiagramData struct {
+ Type messageflow.TargetType
+ SVGPath string // For D2
+ Mermaid string // For Mermaid
+}
+
+// DiagramCollection holds all diagram data
+type DiagramCollection struct {
+ ContextDiagram DiagramData
+ ServiceDiagrams map[string]DiagramData // service name -> diagram
+ ChannelDiagrams map[string]DiagramData // channel name -> diagram
+}
+
func Generate(
ctx context.Context,
schema messageflow.Schema,
@@ -36,11 +51,12 @@ func Generate(
return nil, fmt.Errorf("error processing metadata: %w", err)
}
- if err := generateDiagrams(ctx, schema, target, outputDir); err != nil {
+ diagramCollection, err := generateDiagrams(ctx, schema, target, outputDir)
+ if err != nil {
return nil, fmt.Errorf("error generating diagrams: %w", err)
}
- if err := createREADMEContent(schema, title, metadata.Changelogs, outputDir); err != nil {
+ if err := createREADMEContent(schema, title, metadata.Changelogs, outputDir, diagramCollection); err != nil {
return nil, fmt.Errorf("error creating README content: %w", err)
}
@@ -87,40 +103,91 @@ func generateDiagrams(
schema messageflow.Schema,
target messageflow.Target,
outputDir string,
-) error {
+) (*DiagramCollection, error) {
diagramsDir := filepath.Join(outputDir, "diagrams")
if err := os.RemoveAll(diagramsDir); err != nil {
- return fmt.Errorf("error removing old diagrams directory: %w", err)
+ return nil, fmt.Errorf("error removing old diagrams directory: %w", err)
}
- if err := os.MkdirAll(diagramsDir, 0755); err != nil {
- return fmt.Errorf("error creating diagrams directory: %w", err)
+ // Only create diagrams directory for D2 (SVG files)
+ // For Mermaid, diagrams are embedded in markdown
+ formatType := getTargetType(target)
+ if formatType == messageflow.TargetType("d2") {
+ if err := os.MkdirAll(diagramsDir, 0755); err != nil {
+ return nil, fmt.Errorf("error creating diagrams directory: %w", err)
+ }
}
channels := extractUniqueChannels(schema)
+ collection := &DiagramCollection{
+ ServiceDiagrams: make(map[string]DiagramData),
+ ChannelDiagrams: make(map[string]DiagramData),
+ }
+
g, ctx := errgroup.WithContext(ctx)
+
+ var contextDiagram DiagramData
g.Go(func() error {
- return generateContextDiagram(ctx, schema, target, outputDir)
+ var err error
+ contextDiagram, err = generateContextDiagram(ctx, schema, target, outputDir, formatType)
+ return err
})
+ serviceDiagrams := make(map[string]DiagramData)
+ serviceMutex := &sync.Mutex{}
for _, service := range schema.Services {
+ serviceName := service.Name
g.Go(func() error {
- return generateServiceServicesDiagram(ctx, schema, target, service.Name, outputDir)
+ diagram, err := generateServiceServicesDiagram(ctx, schema, target, serviceName, outputDir, formatType)
+ if err != nil {
+ return err
+ }
+ serviceMutex.Lock()
+ serviceDiagrams[serviceName] = diagram
+ serviceMutex.Unlock()
+ return nil
})
}
+ channelDiagrams := make(map[string]DiagramData)
+ channelMutex := &sync.Mutex{}
for _, channel := range channels {
+ channelName := channel
g.Go(func() error {
- return generateChannelServicesDiagram(ctx, schema, target, channel, outputDir)
+ diagram, err := generateChannelServicesDiagram(ctx, schema, target, channelName, outputDir, formatType)
+ if err != nil {
+ return err
+ }
+ channelMutex.Lock()
+ channelDiagrams[channelName] = diagram
+ channelMutex.Unlock()
+ return nil
})
}
if err := g.Wait(); err != nil {
- return fmt.Errorf("error generating diagrams: %w", err)
+ return nil, fmt.Errorf("error generating diagrams: %w", err)
}
- return nil
+ collection.ContextDiagram = contextDiagram
+ collection.ServiceDiagrams = serviceDiagrams
+ collection.ChannelDiagrams = channelDiagrams
+
+ return collection, nil
+}
+
+func getTargetType(target messageflow.Target) messageflow.TargetType {
+ // Create a dummy formatted schema to determine the type
+ // We'll use FormatSchema with a minimal schema to get the type
+ ctx := context.Background()
+ opts := messageflow.FormatOptions{Mode: messageflow.FormatModeContextServices}
+ fs, err := target.FormatSchema(ctx, messageflow.Schema{Services: []messageflow.Service{}}, opts)
+ if err != nil {
+ // Fallback: assume d2 for backward compatibility
+ return messageflow.TargetType("d2")
+ }
+ return fs.Type
}
func generateContextDiagram(
@@ -128,27 +195,37 @@ func generateContextDiagram(
schema messageflow.Schema,
target messageflow.Target,
outputDir string,
-) error {
+ formatType messageflow.TargetType,
+) (DiagramData, error) {
formatOpts := messageflow.FormatOptions{
Mode: messageflow.FormatModeContextServices,
}
formattedSchema, err := target.FormatSchema(ctx, schema, formatOpts)
if err != nil {
- return fmt.Errorf("error formatting context schema: %w", err)
+ return DiagramData{}, fmt.Errorf("error formatting context schema: %w", err)
}
diagram, err := target.RenderSchema(ctx, formattedSchema)
if err != nil {
- return fmt.Errorf("error rendering context diagram: %w", err)
+ return DiagramData{}, fmt.Errorf("error rendering context diagram: %w", err)
}
- contextPath := filepath.Join(outputDir, "diagrams", "context.svg")
- if err := os.WriteFile(contextPath, diagram, 0644); err != nil {
- return fmt.Errorf("error writing context diagram: %w", err)
+ if formatType == messageflow.TargetType("d2") {
+ contextPath := filepath.Join(outputDir, "diagrams", "context.svg")
+ if err := os.WriteFile(contextPath, diagram, 0644); err != nil {
+ return DiagramData{}, fmt.Errorf("error writing context diagram: %w", err)
+ }
+ return DiagramData{
+ Type: formatType,
+ SVGPath: "diagrams/context.svg",
+ }, nil
}
- return nil
+ return DiagramData{
+ Type: formatType,
+ Mermaid: string(diagram),
+ }, nil
}
func generateServiceServicesDiagram(
@@ -157,7 +234,8 @@ func generateServiceServicesDiagram(
target messageflow.Target,
serviceName string,
outputDir string,
-) error {
+ formatType messageflow.TargetType,
+) (DiagramData, error) {
formatOpts := messageflow.FormatOptions{
Mode: messageflow.FormatModeServiceServices,
Service: serviceName,
@@ -165,21 +243,30 @@ func generateServiceServicesDiagram(
formattedSchema, err := target.FormatSchema(ctx, schema, formatOpts)
if err != nil {
- return fmt.Errorf("error formatting service services schema: %w", err)
+ return DiagramData{}, fmt.Errorf("error formatting service services schema: %w", err)
}
diagram, err := target.RenderSchema(ctx, formattedSchema)
if err != nil {
- return fmt.Errorf("error rendering service services diagram: %w", err)
+ return DiagramData{}, fmt.Errorf("error rendering service services diagram: %w", err)
}
- serviceAnchor := sanitizeAnchor(serviceName)
- servicePath := filepath.Join(outputDir, "diagrams", fmt.Sprintf("service_%s.svg", serviceAnchor))
- if err := os.WriteFile(servicePath, diagram, 0644); err != nil {
- return fmt.Errorf("error writing service diagram for %s: %w", serviceName, err)
+ if formatType == messageflow.TargetType("d2") {
+ serviceAnchor := sanitizeAnchor(serviceName)
+ servicePath := filepath.Join(outputDir, "diagrams", fmt.Sprintf("service_%s.svg", serviceAnchor))
+ if err := os.WriteFile(servicePath, diagram, 0644); err != nil {
+ return DiagramData{}, fmt.Errorf("error writing service diagram for %s: %w", serviceName, err)
+ }
+ return DiagramData{
+ Type: formatType,
+ SVGPath: fmt.Sprintf("diagrams/service_%s.svg", serviceAnchor),
+ }, nil
}
- return nil
+ return DiagramData{
+ Type: formatType,
+ Mermaid: string(diagram),
+ }, nil
}
func generateChannelServicesDiagram(
@@ -188,7 +275,8 @@ func generateChannelServicesDiagram(
target messageflow.Target,
channel string,
outputDir string,
-) error {
+ formatType messageflow.TargetType,
+) (DiagramData, error) {
formatOpts := messageflow.FormatOptions{
Mode: messageflow.FormatModeChannelServices,
Channel: channel,
@@ -197,21 +285,30 @@ func generateChannelServicesDiagram(
formattedSchema, err := target.FormatSchema(ctx, schema, formatOpts)
if err != nil {
- return fmt.Errorf("error formatting channel services schema: %w", err)
+ return DiagramData{}, fmt.Errorf("error formatting channel services schema: %w", err)
}
diagram, err := target.RenderSchema(ctx, formattedSchema)
if err != nil {
- return fmt.Errorf("error rendering channel services diagram: %w", err)
+ return DiagramData{}, fmt.Errorf("error rendering channel services diagram: %w", err)
}
- channelAnchor := sanitizeAnchor(channel)
- channelPath := filepath.Join(outputDir, "diagrams", fmt.Sprintf("channel_%s.svg", channelAnchor))
- if err := os.WriteFile(channelPath, diagram, 0644); err != nil {
- return fmt.Errorf("error writing channel diagram for %s: %w", channel, err)
+ if formatType == messageflow.TargetType("d2") {
+ channelAnchor := sanitizeAnchor(channel)
+ channelPath := filepath.Join(outputDir, "diagrams", fmt.Sprintf("channel_%s.svg", channelAnchor))
+ if err := os.WriteFile(channelPath, diagram, 0644); err != nil {
+ return DiagramData{}, fmt.Errorf("error writing channel diagram for %s: %w", channel, err)
+ }
+ return DiagramData{
+ Type: formatType,
+ SVGPath: fmt.Sprintf("diagrams/channel_%s.svg", channelAnchor),
+ }, nil
}
- return nil
+ return DiagramData{
+ Type: formatType,
+ Mermaid: string(diagram),
+ }, nil
}
func extractUniqueChannels(schema messageflow.Schema) []string {
@@ -235,7 +332,7 @@ func extractUniqueChannels(schema messageflow.Schema) []string {
return channels
}
-func createREADMEContent(schema messageflow.Schema, title string, changelogs []messageflow.Changelog, outputDir string) error {
+func createREADMEContent(schema messageflow.Schema, title string, changelogs []messageflow.Changelog, outputDir string, diagramCollection *DiagramCollection) error {
tmpl, err := template.New("readme.tmpl").Funcs(template.FuncMap{
"Anchor": func(name string) string {
return sanitizeAnchor(name)
@@ -277,17 +374,19 @@ func createREADMEContent(schema messageflow.Schema, title string, changelogs []m
})
data := struct {
- Title string
- Services []messageflow.Service
- Channels []string
- ChannelInfo map[string]ChannelInfo
- Changelogs []messageflow.Changelog
+ Title string
+ Services []messageflow.Service
+ Channels []string
+ ChannelInfo map[string]ChannelInfo
+ Changelogs []messageflow.Changelog
+ DiagramCollection *DiagramCollection
}{
- Title: title,
- Services: schema.Services,
- Channels: channels,
- ChannelInfo: channelInfo,
- Changelogs: changelogs,
+ Title: title,
+ Services: schema.Services,
+ Channels: channels,
+ ChannelInfo: channelInfo,
+ Changelogs: changelogs,
+ DiagramCollection: diagramCollection,
}
var buf strings.Builder
diff --git a/internal/docs/templates/readme.tmpl b/internal/docs/templates/readme.tmpl
index ffe538e..b1ca34e 100644
--- a/internal/docs/templates/readme.tmpl
+++ b/internal/docs/templates/readme.tmpl
@@ -17,7 +17,14 @@
## Context
-
+{{- $diagram := $.DiagramCollection.ContextDiagram }}
+{{- if eq $diagram.Type "d2" }}
+
+{{- else if eq $diagram.Type "mermaid" }}
+```mermaid
+{{$diagram.Mermaid}}
+```
+{{- end }}
## Services
@@ -27,7 +34,14 @@
{{.Description}}
-
+{{- $diagram := index $.DiagramCollection.ServiceDiagrams .Name }}
+{{- if eq $diagram.Type "d2" }}
+
+{{- else if eq $diagram.Type "mermaid" }}
+```mermaid
+{{$diagram.Mermaid}}
+```
+{{- end }}
{{- end }}
@@ -37,7 +51,14 @@
### {{.}}
-
+{{- $diagram := index $.DiagramCollection.ChannelDiagrams . }}
+{{- if eq $diagram.Type "d2" }}
+
+{{- else if eq $diagram.Type "mermaid" }}
+```mermaid
+{{$diagram.Mermaid}}
+```
+{{- end }}
{{- $channelInfo := index $.ChannelInfo . }}
{{- if $channelInfo.Messages }}
diff --git a/pkg/schema/target/mermaid/mermaid.go b/pkg/schema/target/mermaid/mermaid.go
new file mode 100644
index 0000000..042bc3b
--- /dev/null
+++ b/pkg/schema/target/mermaid/mermaid.go
@@ -0,0 +1,473 @@
+// Package mermaid provides functionality for generating and rendering Mermaid diagrams.
+package mermaid
+
+import (
+ "bytes"
+ "context"
+ "embed"
+ "fmt"
+ "sort"
+ "strings"
+ "text/template"
+
+ "github.com/holydocs/messageflow/pkg/messageflow"
+)
+
+// targetType defines the schema format type for Mermaid diagrams
+const targetType = messageflow.TargetType("mermaid")
+
+var (
+ //go:embed templates/service_channels.tmpl
+ serviceChannelsTemplateFS embed.FS
+
+ //go:embed templates/channel_services.tmpl
+ channelServicesTemplateFS embed.FS
+
+ //go:embed templates/context_services.tmpl
+ contextServicesTemplateFS embed.FS
+
+ //go:embed templates/service_services.tmpl
+ serviceServicesTemplateFS embed.FS
+)
+
+// Ensure Target implements messageflow interfaces.
+var (
+ _ messageflow.Target = (*Target)(nil)
+)
+
+// Target handles the generation and rendering of Mermaid diagrams from message flow schemas.
+type Target struct {
+ serviceChannelsTemplate *template.Template
+ channelServicesTemplate *template.Template
+ contextServicesTemplate *template.Template
+ serviceServicesTemplate *template.Template
+}
+
+// NewTarget creates a new Mermaid diagram formatter instance.
+// It initializes the templates from the embedded template files.
+func NewTarget() (*Target, error) {
+ funcMap := template.FuncMap{
+ "sanitizeNodeID": sanitizeNodeID,
+ "hasOperation": func(ops []messageflow.Operation, actionStr string, hasReply bool) bool {
+ action := messageflow.Action(actionStr)
+ for _, op := range ops {
+ if op.Action == action {
+ if hasReply {
+ if op.Reply != nil {
+ return true
+ }
+ } else {
+ if op.Reply == nil {
+ return true
+ }
+ }
+ }
+ }
+ return false
+ },
+ }
+
+ serviceChannelsTemplate, err := template.New("service_channels.tmpl").Funcs(funcMap).ParseFS(serviceChannelsTemplateFS, "templates/service_channels.tmpl")
+ if err != nil {
+ return nil, fmt.Errorf("parsing service channels template: %w", err)
+ }
+
+ channelServicesTemplate, err := template.New("channel_services.tmpl").Funcs(funcMap).ParseFS(channelServicesTemplateFS, "templates/channel_services.tmpl")
+ if err != nil {
+ return nil, fmt.Errorf("parsing channel services template: %w", err)
+ }
+
+ contextServicesTemplate, err := template.New("context_services.tmpl").Funcs(funcMap).ParseFS(contextServicesTemplateFS, "templates/context_services.tmpl")
+ if err != nil {
+ return nil, fmt.Errorf("parsing context services template: %w", err)
+ }
+
+ serviceServicesTemplate, err := template.New("service_services.tmpl").Funcs(funcMap).ParseFS(serviceServicesTemplateFS, "templates/service_services.tmpl")
+ if err != nil {
+ return nil, fmt.Errorf("parsing service services template: %w", err)
+ }
+
+ return &Target{
+ serviceChannelsTemplate: serviceChannelsTemplate,
+ channelServicesTemplate: channelServicesTemplate,
+ contextServicesTemplate: contextServicesTemplate,
+ serviceServicesTemplate: serviceServicesTemplate,
+ }, nil
+}
+
+// Capabilities returns target capabilities.
+func (t *Target) Capabilities() messageflow.TargetCapabilities {
+ return messageflow.TargetCapabilities{
+ Format: true,
+ Render: true,
+ }
+}
+
+type channelServicesPayload struct {
+ Channel string
+ Message string
+ MessageName string
+ ReplyMessage *string
+ ReplyMessageName *string
+ Senders []string
+ Receivers []string
+ OmitPayloads bool
+}
+
+type contextServicesPayload struct {
+ Services []messageflow.Service
+ Connections []connection
+}
+
+type serviceServicesPayload struct {
+ MainService messageflow.Service
+ NeighborServices []messageflow.Service
+}
+
+type connection struct {
+ From string
+ To string
+ Label string
+ Bidirectional bool
+}
+
+func (t *Target) FormatSchema(
+ _ context.Context,
+ s messageflow.Schema,
+ opts messageflow.FormatOptions,
+) (messageflow.FormattedSchema, error) {
+ fs := messageflow.FormattedSchema{
+ Type: targetType,
+ }
+
+ var buf bytes.Buffer
+
+ switch opts.Mode {
+ case messageflow.FormatModeContextServices:
+ payload := prepareContextServicesPayload(s)
+
+ err := t.contextServicesTemplate.Execute(&buf, payload)
+ if err != nil {
+ return messageflow.FormattedSchema{}, fmt.Errorf("executing context services template: %w", err)
+ }
+ case messageflow.FormatModeServiceChannels:
+ payload := prepareServiceChannelsPayload(s, opts.Service)
+
+ err := t.serviceChannelsTemplate.Execute(&buf, payload)
+ if err != nil {
+ return messageflow.FormattedSchema{}, fmt.Errorf("executing service channels template: %w", err)
+ }
+ case messageflow.FormatModeChannelServices:
+ payload := prepareChannelServicesPayload(s, opts.Channel, opts.OmitPayloads)
+
+ err := t.channelServicesTemplate.Execute(&buf, payload)
+ if err != nil {
+ return messageflow.FormattedSchema{}, fmt.Errorf("executing channel services template: %w", err)
+ }
+ case messageflow.FormatModeServiceServices:
+ payload := prepareServiceServicesPayload(s, opts.Service)
+
+ err := t.serviceServicesTemplate.Execute(&buf, payload)
+ if err != nil {
+ return messageflow.FormattedSchema{}, fmt.Errorf("executing service services template: %w", err)
+ }
+ default:
+ return messageflow.FormattedSchema{}, messageflow.NewUnsupportedFormatModeError(opts.Mode, []messageflow.FormatMode{
+ messageflow.FormatModeServiceChannels,
+ messageflow.FormatModeChannelServices,
+ messageflow.FormatModeContextServices,
+ messageflow.FormatModeServiceServices,
+ })
+ }
+
+ fs.Data = buf.Bytes()
+
+ return fs, nil
+}
+
+// RenderSchema renders a formatted Mermaid diagram to Mermaid code.
+// For Mermaid, this simply returns the formatted code as-is since it will be embedded in markdown.
+func (t *Target) RenderSchema(_ context.Context, s messageflow.FormattedSchema) ([]byte, error) {
+ if s.Type != targetType {
+ return nil, messageflow.NewUnsupportedFormatError(s.Type, targetType)
+ }
+
+ return s.Data, nil
+}
+
+// sanitizeNodeID converts a string to a valid Mermaid node ID (no spaces, special chars).
+func sanitizeNodeID(name string) string {
+ // Replace spaces and special characters with underscores
+ result := strings.Builder{}
+ for _, r := range name {
+ if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' {
+ result.WriteRune(r)
+ } else if r == ' ' || r == '-' {
+ result.WriteRune('_')
+ }
+ }
+ return result.String()
+}
+
+func prepareServiceChannelsPayload(s messageflow.Schema, serviceName string) messageflow.Service {
+ if serviceName == "" && len(s.Services) == 1 {
+ return s.Services[0]
+ }
+
+ for _, service := range s.Services {
+ if service.Name == serviceName {
+ return service
+ }
+ }
+
+ return messageflow.Service{}
+}
+
+func prepareChannelServicesPayload(s messageflow.Schema, channel string, omitPayloads bool) channelServicesPayload {
+ payload := channelServicesPayload{
+ Channel: channel,
+ OmitPayloads: omitPayloads,
+ }
+
+ for _, service := range s.Services {
+ for _, op := range service.Operation {
+ if op.Channel.Name == channel {
+ switch op.Action {
+ case messageflow.ActionSend:
+ payload.Senders = append(payload.Senders, service.Name)
+ case messageflow.ActionReceive:
+ payload.Receivers = append(payload.Receivers, service.Name)
+ }
+
+ if len(op.Channel.Messages) > 0 {
+ firstMessage := op.Channel.Messages[0]
+ if len(payload.Message) < len(firstMessage.Payload) {
+ payload.Message = firstMessage.Payload
+ payload.MessageName = firstMessage.Name
+ }
+ }
+
+ if op.Reply != nil && len(op.Reply.Messages) > 0 {
+ firstReplyMessage := op.Reply.Messages[0]
+ if payload.ReplyMessage == nil ||
+ (len(*payload.ReplyMessage) < len(firstReplyMessage.Payload)) {
+ payload.ReplyMessage = &firstReplyMessage.Payload
+ payload.ReplyMessageName = &firstReplyMessage.Name
+ }
+ }
+ }
+ }
+ }
+
+ return payload
+}
+
+func prepareContextServicesPayload(s messageflow.Schema) contextServicesPayload {
+ formattedServices := make([]messageflow.Service, len(s.Services))
+ for i, service := range s.Services {
+ formattedServices[i] = messageflow.Service{
+ Name: service.Name,
+ Description: formatDescription(service.Description),
+ Operation: service.Operation,
+ }
+ }
+
+ payload := contextServicesPayload{
+ Services: formattedServices,
+ Connections: []connection{},
+ }
+
+ servicePairs := make(map[string]map[string]bool) // service1->service2 -> hasSendOperation
+
+ // First pass: collect all send operations between service pairs
+ for _, service := range s.Services {
+ for _, op := range service.Operation {
+ if op.Action == messageflow.ActionSend {
+ for _, otherService := range s.Services {
+ if otherService.Name == service.Name {
+ continue
+ }
+
+ for _, otherOp := range otherService.Operation {
+ if otherOp.Channel.Name == op.Channel.Name && otherOp.Action == messageflow.ActionReceive {
+ if servicePairs[service.Name] == nil {
+ servicePairs[service.Name] = make(map[string]bool)
+ }
+ servicePairs[service.Name][otherService.Name] = true
+ break
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Second pass: create connections and detect bidirectional communication
+ connectionMap := make(map[string]connection)
+
+ for service1, receivers := range servicePairs {
+ for service2 := range receivers {
+ bidirectional := servicePairs[service2] != nil && servicePairs[service2][service1]
+
+ var from, to string
+ switch {
+ case bidirectional && service1 < service2:
+ from, to = service1, service2
+ case bidirectional && service1 >= service2:
+ from, to = service2, service1
+ default:
+ from, to = service1, service2
+ }
+
+ key := fmt.Sprintf("%s->%s", from, to)
+
+ label := determineConnectionLabel(s, from, to)
+
+ conn := connection{
+ From: from,
+ To: to,
+ Label: label,
+ Bidirectional: bidirectional,
+ }
+
+ connectionMap[key] = conn
+ }
+ }
+
+ keys := make([]string, 0, len(connectionMap))
+ for key := range connectionMap {
+ keys = append(keys, key)
+ }
+ sort.Strings(keys)
+
+ for _, key := range keys {
+ payload.Connections = append(payload.Connections, connectionMap[key])
+ }
+
+ return payload
+}
+
+// formatDescription formats a description string for better readability in Mermaid diagrams.
+func formatDescription(desc string) string {
+ if desc == "" {
+ return ""
+ }
+ // For Mermaid, we can use line breaks with
or keep it simple
+ return desc
+}
+
+func determineConnectionLabel(s messageflow.Schema, service1, service2 string) string {
+ var hasPub, hasReq bool
+
+ svc1 := findServiceByName(s, service1)
+ svc2 := findServiceByName(s, service2)
+
+ for _, op1 := range svc1.Operation {
+ for _, op2 := range svc2.Operation {
+ if op1.Channel.Name != op2.Channel.Name {
+ continue
+ }
+
+ switch {
+ case op1.Action == messageflow.ActionSend && op2.Action == messageflow.ActionReceive:
+ if op1.Reply != nil {
+ hasReq = true
+ continue
+ }
+
+ hasPub = true
+ case op1.Action == messageflow.ActionReceive && op2.Action == messageflow.ActionSend:
+ if op2.Reply != nil {
+ hasReq = true
+ continue
+ }
+
+ hasPub = true
+ }
+ }
+ }
+
+ switch {
+ case hasPub && hasReq:
+ return "Pub/Req"
+ case hasReq:
+ return "Req"
+ default:
+ return "Pub"
+ }
+}
+
+func findServiceByName(s messageflow.Schema, name string) messageflow.Service {
+ for _, service := range s.Services {
+ if service.Name == name {
+ return service
+ }
+ }
+ return messageflow.Service{}
+}
+
+func prepareServiceServicesPayload(s messageflow.Schema, serviceName string) serviceServicesPayload {
+ var mainService messageflow.Service
+ if serviceName == "" && len(s.Services) == 1 {
+ mainService = s.Services[0]
+ } else {
+ for _, service := range s.Services {
+ if service.Name == serviceName {
+ mainService = service
+ break
+ }
+ }
+ }
+
+ var (
+ neighborServices = make([]messageflow.Service, 0)
+ neighborServiceMap = make(map[string]bool)
+ mainServiceSendChannels = make(map[string]bool)
+ mainServiceReceiveChannels = make(map[string]bool)
+ )
+
+ for _, op := range mainService.Operation {
+ switch op.Action {
+ case messageflow.ActionSend:
+ mainServiceSendChannels[op.Channel.Name] = true
+ case messageflow.ActionReceive:
+ mainServiceReceiveChannels[op.Channel.Name] = true
+ }
+ }
+
+ for _, service := range s.Services {
+ if service.Name == mainService.Name {
+ continue
+ }
+
+ isNeighbor := false
+
+ // Check if this service sends to channels that main service receives from
+ for _, op := range service.Operation {
+ if op.Action == messageflow.ActionSend && mainServiceReceiveChannels[op.Channel.Name] {
+ isNeighbor = true
+ break
+ }
+ }
+
+ // Check if this service receives from channels that main service sends to
+ if !isNeighbor {
+ for _, op := range service.Operation {
+ if op.Action == messageflow.ActionReceive && mainServiceSendChannels[op.Channel.Name] {
+ isNeighbor = true
+ break
+ }
+ }
+ }
+
+ if isNeighbor && !neighborServiceMap[service.Name] {
+ neighborServices = append(neighborServices, service)
+ neighborServiceMap[service.Name] = true
+ }
+ }
+
+ return serviceServicesPayload{
+ MainService: mainService,
+ NeighborServices: neighborServices,
+ }
+}
diff --git a/pkg/schema/target/mermaid/templates/channel_services.tmpl b/pkg/schema/target/mermaid/templates/channel_services.tmpl
new file mode 100644
index 0000000..b953cbb
--- /dev/null
+++ b/pkg/schema/target/mermaid/templates/channel_services.tmpl
@@ -0,0 +1,43 @@
+flowchart LR
+ {{- $channelID := sanitizeNodeID .Channel }}
+ {{ $channelID }}@{ shape: das, label: "{{.Channel}}" }
+
+{{- if .Senders }}
+ {{- range .Senders }}
+ {{- $senderID := sanitizeNodeID . }}
+ {{ $senderID }}["{{.}}"]
+ {{- if $.ReplyMessage }}
+ {{ $senderID }} -->|"Request"| {{ $channelID }}
+
+ {{- else }}
+ {{ $senderID }} -->|"Send"| {{ $channelID }}
+
+ {{- end }}
+ {{- end }}
+{{- end }}
+{{- if .Receivers }}
+ {{- range .Receivers }}
+ {{- $receiverID := sanitizeNodeID . }}
+ {{ $receiverID }}["{{.}}"]
+ {{- if $.ReplyMessage }}
+ {{ $channelID }} -->|"Reply"| {{ $receiverID }}
+
+ {{- else }}
+ {{ $channelID }} -->|"Receive"| {{ $receiverID }}
+
+ {{- end }}
+ {{- end }}
+{{- end }}
+
+{{- if .Senders }}
+ {{- range .Senders }}
+ {{- $senderID := sanitizeNodeID . }}
+ style {{ $senderID }} fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+ {{- end }}
+{{- end }}
+{{- if .Receivers }}
+ {{- range .Receivers }}
+ {{- $receiverID := sanitizeNodeID . }}
+ style {{ $receiverID }} fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+ {{- end }}
+{{- end }}
diff --git a/pkg/schema/target/mermaid/templates/context_services.tmpl b/pkg/schema/target/mermaid/templates/context_services.tmpl
new file mode 100644
index 0000000..72f7138
--- /dev/null
+++ b/pkg/schema/target/mermaid/templates/context_services.tmpl
@@ -0,0 +1,22 @@
+flowchart TD
+{{- range .Services }}
+ {{- $nodeID := sanitizeNodeID .Name }}
+ {{ $nodeID }}["{{.Name}}
{{.Description}}"]
+
+{{- end }}
+{{- range .Connections }}
+ {{- $fromID := sanitizeNodeID .From }}
+ {{- $toID := sanitizeNodeID .To }}
+ {{- if .Bidirectional }}
+ {{ $fromID }} <-->|"{{.Label}}"| {{ $toID }}
+
+ {{- else }}
+ {{ $fromID }} -->|"{{.Label}}"| {{ $toID }}
+
+ {{- end }}
+{{- end }}
+
+{{- range .Services }}
+ {{- $nodeID := sanitizeNodeID .Name }}
+ style {{ $nodeID }} fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+{{- end }}
diff --git a/pkg/schema/target/mermaid/templates/service_channels.tmpl b/pkg/schema/target/mermaid/templates/service_channels.tmpl
new file mode 100644
index 0000000..8178822
--- /dev/null
+++ b/pkg/schema/target/mermaid/templates/service_channels.tmpl
@@ -0,0 +1,58 @@
+flowchart TD
+ {{- $serviceID := sanitizeNodeID .Name }}
+ {{ $serviceID }}["{{.Name}}"]
+
+{{- if hasOperation .Operation "receive" false -}}
+ subgraph ReceiveFrom["Receive From"]
+ {{- range .Operation }}
+ {{- if and (eq .Action "receive") (not .Reply) }}
+ {{- $channelID := sanitizeNodeID .Channel.Name }}
+ {{ $channelID }}@{ shape: das, label: "{{.Channel.Name}}" }
+
+ {{- end }}
+ {{- end }}
+ end
+ ReceiveFrom --> {{ $serviceID }}
+
+{{- end }}
+{{- if hasOperation .Operation "send" false }}
+ subgraph SendTo["Send To"]
+ {{- range .Operation }}
+ {{- if and (eq .Action "send") (not .Reply) }}
+ {{- $channelID := sanitizeNodeID .Channel.Name }}
+ {{ $channelID }}@{ shape: das, label: "{{.Channel.Name}}" }
+
+ {{- end }}
+ {{- end }}
+ end
+ {{ $serviceID }} --> SendTo
+
+{{- end }}
+{{- if hasOperation .Operation "receive" true }}
+ subgraph ReplyTo["Reply To"]
+ {{- range .Operation }}
+ {{- if and (eq .Action "receive") .Reply }}
+ {{- $channelID := sanitizeNodeID .Channel.Name }}
+ {{ $channelID }}@{ shape: das, label: "{{.Channel.Name}}" }
+
+ {{- end }}
+ {{- end }}
+ end
+ ReplyTo --> {{ $serviceID }}
+
+{{- end }}
+{{- if hasOperation .Operation "send" true }}
+ subgraph RequestFrom["Request From"]
+ {{- range .Operation }}
+ {{- if and (eq .Action "send") .Reply }}
+ {{- $channelID := sanitizeNodeID .Channel.Name }}
+ {{ $channelID }}@{ shape: das, label: "{{.Channel.Name}}" }
+
+ {{- end }}
+ {{- end }}
+ end
+ {{ $serviceID }} --> RequestFrom
+
+{{- end }}
+
+ style {{ $serviceID }} fill:#3498db,stroke:#2980b9,stroke-width:2px,color:#fff
diff --git a/pkg/schema/target/mermaid/templates/service_services.tmpl b/pkg/schema/target/mermaid/templates/service_services.tmpl
new file mode 100644
index 0000000..6911a7d
--- /dev/null
+++ b/pkg/schema/target/mermaid/templates/service_services.tmpl
@@ -0,0 +1,59 @@
+flowchart TD
+ {{- $mainServiceID := sanitizeNodeID .MainService.Name }}
+ {{ $mainServiceID }}["{{.MainService.Name}}"]
+
+ {{- range .MainService.Operation }}
+ {{- $channelID := sanitizeNodeID .Channel.Name }}
+ {{ $channelID }}@{ shape: das, label: "{{.Channel.Name}}" }
+ {{- if eq .Action "receive" }}
+ {{- if .Reply }}
+ {{ $channelID }} -->|"Reply"| {{ $mainServiceID }}
+
+ {{- else }}
+ {{ $channelID }} -->|"Receive"| {{ $mainServiceID }}
+
+ {{- end }}
+ {{- end }}
+ {{- if eq .Action "send" }}
+ {{- if .Reply }}
+ {{ $mainServiceID }} -->|"Request"| {{ $channelID }}
+
+ {{- else }}
+ {{ $mainServiceID }} -->|"Send"| {{ $channelID }}
+
+ {{- end }}
+ {{- end }}
+ {{- end }}
+
+ {{- range .NeighborServices }}
+ {{- $neighborID := sanitizeNodeID .Name }}
+ {{ $neighborID }}["{{.Name}}"]
+ {{- $neighborService := . -}}
+ {{- range .Operation }}
+ {{- $neighborOp := . -}}
+ {{- if eq .Action "send" }}
+ {{- range $.MainService.Operation }}
+ {{- if and (eq .Action "receive") (eq .Channel.Name $neighborOp.Channel.Name) }}
+ {{- $channelID := sanitizeNodeID $neighborOp.Channel.Name }}
+ {{ $neighborID }} --> {{ $channelID }}
+
+ {{- end }}
+ {{- end }}
+ {{- end }}
+ {{- if eq .Action "receive" }}
+ {{- range $.MainService.Operation }}
+ {{- if and (eq .Action "send") (eq .Channel.Name $neighborOp.Channel.Name) }}
+ {{- $channelID := sanitizeNodeID $neighborOp.Channel.Name }}
+ {{ $channelID }} --> {{ $neighborID }}
+
+ {{- end }}
+ {{- end }}
+ {{- end }}
+ {{- end }}
+ {{- end }}
+
+ style {{ $mainServiceID }} fill:#3498db,stroke:#2980b9,stroke-width:2px,color:#fff
+ {{- range .NeighborServices }}
+ {{- $neighborID := sanitizeNodeID .Name }}
+ style {{ $neighborID }} fill:#95A5A6,stroke:#7F8C8D,stroke-width:2px,color:#fff
+ {{- end }}