MermaidDiagramGenerator.cs 3.3 KB

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