mod builder;
mod d2;
mod graphviz;
mod mermaid;
pub use builder::{
Edge, GraphBuilder, Node, escape_label_graphviz, escape_label_mermaid, sanitize_node_id,
};
pub use d2::D2Formatter;
pub use graphviz::GraphVizFormatter;
pub use mermaid::MermaidFormatter;
use crate::Result;
use crate::graph::unified::GraphSnapshot;
use crate::graph::unified::node::NodeId;
use anyhow::anyhow;
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiagramFormat {
Mermaid,
GraphViz,
D2,
}
impl DiagramFormat {
pub fn parse_format(value: &str) -> Result<Self> {
value.parse()
}
#[must_use]
pub fn file_extension(&self) -> &'static str {
match self {
DiagramFormat::Mermaid => "mmd",
DiagramFormat::GraphViz => "dot",
DiagramFormat::D2 => "d2",
}
}
}
impl fmt::Display for DiagramFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DiagramFormat::Mermaid => write!(f, "mermaid"),
DiagramFormat::GraphViz => write!(f, "graphviz"),
DiagramFormat::D2 => write!(f, "d2"),
}
}
}
impl FromStr for DiagramFormat {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
match s.trim().to_lowercase().as_str() {
"mermaid" | "mmd" => Ok(DiagramFormat::Mermaid),
"graphviz" | "dot" => Ok(DiagramFormat::GraphViz),
"d2" => Ok(DiagramFormat::D2),
other => Err(anyhow!("unknown diagram format: {other}")),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GraphType {
CallGraph,
DependencyGraph,
TypeHierarchy,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Direction {
#[default]
TopDown,
BottomUp,
LeftRight,
RightLeft,
}
#[derive(Debug, Clone)]
pub struct DiagramOptions {
pub format: DiagramFormat,
pub graph_type: GraphType,
pub max_depth: Option<usize>,
pub max_nodes: usize,
pub include_file_paths: bool,
pub direction: Direction,
}
impl Default for DiagramOptions {
fn default() -> Self {
Self {
format: DiagramFormat::Mermaid,
graph_type: GraphType::CallGraph,
max_depth: Some(3),
max_nodes: 100,
include_file_paths: true,
direction: Direction::TopDown,
}
}
}
#[derive(Debug, Clone)]
pub struct DiagramEdge {
pub source: NodeId,
pub target: NodeId,
pub label: Option<String>,
}
impl DiagramEdge {
#[must_use]
pub fn new(source: NodeId, target: NodeId) -> Self {
Self {
source,
target,
label: None,
}
}
#[must_use]
pub fn with_label(source: NodeId, target: NodeId, label: impl Into<String>) -> Self {
Self {
source,
target,
label: Some(label.into()),
}
}
}
#[derive(Debug, Clone)]
pub struct Diagram {
pub format: DiagramFormat,
pub content: String,
pub node_count: usize,
pub edge_count: usize,
pub is_truncated: bool,
}
impl Diagram {
#[must_use]
pub fn is_empty(&self) -> bool {
self.node_count == 0
}
}
pub trait DiagramFormatter {
fn format_call_graph(
&self,
snapshot: &GraphSnapshot,
nodes: &[NodeId],
edges: &[DiagramEdge],
extra_nodes: &[Node],
options: &DiagramOptions,
) -> Result<Diagram>;
fn format_dependency_graph(
&self,
snapshot: &GraphSnapshot,
nodes: &[NodeId],
edges: &[DiagramEdge],
extra_nodes: &[Node],
options: &DiagramOptions,
) -> Result<Diagram>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn diagram_format_from_str() {
assert_eq!(
DiagramFormat::parse_format("mermaid").unwrap(),
DiagramFormat::Mermaid
);
assert_eq!(
DiagramFormat::parse_format("MMD").unwrap(),
DiagramFormat::Mermaid
);
assert_eq!(
DiagramFormat::parse_format("graphviz").unwrap(),
DiagramFormat::GraphViz
);
assert_eq!(
DiagramFormat::parse_format("dot").unwrap(),
DiagramFormat::GraphViz
);
assert_eq!(
DiagramFormat::parse_format("d2").unwrap(),
DiagramFormat::D2
);
assert!(DiagramFormat::parse_format("invalid").is_err());
}
#[test]
fn diagram_format_file_extensions() {
assert_eq!(DiagramFormat::Mermaid.file_extension(), "mmd");
assert_eq!(DiagramFormat::GraphViz.file_extension(), "dot");
assert_eq!(DiagramFormat::D2.file_extension(), "d2");
}
#[test]
fn diagram_options_defaults() {
let opts = DiagramOptions::default();
assert_eq!(opts.format, DiagramFormat::Mermaid);
assert_eq!(opts.graph_type, GraphType::CallGraph);
assert_eq!(opts.max_depth, Some(3));
assert_eq!(opts.max_nodes, 100);
assert!(opts.include_file_paths);
assert_eq!(opts.direction, Direction::TopDown);
}
}