sqry_core/output/diagram/
mod.rs1mod builder;
7mod d2;
8mod graphviz;
9mod mermaid;
10
11pub use builder::{
12 Edge, GraphBuilder, Node, escape_label_graphviz, escape_label_mermaid, sanitize_node_id,
13};
14pub use d2::D2Formatter;
15pub use graphviz::GraphVizFormatter;
16pub use mermaid::MermaidFormatter;
17
18use crate::Result;
19use crate::graph::unified::GraphSnapshot;
20use crate::graph::unified::node::NodeId;
21use anyhow::anyhow;
22use std::fmt;
23use std::str::FromStr;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum DiagramFormat {
28 Mermaid,
30 GraphViz,
32 D2,
34}
35
36impl DiagramFormat {
37 pub fn parse_format(value: &str) -> Result<Self> {
44 value.parse()
45 }
46
47 #[must_use]
49 pub fn file_extension(&self) -> &'static str {
50 match self {
51 DiagramFormat::Mermaid => "mmd",
52 DiagramFormat::GraphViz => "dot",
53 DiagramFormat::D2 => "d2",
54 }
55 }
56}
57
58impl fmt::Display for DiagramFormat {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 match self {
61 DiagramFormat::Mermaid => write!(f, "mermaid"),
62 DiagramFormat::GraphViz => write!(f, "graphviz"),
63 DiagramFormat::D2 => write!(f, "d2"),
64 }
65 }
66}
67
68impl FromStr for DiagramFormat {
69 type Err = anyhow::Error;
70
71 fn from_str(s: &str) -> Result<Self> {
72 match s.trim().to_lowercase().as_str() {
73 "mermaid" | "mmd" => Ok(DiagramFormat::Mermaid),
74 "graphviz" | "dot" => Ok(DiagramFormat::GraphViz),
75 "d2" => Ok(DiagramFormat::D2),
76 other => Err(anyhow!("unknown diagram format: {other}")),
77 }
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum GraphType {
84 CallGraph,
86 DependencyGraph,
88 TypeHierarchy,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
94pub enum Direction {
95 #[default]
97 TopDown,
98 BottomUp,
100 LeftRight,
102 RightLeft,
104}
105
106#[derive(Debug, Clone)]
108pub struct DiagramOptions {
109 pub format: DiagramFormat,
111 pub graph_type: GraphType,
113 pub max_depth: Option<usize>,
115 pub max_nodes: usize,
117 pub include_file_paths: bool,
119 pub direction: Direction,
121}
122
123impl Default for DiagramOptions {
124 fn default() -> Self {
125 Self {
126 format: DiagramFormat::Mermaid,
127 graph_type: GraphType::CallGraph,
128 max_depth: Some(3),
129 max_nodes: 100,
130 include_file_paths: true,
131 direction: Direction::TopDown,
132 }
133 }
134}
135
136#[derive(Debug, Clone)]
140pub struct DiagramEdge {
141 pub source: NodeId,
143 pub target: NodeId,
145 pub label: Option<String>,
147}
148
149impl DiagramEdge {
150 #[must_use]
152 pub fn new(source: NodeId, target: NodeId) -> Self {
153 Self {
154 source,
155 target,
156 label: None,
157 }
158 }
159
160 #[must_use]
162 pub fn with_label(source: NodeId, target: NodeId, label: impl Into<String>) -> Self {
163 Self {
164 source,
165 target,
166 label: Some(label.into()),
167 }
168 }
169}
170
171#[derive(Debug, Clone)]
173pub struct Diagram {
174 pub format: DiagramFormat,
176 pub content: String,
178 pub node_count: usize,
180 pub edge_count: usize,
182 pub is_truncated: bool,
184}
185
186impl Diagram {
187 #[must_use]
189 pub fn is_empty(&self) -> bool {
190 self.node_count == 0
191 }
192}
193
194pub trait DiagramFormatter {
196 fn format_call_graph(
210 &self,
211 snapshot: &GraphSnapshot,
212 nodes: &[NodeId],
213 edges: &[DiagramEdge],
214 extra_nodes: &[Node],
215 options: &DiagramOptions,
216 ) -> Result<Diagram>;
217
218 fn format_dependency_graph(
232 &self,
233 snapshot: &GraphSnapshot,
234 nodes: &[NodeId],
235 edges: &[DiagramEdge],
236 extra_nodes: &[Node],
237 options: &DiagramOptions,
238 ) -> Result<Diagram>;
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn diagram_format_from_str() {
247 assert_eq!(
248 DiagramFormat::parse_format("mermaid").unwrap(),
249 DiagramFormat::Mermaid
250 );
251 assert_eq!(
252 DiagramFormat::parse_format("MMD").unwrap(),
253 DiagramFormat::Mermaid
254 );
255 assert_eq!(
256 DiagramFormat::parse_format("graphviz").unwrap(),
257 DiagramFormat::GraphViz
258 );
259 assert_eq!(
260 DiagramFormat::parse_format("dot").unwrap(),
261 DiagramFormat::GraphViz
262 );
263 assert_eq!(
264 DiagramFormat::parse_format("d2").unwrap(),
265 DiagramFormat::D2
266 );
267 assert!(DiagramFormat::parse_format("invalid").is_err());
268 }
269
270 #[test]
271 fn diagram_format_file_extensions() {
272 assert_eq!(DiagramFormat::Mermaid.file_extension(), "mmd");
273 assert_eq!(DiagramFormat::GraphViz.file_extension(), "dot");
274 assert_eq!(DiagramFormat::D2.file_extension(), "d2");
275 }
276
277 #[test]
278 fn diagram_options_defaults() {
279 let opts = DiagramOptions::default();
280 assert_eq!(opts.format, DiagramFormat::Mermaid);
281 assert_eq!(opts.graph_type, GraphType::CallGraph);
282 assert_eq!(opts.max_depth, Some(3));
283 assert_eq!(opts.max_nodes, 100);
284 assert!(opts.include_file_paths);
285 assert_eq!(opts.direction, Direction::TopDown);
286 }
287}