1use quick_xml::{
2 Writer,
3 events::{BytesStart, BytesText, Event},
4};
5use std::{collections::BTreeMap, io::Write};
6use thiserror::Error;
7
8pub type Result<T> = std::result::Result<T, GexfError>;
10
11#[derive(Debug, Error)]
12pub enum GexfError {
13 #[error("duplicate node id: {0}")]
14 DuplicateNodeId(String),
15 #[error("edge references unknown node id: {0}")]
16 UnknownNodeId(String),
17 #[error("I/O error: {0}")]
18 Io(#[from] std::io::Error),
19 #[error("XML write error: {0}")]
20 Xml(String),
21}
22
23#[derive(Debug, Clone, Copy, Default)]
25pub enum EdgeType {
26 Directed,
27 #[default]
28 Undirected,
29 Mutual,
30}
31
32impl EdgeType {
33 fn as_str(&self) -> &'static str {
34 match self {
35 EdgeType::Directed => "directed",
36 EdgeType::Undirected => "undirected",
37 EdgeType::Mutual => "mutual",
38 }
39 }
40}
41
42#[derive(Debug, Clone, Copy, Default)]
44pub enum GraphMode {
45 #[default]
46 Static,
47}
48
49impl GraphMode {
50 fn as_str(&self) -> &'static str {
51 match self {
52 GraphMode::Static => "static",
53 }
54 }
55}
56
57#[derive(Debug, Clone, Copy)]
59pub struct Color {
60 pub r: u8,
61 pub g: u8,
62 pub b: u8,
63}
64
65#[derive(Debug, Clone, Copy)]
67pub struct Position {
68 pub x: f32,
69 pub y: f32,
70 pub z: f32,
71}
72
73#[derive(Debug, Clone)]
75pub struct Node {
76 pub id: String,
77 pub label: Option<String>,
78 pub attributes: BTreeMap<String, String>,
79 pub position: Option<Position>,
80 pub size: Option<f32>,
81 pub color: Option<Color>,
82}
83
84impl Node {
85 pub fn new<T: Into<String>>(id: T) -> Self {
86 Self {
87 id: id.into(),
88 label: None,
89 attributes: BTreeMap::new(),
90 position: None,
91 size: None,
92 color: None,
93 }
94 }
95
96 pub fn with_label<T: Into<String>>(mut self, label: T) -> Self {
97 self.label = Some(label.into());
98 self
99 }
100
101 pub fn with_attr<K: Into<String>, V: Into<String>>(mut self, key: K, val: V) -> Self {
102 self.attributes.insert(key.into(), val.into());
103 self
104 }
105
106 pub fn with_position(mut self, x: f32, y: f32, z: f32) -> Self {
107 self.position = Some(Position { x, y, z });
108 self
109 }
110
111 pub fn with_size(mut self, size: f32) -> Self {
112 self.size = Some(size);
113 self
114 }
115
116 pub fn with_color(mut self, r: u8, g: u8, b: u8) -> Self {
117 self.color = Some(Color { r, g, b });
118 self
119 }
120}
121
122#[derive(Debug, Clone)]
124pub struct Edge {
125 pub id: String,
126 pub source: String,
127 pub target: String,
128 pub weight: Option<f32>,
129 pub label: Option<String>,
130}
131
132impl Edge {
133 pub fn new<I: Into<String>, S: Into<String>, T: Into<String>>(
134 id: I,
135 source: S,
136 target: T,
137 ) -> Self {
138 Self {
139 id: id.into(),
140 source: source.into(),
141 target: target.into(),
142 weight: None,
143 label: None,
144 }
145 }
146
147 pub fn with_weight(mut self, w: f32) -> Self {
148 self.weight = Some(w);
149 self
150 }
151
152 pub fn with_label<L: Into<String>>(mut self, label: L) -> Self {
153 self.label = Some(label.into());
154 self
155 }
156}
157
158#[derive(Debug, Clone, Default)]
160pub struct Meta {
161 pub creator: Option<String>,
162 pub description: Option<String>,
163 pub last_modified: Option<String>,
165}
166
167#[derive(Debug, Default)]
169pub struct Gexf {
170 pub edge_type: EdgeType,
171 pub mode: GraphMode,
172 pub meta: Meta,
173 nodes: BTreeMap<String, Node>,
174 edges: Vec<Edge>,
175}
176
177impl Gexf {
178 pub fn new(edge_type: EdgeType) -> Self {
179 Self {
180 edge_type,
181 mode: GraphMode::Static,
182 meta: Meta::default(),
183 nodes: BTreeMap::new(),
184 edges: Vec::new(),
185 }
186 }
187
188 pub fn set_meta(mut self, meta: Meta) -> Self {
189 self.meta = meta;
190 self
191 }
192
193 pub fn add_node(&mut self, node: Node) -> Result<()> {
194 if self.nodes.contains_key(&node.id) {
195 return Err(GexfError::DuplicateNodeId(node.id));
196 }
197 self.nodes.insert(node.id.clone(), node);
198 Ok(())
199 }
200
201 pub fn add_edge(&mut self, edge: Edge) -> Result<()> {
202 if !self.nodes.contains_key(&edge.source) {
203 return Err(GexfError::UnknownNodeId(edge.source));
204 }
205 if !self.nodes.contains_key(&edge.target) {
206 return Err(GexfError::UnknownNodeId(edge.target));
207 }
208 self.edges.push(edge);
209 Ok(())
210 }
211
212 pub fn write<W: Write>(&self, mut sink: W) -> Result<()> {
214 let mut w = Writer::new_with_indent(&mut sink, b' ', 2);
215
216 w.write_event(Event::Decl(quick_xml::events::BytesDecl::new(
218 "1.0",
219 Some("UTF-8"),
220 None,
221 )))
222 .map_err(|e| GexfError::Xml(e.to_string()))?;
223
224 let mut gexf = BytesStart::new("gexf");
226 gexf.push_attribute(("xmlns", "http://www.gexf.net/1.3draft"));
227 gexf.push_attribute(("xmlns:viz", "http://www.gexf.net/1.3draft/viz"));
228 gexf.push_attribute(("version", "1.3"));
229 w.write_event(Event::Start(gexf))
230 .map_err(|e| GexfError::Xml(e.to_string()))?;
231
232 if self.meta.creator.is_some()
234 || self.meta.description.is_some()
235 || self.meta.last_modified.is_some()
236 {
237 let mut meta_tag = BytesStart::new("meta");
238 if let Some(date) = &self.meta.last_modified {
239 meta_tag.push_attribute(("lastmodifieddate", date.as_str()));
240 }
241 w.write_event(Event::Start(meta_tag))
242 .map_err(|e| GexfError::Xml(e.to_string()))?;
243
244 if let Some(creator) = &self.meta.creator {
245 w.write_event(Event::Start(BytesStart::new("creator")))
246 .map_err(|e| GexfError::Xml(e.to_string()))?;
247 w.write_event(Event::Text(BytesText::new(creator)))
248 .map_err(|e| GexfError::Xml(e.to_string()))?;
249 w.write_event(Event::End(BytesStart::new("creator").to_end()))
250 .map_err(|e| GexfError::Xml(e.to_string()))?;
251 }
252 if let Some(desc) = &self.meta.description {
253 w.write_event(Event::Start(BytesStart::new("description")))
254 .map_err(|e| GexfError::Xml(e.to_string()))?;
255 w.write_event(Event::Text(BytesText::new(desc)))
256 .map_err(|e| GexfError::Xml(e.to_string()))?;
257 w.write_event(Event::End(BytesStart::new("description").to_end()))
258 .map_err(|e| GexfError::Xml(e.to_string()))?;
259 }
260
261 w.write_event(Event::End(BytesStart::new("meta").to_end()))
262 .map_err(|e| GexfError::Xml(e.to_string()))?;
263 }
264
265 let mut graph = BytesStart::new("graph");
267 graph.push_attribute(("mode", self.mode.as_str()));
268 graph.push_attribute(("defaultedgetype", self.edge_type.as_str()));
269 w.write_event(Event::Start(graph))
270 .map_err(|e| GexfError::Xml(e.to_string()))?;
271
272 w.write_event(Event::Start(BytesStart::new("nodes")))
274 .map_err(|e| GexfError::Xml(e.to_string()))?;
275
276 for node in self.nodes.values() {
277 let mut n = BytesStart::new("node");
278 n.push_attribute(("id", node.id.as_str()));
279 if let Some(label) = &node.label {
280 n.push_attribute(("label", label.as_str()));
281 }
282 w.write_event(Event::Start(n))
283 .map_err(|e| GexfError::Xml(e.to_string()))?;
284
285 if !node.attributes.is_empty() {
287 w.write_event(Event::Start(BytesStart::new("attvalues")))
288 .map_err(|e| GexfError::Xml(e.to_string()))?;
289 for (k, v) in &node.attributes {
290 let mut att = BytesStart::new("attvalue");
291 att.push_attribute(("for", k.as_str()));
292 att.push_attribute(("value", v.as_str()));
293 w.write_event(Event::Empty(att))
294 .map_err(|e| GexfError::Xml(e.to_string()))?;
295 }
296 w.write_event(Event::End(BytesStart::new("attvalues").to_end()))
297 .map_err(|e| GexfError::Xml(e.to_string()))?;
298 }
299
300 if let Some(pos) = node.position {
302 let mut p = BytesStart::new("viz:position");
303 p.push_attribute(("x", pos.x.to_string().as_str()));
304 p.push_attribute(("y", pos.y.to_string().as_str()));
305 p.push_attribute(("z", pos.z.to_string().as_str()));
306 w.write_event(Event::Empty(p))
307 .map_err(|e| GexfError::Xml(e.to_string()))?;
308 }
309 if let Some(size) = node.size {
310 let mut s = BytesStart::new("viz:size");
311 s.push_attribute(("value", size.to_string().as_str()));
312 w.write_event(Event::Empty(s))
313 .map_err(|e| GexfError::Xml(e.to_string()))?;
314 }
315 if let Some(color) = node.color {
316 let mut c = BytesStart::new("viz:color");
317 c.push_attribute(("r", color.r.to_string().as_str()));
318 c.push_attribute(("g", color.g.to_string().as_str()));
319 c.push_attribute(("b", color.b.to_string().as_str()));
320 w.write_event(Event::Empty(c))
321 .map_err(|e| GexfError::Xml(e.to_string()))?;
322 }
323
324 w.write_event(Event::End(BytesStart::new("node").to_end()))
325 .map_err(|e| GexfError::Xml(e.to_string()))?;
326 }
327
328 w.write_event(Event::End(BytesStart::new("nodes").to_end()))
329 .map_err(|e| GexfError::Xml(e.to_string()))?;
330
331 w.write_event(Event::Start(BytesStart::new("edges")))
333 .map_err(|e| GexfError::Xml(e.to_string()))?;
334
335 for e in &self.edges {
336 let mut ed = BytesStart::new("edge");
337 ed.push_attribute(("id", e.id.as_str()));
338 ed.push_attribute(("source", e.source.as_str()));
339 ed.push_attribute(("target", e.target.as_str()));
340 if let Some(wt) = e.weight {
341 ed.push_attribute(("weight", wt.to_string().as_str()));
342 }
343 if let Some(label) = &e.label {
344 ed.push_attribute(("label", label.as_str()));
345 }
346 w.write_event(Event::Empty(ed))
347 .map_err(|e| GexfError::Xml(e.to_string()))?;
348 }
349
350 w.write_event(Event::End(BytesStart::new("edges").to_end()))
351 .map_err(|e| GexfError::Xml(e.to_string()))?;
352
353 w.write_event(Event::End(BytesStart::new("graph").to_end()))
355 .map_err(|e| GexfError::Xml(e.to_string()))?;
356
357 w.write_event(Event::End(BytesStart::new("gexf").to_end()))
359 .map_err(|e| GexfError::Xml(e.to_string()))?;
360
361 Ok(())
362 }
363
364 pub fn to_string(&self) -> Result<String> {
366 let mut buf = Vec::new();
367 self.write(&mut buf)?;
368 Ok(String::from_utf8(buf).expect("valid utf8"))
369 }
370}
371
372#[derive(Debug, Default)]
374pub struct GraphBuilder {
375 gexf: Gexf,
376 next_edge_id: u64,
377}
378
379impl GraphBuilder {
380 pub fn new(edge_type: EdgeType) -> Self {
381 Self {
382 gexf: Gexf::new(edge_type),
383 next_edge_id: 0,
384 }
385 }
386
387 pub fn meta(mut self, creator: impl Into<String>, description: impl Into<String>) -> Self {
388 self.gexf.meta.creator = Some(creator.into());
389 self.gexf.meta.description = Some(description.into());
390 self
391 }
392
393 pub fn last_modified(mut self, date_iso: impl Into<String>) -> Self {
394 self.gexf.meta.last_modified = Some(date_iso.into());
395 self
396 }
397
398 pub fn add_node(mut self, node: Node) -> Result<Self> {
399 self.gexf.add_node(node)?;
400 Ok(self)
401 }
402
403 pub fn add_edge(
404 mut self,
405 source: impl Into<String>,
406 target: impl Into<String>,
407 ) -> Result<Self> {
408 let id = format!("e{}", self.next_edge_id);
409 self.next_edge_id += 1;
410 let e = Edge::new(id, source.into(), target.into());
411 self.gexf.add_edge(e)?;
412 Ok(self)
413 }
414
415 pub fn try_build(self) -> Result<Gexf> {
416 Ok(self.gexf)
417 }
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423
424 #[test]
425 fn basic_write() {
426 let g = GraphBuilder::new(EdgeType::Directed)
427 .meta("gexf", "Example graph")
428 .last_modified("2025-10-13")
429 .add_node(
430 Node::new("n0")
431 .with_label("Alice")
432 .with_color(200, 50, 50)
433 .with_size(2.0)
434 .with_position(0.0, 0.0, 0.0),
435 )
436 .unwrap()
437 .add_node(
438 Node::new("n1")
439 .with_label("Bob")
440 .with_attr("role", "engineer"),
441 )
442 .unwrap()
443 .add_edge("n0", "n1")
444 .unwrap()
445 .try_build()
446 .unwrap();
447
448 let xml = g.to_string().unwrap();
449 assert!(xml.contains("<gexf"));
450 assert!(xml.contains("<nodes>"));
451 assert!(xml.contains("<edges>"));
452 assert!(xml.contains("viz:position"));
453 }
454}