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_gexf_type,
12 value_to_xml_string,
13};
14
15pub fn write_gexf<W: Write>(
21 writer: &mut W,
22 nodes: &[Node],
23 edges: &[Edge],
24) -> Result<(), ExportError> {
25 let node_schema = discover_node_schema(nodes, value_to_gexf_type);
26 let edge_schema = discover_edge_schema(edges, value_to_gexf_type);
27
28 writeln!(writer, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>")?;
30 writeln!(
31 writer,
32 "<gexf xmlns=\"http://gexf.net/1.3\" version=\"1.3\">"
33 )?;
34 writeln!(writer, " <meta>")?;
35 writeln!(writer, " <creator>GrafeoDB</creator>")?;
36 writeln!(writer, " </meta>")?;
37 writeln!(
38 writer,
39 " <graph defaultedgetype=\"directed\" mode=\"static\">"
40 )?;
41
42 if !node_schema.is_empty() {
44 writeln!(writer, " <attributes class=\"node\">")?;
45 for (key, (id, type_str)) in &node_schema {
46 writeln!(
47 writer,
48 " <attribute id=\"{id}\" title=\"{}\" type=\"{type_str}\"/>",
49 escape_xml(key.as_str())
50 )?;
51 }
52 writeln!(writer, " </attributes>")?;
53 }
54
55 if !edge_schema.is_empty() {
57 writeln!(writer, " <attributes class=\"edge\">")?;
58 for (key, (id, type_str)) in &edge_schema {
59 writeln!(
60 writer,
61 " <attribute id=\"{id}\" title=\"{}\" type=\"{type_str}\"/>",
62 escape_xml(key.as_str())
63 )?;
64 }
65 writeln!(writer, " </attributes>")?;
66 }
67
68 writeln!(writer, " <nodes>")?;
70 for node in nodes {
71 let label: String = node
72 .labels
73 .iter()
74 .map(|s| s.as_str())
75 .collect::<Vec<_>>()
76 .join(",");
77 write!(
78 writer,
79 " <node id=\"{}\" label=\"{}\"",
80 node.id.0,
81 escape_xml(&label)
82 )?;
83
84 let attvalues: Vec<_> = node_schema
86 .iter()
87 .filter_map(|(key, (id, _))| {
88 node.properties
89 .get(key)
90 .and_then(|v| value_to_xml_string(v).map(|s| (*id, s)))
91 })
92 .collect();
93
94 if attvalues.is_empty() {
95 writeln!(writer, "/>")?;
96 } else {
97 writeln!(writer, ">")?;
98 writeln!(writer, " <attvalues>")?;
99 for (id, val_str) in &attvalues {
100 writeln!(
101 writer,
102 " <attvalue for=\"{id}\" value=\"{}\"/>",
103 val_str
104 )?;
105 }
106 writeln!(writer, " </attvalues>")?;
107 writeln!(writer, " </node>")?;
108 }
109 }
110 writeln!(writer, " </nodes>")?;
111
112 writeln!(writer, " <edges>")?;
114 for edge in edges {
115 write!(
116 writer,
117 " <edge id=\"{}\" source=\"{}\" target=\"{}\" label=\"{}\"",
118 edge.id.0,
119 edge.src.0,
120 edge.dst.0,
121 escape_xml(edge.edge_type.as_str())
122 )?;
123
124 let attvalues: Vec<_> = edge_schema
125 .iter()
126 .filter_map(|(key, (id, _))| {
127 edge.properties
128 .get(key)
129 .and_then(|v| value_to_xml_string(v).map(|s| (*id, s)))
130 })
131 .collect();
132
133 if attvalues.is_empty() {
134 writeln!(writer, "/>")?;
135 } else {
136 writeln!(writer, ">")?;
137 writeln!(writer, " <attvalues>")?;
138 for (id, val_str) in &attvalues {
139 writeln!(
140 writer,
141 " <attvalue for=\"{id}\" value=\"{}\"/>",
142 val_str
143 )?;
144 }
145 writeln!(writer, " </attvalues>")?;
146 writeln!(writer, " </edge>")?;
147 }
148 }
149 writeln!(writer, " </edges>")?;
150
151 writeln!(writer, " </graph>")?;
153 writeln!(writer, "</gexf>")?;
154
155 Ok(())
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use grafeo_common::PropertyKey;
162 use grafeo_common::types::PropertyMap;
163 use grafeo_common::types::{EdgeId, NodeId, Value};
164
165 fn make_node(id: u64, labels: &[&str], props: &[(&str, Value)]) -> Node {
166 let mut properties = PropertyMap::new();
167 for (k, v) in props {
168 properties.insert(PropertyKey::from(*k), v.clone());
169 }
170 Node {
171 id: NodeId(id),
172 labels: labels.iter().map(|s| arcstr::ArcStr::from(*s)).collect(),
173 properties,
174 }
175 }
176
177 fn make_edge(id: u64, src: u64, dst: u64, edge_type: &str, props: &[(&str, Value)]) -> Edge {
178 let mut properties = PropertyMap::new();
179 for (k, v) in props {
180 properties.insert(PropertyKey::from(*k), v.clone());
181 }
182 Edge {
183 id: EdgeId(id),
184 src: NodeId(src),
185 dst: NodeId(dst),
186 edge_type: arcstr::ArcStr::from(edge_type),
187 properties,
188 }
189 }
190
191 #[test]
192 fn test_empty_graph() {
193 let mut buf = Vec::new();
194 write_gexf(&mut buf, &[], &[]).unwrap();
195 let output = String::from_utf8(buf).unwrap();
196 assert!(output.contains("<gexf"));
197 assert!(output.contains("<nodes>"));
198 assert!(output.contains("</nodes>"));
199 assert!(output.contains("<edges>"));
200 assert!(output.contains("</edges>"));
201 }
202
203 #[test]
204 fn test_single_node() {
205 let nodes = vec![make_node(
206 1,
207 &["Person"],
208 &[("name", Value::String("Alix".into()))],
209 )];
210 let mut buf = Vec::new();
211 write_gexf(&mut buf, &nodes, &[]).unwrap();
212 let output = String::from_utf8(buf).unwrap();
213 assert!(output.contains("id=\"1\""));
214 assert!(output.contains("label=\"Person\""));
215 assert!(output.contains("title=\"name\""));
216 assert!(output.contains("value=\"Alix\""));
217 }
218
219 #[test]
220 fn test_node_with_multiple_labels() {
221 let nodes = vec![make_node(1, &["Person", "Employee"], &[])];
222 let mut buf = Vec::new();
223 write_gexf(&mut buf, &nodes, &[]).unwrap();
224 let output = String::from_utf8(buf).unwrap();
225 assert!(output.contains("label=\"Person,Employee\""));
226 }
227
228 #[test]
229 fn test_edge_with_properties() {
230 let nodes = vec![
231 make_node(1, &["Person"], &[]),
232 make_node(2, &["Person"], &[]),
233 ];
234 let edges = vec![make_edge(
235 0,
236 1,
237 2,
238 "KNOWS",
239 &[("since", Value::Int64(2020))],
240 )];
241 let mut buf = Vec::new();
242 write_gexf(&mut buf, &nodes, &edges).unwrap();
243 let output = String::from_utf8(buf).unwrap();
244 assert!(output.contains("source=\"1\""));
245 assert!(output.contains("target=\"2\""));
246 assert!(output.contains("label=\"KNOWS\""));
247 assert!(output.contains("value=\"2020\""));
248 }
249
250 #[test]
251 fn test_xml_escaping_in_values() {
252 let nodes = vec![make_node(
253 1,
254 &["Type<A>"],
255 &[("desc", Value::String("a & b".into()))],
256 )];
257 let mut buf = Vec::new();
258 write_gexf(&mut buf, &nodes, &[]).unwrap();
259 let output = String::from_utf8(buf).unwrap();
260 assert!(output.contains("label=\"Type<A>\""));
261 assert!(output.contains("value=\"a & b\""));
262 }
263
264 #[test]
265 fn test_null_properties_omitted() {
266 let nodes = vec![make_node(
267 1,
268 &["Person"],
269 &[("name", Value::String("Alix".into())), ("age", Value::Null)],
270 )];
271 let mut buf = Vec::new();
272 write_gexf(&mut buf, &nodes, &[]).unwrap();
273 let output = String::from_utf8(buf).unwrap();
274 assert!(output.contains("value=\"Alix\""));
276 assert_eq!(output.matches("attvalue for=").count(), 1);
278 }
279}