use quick_xml::{
Writer,
events::{BytesStart, BytesText, Event},
};
use std::{collections::BTreeMap, io::Write};
use thiserror::Error;
pub type Result<T> = std::result::Result<T, GexfError>;
#[derive(Debug, Error)]
pub enum GexfError {
#[error("duplicate node id: {0}")]
DuplicateNodeId(String),
#[error("edge references unknown node id: {0}")]
UnknownNodeId(String),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("XML write error: {0}")]
Xml(String),
}
#[derive(Debug, Clone, Copy, Default)]
pub enum EdgeType {
Directed,
#[default]
Undirected,
Mutual,
}
impl EdgeType {
fn as_str(&self) -> &'static str {
match self {
EdgeType::Directed => "directed",
EdgeType::Undirected => "undirected",
EdgeType::Mutual => "mutual",
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub enum GraphMode {
#[default]
Static,
}
impl GraphMode {
fn as_str(&self) -> &'static str {
match self {
GraphMode::Static => "static",
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Color {
pub r: u8,
pub g: u8,
pub b: u8,
}
#[derive(Debug, Clone, Copy)]
pub struct Position {
pub x: f32,
pub y: f32,
pub z: f32,
}
#[derive(Debug, Clone)]
pub struct Node {
pub id: String,
pub label: Option<String>,
pub attributes: BTreeMap<String, String>,
pub position: Option<Position>,
pub size: Option<f32>,
pub color: Option<Color>,
}
impl Node {
pub fn new<T: Into<String>>(id: T) -> Self {
Self {
id: id.into(),
label: None,
attributes: BTreeMap::new(),
position: None,
size: None,
color: None,
}
}
pub fn with_label<T: Into<String>>(mut self, label: T) -> Self {
self.label = Some(label.into());
self
}
pub fn with_attr<K: Into<String>, V: Into<String>>(mut self, key: K, val: V) -> Self {
self.attributes.insert(key.into(), val.into());
self
}
pub fn with_position(mut self, x: f32, y: f32, z: f32) -> Self {
self.position = Some(Position { x, y, z });
self
}
pub fn with_size(mut self, size: f32) -> Self {
self.size = Some(size);
self
}
pub fn with_color(mut self, r: u8, g: u8, b: u8) -> Self {
self.color = Some(Color { r, g, b });
self
}
}
#[derive(Debug, Clone)]
pub struct Edge {
pub id: String,
pub source: String,
pub target: String,
pub weight: Option<f32>,
pub label: Option<String>,
}
impl Edge {
pub fn new<I: Into<String>, S: Into<String>, T: Into<String>>(
id: I,
source: S,
target: T,
) -> Self {
Self {
id: id.into(),
source: source.into(),
target: target.into(),
weight: None,
label: None,
}
}
pub fn with_weight(mut self, w: f32) -> Self {
self.weight = Some(w);
self
}
pub fn with_label<L: Into<String>>(mut self, label: L) -> Self {
self.label = Some(label.into());
self
}
}
#[derive(Debug, Clone, Default)]
pub struct Meta {
pub creator: Option<String>,
pub description: Option<String>,
pub last_modified: Option<String>,
}
#[derive(Debug, Default)]
pub struct Gexf {
pub edge_type: EdgeType,
pub mode: GraphMode,
pub meta: Meta,
nodes: BTreeMap<String, Node>,
edges: Vec<Edge>,
}
impl Gexf {
pub fn new(edge_type: EdgeType) -> Self {
Self {
edge_type,
mode: GraphMode::Static,
meta: Meta::default(),
nodes: BTreeMap::new(),
edges: Vec::new(),
}
}
pub fn set_meta(mut self, meta: Meta) -> Self {
self.meta = meta;
self
}
pub fn add_node(&mut self, node: Node) -> Result<()> {
if self.nodes.contains_key(&node.id) {
return Err(GexfError::DuplicateNodeId(node.id));
}
self.nodes.insert(node.id.clone(), node);
Ok(())
}
pub fn add_edge(&mut self, edge: Edge) -> Result<()> {
if !self.nodes.contains_key(&edge.source) {
return Err(GexfError::UnknownNodeId(edge.source));
}
if !self.nodes.contains_key(&edge.target) {
return Err(GexfError::UnknownNodeId(edge.target));
}
self.edges.push(edge);
Ok(())
}
pub fn write<W: Write>(&self, mut sink: W) -> Result<()> {
let mut w = Writer::new_with_indent(&mut sink, b' ', 2);
w.write_event(Event::Decl(quick_xml::events::BytesDecl::new(
"1.0",
Some("UTF-8"),
None,
)))
.map_err(|e| GexfError::Xml(e.to_string()))?;
let mut gexf = BytesStart::new("gexf");
gexf.push_attribute(("xmlns", "http://www.gexf.net/1.3draft"));
gexf.push_attribute(("xmlns:viz", "http://www.gexf.net/1.3draft/viz"));
gexf.push_attribute(("version", "1.3"));
w.write_event(Event::Start(gexf))
.map_err(|e| GexfError::Xml(e.to_string()))?;
if self.meta.creator.is_some()
|| self.meta.description.is_some()
|| self.meta.last_modified.is_some()
{
let mut meta_tag = BytesStart::new("meta");
if let Some(date) = &self.meta.last_modified {
meta_tag.push_attribute(("lastmodifieddate", date.as_str()));
}
w.write_event(Event::Start(meta_tag))
.map_err(|e| GexfError::Xml(e.to_string()))?;
if let Some(creator) = &self.meta.creator {
w.write_event(Event::Start(BytesStart::new("creator")))
.map_err(|e| GexfError::Xml(e.to_string()))?;
w.write_event(Event::Text(BytesText::new(creator)))
.map_err(|e| GexfError::Xml(e.to_string()))?;
w.write_event(Event::End(BytesStart::new("creator").to_end()))
.map_err(|e| GexfError::Xml(e.to_string()))?;
}
if let Some(desc) = &self.meta.description {
w.write_event(Event::Start(BytesStart::new("description")))
.map_err(|e| GexfError::Xml(e.to_string()))?;
w.write_event(Event::Text(BytesText::new(desc)))
.map_err(|e| GexfError::Xml(e.to_string()))?;
w.write_event(Event::End(BytesStart::new("description").to_end()))
.map_err(|e| GexfError::Xml(e.to_string()))?;
}
w.write_event(Event::End(BytesStart::new("meta").to_end()))
.map_err(|e| GexfError::Xml(e.to_string()))?;
}
let mut graph = BytesStart::new("graph");
graph.push_attribute(("mode", self.mode.as_str()));
graph.push_attribute(("defaultedgetype", self.edge_type.as_str()));
w.write_event(Event::Start(graph))
.map_err(|e| GexfError::Xml(e.to_string()))?;
w.write_event(Event::Start(BytesStart::new("nodes")))
.map_err(|e| GexfError::Xml(e.to_string()))?;
for node in self.nodes.values() {
let mut n = BytesStart::new("node");
n.push_attribute(("id", node.id.as_str()));
if let Some(label) = &node.label {
n.push_attribute(("label", label.as_str()));
}
w.write_event(Event::Start(n))
.map_err(|e| GexfError::Xml(e.to_string()))?;
if !node.attributes.is_empty() {
w.write_event(Event::Start(BytesStart::new("attvalues")))
.map_err(|e| GexfError::Xml(e.to_string()))?;
for (k, v) in &node.attributes {
let mut att = BytesStart::new("attvalue");
att.push_attribute(("for", k.as_str()));
att.push_attribute(("value", v.as_str()));
w.write_event(Event::Empty(att))
.map_err(|e| GexfError::Xml(e.to_string()))?;
}
w.write_event(Event::End(BytesStart::new("attvalues").to_end()))
.map_err(|e| GexfError::Xml(e.to_string()))?;
}
if let Some(pos) = node.position {
let mut p = BytesStart::new("viz:position");
p.push_attribute(("x", pos.x.to_string().as_str()));
p.push_attribute(("y", pos.y.to_string().as_str()));
p.push_attribute(("z", pos.z.to_string().as_str()));
w.write_event(Event::Empty(p))
.map_err(|e| GexfError::Xml(e.to_string()))?;
}
if let Some(size) = node.size {
let mut s = BytesStart::new("viz:size");
s.push_attribute(("value", size.to_string().as_str()));
w.write_event(Event::Empty(s))
.map_err(|e| GexfError::Xml(e.to_string()))?;
}
if let Some(color) = node.color {
let mut c = BytesStart::new("viz:color");
c.push_attribute(("r", color.r.to_string().as_str()));
c.push_attribute(("g", color.g.to_string().as_str()));
c.push_attribute(("b", color.b.to_string().as_str()));
w.write_event(Event::Empty(c))
.map_err(|e| GexfError::Xml(e.to_string()))?;
}
w.write_event(Event::End(BytesStart::new("node").to_end()))
.map_err(|e| GexfError::Xml(e.to_string()))?;
}
w.write_event(Event::End(BytesStart::new("nodes").to_end()))
.map_err(|e| GexfError::Xml(e.to_string()))?;
w.write_event(Event::Start(BytesStart::new("edges")))
.map_err(|e| GexfError::Xml(e.to_string()))?;
for e in &self.edges {
let mut ed = BytesStart::new("edge");
ed.push_attribute(("id", e.id.as_str()));
ed.push_attribute(("source", e.source.as_str()));
ed.push_attribute(("target", e.target.as_str()));
if let Some(wt) = e.weight {
ed.push_attribute(("weight", wt.to_string().as_str()));
}
if let Some(label) = &e.label {
ed.push_attribute(("label", label.as_str()));
}
w.write_event(Event::Empty(ed))
.map_err(|e| GexfError::Xml(e.to_string()))?;
}
w.write_event(Event::End(BytesStart::new("edges").to_end()))
.map_err(|e| GexfError::Xml(e.to_string()))?;
w.write_event(Event::End(BytesStart::new("graph").to_end()))
.map_err(|e| GexfError::Xml(e.to_string()))?;
w.write_event(Event::End(BytesStart::new("gexf").to_end()))
.map_err(|e| GexfError::Xml(e.to_string()))?;
Ok(())
}
pub fn to_string(&self) -> Result<String> {
let mut buf = Vec::new();
self.write(&mut buf)?;
Ok(String::from_utf8(buf).expect("valid utf8"))
}
}
#[derive(Debug, Default)]
pub struct GraphBuilder {
gexf: Gexf,
next_edge_id: u64,
}
impl GraphBuilder {
pub fn new(edge_type: EdgeType) -> Self {
Self {
gexf: Gexf::new(edge_type),
next_edge_id: 0,
}
}
pub fn meta(mut self, creator: impl Into<String>, description: impl Into<String>) -> Self {
self.gexf.meta.creator = Some(creator.into());
self.gexf.meta.description = Some(description.into());
self
}
pub fn last_modified(mut self, date_iso: impl Into<String>) -> Self {
self.gexf.meta.last_modified = Some(date_iso.into());
self
}
pub fn add_node(mut self, node: Node) -> Result<Self> {
self.gexf.add_node(node)?;
Ok(self)
}
pub fn add_edge(
mut self,
source: impl Into<String>,
target: impl Into<String>,
) -> Result<Self> {
let id = format!("e{}", self.next_edge_id);
self.next_edge_id += 1;
let e = Edge::new(id, source.into(), target.into());
self.gexf.add_edge(e)?;
Ok(self)
}
pub fn try_build(self) -> Result<Gexf> {
Ok(self.gexf)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic_write() {
let g = GraphBuilder::new(EdgeType::Directed)
.meta("gexf", "Example graph")
.last_modified("2025-10-13")
.add_node(
Node::new("n0")
.with_label("Alice")
.with_color(200, 50, 50)
.with_size(2.0)
.with_position(0.0, 0.0, 0.0),
)
.unwrap()
.add_node(
Node::new("n1")
.with_label("Bob")
.with_attr("role", "engineer"),
)
.unwrap()
.add_edge("n0", "n1")
.unwrap()
.try_build()
.unwrap();
let xml = g.to_string().unwrap();
assert!(xml.contains("<gexf"));
assert!(xml.contains("<nodes>"));
assert!(xml.contains("<edges>"));
assert!(xml.contains("viz:position"));
}
}