1use std::collections::HashMap;
30use std::io::{BufRead, BufReader, Read, Write};
31
32use crate::core::attributes::AttributeValue;
33use crate::core::{Graph, IgraphError, IgraphResult};
34
35pub struct DotGraph {
37 pub graph: Graph,
39 pub labels: Vec<String>,
42}
43
44#[allow(clippy::too_many_lines)]
86pub fn read_dot<R: Read>(input: R) -> IgraphResult<DotGraph> {
87 let reader = BufReader::new(input);
88 let mut lines_buf: Vec<String> = Vec::new();
89 for line in reader.lines() {
90 lines_buf.push(line?);
91 }
92 let content = lines_buf.join("\n");
93
94 let content = strip_comments(&content);
95 let directed = detect_directed(&content)?;
96
97 let body_start = content.find('{').ok_or_else(|| IgraphError::Parse {
98 line: 0,
99 message: "no opening '{' found in DOT input".to_owned(),
100 })? + 1;
101 let body_end = content.rfind('}').ok_or_else(|| IgraphError::Parse {
102 line: 0,
103 message: "no closing '}' found in DOT input".to_owned(),
104 })?;
105 let body = &content[body_start..body_end];
106
107 let mut node_ids: Vec<String> = Vec::new();
108 let mut node_map: HashMap<String, u32> = HashMap::new();
109 let mut edges: Vec<(u32, u32)> = Vec::new();
110 let mut node_attr_list: Vec<Vec<(String, AttributeValue)>> = Vec::new();
111 let mut edge_attr_list: Vec<Vec<(String, AttributeValue)>> = Vec::new();
112 let mut graph_attrs: Vec<(String, AttributeValue)> = Vec::new();
113
114 let edge_op = if directed { "->" } else { "--" };
115
116 let ensure_node = |name: &str,
117 ids: &mut Vec<String>,
118 map: &mut HashMap<String, u32>,
119 attrs: &mut Vec<Vec<(String, AttributeValue)>>|
120 -> IgraphResult<u32> {
121 if let Some(&idx) = map.get(name) {
122 return Ok(idx);
123 }
124 let idx = u32::try_from(ids.len())
125 .map_err(|_| IgraphError::InvalidArgument("too many nodes for u32".to_owned()))?;
126 map.insert(name.to_owned(), idx);
127 ids.push(name.to_owned());
128 attrs.push(Vec::new());
129 Ok(idx)
130 };
131
132 for raw_stmt in body.split(';') {
133 let stmt = raw_stmt.trim();
134 if stmt.is_empty() {
135 continue;
136 }
137
138 let lower = stmt.to_ascii_lowercase();
139
140 if lower.starts_with("graph ") && !stmt.contains(edge_op) {
142 if let Some(attrs) = extract_attr_block(stmt) {
143 let parsed = parse_attr_pairs(&attrs);
144 graph_attrs.extend(parsed);
145 }
146 continue;
147 }
148
149 if (lower.starts_with("node ")
151 || lower.starts_with("edge ")
152 || lower.starts_with("subgraph "))
153 && !stmt.contains(edge_op)
154 {
155 continue;
156 }
157 if lower.starts_with('{') || lower.starts_with('}') {
158 continue;
159 }
160
161 if stmt.contains(edge_op) {
163 let (structural, attrs) = split_attr_block(stmt);
164 let edge_attrs_parsed = attrs.map(|a| parse_attr_pairs(&a)).unwrap_or_default();
165 let parts = split_edge_statement(&structural, edge_op);
166 if parts.len() >= 2 {
167 for pair in parts.windows(2) {
168 let src_name = clean_node_id(pair[0]);
169 let tgt_name = clean_node_id(pair[1]);
170 if src_name.is_empty() || tgt_name.is_empty() {
171 continue;
172 }
173 let src =
174 ensure_node(&src_name, &mut node_ids, &mut node_map, &mut node_attr_list)?;
175 let tgt =
176 ensure_node(&tgt_name, &mut node_ids, &mut node_map, &mut node_attr_list)?;
177 edges.push((src, tgt));
178 edge_attr_list.push(edge_attrs_parsed.clone());
179 }
180 continue;
181 }
182 }
183
184 let (structural, attrs) = split_attr_block(stmt);
186 let node_name = clean_node_id(&structural);
187 if !node_name.is_empty() {
188 let idx = ensure_node(
189 &node_name,
190 &mut node_ids,
191 &mut node_map,
192 &mut node_attr_list,
193 )?;
194 if let Some(a) = attrs {
195 let parsed = parse_attr_pairs(&a);
196 node_attr_list[idx as usize].extend(parsed);
197 }
198 }
199 }
200
201 let n = u32::try_from(node_ids.len())
203 .map_err(|_| IgraphError::InvalidArgument("too many nodes for u32".to_owned()))?;
204 let mut graph = Graph::new(n, directed)?;
205 for (src, tgt) in &edges {
206 graph.add_edge(*src, *tgt)?;
207 }
208
209 apply_vertex_attrs(&mut graph, &node_attr_list)?;
211
212 apply_edge_attrs(&mut graph, &edge_attr_list)?;
214
215 for (key, val) in graph_attrs {
217 graph.set_graph_attribute(key, val);
218 }
219
220 Ok(DotGraph {
221 graph,
222 labels: node_ids,
223 })
224}
225
226fn apply_vertex_attrs(
228 graph: &mut Graph,
229 node_attr_list: &[Vec<(String, AttributeValue)>],
230) -> IgraphResult<()> {
231 let mut attr_names: HashMap<String, Vec<AttributeValue>> = HashMap::new();
232 let n = graph.vcount() as usize;
233
234 for (vid, attrs) in node_attr_list.iter().enumerate() {
235 for (key, val) in attrs {
236 let col = attr_names.entry(key.clone()).or_insert_with(|| {
237 let default = val.default_for_same_type();
238 vec![default; n]
239 });
240 if vid < col.len() {
241 col[vid] = val.clone();
242 }
243 }
244 }
245
246 for (name, values) in attr_names {
247 graph.set_vertex_attribute_all(name, values)?;
248 }
249 Ok(())
250}
251
252fn apply_edge_attrs(
254 graph: &mut Graph,
255 edge_attr_list: &[Vec<(String, AttributeValue)>],
256) -> IgraphResult<()> {
257 let mut attr_names: HashMap<String, Vec<AttributeValue>> = HashMap::new();
258 let m = graph.ecount();
259
260 for (eid, attrs) in edge_attr_list.iter().enumerate() {
261 for (key, val) in attrs {
262 let col = attr_names.entry(key.clone()).or_insert_with(|| {
263 let default = val.default_for_same_type();
264 vec![default; m]
265 });
266 if eid < col.len() {
267 col[eid] = val.clone();
268 }
269 }
270 }
271
272 for (name, values) in attr_names {
273 graph.set_edge_attribute_all(name, values)?;
274 }
275 Ok(())
276}
277
278fn extract_attr_block(stmt: &str) -> Option<String> {
280 let open = stmt.find('[')?;
281 let close = stmt.rfind(']')?;
282 if close > open {
283 Some(stmt[open + 1..close].to_owned())
284 } else {
285 None
286 }
287}
288
289fn split_attr_block(stmt: &str) -> (String, Option<String>) {
291 if let Some(open) = stmt.find('[') {
292 let structural = stmt[..open].trim().to_owned();
293 let close = stmt.rfind(']').unwrap_or(stmt.len());
294 let attrs = stmt[open + 1..close].to_owned();
295 (structural, Some(attrs))
296 } else {
297 (stmt.to_owned(), None)
298 }
299}
300
301fn parse_attr_pairs(content: &str) -> Vec<(String, AttributeValue)> {
303 let mut result = Vec::new();
304 let mut remaining = content.trim();
305
306 while !remaining.is_empty() {
307 let Some(eq_pos) = remaining.find('=') else {
309 break;
310 };
311 let key = remaining[..eq_pos].trim();
312 remaining = remaining[eq_pos + 1..].trim();
313
314 let (value_str, rest) = if remaining.starts_with('"') {
316 parse_quoted_value(remaining)
317 } else {
318 parse_unquoted_value(remaining)
319 };
320
321 if !key.is_empty() {
322 result.push((key.to_owned(), dot_value_to_attribute(&value_str)));
323 }
324
325 remaining = rest.trim();
326 if remaining.starts_with(',') || remaining.starts_with(';') {
328 remaining = remaining[1..].trim();
329 }
330 }
331
332 result
333}
334
335fn parse_quoted_value(s: &str) -> (String, &str) {
337 let bytes = s.as_bytes();
338 let mut i = 1; let mut value = String::new();
340 while i < bytes.len() {
341 if bytes[i] == b'\\' && i + 1 < bytes.len() {
342 match bytes[i + 1] {
343 b'"' => value.push('"'),
344 b'\\' => value.push('\\'),
345 b'n' => value.push('\n'),
346 other => {
347 value.push('\\');
348 value.push(other as char);
349 }
350 }
351 i += 2;
352 } else if bytes[i] == b'"' {
353 return (value, &s[i + 1..]);
354 } else {
355 value.push(bytes[i] as char);
356 i += 1;
357 }
358 }
359 (value, "")
360}
361
362fn parse_unquoted_value(s: &str) -> (String, &str) {
364 let end = s.find([',', ';', ']']).unwrap_or(s.len());
365 let value = s[..end].trim().to_owned();
366 (value, &s[end..])
367}
368
369fn dot_value_to_attribute(s: &str) -> AttributeValue {
371 match s.to_ascii_lowercase().as_str() {
373 "true" => return AttributeValue::Boolean(true),
374 "false" => return AttributeValue::Boolean(false),
375 _ => {}
376 }
377 if let Ok(v) = s.parse::<f64>() {
379 return AttributeValue::Numeric(v);
380 }
381 AttributeValue::String(s.to_owned())
382}
383
384fn strip_comments(s: &str) -> String {
386 let mut out = String::with_capacity(s.len());
387 let bytes = s.as_bytes();
388 let len = bytes.len();
389 let mut i = 0;
390 while i < len {
391 if i + 1 < len && bytes[i] == b'/' && bytes[i + 1] == b'*' {
392 i += 2;
393 while i + 1 < len && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
394 i += 1;
395 }
396 i += 2;
397 } else if i + 1 < len && bytes[i] == b'/' && bytes[i + 1] == b'/' {
398 while i < len && bytes[i] != b'\n' {
399 i += 1;
400 }
401 } else if bytes[i] == b'"' {
402 out.push('"');
403 i += 1;
404 while i < len && bytes[i] != b'"' {
405 if bytes[i] == b'\\' && i + 1 < len {
406 out.push(bytes[i] as char);
407 i += 1;
408 }
409 out.push(bytes[i] as char);
410 i += 1;
411 }
412 if i < len {
413 out.push('"');
414 i += 1;
415 }
416 } else {
417 out.push(bytes[i] as char);
418 i += 1;
419 }
420 }
421 out
422}
423
424fn detect_directed(content: &str) -> IgraphResult<bool> {
426 let lower = content.to_ascii_lowercase();
427 let di_pos = lower.find("digraph");
428 let g_pos = lower.find("graph");
429 match (di_pos, g_pos) {
430 (Some(dp), Some(gp)) => {
431 if dp <= gp {
432 Ok(true)
433 } else {
434 Ok(false)
435 }
436 }
437 (Some(_), None) => Ok(true),
438 (None, Some(_)) => Ok(false),
439 (None, None) => Err(IgraphError::Parse {
440 line: 0,
441 message: "no 'graph' or 'digraph' keyword found".to_owned(),
442 }),
443 }
444}
445
446fn split_edge_statement<'a>(stmt: &'a str, op: &str) -> Vec<&'a str> {
449 stmt.split(op).map(str::trim).collect()
450}
451
452fn clean_node_id(raw: &str) -> String {
455 let mut s = raw.trim();
456 if let Some(brace) = s.find('{') {
458 s = s[..brace].trim();
459 }
460 if let Some(brace) = s.find('}') {
461 s = s[..brace].trim();
462 }
463 if s.len() >= 2 && s.starts_with('"') && s.ends_with('"') {
465 let inner = &s[1..s.len() - 1];
466 return inner.replace("\\\"", "\"").replace("\\\\", "\\");
467 }
468 s.to_owned()
469}
470
471#[allow(clippy::too_many_lines)]
499pub fn write_dot<W: Write>(
500 graph: &Graph,
501 labels: Option<&[String]>,
502 writer: &mut W,
503) -> IgraphResult<()> {
504 if let Some(l) = labels {
505 if l.len() != graph.vcount() as usize {
506 return Err(IgraphError::InvalidArgument(format!(
507 "labels length {} does not match vcount {}",
508 l.len(),
509 graph.vcount()
510 )));
511 }
512 }
513
514 let edge_op = if graph.is_directed() { "->" } else { "--" };
515 let graph_type = if graph.is_directed() {
516 "digraph"
517 } else {
518 "graph"
519 };
520
521 writeln!(writer, "{graph_type} {{")?;
522
523 write_graph_attrs(graph, writer)?;
525
526 let v_attr_names = graph.vertex_attribute_names();
528 let e_attr_names = graph.edge_attribute_names();
529
530 let mut has_edge = vec![false; graph.vcount() as usize];
532
533 for eid in 0..graph.ecount() {
534 let eid_u32 = u32::try_from(eid)
535 .map_err(|_| IgraphError::InvalidArgument("edge id overflow".to_owned()))?;
536 let (src, tgt) = graph.edge(eid_u32)?;
537 has_edge[src as usize] = true;
538 has_edge[tgt as usize] = true;
539
540 let src_label = vertex_label(src, labels);
541 let tgt_label = vertex_label(tgt, labels);
542
543 write!(writer, " {src_label} {edge_op} {tgt_label}")?;
544 write_edge_attr_block(graph, eid_u32, &e_attr_names, writer)?;
545 writeln!(writer, ";")?;
546 }
547
548 for v in 0..graph.vcount() {
550 let has_attrs = v_attr_names.iter().any(|name| {
551 graph
552 .vertex_attribute(name, v)
553 .is_some_and(|val| !is_default_attr(val))
554 });
555
556 if has_attrs || !has_edge[v as usize] {
557 let lbl = vertex_label(v, labels);
558 write!(writer, " {lbl}")?;
559 write_vertex_attr_block(graph, v, &v_attr_names, writer)?;
560 writeln!(writer, ";")?;
561 }
562 }
563
564 writeln!(writer, "}}")?;
565
566 Ok(())
567}
568
569fn write_graph_attrs<W: Write>(graph: &Graph, writer: &mut W) -> IgraphResult<()> {
571 let names = graph.graph_attribute_names();
572 if names.is_empty() {
573 return Ok(());
574 }
575
576 write!(writer, " graph [")?;
577 let mut first = true;
578 for name in &names {
579 if let Some(val) = graph.graph_attribute(name) {
580 if !first {
581 write!(writer, ", ")?;
582 }
583 write_attr_pair(name, val, writer)?;
584 first = false;
585 }
586 }
587 writeln!(writer, "];")?;
588 Ok(())
589}
590
591fn write_vertex_attr_block<W: Write>(
593 graph: &Graph,
594 v: u32,
595 attr_names: &[&str],
596 writer: &mut W,
597) -> IgraphResult<()> {
598 let mut pairs: Vec<(&str, &AttributeValue)> = Vec::new();
599 for name in attr_names {
600 if let Some(val) = graph.vertex_attribute(name, v) {
601 if !is_default_attr(val) {
602 pairs.push((name, val));
603 }
604 }
605 }
606
607 if pairs.is_empty() {
608 return Ok(());
609 }
610
611 write!(writer, " [")?;
612 for (i, (name, val)) in pairs.iter().enumerate() {
613 if i > 0 {
614 write!(writer, ", ")?;
615 }
616 write_attr_pair(name, val, writer)?;
617 }
618 write!(writer, "]")?;
619 Ok(())
620}
621
622fn write_edge_attr_block<W: Write>(
624 graph: &Graph,
625 eid: u32,
626 attr_names: &[&str],
627 writer: &mut W,
628) -> IgraphResult<()> {
629 let mut pairs: Vec<(&str, &AttributeValue)> = Vec::new();
630 for name in attr_names {
631 if let Some(val) = graph.edge_attribute(name, eid) {
632 if !is_default_attr(val) {
633 pairs.push((name, val));
634 }
635 }
636 }
637
638 if pairs.is_empty() {
639 return Ok(());
640 }
641
642 write!(writer, " [")?;
643 for (i, (name, val)) in pairs.iter().enumerate() {
644 if i > 0 {
645 write!(writer, ", ")?;
646 }
647 write_attr_pair(name, val, writer)?;
648 }
649 write!(writer, "]")?;
650 Ok(())
651}
652
653fn is_default_attr(val: &AttributeValue) -> bool {
656 match val {
657 AttributeValue::Numeric(v) => v.is_nan(),
658 AttributeValue::String(s) => s.is_empty(),
659 AttributeValue::Boolean(_) => false,
660 }
661}
662
663fn write_attr_pair<W: Write>(name: &str, val: &AttributeValue, writer: &mut W) -> IgraphResult<()> {
665 match val {
666 AttributeValue::Numeric(v) => {
667 write!(writer, "{name}={v}")?;
668 }
669 AttributeValue::Boolean(b) => {
670 write!(writer, "{name}={b}")?;
671 }
672 AttributeValue::String(s) => {
673 let escaped = dot_escape_value(s);
674 write!(writer, "{name}={escaped}")?;
675 }
676 }
677 Ok(())
678}
679
680fn vertex_label(v: u32, labels: Option<&[String]>) -> String {
681 match labels {
682 Some(l) => dot_escape_id(&l[v as usize]),
683 None => v.to_string(),
684 }
685}
686
687fn dot_escape_id(s: &str) -> String {
689 let is_simple = !s.is_empty()
690 && !s.as_bytes()[0].is_ascii_digit()
691 && s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
692
693 let reserved = matches!(
694 s.to_ascii_lowercase().as_str(),
695 "graph" | "digraph" | "node" | "edge" | "strict" | "subgraph"
696 );
697
698 if is_simple && !reserved {
699 s.to_owned()
700 } else {
701 dot_quote(s)
702 }
703}
704
705fn dot_escape_value(s: &str) -> String {
707 dot_quote(s)
708}
709
710fn dot_quote(s: &str) -> String {
712 let mut out = String::with_capacity(s.len() + 2);
713 out.push('"');
714 for c in s.chars() {
715 match c {
716 '"' => out.push_str("\\\""),
717 '\\' => out.push_str("\\\\"),
718 '\n' => out.push_str("\\n"),
719 _ => out.push(c),
720 }
721 }
722 out.push('"');
723 out
724}
725
726#[cfg(test)]
727mod tests {
728 use super::*;
729
730 #[test]
731 fn test_undirected_basic() {
732 let mut g = Graph::with_vertices(3);
733 g.add_edge(0, 1).unwrap();
734 g.add_edge(1, 2).unwrap();
735
736 let mut buf = Vec::new();
737 write_dot(&g, None, &mut buf).unwrap();
738 let s = String::from_utf8(buf).unwrap();
739 assert!(s.starts_with("graph {\n"));
740 assert!(s.contains("0 -- 1;"));
741 assert!(s.contains("1 -- 2;"));
742 assert!(s.ends_with("}\n"));
743 }
744
745 #[test]
746 fn test_directed_basic() {
747 let mut g = Graph::new(3, true).unwrap();
748 g.add_edge(0, 1).unwrap();
749 g.add_edge(1, 2).unwrap();
750
751 let mut buf = Vec::new();
752 write_dot(&g, None, &mut buf).unwrap();
753 let s = String::from_utf8(buf).unwrap();
754 assert!(s.starts_with("digraph {\n"));
755 assert!(s.contains("0 -> 1;"));
756 assert!(s.contains("1 -> 2;"));
757 }
758
759 #[test]
760 fn test_with_labels() {
761 let mut g = Graph::with_vertices(3);
762 g.add_edge(0, 1).unwrap();
763
764 let labels = vec!["Alice".to_string(), "Bob".to_string(), "Carol".to_string()];
765 let mut buf = Vec::new();
766 write_dot(&g, Some(&labels), &mut buf).unwrap();
767 let s = String::from_utf8(buf).unwrap();
768 assert!(s.contains("Alice -- Bob;"));
769 assert!(s.contains("Carol;"));
770 }
771
772 #[test]
773 fn test_isolated_vertices() {
774 let g = Graph::with_vertices(3);
775
776 let mut buf = Vec::new();
777 write_dot(&g, None, &mut buf).unwrap();
778 let s = String::from_utf8(buf).unwrap();
779 assert!(s.contains(" 0;\n"));
780 assert!(s.contains(" 1;\n"));
781 assert!(s.contains(" 2;\n"));
782 }
783
784 #[test]
785 fn test_empty_graph() {
786 let g = Graph::with_vertices(0);
787
788 let mut buf = Vec::new();
789 write_dot(&g, None, &mut buf).unwrap();
790 let s = String::from_utf8(buf).unwrap();
791 assert_eq!(s, "graph {\n}\n");
792 }
793
794 #[test]
795 fn test_reserved_word_label_escaped() {
796 let mut g = Graph::with_vertices(2);
797 g.add_edge(0, 1).unwrap();
798
799 let labels = vec!["graph".to_string(), "node".to_string()];
800 let mut buf = Vec::new();
801 write_dot(&g, Some(&labels), &mut buf).unwrap();
802 let s = String::from_utf8(buf).unwrap();
803 assert!(s.contains("\"graph\" -- \"node\";"));
804 }
805
806 #[test]
807 fn test_label_with_spaces_escaped() {
808 let mut g = Graph::with_vertices(2);
809 g.add_edge(0, 1).unwrap();
810
811 let labels = vec!["hello world".to_string(), "foo bar".to_string()];
812 let mut buf = Vec::new();
813 write_dot(&g, Some(&labels), &mut buf).unwrap();
814 let s = String::from_utf8(buf).unwrap();
815 assert!(s.contains("\"hello world\" -- \"foo bar\";"));
816 }
817
818 #[test]
819 fn test_label_with_quotes_escaped() {
820 let mut g = Graph::with_vertices(2);
821 g.add_edge(0, 1).unwrap();
822
823 let labels = vec!["say \"hi\"".to_string(), "ok".to_string()];
824 let mut buf = Vec::new();
825 write_dot(&g, Some(&labels), &mut buf).unwrap();
826 let s = String::from_utf8(buf).unwrap();
827 assert!(s.contains("\"say \\\"hi\\\"\" -- ok;"));
828 }
829
830 #[test]
831 fn test_label_starting_with_digit() {
832 let mut g = Graph::with_vertices(2);
833 g.add_edge(0, 1).unwrap();
834
835 let labels = vec!["123abc".to_string(), "valid_name".to_string()];
836 let mut buf = Vec::new();
837 write_dot(&g, Some(&labels), &mut buf).unwrap();
838 let s = String::from_utf8(buf).unwrap();
839 assert!(s.contains("\"123abc\" -- valid_name;"));
840 }
841
842 #[test]
843 fn test_self_loop() {
844 let mut g = Graph::with_vertices(1);
845 g.add_edge(0, 0).unwrap();
846
847 let mut buf = Vec::new();
848 write_dot(&g, None, &mut buf).unwrap();
849 let s = String::from_utf8(buf).unwrap();
850 assert!(s.contains("0 -- 0;"));
851 }
852
853 #[test]
854 fn test_labels_mismatch_error() {
855 let g = Graph::with_vertices(3);
856 let labels = vec!["A".to_string()];
857 let mut buf = Vec::new();
858 assert!(write_dot(&g, Some(&labels), &mut buf).is_err());
859 }
860
861 #[test]
864 fn write_vertex_attributes() {
865 let mut g = Graph::with_vertices(2);
866 g.add_edge(0, 1).unwrap();
867 g.set_vertex_attribute("color", 0, AttributeValue::String("red".into()))
868 .unwrap();
869 g.set_vertex_attribute("color", 1, AttributeValue::String("blue".into()))
870 .unwrap();
871
872 let mut buf = Vec::new();
873 write_dot(&g, None, &mut buf).unwrap();
874 let s = String::from_utf8(buf).unwrap();
875 assert!(s.contains("[color=\"red\"]"));
876 assert!(s.contains("[color=\"blue\"]"));
877 }
878
879 #[test]
880 fn write_edge_attributes() {
881 let mut g = Graph::with_vertices(2);
882 g.add_edge(0, 1).unwrap();
883 g.set_edge_attribute("weight", 0, AttributeValue::Numeric(2.5))
884 .unwrap();
885
886 let mut buf = Vec::new();
887 write_dot(&g, None, &mut buf).unwrap();
888 let s = String::from_utf8(buf).unwrap();
889 assert!(s.contains("0 -- 1 [weight=2.5];"));
890 }
891
892 #[test]
893 fn write_graph_attributes() {
894 let mut g = Graph::with_vertices(1);
895 g.set_graph_attribute("label", AttributeValue::String("test graph".into()));
896
897 let mut buf = Vec::new();
898 write_dot(&g, None, &mut buf).unwrap();
899 let s = String::from_utf8(buf).unwrap();
900 assert!(s.contains("graph [label=\"test graph\"];"));
901 }
902
903 #[test]
904 fn write_boolean_attribute() {
905 let mut g = Graph::with_vertices(2);
906 g.add_edge(0, 1).unwrap();
907 g.set_vertex_attribute("visited", 0, AttributeValue::Boolean(true))
908 .unwrap();
909
910 let mut buf = Vec::new();
911 write_dot(&g, None, &mut buf).unwrap();
912 let s = String::from_utf8(buf).unwrap();
913 assert!(s.contains("[visited=true]"));
914 }
915
916 #[test]
917 fn write_multiple_attributes() {
918 let mut g = Graph::with_vertices(2);
919 g.add_edge(0, 1).unwrap();
920 g.set_edge_attribute("weight", 0, AttributeValue::Numeric(1.5))
921 .unwrap();
922 g.set_edge_attribute("label", 0, AttributeValue::String("edge0".into()))
923 .unwrap();
924
925 let mut buf = Vec::new();
926 write_dot(&g, None, &mut buf).unwrap();
927 let s = String::from_utf8(buf).unwrap();
928 assert!(s.contains("weight=1.5"));
930 assert!(s.contains("label=\"edge0\""));
931 }
932
933 #[test]
936 fn read_undirected_basic() {
937 let dot = "graph {\n 0 -- 1;\n 1 -- 2;\n}";
938 let result = read_dot(dot.as_bytes()).unwrap();
939 assert_eq!(result.graph.vcount(), 3);
940 assert_eq!(result.graph.ecount(), 2);
941 assert!(!result.graph.is_directed());
942 }
943
944 #[test]
945 fn read_directed_basic() {
946 let dot = "digraph {\n a -> b;\n b -> c;\n}";
947 let result = read_dot(dot.as_bytes()).unwrap();
948 assert_eq!(result.graph.vcount(), 3);
949 assert_eq!(result.graph.ecount(), 2);
950 assert!(result.graph.is_directed());
951 assert_eq!(result.labels, vec!["a", "b", "c"]);
952 }
953
954 #[test]
955 fn read_with_labels() {
956 let dot = "graph {\n Alice -- Bob;\n Bob -- Carol;\n}";
957 let result = read_dot(dot.as_bytes()).unwrap();
958 assert_eq!(result.labels, vec!["Alice", "Bob", "Carol"]);
959 assert_eq!(result.graph.ecount(), 2);
960 }
961
962 #[test]
963 fn read_quoted_labels() {
964 let dot = r#"graph { "hello world" -- "foo bar"; }"#;
965 let result = read_dot(dot.as_bytes()).unwrap();
966 assert_eq!(result.labels, vec!["hello world", "foo bar"]);
967 assert_eq!(result.graph.ecount(), 1);
968 }
969
970 #[test]
971 fn read_with_attributes_parsed() {
972 let dot = r#"graph {
973 a [color="red", shape=circle];
974 b [color="blue"];
975 a -- b [weight=1.5];
976}"#;
977 let result = read_dot(dot.as_bytes()).unwrap();
978 assert_eq!(result.graph.vcount(), 2);
979 assert_eq!(result.graph.ecount(), 1);
980 assert_eq!(
981 result
982 .graph
983 .vertex_attribute("color", 0)
984 .and_then(AttributeValue::as_str),
985 Some("red"),
986 );
987 assert_eq!(
988 result
989 .graph
990 .vertex_attribute("color", 1)
991 .and_then(AttributeValue::as_str),
992 Some("blue"),
993 );
994 assert_eq!(
995 result
996 .graph
997 .vertex_attribute("shape", 0)
998 .and_then(AttributeValue::as_str),
999 Some("circle"),
1000 );
1001 assert_eq!(
1002 result
1003 .graph
1004 .edge_attribute("weight", 0)
1005 .and_then(AttributeValue::as_f64),
1006 Some(1.5),
1007 );
1008 }
1009
1010 #[test]
1011 fn read_graph_level_attributes() {
1012 let dot = r#"graph {
1013 graph [label="My Graph", rankdir="LR"];
1014 a -- b;
1015}"#;
1016 let result = read_dot(dot.as_bytes()).unwrap();
1017 assert_eq!(
1018 result
1019 .graph
1020 .graph_attribute("label")
1021 .and_then(AttributeValue::as_str),
1022 Some("My Graph"),
1023 );
1024 assert_eq!(
1025 result
1026 .graph
1027 .graph_attribute("rankdir")
1028 .and_then(AttributeValue::as_str),
1029 Some("LR"),
1030 );
1031 }
1032
1033 #[test]
1034 fn read_boolean_attribute() {
1035 let dot = r"graph {
1036 a [visited=true];
1037 b [visited=false];
1038 a -- b;
1039}";
1040 let result = read_dot(dot.as_bytes()).unwrap();
1041 assert_eq!(
1042 result
1043 .graph
1044 .vertex_attribute("visited", 0)
1045 .and_then(AttributeValue::as_bool),
1046 Some(true),
1047 );
1048 assert_eq!(
1049 result
1050 .graph
1051 .vertex_attribute("visited", 1)
1052 .and_then(AttributeValue::as_bool),
1053 Some(false),
1054 );
1055 }
1056
1057 #[test]
1058 fn read_edge_chain() {
1059 let dot = "digraph { a -> b -> c -> d; }";
1060 let result = read_dot(dot.as_bytes()).unwrap();
1061 assert_eq!(result.graph.vcount(), 4);
1062 assert_eq!(result.graph.ecount(), 3);
1063 }
1064
1065 #[test]
1066 fn read_isolated_nodes() {
1067 let dot = "graph {\n x;\n y;\n z;\n x -- y;\n}";
1068 let result = read_dot(dot.as_bytes()).unwrap();
1069 assert_eq!(result.graph.vcount(), 3);
1070 assert_eq!(result.graph.ecount(), 1);
1071 }
1072
1073 #[test]
1074 fn read_with_comments() {
1075 let dot = "// comment\ngraph {\n /* block comment */\n a -- b;\n}";
1076 let result = read_dot(dot.as_bytes()).unwrap();
1077 assert_eq!(result.graph.vcount(), 2);
1078 assert_eq!(result.graph.ecount(), 1);
1079 }
1080
1081 #[test]
1082 fn read_empty_graph() {
1083 let dot = "graph { }";
1084 let result = read_dot(dot.as_bytes()).unwrap();
1085 assert_eq!(result.graph.vcount(), 0);
1086 assert_eq!(result.graph.ecount(), 0);
1087 }
1088
1089 #[test]
1090 fn read_no_keyword_is_error() {
1091 let dot = "{ a -- b; }";
1092 assert!(read_dot(dot.as_bytes()).is_err());
1093 }
1094
1095 #[test]
1096 fn read_self_loop() {
1097 let dot = "graph { a -- a; }";
1098 let result = read_dot(dot.as_bytes()).unwrap();
1099 assert_eq!(result.graph.vcount(), 1);
1100 assert_eq!(result.graph.ecount(), 1);
1101 }
1102
1103 #[test]
1104 fn roundtrip_dot() {
1105 let mut g = Graph::new(3, true).unwrap();
1106 g.add_edge(0, 1).unwrap();
1107 g.add_edge(1, 2).unwrap();
1108 g.add_edge(2, 0).unwrap();
1109
1110 let mut buf = Vec::new();
1111 write_dot(&g, None, &mut buf).unwrap();
1112
1113 let result = read_dot(buf.as_slice()).unwrap();
1114 assert_eq!(result.graph.vcount(), 3);
1115 assert_eq!(result.graph.ecount(), 3);
1116 assert!(result.graph.is_directed());
1117 }
1118
1119 #[test]
1120 fn roundtrip_with_attributes() {
1121 let mut g = Graph::new(3, true).unwrap();
1122 g.add_edge(0, 1).unwrap();
1123 g.add_edge(1, 2).unwrap();
1124
1125 g.set_vertex_attribute("color", 0, AttributeValue::String("red".into()))
1126 .unwrap();
1127 g.set_vertex_attribute("color", 1, AttributeValue::String("blue".into()))
1128 .unwrap();
1129 g.set_edge_attribute("weight", 0, AttributeValue::Numeric(1.5))
1130 .unwrap();
1131 g.set_graph_attribute("label", AttributeValue::String("test".into()));
1132
1133 let mut buf = Vec::new();
1134 write_dot(&g, None, &mut buf).unwrap();
1135
1136 let result = read_dot(buf.as_slice()).unwrap();
1137 assert_eq!(result.graph.vcount(), 3);
1138 assert_eq!(result.graph.ecount(), 2);
1139 assert!(result.graph.is_directed());
1140 assert_eq!(
1141 result
1142 .graph
1143 .vertex_attribute("color", 0)
1144 .and_then(AttributeValue::as_str),
1145 Some("red"),
1146 );
1147 assert_eq!(
1148 result
1149 .graph
1150 .vertex_attribute("color", 1)
1151 .and_then(AttributeValue::as_str),
1152 Some("blue"),
1153 );
1154 assert_eq!(
1155 result
1156 .graph
1157 .edge_attribute("weight", 0)
1158 .and_then(AttributeValue::as_f64),
1159 Some(1.5),
1160 );
1161 assert_eq!(
1162 result
1163 .graph
1164 .graph_attribute("label")
1165 .and_then(AttributeValue::as_str),
1166 Some("test"),
1167 );
1168 }
1169
1170 #[test]
1171 fn read_strict_keyword() {
1172 let dot = "strict graph {\n a -- b;\n a -- b;\n}";
1173 let result = read_dot(dot.as_bytes()).unwrap();
1174 assert!(!result.graph.is_directed());
1175 assert_eq!(result.graph.vcount(), 2);
1176 }
1177
1178 #[test]
1179 fn read_node_attrs_ignored() {
1180 let dot = r"graph {
1182 node [shape=circle];
1183 edge [color=red];
1184 a -- b;
1185}";
1186 let result = read_dot(dot.as_bytes()).unwrap();
1187 assert_eq!(result.graph.vcount(), 2);
1188 assert_eq!(result.graph.ecount(), 1);
1189 assert!(!result.graph.has_vertex_attribute("shape"));
1191 }
1192
1193 #[test]
1194 fn parse_attr_pairs_basic() {
1195 let pairs = parse_attr_pairs(r#"weight=1.5, color="red", active=true"#);
1196 assert_eq!(pairs.len(), 3);
1197 assert_eq!(pairs[0], ("weight".into(), AttributeValue::Numeric(1.5)));
1198 assert_eq!(
1199 pairs[1],
1200 ("color".into(), AttributeValue::String("red".into()))
1201 );
1202 assert_eq!(pairs[2], ("active".into(), AttributeValue::Boolean(true)));
1203 }
1204}