1use std::io::Write;
7
8use grafeo_core::graph::lpg::{Edge, Node};
9
10use super::{
11 ExportError, discover_edge_schema, discover_node_schema, escape_xml, value_to_graphml_type,
12 value_to_xml_string,
13};
14
15pub fn write_graphml<W: Write>(
24 writer: &mut W,
25 nodes: &[Node],
26 edges: &[Edge],
27) -> Result<(), ExportError> {
28 let node_schema = discover_node_schema(nodes, value_to_graphml_type);
29 let edge_schema = discover_edge_schema(edges, value_to_graphml_type);
30
31 writeln!(writer, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>")?;
33 writeln!(
34 writer,
35 "<graphml xmlns=\"http://graphml.graphdrawing.org/xmlns\">"
36 )?;
37
38 writeln!(
41 writer,
42 " <key id=\"d0\" for=\"node\" attr.name=\"_labels\" attr.type=\"string\"/>"
43 )?;
44 for (key, (id, type_str)) in &node_schema {
45 let key_id = id + 1;
47 writeln!(
48 writer,
49 " <key id=\"d{key_id}\" for=\"node\" attr.name=\"{}\" attr.type=\"{type_str}\"/>",
50 escape_xml(key.as_str())
51 )?;
52 }
53
54 let edge_key_offset = node_schema.len() + 1;
57 writeln!(
58 writer,
59 " <key id=\"d{edge_key_offset}\" for=\"edge\" attr.name=\"_type\" attr.type=\"string\"/>"
60 )?;
61 for (key, (id, type_str)) in &edge_schema {
62 let key_id = edge_key_offset + 1 + id;
63 writeln!(
64 writer,
65 " <key id=\"d{key_id}\" for=\"edge\" attr.name=\"{}\" attr.type=\"{type_str}\"/>",
66 escape_xml(key.as_str())
67 )?;
68 }
69
70 writeln!(writer, " <graph edgedefault=\"directed\">")?;
72
73 for node in nodes {
75 writeln!(writer, " <node id=\"n{}\">", node.id.0)?;
76
77 let labels: String = node
79 .labels
80 .iter()
81 .map(|s| s.as_str())
82 .collect::<Vec<_>>()
83 .join(",");
84 writeln!(
85 writer,
86 " <data key=\"d0\">{}</data>",
87 escape_xml(&labels)
88 )?;
89
90 for (key, (schema_id, _)) in &node_schema {
92 if let Some(value) = node.properties.get(key)
93 && let Some(val_str) = value_to_xml_string(value)
94 {
95 let key_id = schema_id + 1;
96 writeln!(writer, " <data key=\"d{key_id}\">{val_str}</data>")?;
97 }
98 }
99
100 writeln!(writer, " </node>")?;
101 }
102
103 for edge in edges {
105 writeln!(
106 writer,
107 " <edge id=\"e{}\" source=\"n{}\" target=\"n{}\">",
108 edge.id.0, edge.src.0, edge.dst.0
109 )?;
110
111 writeln!(
113 writer,
114 " <data key=\"d{edge_key_offset}\">{}</data>",
115 escape_xml(edge.edge_type.as_str())
116 )?;
117
118 for (key, (schema_id, _)) in &edge_schema {
120 if let Some(value) = edge.properties.get(key)
121 && let Some(val_str) = value_to_xml_string(value)
122 {
123 let key_id = edge_key_offset + 1 + schema_id;
124 writeln!(writer, " <data key=\"d{key_id}\">{val_str}</data>")?;
125 }
126 }
127
128 writeln!(writer, " </edge>")?;
129 }
130
131 writeln!(writer, " </graph>")?;
133 writeln!(writer, "</graphml>")?;
134
135 Ok(())
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use grafeo_common::PropertyKey;
142 use grafeo_common::types::PropertyMap;
143 use grafeo_common::types::{EdgeId, NodeId, Value};
144
145 fn make_node(id: u64, labels: &[&str], props: &[(&str, Value)]) -> Node {
146 let mut properties = PropertyMap::new();
147 for (k, v) in props {
148 properties.insert(PropertyKey::from(*k), v.clone());
149 }
150 Node {
151 id: NodeId(id),
152 labels: labels.iter().map(|s| arcstr::ArcStr::from(*s)).collect(),
153 properties,
154 }
155 }
156
157 fn make_edge(id: u64, src: u64, dst: u64, edge_type: &str, props: &[(&str, Value)]) -> Edge {
158 let mut properties = PropertyMap::new();
159 for (k, v) in props {
160 properties.insert(PropertyKey::from(*k), v.clone());
161 }
162 Edge {
163 id: EdgeId(id),
164 src: NodeId(src),
165 dst: NodeId(dst),
166 edge_type: arcstr::ArcStr::from(edge_type),
167 properties,
168 }
169 }
170
171 #[test]
172 fn test_empty_graph() {
173 let mut buf = Vec::new();
174 write_graphml(&mut buf, &[], &[]).unwrap();
175 let output = String::from_utf8(buf).unwrap();
176 assert!(output.contains("<graphml"));
177 assert!(output.contains("<graph"));
178 assert!(output.contains("</graph>"));
179 assert!(output.contains("</graphml>"));
180 }
181
182 #[test]
183 fn test_single_node_with_labels() {
184 let nodes = vec![make_node(
185 1,
186 &["Person"],
187 &[("name", Value::String("Alix".into()))],
188 )];
189 let mut buf = Vec::new();
190 write_graphml(&mut buf, &nodes, &[]).unwrap();
191 let output = String::from_utf8(buf).unwrap();
192 assert!(output.contains("id=\"n1\""));
193 assert!(output.contains("<data key=\"d0\">Person</data>"));
194 assert!(output.contains("attr.name=\"name\""));
195 assert!(output.contains("<data key=\"d1\">Alix</data>"));
196 }
197
198 #[test]
199 fn test_edge_with_type_and_properties() {
200 let nodes = vec![
201 make_node(1, &["Person"], &[]),
202 make_node(2, &["Person"], &[]),
203 ];
204 let edges = vec![make_edge(
205 0,
206 1,
207 2,
208 "KNOWS",
209 &[("since", Value::Int64(2020))],
210 )];
211 let mut buf = Vec::new();
212 write_graphml(&mut buf, &nodes, &edges).unwrap();
213 let output = String::from_utf8(buf).unwrap();
214 assert!(output.contains("source=\"n1\""));
215 assert!(output.contains("target=\"n2\""));
216 assert!(output.contains(">KNOWS</data>"));
217 assert!(output.contains(">2020</data>"));
218 }
219
220 #[test]
221 fn test_xml_escaping() {
222 let nodes = vec![make_node(
223 1,
224 &["A&B"],
225 &[("note", Value::String("<important>".into()))],
226 )];
227 let mut buf = Vec::new();
228 write_graphml(&mut buf, &nodes, &[]).unwrap();
229 let output = String::from_utf8(buf).unwrap();
230 assert!(output.contains(">A&B</data>"));
231 assert!(output.contains("><important></data>"));
232 }
233
234 #[test]
235 fn test_null_properties_omitted() {
236 let nodes = vec![make_node(
237 1,
238 &["Person"],
239 &[("name", Value::String("Gus".into())), ("age", Value::Null)],
240 )];
241 let mut buf = Vec::new();
242 write_graphml(&mut buf, &nodes, &[]).unwrap();
243 let output = String::from_utf8(buf).unwrap();
244 assert!(output.contains(">Gus</data>"));
245 let data_count = output.matches("<data key=").count();
247 assert_eq!(data_count, 2);
249 }
250
251 #[test]
252 fn test_key_id_namespacing() {
253 let nodes = vec![make_node(
255 1,
256 &["Person"],
257 &[("name", Value::String("Alix".into()))],
258 )];
259 let edges = vec![make_edge(
260 0,
261 1,
262 1,
263 "SELF",
264 &[("weight", Value::Float64(1.0))],
265 )];
266 let mut buf = Vec::new();
267 write_graphml(&mut buf, &nodes, &edges).unwrap();
268 let output = String::from_utf8(buf).unwrap();
269
270 assert!(output.contains("id=\"d0\" for=\"node\" attr.name=\"_labels\""));
272 assert!(output.contains("id=\"d1\" for=\"node\" attr.name=\"name\""));
273 assert!(output.contains("id=\"d2\" for=\"edge\" attr.name=\"_type\""));
274 assert!(output.contains("id=\"d3\" for=\"edge\" attr.name=\"weight\""));
275 }
276}