Skip to main content

sqry_core/schema/
format.rs

1//! Canonical output format enumeration.
2//!
3//! Defines output formats for graph exports and visualizations.
4
5use serde::{Deserialize, Serialize};
6use std::fmt;
7
8/// Output formats for graph exports and visualizations.
9///
10/// Used by `export_graph` and visualization tools to specify
11/// the desired output format.
12///
13/// # Serialization
14///
15/// All variants serialize to lowercase: `"json"`, `"dot"`, etc.
16///
17/// # Examples
18///
19/// ```
20/// use sqry_core::schema::OutputFormat;
21///
22/// let fmt = OutputFormat::Mermaid;
23/// assert_eq!(fmt.as_str(), "mermaid");
24///
25/// let parsed = OutputFormat::parse("dot").unwrap();
26/// assert_eq!(parsed, OutputFormat::Dot);
27/// ```
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
29#[serde(rename_all = "lowercase")]
30#[derive(Default)]
31pub enum OutputFormat {
32    /// JSON format (default).
33    ///
34    /// Structured JSON with nodes, edges, and metadata.
35    /// Best for programmatic consumption and further processing.
36    #[default]
37    Json,
38
39    /// Graphviz DOT format.
40    ///
41    /// Standard graph description language for Graphviz tools.
42    /// Render with: `dot -Tpng graph.dot -o graph.png`
43    Dot,
44
45    /// D2 diagram format.
46    ///
47    /// Modern declarative diagramming language.
48    /// Render with: `d2 graph.d2 graph.svg`
49    D2,
50
51    /// Mermaid diagram format.
52    ///
53    /// Markdown-friendly diagram syntax.
54    /// Renders in GitHub, GitLab, and many documentation tools.
55    Mermaid,
56
57    /// Archify architecture-diagram JSON.
58    ///
59    /// A higher-level, presentation-oriented serialization: symbols are
60    /// grouped into tier-typed components, package boundaries, aggregated
61    /// connections, and summary cards, conforming to Archify's
62    /// `architecture.schema.json`. Unlike `Json` (which mirrors the raw
63    /// node/edge graph) this format is a seeded, depth-limited, deliberately
64    /// lossy architecture view. See
65    /// [`crate::visualization::archify`].
66    Archify,
67}
68
69impl OutputFormat {
70    /// Returns all variants in definition order.
71    #[must_use]
72    pub const fn all() -> &'static [Self] {
73        &[
74            Self::Json,
75            Self::Dot,
76            Self::D2,
77            Self::Mermaid,
78            Self::Archify,
79        ]
80    }
81
82    /// Returns the canonical string representation.
83    #[must_use]
84    pub const fn as_str(self) -> &'static str {
85        match self {
86            Self::Json => "json",
87            Self::Dot => "dot",
88            Self::D2 => "d2",
89            Self::Mermaid => "mermaid",
90            Self::Archify => "archify",
91        }
92    }
93
94    /// Returns the typical file extension for this format.
95    ///
96    /// Archify output is architecture JSON, so it carries a `.archify.json`
97    /// suffix that keeps it distinguishable from the raw-graph `.json`
98    /// export while staying a valid JSON document.
99    #[must_use]
100    pub const fn file_extension(self) -> &'static str {
101        match self {
102            Self::Json => "json",
103            Self::Dot => "dot",
104            Self::D2 => "d2",
105            Self::Mermaid => "mmd",
106            Self::Archify => "archify.json",
107        }
108    }
109
110    /// Parses a string into an `OutputFormat`.
111    ///
112    /// Returns `None` if the string doesn't match any known format.
113    /// Case-insensitive.
114    #[must_use]
115    pub fn parse(s: &str) -> Option<Self> {
116        match s.to_lowercase().as_str() {
117            "json" => Some(Self::Json),
118            "dot" | "graphviz" => Some(Self::Dot),
119            "d2" => Some(Self::D2),
120            "mermaid" | "mmd" => Some(Self::Mermaid),
121            "archify" => Some(Self::Archify),
122            _ => None,
123        }
124    }
125
126    /// Returns `true` if this format produces text output.
127    ///
128    /// All current formats are text-based; this is for future
129    /// binary format support (e.g., PNG, SVG).
130    #[must_use]
131    pub const fn is_text_format(self) -> bool {
132        true
133    }
134
135    /// Returns `true` if this format is a diagram/visualization format.
136    #[must_use]
137    pub const fn is_diagram_format(self) -> bool {
138        matches!(self, Self::Dot | Self::D2 | Self::Mermaid | Self::Archify)
139    }
140}
141
142impl fmt::Display for OutputFormat {
143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144        f.write_str(self.as_str())
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn test_as_str() {
154        assert_eq!(OutputFormat::Json.as_str(), "json");
155        assert_eq!(OutputFormat::Dot.as_str(), "dot");
156        assert_eq!(OutputFormat::D2.as_str(), "d2");
157        assert_eq!(OutputFormat::Mermaid.as_str(), "mermaid");
158        assert_eq!(OutputFormat::Archify.as_str(), "archify");
159    }
160
161    #[test]
162    fn test_file_extension() {
163        assert_eq!(OutputFormat::Json.file_extension(), "json");
164        assert_eq!(OutputFormat::Dot.file_extension(), "dot");
165        assert_eq!(OutputFormat::D2.file_extension(), "d2");
166        assert_eq!(OutputFormat::Mermaid.file_extension(), "mmd");
167        assert_eq!(OutputFormat::Archify.file_extension(), "archify.json");
168    }
169
170    #[test]
171    fn test_parse() {
172        assert_eq!(OutputFormat::parse("json"), Some(OutputFormat::Json));
173        assert_eq!(OutputFormat::parse("DOT"), Some(OutputFormat::Dot));
174        assert_eq!(OutputFormat::parse("graphviz"), Some(OutputFormat::Dot));
175        assert_eq!(OutputFormat::parse("mermaid"), Some(OutputFormat::Mermaid));
176        assert_eq!(OutputFormat::parse("mmd"), Some(OutputFormat::Mermaid));
177        assert_eq!(OutputFormat::parse("archify"), Some(OutputFormat::Archify));
178        assert_eq!(OutputFormat::parse("ARCHIFY"), Some(OutputFormat::Archify));
179        assert_eq!(OutputFormat::parse("unknown"), None);
180    }
181
182    #[test]
183    fn test_display() {
184        assert_eq!(format!("{}", OutputFormat::Json), "json");
185        assert_eq!(format!("{}", OutputFormat::Mermaid), "mermaid");
186    }
187
188    #[test]
189    fn test_serde_roundtrip() {
190        for fmt in OutputFormat::all() {
191            let json = serde_json::to_string(fmt).unwrap();
192            let deserialized: OutputFormat = serde_json::from_str(&json).unwrap();
193            assert_eq!(*fmt, deserialized);
194        }
195    }
196
197    #[test]
198    fn test_classification() {
199        assert!(OutputFormat::Json.is_text_format());
200        assert!(!OutputFormat::Json.is_diagram_format());
201        assert!(OutputFormat::Dot.is_diagram_format());
202        assert!(OutputFormat::Mermaid.is_diagram_format());
203        assert!(OutputFormat::Archify.is_diagram_format());
204    }
205
206    #[test]
207    fn test_default() {
208        assert_eq!(OutputFormat::default(), OutputFormat::Json);
209    }
210}