diff options
author | seancarroll <seanc28@gmail.com> | 2021-01-20 21:51:57 -0600 |
---|---|---|
committer | seancarroll <seanc28@gmail.com> | 2021-01-20 21:51:57 -0600 |
commit | 7d5d439ed2f465120825c918f920bd9fbfd3169f (patch) | |
tree | 4545ef6d834d8bd5f6b0d3c860511d3ddc042984 | |
parent | ec3468a275314f16769439736b7514b40e3b49c0 (diff) | |
download | dotavious-7d5d439ed2f465120825c918f920bd9fbfd3169f.zip |
Attribute Constraints
This adds some attribute constraints. We validate certain constraints
based on information from http://www.graphviz.org/doc/info/attrs.html.
Validation errors are collected as part of builders, we still add the
attribute regardless or not if it fails validation, and build methods
return ValidationResult<T> where errors is a Vec<ValidationError>. We
still add attributes even if the validation fails so that users can
ignore validation errors and build appropriate struct.
-rw-r--r-- | src/attributes/mod.rs | 86 | ||||
-rw-r--r-- | src/dot.rs | 108 | ||||
-rw-r--r-- | src/lib.rs | 31 | ||||
-rw-r--r-- | src/validation.rs | 9 | ||||
-rw-r--r-- | tests/dot.rs | 236 |
5 files changed, 396 insertions, 74 deletions
diff --git a/src/attributes/mod.rs b/src/attributes/mod.rs index dafbf4a..7f4fb0d 100644 --- a/src/attributes/mod.rs +++ b/src/attributes/mod.rs @@ -44,9 +44,11 @@ pub use crate::attributes::style::{EdgeStyle, GraphStyle, NodeStyle, Styles}; #[doc(hidden)] pub use crate::attributes::AttributeText::{AttrStr, EscStr, HtmlStr, QuotedStr}; use crate::dot::DotString; +use crate::validation::{ValidationError, ValidationResult}; use indexmap::map::IndexMap; use std::borrow::Cow; use std::collections::HashMap; +use Cow::Borrowed; /// The text for a graphviz label on a node or edge. #[derive(Clone, PartialEq, Eq, Debug)] @@ -459,10 +461,12 @@ pub trait GraphAttributes<'a> { self.add_attribute("fontpath", AttributeText::quoted(font_path)) } - // TODO: constrain /// Font size, in points, used for text. /// default: 14.0, minimum: 1.0 fn font_size(&mut self, font_size: f32) -> &mut Self { + if font_size < 1.0 { + self.add_validation_error("fontsize", "Must be greater than or equal to 1.0") + } Attributes::font_size(self.get_attributes_mut(), font_size); self } @@ -605,10 +609,12 @@ pub trait GraphAttributes<'a> { self.add_attribute("newrank", AttributeText::from(newrank)) } - // TODO: add constraint /// specifies the minimum space between two adjacent nodes in the same rank, in inches. /// default: 0.25, minimum: 0.02 fn nodesep(&mut self, nodesep: f32) -> &mut Self { + if nodesep < 0.02 { + self.add_validation_error("nodesep", "Must be greater than or equal to 0.02") + } self.add_attribute("nodesep", AttributeText::from(nodesep)) } @@ -653,6 +659,9 @@ pub trait GraphAttributes<'a> { /// Used only if rotate is not defined. /// Default: 0.0 and minimum: 360.0 fn orientation(&mut self, orientation: f32) -> &mut Self { + if orientation < 0.0 || orientation > 360.0 { + self.add_validation_error("orientation", "Must be between 0 and 360") + } Attributes::orientation(self.get_attributes_mut(), orientation); self } @@ -671,7 +680,6 @@ pub trait GraphAttributes<'a> { self.add_attribute("pack", AttributeText::from(pack)) } - // TODO: constrain to non-negative integer. /// Whether each connected component of the graph should be laid out separately, and then /// the graphs packed together. /// This is used as the size, in points,of a margin around each part; otherwise, a default @@ -721,10 +729,12 @@ pub trait GraphAttributes<'a> { self.add_attribute("pagedir", AttributeText::from(page_dir)) } - // TODO: constrain /// If quantum > 0.0, node label dimensions will be rounded to integral multiples of the quantum. /// default: 0.0, minimum: 0.0 fn quantum(&mut self, quantum: f32) -> &mut Self { + if quantum < 0.0 { + self.add_validation_error("quantum", "Must be greater than or equal to 0") + } self.add_attribute("quantum", AttributeText::from(quantum)) } @@ -762,7 +772,6 @@ pub trait GraphAttributes<'a> { self.add_attribute("rotate", AttributeText::from(rotate)) } - // TODO: constrain /// Print guide boxes in PostScript at the beginning of routesplines if showboxes=1, or at /// the end if showboxes=2. /// (Debugging, TB mode only!) @@ -874,6 +883,8 @@ pub trait GraphAttributes<'a> { ) -> &mut Self; fn get_attributes_mut(&mut self) -> &mut IndexMap<String, AttributeText<'a>>; + + fn add_validation_error(&mut self, field: &'static str, message: &'static str); } impl<'a> GraphAttributes<'a> for GraphAttributeStatementBuilder<'a> { @@ -898,21 +909,37 @@ impl<'a> GraphAttributes<'a> for GraphAttributeStatementBuilder<'a> { fn get_attributes_mut(&mut self) -> &mut IndexMap<String, AttributeText<'a>> { &mut self.attributes } + + fn add_validation_error(&mut self, field: &'static str, message: &'static str) { + self.errors.push(ValidationError { + field: Borrowed(field), + message: Borrowed(message), + }) + } } // I'm not a huge fan of needing this builder but having a hard time getting around &mut without it pub struct GraphAttributeStatementBuilder<'a> { pub attributes: IndexMap<String, AttributeText<'a>>, + errors: Vec<ValidationError>, } impl<'a> GraphAttributeStatementBuilder<'a> { pub fn new() -> Self { Self { attributes: IndexMap::new(), + errors: Vec::new(), } } - pub fn build(&self) -> IndexMap<String, AttributeText<'a>> { + pub fn build(&self) -> ValidationResult<IndexMap<String, AttributeText<'a>>> { + if !self.errors.is_empty() { + return Err(self.errors.clone()); + } + Ok(self.build_ignore_validation()) + } + + pub fn build_ignore_validation(&self) -> IndexMap<String, AttributeText<'a>> { self.attributes.clone() } } @@ -1112,10 +1139,12 @@ impl Attributes { } pub trait NodeAttributes<'a> { - // TODO: constrain /// Indicates the preferred area for a node or empty cluster when laid out by patchwork. - /// default: 1.0, minimum: >0 + /// default: 1.0, minimum: > 0 fn area(&mut self, area: f32) -> &mut Self { + if area <= 0.0 { + self.add_validation_error("area", "Must be greater than 0") + } self.add_attribute("area", AttributeText::from(area)) } @@ -1219,10 +1248,12 @@ pub trait NodeAttributes<'a> { self.add_attribute("group", AttributeText::attr(group)) } - // TODO: constrain /// Height of node, in inches. /// default: 0.5, minimum: 0.02 fn height(&mut self, height: f32) -> &mut Self { + if height < 0.02 { + self.add_validation_error("height", "Must be greater than or equal to 0.02") + } self.add_attribute("height", AttributeText::from(height)) } @@ -1336,6 +1367,9 @@ pub trait NodeAttributes<'a> { /// Used only if rotate is not defined. /// Default: 0.0 and minimum: 360.0 fn orientation(&mut self, orientation: f32) -> &mut Self { + if orientation < 0.0 || orientation > 360.0 { + self.add_validation_error("orientation", "Must be between 0 and 360") + } Attributes::orientation(self.get_attributes_mut(), orientation); self } @@ -1383,7 +1417,6 @@ pub trait NodeAttributes<'a> { self.add_attribute("shape", AttributeText::from(shape)) } - // TODO: constrain /// Print guide boxes in PostScript at the beginning of routesplines if /// showboxes=1, or at the end if showboxes=2. /// (Debugging, TB mode only!) @@ -1398,11 +1431,13 @@ pub trait NodeAttributes<'a> { self.add_attribute("sides", AttributeText::from(sides)) } - // TODO: constrain /// Skew factor for shape=polygon. /// Positive values skew top of polygon to right; negative to left. /// default: 0.0, minimum: -100.0 fn skew(&mut self, skew: f32) -> &mut Self { + if skew < -100.0 { + self.add_validation_error("skew", "Must be greater than or equal to -100") + } self.add_attribute("skew", AttributeText::from(skew)) } @@ -1491,6 +1526,8 @@ pub trait NodeAttributes<'a> { ) -> &mut Self; fn get_attributes_mut(&mut self) -> &mut IndexMap<String, AttributeText<'a>>; + + fn add_validation_error(&mut self, field: &'static str, message: &'static str); } pub trait EdgeAttributes<'a> { @@ -1500,10 +1537,12 @@ pub trait EdgeAttributes<'a> { self.add_attribute("arrowhead", AttributeText::from(arrowhead)) } - // TODO: constrain /// Multiplicative scale factor for arrowheads. /// default: 1.0, minimum: 0.0 fn arrow_size(&mut self, arrow_size: f32) -> &mut Self { + if arrow_size < 0.0 { + self.add_validation_error("arrowsize", "Must be greater than or equal to 0") + } self.add_attribute("arrowsize", AttributeText::from(arrow_size)) } @@ -1685,7 +1724,6 @@ pub trait EdgeAttributes<'a> { self } - // TODO: constrain /// Determines, along with labeldistance, where the headlabel / taillabel are /// placed with respect to the head / tail in polar coordinates. /// The origin in the coordinate system is the point where the edge touches the node. @@ -1694,6 +1732,12 @@ pub trait EdgeAttributes<'a> { /// with positive angles moving counterclockwise and negative angles moving clockwise. /// default: -25.0, minimum: -180.0 fn label_angle(&mut self, label_angle: f32) -> &mut Self { + if label_angle < -180.0 { + self.add_validation_error( + "labelangle", + "Must be greater than or equal to -180", + ) + } self.add_attribute("labelangle", AttributeText::from(label_angle)) } @@ -1721,11 +1765,16 @@ pub trait EdgeAttributes<'a> { self.add_attribute("labelfontname", AttributeText::attr(label_font_name)) } - // TODO: constrains /// Font size, in points, used for headlabel and taillabel. /// If not set, defaults to edge’s fontsize. /// default: 14.0, minimum: 1.0 fn label_font_size(&mut self, label_font_size: f32) -> &mut Self { + if label_font_size < 1.0 { + self.add_validation_error( + "labelfontsize", + "Must be greater than or equal to 1", + ) + } self.add_attribute("labelfontsize", AttributeText::from(label_font_size)) } @@ -1801,7 +1850,6 @@ pub trait EdgeAttributes<'a> { self.add_attribute("sametail", AttributeText::quoted(same_tail)) } - // TODO: constrain /// Print guide boxes in PostScript at the beginning of routesplines if showboxes=1, or at the /// end if showboxes=2. /// (Debugging, TB mode only!) @@ -1876,12 +1924,11 @@ pub trait EdgeAttributes<'a> { self } - // TODO: contrain /// Weight of edge. /// The heavier the weight, the shorter, straighter and more vertical the edge is. /// default: 1, minimum: 0 fn weight(&mut self, weight: u32) -> &mut Self { - self.add_attribute("weight", AttributeText::attr(weight.to_string())) + self.add_attribute("weight", AttributeText::from(weight)) } /// External label for a node or edge. @@ -1909,6 +1956,8 @@ pub trait EdgeAttributes<'a> { ) -> &mut Self; fn get_attributes_mut(&mut self) -> &mut IndexMap<String, AttributeText<'a>>; + + fn add_validation_error(&mut self, field: &'static str, message: &'static str); } pub(crate) fn fmt_attributes(attributes: &IndexMap<String, AttributeText>) -> String { @@ -1943,7 +1992,8 @@ mod test { (Color::Named("yellow"), Some(0.3)), (Color::Named("blue"), None), ]) - .build(); + .build() + .unwrap(); assert_eq!( graph_attributes.get("fillcolor").unwrap().dot_string(), @@ -6,6 +6,7 @@ use crate::attributes::{ }; use indexmap::IndexMap; use std::borrow::Cow; +use std::borrow::Cow::Borrowed; use std::collections::HashMap; use std::fmt::{Debug, Display, Formatter}; use std::io; @@ -13,6 +14,14 @@ use std::io::prelude::*; static INDENT: &str = " "; +pub type ValidationResult<T> = std::result::Result<T, Vec<ValidationError>>; + +#[derive(Debug, PartialEq, Clone)] +pub struct ValidationError { + pub message: Cow<'static, str>, + pub field: Cow<'static, str>, +} + pub trait DotString<'a> { fn dot_string(&self) -> Cow<'a, str>; } @@ -301,6 +310,8 @@ pub struct GraphBuilder<'a> { edges: Vec<Edge<'a>>, comment: Option<String>, + + errors: Vec<ValidationError>, } // TODO: id should be an escString @@ -317,6 +328,7 @@ impl<'a> GraphBuilder<'a> { nodes: Vec::new(), edges: Vec::new(), comment: None, + errors: Vec::new(), } } @@ -332,6 +344,7 @@ impl<'a> GraphBuilder<'a> { nodes: Vec::new(), edges: Vec::new(), comment: None, + errors: Vec::new(), } } @@ -411,7 +424,14 @@ impl<'a> GraphBuilder<'a> { self } - pub fn build(&self) -> Graph<'a> { + pub fn build(&self) -> ValidationResult<Graph<'a>> { + if !self.errors.is_empty() { + return Err(self.errors.clone()); + } + Ok(self.build_ignore_validation()) + } + + pub fn build_ignore_validation(&self) -> Graph<'a> { Graph { id: self.id.to_owned(), is_directed: self.is_directed, @@ -479,6 +499,8 @@ pub struct SubGraphBuilder<'a> { nodes: Vec<Node<'a>>, edges: Vec<Edge<'a>>, + + errors: Vec<ValidationError>, } // TODO: id should be an escString @@ -492,6 +514,7 @@ impl<'a> SubGraphBuilder<'a> { sub_graphs: Vec::new(), nodes: Vec::new(), edges: Vec::new(), + errors: Vec::new(), } } @@ -573,8 +596,12 @@ impl<'a> SubGraphBuilder<'a> { self } - pub fn build(&self) -> SubGraph<'a> { - SubGraph { + pub fn build(&self) -> ValidationResult<SubGraph<'a>> { + if !self.errors.is_empty() { + return Err(self.errors.clone()); + } + + Ok(SubGraph { id: self.id.to_owned(), graph_attributes: self.graph_attributes.clone(), node_attributes: self.node_attributes.clone(), @@ -582,7 +609,7 @@ impl<'a> SubGraphBuilder<'a> { sub_graphs: self.sub_graphs.clone(), nodes: self.nodes.clone(), // TODO: is clone the only option here? edges: self.edges.clone(), // TODO: is clone the only option here? - } + }) } } @@ -614,6 +641,7 @@ impl<'a> DotString<'a> for Node<'a> { pub struct NodeBuilder<'a> { id: String, attributes: IndexMap<String, AttributeText<'a>>, + errors: Vec<ValidationError>, } impl<'a> NodeAttributes<'a> for NodeBuilder<'a> { @@ -638,6 +666,13 @@ impl<'a> NodeAttributes<'a> for NodeBuilder<'a> { fn get_attributes_mut(&mut self) -> &mut IndexMap<String, AttributeText<'a>> { &mut self.attributes } + + fn add_validation_error(&mut self, field: &'static str, message: &'static str) { + self.errors.push(ValidationError { + field: Borrowed(field), + message: Borrowed(message), + }) + } } impl<'a> NodeBuilder<'a> { @@ -645,10 +680,18 @@ impl<'a> NodeBuilder<'a> { Self { id, attributes: IndexMap::new(), + errors: Vec::new(), + } + } + + pub fn build(&self) -> ValidationResult<Node<'a>> { + if !self.errors.is_empty() { + return Err(self.errors.clone()); } + Ok(self.build_ignore_validation()) } - pub fn build(&self) -> Node<'a> { + pub fn build_ignore_validation(&self) -> Node<'a> { Node { // TODO: are these to_owned and clones necessary? id: self.id.to_owned(), @@ -699,6 +742,7 @@ pub struct EdgeBuilder<'a> { pub target: String, pub target_port_position: Option<PortPosition>, attributes: IndexMap<String, AttributeText<'a>>, + errors: Vec<ValidationError>, } impl<'a> EdgeAttributes<'a> for EdgeBuilder<'a> { @@ -714,6 +758,13 @@ impl<'a> EdgeAttributes<'a> for EdgeBuilder<'a> { fn get_attributes_mut(&mut self) -> &mut IndexMap<String, AttributeText<'a>> { &mut self.attributes } + + fn add_validation_error(&mut self, field: &'static str, message: &'static str) { + self.errors.push(ValidationError { + field: Borrowed(field), + message: Borrowed(message), + }) + } } impl<'a> EdgeBuilder<'a> { @@ -724,6 +775,7 @@ impl<'a> EdgeBuilder<'a> { source_port_position: None, target_port_position: None, attributes: IndexMap::new(), + errors: Vec::new(), } } @@ -739,6 +791,7 @@ impl<'a> EdgeBuilder<'a> { source_port_position: Some(source_port_position), target_port_position: Some(target_port_position), attributes: IndexMap::new(), + errors: Vec::new(), } } @@ -770,7 +823,14 @@ impl<'a> EdgeBuilder<'a> { self } - pub fn build(&self) -> Edge<'a> { + pub fn build(&self) -> ValidationResult<Edge<'a>> { + if !self.errors.is_empty() { + return Err(self.errors.clone()); + } + Ok(self.build_ignore_validation()) + } + + pub fn build_ignore_validation(&self) -> Edge<'a> { Edge { // TODO: are these to_owned and clones necessary? source: self.source.to_owned(), @@ -804,21 +864,37 @@ impl<'a> NodeAttributes<'a> for NodeAttributeStatementBuilder<'a> { fn get_attributes_mut(&mut self) -> &mut IndexMap<String, AttributeText<'a>> { &mut self.attributes } + + fn add_validation_error(&mut self, field: &'static str, message: &'static str) { + self.errors.push(ValidationError { + field: Borrowed(field), + message: Borrowed(message), + }) + } } // I'm not a huge fan of needing this builder but having a hard time getting around &mut without it pub struct NodeAttributeStatementBuilder<'a> { pub attributes: IndexMap<String, AttributeText<'a>>, + errors: Vec<ValidationError>, } impl<'a> NodeAttributeStatementBuilder<'a> { pub fn new() -> Self { Self { attributes: IndexMap::new(), + errors: Vec::new(), } } - pub fn build(&self) -> IndexMap<String, AttributeText<'a>> { + pub fn build(&self) -> ValidationResult<IndexMap<String, AttributeText<'a>>> { + if !self.errors.is_empty() { + return Err(self.errors.clone()); + } + Ok(self.build_ignore_validation()) + } + + pub fn build_ignore_validation(&self) -> IndexMap<String, AttributeText<'a>> { self.attributes.clone() } } @@ -836,21 +912,37 @@ impl<'a> EdgeAttributes<'a> for EdgeAttributeStatementBuilder<'a> { fn get_attributes_mut(&mut self) -> &mut IndexMap<String, AttributeText<'a>> { &mut self.attributes } + + fn add_validation_error(&mut self, field: &'static str, message: &'static str) { + self.errors.push(ValidationError { + field: Borrowed(field), + message: Borrowed(message), + }) + } } // I'm not a huge fan of needing this builder but having a hard time getting around &mut without it pub struct EdgeAttributeStatementBuilder<'a> { pub attributes: IndexMap<String, AttributeText<'a>>, + errors: Vec<ValidationError>, } impl<'a> EdgeAttributeStatementBuilder<'a> { pub fn new() -> Self { Self { attributes: IndexMap::new(), + errors: Vec::new(), + } + } + + pub fn build(&self) -> ValidationResult<IndexMap<String, AttributeText<'a>>> { + if !self.errors.is_empty() { + return Err(self.errors.clone()); } + Ok(self.build_ignore_validation()) } - pub fn build(&self) -> IndexMap<String, AttributeText<'a>> { + pub fn build_ignore_validation(&self) -> IndexMap<String, AttributeText<'a>> { self.attributes.clone() } } @@ -18,7 +18,8 @@ //! .add_node(Node::new("N0".to_string())) //! .add_node(Node::new("N1".to_string())) //! .add_edge(Edge::new("N0".to_string(), "N1".to_string())) -//! .build(); +//! .build() +//! .unwrap(); //! //! let dot = Dot { graph: g }; //! println!("{}", dot); @@ -42,7 +43,8 @@ //! .add_node(Node::new("N0".to_string())) //! .add_node(Node::new("N1".to_string())) //! .add_edge(Edge::new("N0".to_string(), "N1".to_string())) -//! .build(); +//! .build() +//! .unwrap(); //! //! let dot = Dot { graph: g }; //! let mut writer= Vec::new(); @@ -78,18 +80,21 @@ //! .label("process #1".to_string()) //! .style(GraphStyle::Filled) //! .color(Color::Named("lightgrey")) -//! .build(), +//! .build() +//! .unwrap(), //! ) //! .add_node_attributes( //! NodeAttributeStatementBuilder::new() //! .style(NodeStyle::Filled) //! .color(Color::Named("white")) -//! .build(), +//! .build() +//! .unwrap(), //! ) //! .add_edge(Edge::new("a0".to_string(), "a1".to_string())) //! .add_edge(Edge::new("a1".to_string(), "a2".to_string())) //! .add_edge(Edge::new("a2".to_string(), "a3".to_string())) -//! .build(); +//! .build() +//! .unwrap(); //! //! let cluster_1 = SubGraphBuilder::new(Some("cluster_1".to_string())) //! .add_graph_attributes( @@ -97,28 +102,33 @@ //! .label("process #2".to_string()) //! .style(GraphStyle::Filled) //! .color(Color::Named("blue")) -//! .build(), +//! .build() +//! .unwrap(), //! ) //! .add_node_attributes( //! NodeAttributeStatementBuilder::new() //! .style(NodeStyle::Filled) -//! .build(), +//! .build() +//! .unwrap(), //! ) //! .add_edge(Edge::new("b0".to_string(), "b1".to_string())) //! .add_edge(Edge::new("b1".to_string(), "b2".to_string())) //! .add_edge(Edge::new("b2".to_string(), "b3".to_string())) -//! .build(); +//! .build() +//! .unwrap(); //! //! let g = GraphBuilder::new_directed(Some("G".to_string())) //! .add_node( //! NodeBuilder::new("start".to_string()) //! .shape(Shape::Mdiamond) -//! .build(), +//! .build() +//! .unwrap(), //! ) //! .add_node( //! NodeBuilder::new("end".to_string()) //! .shape(Shape::Msquare) -//! .build(), +//! .build() +//! .unwrap(), //! ) //! .add_sub_graph(cluster_0) //! .add_sub_graph(cluster_1) @@ -165,6 +175,7 @@ pub mod attributes; pub mod dot; +pub mod validation; #[doc(hidden)] pub use crate::dot::{ diff --git a/src/validation.rs b/src/validation.rs new file mode 100644 index 0000000..f9990ae --- /dev/null +++ b/src/validation.rs @@ -0,0 +1,9 @@ +use std::borrow::Cow; + +pub type ValidationResult<T> = std::result::Result<T, Vec<ValidationError>>; + +#[derive(Debug, PartialEq, Clone)] +pub struct ValidationError { + pub message: Cow<'static, str>, + pub field: Cow<'static, str>, +}
\ No newline at end of file diff --git a/tests/dot.rs b/tests/dot.rs index 8244a52..fbfd935 100644 --- a/tests/dot.rs +++ b/tests/dot.rs @@ -23,7 +23,7 @@ fn test_input(g: Graph) -> io::Result<String> { #[test] fn empty_digraph_without_id() { - let g = GraphBuilder::new_directed(None).build(); + let g = GraphBuilder::new_directed(None).build().unwrap(); let r = test_input(g); assert_eq!( r.unwrap(), @@ -35,7 +35,7 @@ fn empty_digraph_without_id() { #[test] fn support_display() { - let g = GraphBuilder::new_directed(None).build(); + let g = GraphBuilder::new_directed(None).build().unwrap(); let dot = Dot { graph: g }; assert_eq!( @@ -50,7 +50,8 @@ fn support_display() { fn graph_comment() { let g = GraphBuilder::new_directed(None) .comment("Comment goes here") - .build(); + .build() + .unwrap(); let r = test_input(g); assert_eq!( r.unwrap(), @@ -63,7 +64,9 @@ digraph { #[test] fn empty_digraph() { - let g = GraphBuilder::new_directed(Some("empty_graph".to_string())).build(); + let g = GraphBuilder::new_directed(Some("empty_graph".to_string())) + .build() + .unwrap(); let r = test_input(g); assert_eq!( r.unwrap(), @@ -75,7 +78,9 @@ fn empty_digraph() { #[test] fn empty_undirected_graph() { - let g = GraphBuilder::new_undirected(Some("empty_graph".to_string())).build(); + let g = GraphBuilder::new_undirected(Some("empty_graph".to_string())) + .build() + .unwrap(); let r = test_input(g); assert_eq!( r.unwrap(), @@ -89,7 +94,8 @@ fn empty_undirected_graph() { fn single_node() { let g = GraphBuilder::new_directed(Some("single_node".to_string())) .add_node(Node::new("N0".to_string())) - .build(); + .build() + .unwrap(); let r = test_input(g); assert_eq!( r.unwrap(), @@ -104,11 +110,13 @@ fn single_node() { fn single_node_with_style() { let node = NodeBuilder::new("N0".to_string()) .style(NodeStyle::Dashed) - .build(); + .build() + .unwrap(); let g = GraphBuilder::new_directed(Some("single_node".to_string())) .add_node(node) - .build(); + .build() + .unwrap(); let r = test_input(g); assert_eq!( @@ -132,10 +140,10 @@ fn support_non_inline_builder() { node_builder.add_attribute("foo", AttributeText::quoted("baz")); } - let node = node_builder.build(); + let node = node_builder.build().unwrap(); g.add_node(node); - let r = test_input(g.build()); + let r = test_input(g.build().unwrap()); assert_eq!( r.unwrap(), r#"digraph single_node { @@ -149,11 +157,13 @@ fn support_non_inline_builder() { fn builder_support_shape() { let node = NodeBuilder::new("N0".to_string()) .shape(Shape::Note) - .build(); + .build() + .unwrap(); let g = GraphBuilder::new_directed(Some("node_shape".to_string())) .add_node(node) - .build(); + .build() + .unwrap(); let r = test_input(g); assert_eq!( @@ -171,7 +181,8 @@ fn single_edge() { .add_node(Node::new("N0".to_string())) .add_node(Node::new("N1".to_string())) .add_edge(Edge::new("N0".to_string(), "N1".to_string())) - .build(); + .build() + .unwrap(); let r = test_input(g); @@ -190,13 +201,15 @@ fn single_edge() { fn single_edge_with_style() { let edge = EdgeBuilder::new("N0".to_string(), "N1".to_string()) .style(EdgeStyle::Bold) - .build(); + .build() + .unwrap(); let g = GraphBuilder::new_directed(Some("single_edge".to_string())) .add_node(Node::new("N0".to_string())) .add_node(Node::new("N1".to_string())) .add_edge(edge) - .build(); + .build() + .unwrap(); let r = test_input(g); @@ -216,12 +229,14 @@ fn edge_statement_port_position() { let node_0 = NodeBuilder::new("N0".to_string()) .shape(Shape::Record) .label("a|<port0>b") - .build(); + .build() + .unwrap(); let node_1 = NodeBuilder::new("N1".to_string()) .shape(Shape::Record) .label("e|<port1>f") - .build(); + .build() + .unwrap(); let edge = EdgeBuilder::new("N0".to_string(), "N1".to_string()) .source_port_position(PortPosition::Port { @@ -232,13 +247,15 @@ fn edge_statement_port_position() { port_name: "port1".to_string(), compass_point: Some(CompassPoint::NE), }) - .build(); + .build() + .unwrap(); let g = GraphBuilder::new_directed(Some("edge_statement_port_position".to_string())) .add_node(node_0) .add_node(node_1) .add_edge(edge) - .build(); + .build() + .unwrap(); let r = test_input(g); @@ -258,12 +275,14 @@ fn port_position_attribute() { let node_0 = NodeBuilder::new("N0".to_string()) .shape(Shape::Record) .label("a|<port0>b") - .build(); + .build() + .unwrap(); let node_1 = NodeBuilder::new("N1".to_string()) .shape(Shape::Record) .label("e|<port1>f") - .build(); + .build() + .unwrap(); let edge = EdgeBuilder::new("N0".to_string(), "N1".to_string()) .tail_port(PortPosition::Port { @@ -274,13 +293,15 @@ fn port_position_attribute() { port_name: "port1".to_string(), compass_point: Some(CompassPoint::NE), }) - .build(); + .build() + .unwrap(); let g = GraphBuilder::new_directed(Some("port_position_attribute".to_string())) .add_node(node_0) .add_node(node_1) .add_edge(edge) - .build(); + .build() + .unwrap(); let r = test_input(g); @@ -313,7 +334,8 @@ fn graph_attributes() { "color".to_string(), AttributeText::from(Color::Named("red")), ) - .build(); + .build() + .unwrap(); let r = test_input(g); @@ -358,7 +380,8 @@ fn graph_attributes_extend() { .cloned() .collect(), ) - .build(); + .build() + .unwrap(); let r = test_input(g); @@ -377,19 +400,23 @@ fn graph_attributes_extend() { fn graph_attributes_statement_builders() { let graph_attributes = GraphAttributeStatementBuilder::new() .rank_dir(RankDir::LeftRight) - .build(); + .build() + .unwrap(); let node_attributes = NodeAttributeStatementBuilder::new() .style(NodeStyle::Filled) - .build(); + .build() + .unwrap(); let edge_attributes = EdgeAttributeStatementBuilder::new() .color(Color::Named("red")) - .build(); + .build() + .unwrap(); let g = GraphBuilder::new_directed(Some("graph_attributes".to_string())) .add_graph_attributes(graph_attributes) .add_node_attributes(node_attributes) .add_edge_attributes(edge_attributes) - .build(); + .build() + .unwrap(); let r = test_input(g); @@ -412,18 +439,21 @@ fn clusters() { .label("process #1".to_string()) .style(GraphStyle::Filled) .color(Color::Named("lightgrey")) - .build(), + .build() + .unwrap(), ) .add_node_attributes( NodeAttributeStatementBuilder::new() .style(NodeStyle::Filled) .color(Color::Named("white")) - .build(), + .build() + .unwrap(), ) .add_edge(Edge::new("a0".to_string(), "a1".to_string())) .add_edge(Edge::new("a1".to_string(), "a2".to_string())) .add_edge(Edge::new("a2".to_string(), "a3".to_string())) - .build(); + .build() + .unwrap(); let cluster_1 = SubGraphBuilder::new(Some("cluster_1".to_string())) .add_graph_attributes( @@ -431,28 +461,33 @@ fn clusters() { .label("process #2".to_string()) .style(GraphStyle::Filled) .color(Color::Named("blue")) - .build(), + .build() + .unwrap(), ) .add_node_attributes( NodeAttributeStatementBuilder::new() .style(NodeStyle::Filled) - .build(), + .build() + .unwrap(), ) .add_edge(Edge::new("b0".to_string(), "b1".to_string())) .add_edge(Edge::new("b1".to_string(), "b2".to_string())) .add_edge(Edge::new("b2".to_string(), "b3".to_string())) - .build(); + .build() + .unwrap(); let g = GraphBuilder::new_directed(Some("G".to_string())) .add_node( NodeBuilder::new("start".to_string()) .shape(Shape::Mdiamond) - .build(), + .build() + .unwrap(), ) .add_node( NodeBuilder::new("end".to_string()) .shape(Shape::Msquare) - .build(), + .build() + .unwrap(), ) .add_sub_graph(cluster_0) .add_sub_graph(cluster_1) @@ -463,7 +498,8 @@ fn clusters() { .add_edge(Edge::new("a3".to_string(), "a0".to_string())) .add_edge(Edge::new("a3".to_string(), "end".to_string())) .add_edge(Edge::new("b3".to_string(), "end".to_string())) - .build(); + .build() + .unwrap(); let r = test_input(g); @@ -499,3 +535,127 @@ fn clusters() { "# ); } + +#[test] +fn edge_validation_error() { + let edge_builder = EdgeBuilder::new("N0".to_string(), "N1".to_string()) + .arrow_size(-1.0) + .build(); + + assert!(edge_builder.is_err()); + + let validation_errors = edge_builder.unwrap_err(); + assert_eq!(1, validation_errors.len()); + assert_eq!("arrowsize", validation_errors.get(0).unwrap().field); + assert_eq!( + "Must be greater than or equal to 0", + validation_errors.get(0).unwrap().message + ); +} + +#[test] +fn edge_build_ignore_validation_error() { + let edge = EdgeBuilder::new("N0".to_string(), "N1".to_string()) + .arrow_size(-1.0) + .build_ignore_validation(); + + assert!(edge.attributes.contains_key("arrowsize")) +} + +#[test] +fn edge_attributes_validation_error() { + let edge_builder = EdgeAttributeStatementBuilder::new() + .arrow_size(-1.0) + .build(); + + assert!(edge_builder.is_err()); + + let validation_errors = edge_builder.unwrap_err(); + assert_eq!(1, validation_errors.len()); + assert_eq!("arrowsize", validation_errors.get(0).unwrap().field); + assert_eq!( + "Must be greater than or equal to 0", + validation_errors.get(0).unwrap().message + ); +} + +#[test] +fn edge_attribute_build_ignore_validation_error() { + let edge = EdgeAttributeStatementBuilder::new() + .arrow_size(-1.0) + .build_ignore_validation(); + + assert!(edge.contains_key("arrowsize")) +} + +#[test] +fn node_validation_error() { + let node_builder = NodeBuilder::new("N0".to_string()).height(0.0).build(); + + assert!(node_builder.is_err()); + + let validation_errors = node_builder.unwrap_err(); + assert_eq!(1, validation_errors.len()); + assert_eq!("height", validation_errors.get(0).unwrap().field); + assert_eq!( + "Must be greater than or equal to 0.02", + validation_errors.get(0).unwrap().message + ); +} + +#[test] +fn node_build_ignore_validation_error() { + let node = NodeBuilder::new("N0".to_string()) + .height(0.0) + .build_ignore_validation(); + + assert!(node.attributes.contains_key("height")) +} + +#[test] +fn node_attribute_validation_error() { + let node_builder = NodeAttributeStatementBuilder::new().height(0.0).build(); + + assert!(node_builder.is_err()); + + let validation_errors = node_builder.unwrap_err(); + assert_eq!(1, validation_errors.len()); + assert_eq!("height", validation_errors.get(0).unwrap().field); + assert_eq!( + "Must be greater than or equal to 0.02", + validation_errors.get(0).unwrap().message + ); +} + +#[test] +fn node_attribute_build_ignore_validation_error() { + let node = NodeAttributeStatementBuilder::new() + .height(0.0) + .build_ignore_validation(); + + assert!(node.contains_key("height")) +} + +#[test] +fn graph_attributes_validation_error() { + let graph_builder = GraphAttributeStatementBuilder::new().font_size(0.0).build(); + + assert!(graph_builder.is_err()); + + let validation_errors = graph_builder.unwrap_err(); + assert_eq!(1, validation_errors.len()); + assert_eq!("fontsize", validation_errors.get(0).unwrap().field); + assert_eq!( + "Must be greater than or equal to 1.0", + validation_errors.get(0).unwrap().message + ); +} + +#[test] +fn graph_attributes_build_ignore_validation_error() { + let graph = GraphAttributeStatementBuilder::new() + .font_size(0.0) + .build_ignore_validation(); + + assert!(graph.contains_key("fontsize")) +} |