Skip to main content

gexf/
lib.rs

1use quick_xml::{
2    Writer,
3    events::{BytesStart, BytesText, Event},
4};
5use std::{collections::BTreeMap, io::Write};
6use thiserror::Error;
7
8/// Result alias for this crate
9pub 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/// Direction type for the whole graph. Per-edge overrides are not supported in core GEXF; use `Mutual` or split graphs if needed.
24#[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/// Static vs dynamic graphs. This crate currently emits `static` graphs only.
43#[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/// RGB color (0..255 per channel)
58#[derive(Debug, Clone, Copy)]
59pub struct Color {
60    pub r: u8,
61    pub g: u8,
62    pub b: u8,
63}
64
65/// 3D position (Gephi viz namespace)
66#[derive(Debug, Clone, Copy)]
67pub struct Position {
68    pub x: f32,
69    pub y: f32,
70    pub z: f32,
71}
72
73/// A node in the graph
74#[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/// An edge between two nodes
123#[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/// Metadata for the `<meta>` section
159#[derive(Debug, Clone, Default)]
160pub struct Meta {
161    pub creator: Option<String>,
162    pub description: Option<String>,
163    /// ISO-8601 date string (e.g., 2025-10-13). If not provided, it's omitted.
164    pub last_modified: Option<String>,
165}
166
167/// Main builder/graph object
168#[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    /// Serialize to any `Write` sink.
213    pub fn write<W: Write>(&self, mut sink: W) -> Result<()> {
214        let mut w = Writer::new_with_indent(&mut sink, b' ', 2);
215
216        // XML declaration
217        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        // <gexf>
225        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        // <meta>
233        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        // <graph>
266        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        // <nodes>
273        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            // <attvalues>
286            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            // viz namespace (position, size, color)
301            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        // <edges>
332        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        // </graph>
354        w.write_event(Event::End(BytesStart::new("graph").to_end()))
355            .map_err(|e| GexfError::Xml(e.to_string()))?;
356
357        // </gexf>
358        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    /// Serialize the graph to a UTF-8 String (convenience)
365    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/// Convenience builder that ensures unique incremental IDs for edges when you don't care about explicit IDs.
373#[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}