Skip to main content

oxihuman_export/
graphml_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Export a graph as GraphML XML.
5
6#![allow(dead_code)]
7
8/// A GraphML node.
9#[allow(dead_code)]
10#[derive(Debug, Clone)]
11pub struct GraphMlNode {
12    pub id: String,
13    pub label: String,
14}
15
16/// A GraphML edge.
17#[allow(dead_code)]
18#[derive(Debug, Clone)]
19pub struct GraphMlEdge {
20    pub source: String,
21    pub target: String,
22    pub id: String,
23}
24
25/// A GraphML export document.
26#[allow(dead_code)]
27#[derive(Debug, Clone, Default)]
28pub struct GraphMlExport {
29    pub graph_id: String,
30    pub directed: bool,
31    pub nodes: Vec<GraphMlNode>,
32    pub edges: Vec<GraphMlEdge>,
33}
34
35/// Create a new GraphML export.
36#[allow(dead_code)]
37pub fn new_graphml_export(graph_id: &str, directed: bool) -> GraphMlExport {
38    GraphMlExport {
39        graph_id: graph_id.to_string(),
40        directed,
41        nodes: Vec::new(),
42        edges: Vec::new(),
43    }
44}
45
46/// Add a node.
47#[allow(dead_code)]
48pub fn add_graphml_node(doc: &mut GraphMlExport, id: &str, label: &str) {
49    doc.nodes.push(GraphMlNode {
50        id: id.to_string(),
51        label: label.to_string(),
52    });
53}
54
55/// Add an edge.
56#[allow(dead_code)]
57pub fn add_graphml_edge(doc: &mut GraphMlExport, source: &str, target: &str) {
58    let edge_id = format!("e{}", doc.edges.len());
59    doc.edges.push(GraphMlEdge {
60        source: source.to_string(),
61        target: target.to_string(),
62        id: edge_id,
63    });
64}
65
66/// Return node count.
67#[allow(dead_code)]
68pub fn graphml_node_count(doc: &GraphMlExport) -> usize {
69    doc.nodes.len()
70}
71
72/// Return edge count.
73#[allow(dead_code)]
74pub fn graphml_edge_count(doc: &GraphMlExport) -> usize {
75    doc.edges.len()
76}
77
78/// Serialise as GraphML XML.
79#[allow(dead_code)]
80pub fn to_graphml_string(doc: &GraphMlExport) -> String {
81    let edge_default = if doc.directed {
82        "directed"
83    } else {
84        "undirected"
85    };
86    let mut out = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
87    out.push_str("<graphml xmlns=\"http://graphml.graphdrawing.org/graphml\">\n");
88    out.push_str(&format!(
89        "  <graph id=\"{}\" edgedefault=\"{}\">\n",
90        doc.graph_id, edge_default
91    ));
92    for node in &doc.nodes {
93        out.push_str(&format!(
94            "    <node id=\"{}\"><data key=\"label\">{}</data></node>\n",
95            node.id, node.label
96        ));
97    }
98    for edge in &doc.edges {
99        out.push_str(&format!(
100            "    <edge id=\"{}\" source=\"{}\" target=\"{}\"/>\n",
101            edge.id, edge.source, edge.target
102        ));
103    }
104    out.push_str("  </graph>\n</graphml>");
105    out
106}
107
108/// Find a node by id.
109#[allow(dead_code)]
110pub fn find_graphml_node<'a>(doc: &'a GraphMlExport, id: &str) -> Option<&'a GraphMlNode> {
111    doc.nodes.iter().find(|n| n.id == id)
112}
113
114/// Export a list of bones as GraphML.
115#[allow(dead_code)]
116pub fn export_bones_graphml(bones: &[(&str, Option<&str>)]) -> String {
117    let mut doc = new_graphml_export("skeleton", true);
118    for &(bone, parent) in bones {
119        add_graphml_node(&mut doc, bone, bone);
120        if let Some(p) = parent {
121            add_graphml_edge(&mut doc, p, bone);
122        }
123    }
124    to_graphml_string(&doc)
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn test_new_graphml_empty() {
133        let doc = new_graphml_export("g", true);
134        assert_eq!(graphml_node_count(&doc), 0);
135        assert_eq!(graphml_edge_count(&doc), 0);
136    }
137
138    #[test]
139    fn test_add_node() {
140        let mut doc = new_graphml_export("g", true);
141        add_graphml_node(&mut doc, "n1", "Node 1");
142        assert_eq!(graphml_node_count(&doc), 1);
143    }
144
145    #[test]
146    fn test_add_edge() {
147        let mut doc = new_graphml_export("g", true);
148        add_graphml_edge(&mut doc, "a", "b");
149        assert_eq!(graphml_edge_count(&doc), 1);
150    }
151
152    #[test]
153    fn test_to_graphml_contains_xml_header() {
154        let doc = new_graphml_export("g", true);
155        let s = to_graphml_string(&doc);
156        assert!(s.contains("<?xml"));
157    }
158
159    #[test]
160    fn test_to_graphml_contains_graph_id() {
161        let doc = new_graphml_export("mygraph", true);
162        let s = to_graphml_string(&doc);
163        assert!(s.contains("mygraph"));
164    }
165
166    #[test]
167    fn test_to_graphml_contains_node() {
168        let mut doc = new_graphml_export("g", true);
169        add_graphml_node(&mut doc, "hip", "Hip");
170        let s = to_graphml_string(&doc);
171        assert!(s.contains("hip"));
172    }
173
174    #[test]
175    fn test_to_graphml_contains_edge() {
176        let mut doc = new_graphml_export("g", true);
177        add_graphml_edge(&mut doc, "a", "b");
178        let s = to_graphml_string(&doc);
179        assert!(s.contains("<edge"));
180    }
181
182    #[test]
183    fn test_find_graphml_node() {
184        let mut doc = new_graphml_export("g", true);
185        add_graphml_node(&mut doc, "knee", "Knee");
186        assert!(find_graphml_node(&doc, "knee").is_some());
187    }
188
189    #[test]
190    fn test_export_bones_graphml() {
191        let bones = vec![("root", None), ("spine", Some("root"))];
192        let s = export_bones_graphml(&bones);
193        assert!(s.contains("skeleton"));
194    }
195
196    #[test]
197    fn test_undirected_edge_default() {
198        let doc = new_graphml_export("g", false);
199        let s = to_graphml_string(&doc);
200        assert!(s.contains("undirected"));
201    }
202}