From 2ae5df28e9055fa520c0a28fd34b5418449d8495 Mon Sep 17 00:00:00 2001 From: Denis Shevchenko Date: Sun, 28 Dec 2025 15:51:52 +0300 Subject: [PATCH] diagram: mermaid --- Makefile | 8 +- cmd/messageflow/commands/docs/docs.go | 31 +- examples/single-page-mermaid-docs/README.md | 762 ++++++++++++++++++ .../single-page-mermaid-docs/messageflow.json | 328 ++++++++ internal/docs/docs.go | 189 +++-- internal/docs/templates/readme.tmpl | 27 +- pkg/schema/target/mermaid/mermaid.go | 473 +++++++++++ .../mermaid/templates/channel_services.tmpl | 43 + .../mermaid/templates/context_services.tmpl | 22 + .../mermaid/templates/service_channels.tmpl | 58 ++ .../mermaid/templates/service_services.tmpl | 59 ++ 11 files changed, 1947 insertions(+), 53 deletions(-) create mode 100644 examples/single-page-mermaid-docs/README.md create mode 100644 examples/single-page-mermaid-docs/messageflow.json create mode 100644 pkg/schema/target/mermaid/mermaid.go create mode 100644 pkg/schema/target/mermaid/templates/channel_services.tmpl create mode 100644 pkg/schema/target/mermaid/templates/context_services.tmpl create mode 100644 pkg/schema/target/mermaid/templates/service_channels.tmpl create mode 100644 pkg/schema/target/mermaid/templates/service_services.tmpl 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 -![Context](diagrams/context.svg) +{{- $diagram := $.DiagramCollection.ContextDiagram }} +{{- if eq $diagram.Type "d2" }} +![Context]({{$diagram.SVGPath}}) +{{- else if eq $diagram.Type "mermaid" }} +```mermaid +{{$diagram.Mermaid}} +``` +{{- end }} ## Services @@ -27,7 +34,14 @@ {{.Description}} -![{{.Name}} Service Channels](diagrams/service_{{Anchor .Name}}.svg) +{{- $diagram := index $.DiagramCollection.ServiceDiagrams .Name }} +{{- if eq $diagram.Type "d2" }} +![{{.Name}} Service Channels]({{$diagram.SVGPath}}) +{{- else if eq $diagram.Type "mermaid" }} +```mermaid +{{$diagram.Mermaid}} +``` +{{- end }} {{- end }} @@ -37,7 +51,14 @@ ### {{.}} -![{{.}} Channel Services](diagrams/channel_{{Anchor .}}.svg) +{{- $diagram := index $.DiagramCollection.ChannelDiagrams . }} +{{- if eq $diagram.Type "d2" }} +![{{.}} Channel Services]({{$diagram.SVGPath}}) +{{- 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 }}