Skip to main content

oximedia_graph/
serialize.rs

1//! Graph serialization: DOT format output, adjacency list, and edge list export.
2
3use std::collections::HashMap;
4
5/// A simple graph representation for serialization purposes.
6#[allow(dead_code)]
7#[derive(Debug, Clone)]
8pub struct SerializableGraph {
9    /// Nodes: id -> optional label.
10    pub nodes: HashMap<usize, String>,
11    /// Edges: (from, to) -> optional label.
12    pub edges: Vec<(usize, usize, Option<String>)>,
13    /// Whether the graph is directed.
14    pub directed: bool,
15    /// Optional graph name.
16    pub name: Option<String>,
17}
18
19impl SerializableGraph {
20    /// Create a new empty directed graph.
21    #[allow(dead_code)]
22    pub fn new_directed() -> Self {
23        Self {
24            nodes: HashMap::new(),
25            edges: Vec::new(),
26            directed: true,
27            name: None,
28        }
29    }
30
31    /// Create a new empty undirected graph.
32    #[allow(dead_code)]
33    pub fn new_undirected() -> Self {
34        Self {
35            nodes: HashMap::new(),
36            edges: Vec::new(),
37            directed: false,
38            name: None,
39        }
40    }
41
42    /// Add a node with an optional label.
43    #[allow(dead_code)]
44    pub fn add_node(&mut self, id: usize, label: impl Into<String>) {
45        self.nodes.insert(id, label.into());
46    }
47
48    /// Add an edge between two nodes with an optional label.
49    #[allow(dead_code)]
50    pub fn add_edge(&mut self, from: usize, to: usize, label: Option<String>) {
51        self.edges.push((from, to, label));
52    }
53
54    /// Serialize to DOT (Graphviz) format.
55    #[allow(dead_code)]
56    pub fn to_dot(&self) -> String {
57        let graph_type = if self.directed { "digraph" } else { "graph" };
58        let name = self.name.as_deref().unwrap_or("G");
59        let edge_op = if self.directed { "->" } else { "--" };
60
61        let mut out = String::new();
62        out.push_str(&format!("{graph_type} {name} {{\n"));
63
64        // Sort node ids for deterministic output
65        let mut node_ids: Vec<usize> = self.nodes.keys().copied().collect();
66        node_ids.sort_unstable();
67        for id in node_ids {
68            let label = &self.nodes[&id];
69            if label.is_empty() {
70                out.push_str(&format!("    {id};\n"));
71            } else {
72                out.push_str(&format!("    {id} [label=\"{label}\"];\n"));
73            }
74        }
75
76        for (from, to, label) in &self.edges {
77            if let Some(lbl) = label {
78                out.push_str(&format!("    {from} {edge_op} {to} [label=\"{lbl}\"];\n"));
79            } else {
80                out.push_str(&format!("    {from} {edge_op} {to};\n"));
81            }
82        }
83
84        out.push_str("}\n");
85        out
86    }
87
88    /// Serialize to adjacency list format.
89    /// Each line: `<node_id>: <neighbor1> <neighbor2> ...`
90    #[allow(dead_code)]
91    pub fn to_adjacency_list(&self) -> String {
92        let mut adj: HashMap<usize, Vec<usize>> = HashMap::new();
93        for &id in self.nodes.keys() {
94            adj.entry(id).or_default();
95        }
96        for &(from, to, _) in &self.edges {
97            adj.entry(from).or_default().push(to);
98            if !self.directed {
99                adj.entry(to).or_default().push(from);
100            }
101        }
102
103        let mut node_ids: Vec<usize> = adj.keys().copied().collect();
104        node_ids.sort_unstable();
105
106        let mut out = String::new();
107        for id in node_ids {
108            let mut neighbors = adj[&id].clone();
109            neighbors.sort_unstable();
110            let neighbor_str = neighbors
111                .iter()
112                .map(|n| n.to_string())
113                .collect::<Vec<_>>()
114                .join(" ");
115            out.push_str(&format!("{id}: {neighbor_str}\n"));
116        }
117        out
118    }
119
120    /// Serialize to edge list format.
121    /// Each line: `<from> <to>` or `<from> <to> <label>`.
122    #[allow(dead_code)]
123    pub fn to_edge_list(&self) -> String {
124        let mut out = String::new();
125        for &(from, to, ref label) in &self.edges {
126            if let Some(lbl) = label {
127                out.push_str(&format!("{from} {to} {lbl}\n"));
128            } else {
129                out.push_str(&format!("{from} {to}\n"));
130            }
131        }
132        out
133    }
134
135    /// Returns the number of nodes.
136    #[allow(dead_code)]
137    pub fn node_count(&self) -> usize {
138        self.nodes.len()
139    }
140
141    /// Returns the number of edges.
142    #[allow(dead_code)]
143    pub fn edge_count(&self) -> usize {
144        self.edges.len()
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    fn make_simple_graph() -> SerializableGraph {
153        let mut g = SerializableGraph::new_directed();
154        g.add_node(0, "A");
155        g.add_node(1, "B");
156        g.add_node(2, "C");
157        g.add_edge(0, 1, None);
158        g.add_edge(1, 2, Some("edge".to_string()));
159        g
160    }
161
162    #[test]
163    fn test_node_count() {
164        let g = make_simple_graph();
165        assert_eq!(g.node_count(), 3);
166    }
167
168    #[test]
169    fn test_edge_count() {
170        let g = make_simple_graph();
171        assert_eq!(g.edge_count(), 2);
172    }
173
174    #[test]
175    fn test_dot_contains_digraph() {
176        let g = make_simple_graph();
177        let dot = g.to_dot();
178        assert!(dot.contains("digraph"));
179    }
180
181    #[test]
182    fn test_dot_contains_nodes() {
183        let g = make_simple_graph();
184        let dot = g.to_dot();
185        assert!(dot.contains("label=\"A\""));
186        assert!(dot.contains("label=\"B\""));
187    }
188
189    #[test]
190    fn test_dot_contains_edge_op() {
191        let g = make_simple_graph();
192        let dot = g.to_dot();
193        assert!(dot.contains("->"));
194    }
195
196    #[test]
197    fn test_dot_undirected_uses_double_dash() {
198        let mut g = SerializableGraph::new_undirected();
199        g.add_node(0, "X");
200        g.add_node(1, "Y");
201        g.add_edge(0, 1, None);
202        let dot = g.to_dot();
203        assert!(dot.contains("--"));
204        assert!(!dot.contains("->"));
205    }
206
207    #[test]
208    fn test_dot_labeled_edge() {
209        let g = make_simple_graph();
210        let dot = g.to_dot();
211        assert!(dot.contains("label=\"edge\""));
212    }
213
214    #[test]
215    fn test_adjacency_list_contains_nodes() {
216        let g = make_simple_graph();
217        let al = g.to_adjacency_list();
218        assert!(al.contains("0:"));
219        assert!(al.contains("1:"));
220        assert!(al.contains("2:"));
221    }
222
223    #[test]
224    fn test_adjacency_list_has_edges() {
225        let g = make_simple_graph();
226        let al = g.to_adjacency_list();
227        // Node 0 should have neighbor 1
228        assert!(al.contains("0: 1"));
229    }
230
231    #[test]
232    fn test_edge_list_format() {
233        let g = make_simple_graph();
234        let el = g.to_edge_list();
235        assert!(el.contains("0 1"));
236        assert!(el.contains("1 2 edge"));
237    }
238
239    #[test]
240    fn test_empty_graph_dot() {
241        let g = SerializableGraph::new_directed();
242        let dot = g.to_dot();
243        assert!(dot.contains("digraph G {"));
244        assert!(dot.contains("}"));
245    }
246
247    #[test]
248    fn test_graph_with_name() {
249        let mut g = SerializableGraph::new_directed();
250        g.name = Some("MyGraph".to_string());
251        let dot = g.to_dot();
252        assert!(dot.contains("digraph MyGraph {"));
253    }
254
255    #[test]
256    fn test_adjacency_list_undirected_bidirectional() {
257        let mut g = SerializableGraph::new_undirected();
258        g.add_node(0, "A");
259        g.add_node(1, "B");
260        g.add_edge(0, 1, None);
261        let al = g.to_adjacency_list();
262        // Both 0->1 and 1->0 should appear
263        assert!(al.contains("0: 1"));
264        assert!(al.contains("1: 0"));
265    }
266}