MermaidDiagramGenerator.cs 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using RackPeek.Domain.Resources;
  6. namespace RackPeek.Domain.UseCases.Mermaid {
  7. public static class MermaidDiagramGenerator {
  8. public static MermaidExportResult ToMermaidDiagram(
  9. this IReadOnlyList<Resource> resources,
  10. MermaidExportOptions? options = null) {
  11. MermaidExportOptions resolvedOptions = options ?? new MermaidExportOptions();
  12. var sb = new StringBuilder();
  13. var warnings = new List<string>();
  14. sb.AppendLine(resolvedOptions.DiagramType);
  15. // Group resources by Kind
  16. IOrderedEnumerable<IGrouping<string, Resource>> grouped = resources
  17. .Where(r => resolvedOptions.IncludeTags.Count == 0
  18. || (r.Tags != null && r.Tags.Any(t => resolvedOptions.IncludeTags.Contains(t, StringComparer.OrdinalIgnoreCase))))
  19. .GroupBy(r => Resource.KindToPlural(r.Kind))
  20. .OrderBy(g => g.Key);
  21. foreach (IGrouping<string, Resource> group in grouped) {
  22. sb.AppendLine($" subgraph {SanitizeId(group.Key)}");
  23. foreach (Resource r in group.OrderBy(x => x.Name)) {
  24. var nodeId = SanitizeId(r.Name);
  25. var label = BuildNodeLabel(r, resolvedOptions);
  26. sb.AppendLine($" {nodeId}[\"{label}\"]");
  27. }
  28. sb.AppendLine(" end");
  29. }
  30. // Map RunsOn relationships if requested
  31. if (resolvedOptions.IncludeEdges) {
  32. var resourceLookup = resources.ToDictionary(r => r.Name, r => SanitizeId(r.Name), StringComparer.OrdinalIgnoreCase);
  33. foreach (Resource r in resources) {
  34. var nodeId = SanitizeId(r.Name);
  35. foreach (var depName in r.RunsOn) {
  36. if (resourceLookup.TryGetValue(depName, out var depId))
  37. {
  38. sb.AppendLine($" {nodeId} --> {depId}");
  39. }
  40. else
  41. {
  42. warnings.Add($"RunsOn reference '{depName}' for '{r.Name}' not found in resources");
  43. }
  44. }
  45. }
  46. }
  47. if (sb.Length == 0)
  48. warnings.Add("No Mermaid diagram entries generated.");
  49. return new MermaidExportResult(sb.ToString().TrimEnd(), warnings);
  50. }
  51. private static string BuildNodeLabel(Resource r, MermaidExportOptions options) {
  52. if (!options.IncludeLabels)
  53. return r.Name;
  54. IEnumerable<KeyValuePair<string, string>> filtered = options.LabelWhitelist is null
  55. ? r.Labels
  56. : r.Labels.Where(kvp => options.LabelWhitelist.Contains(kvp.Key, StringComparer.OrdinalIgnoreCase));
  57. var labelParts = filtered.Select(kvp => $"{kvp.Key}: {kvp.Value}").ToList();
  58. return labelParts.Count == 0 ? r.Name : $"{r.Name}\\n{string.Join("\\n", labelParts)}";
  59. }
  60. private static string SanitizeId(string name) {
  61. var sb = new StringBuilder();
  62. foreach (var ch in name.Trim().ToLowerInvariant()) {
  63. if (char.IsLetterOrDigit(ch) || ch == '_')
  64. sb.Append(ch);
  65. else if (ch == '-' || ch == '.' || ch == ' ')
  66. sb.Append('_');
  67. }
  68. return sb.Length == 0 ? "node" : sb.ToString();
  69. }
  70. }
  71. }