summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorseancarroll <seanc28@gmail.com>2021-01-20 21:51:57 -0600
committerseancarroll <seanc28@gmail.com>2021-01-20 21:51:57 -0600
commit7d5d439ed2f465120825c918f920bd9fbfd3169f (patch)
tree4545ef6d834d8bd5f6b0d3c860511d3ddc042984
parentec3468a275314f16769439736b7514b40e3b49c0 (diff)
downloaddotavious-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.rs86
-rw-r--r--src/dot.rs108
-rw-r--r--src/lib.rs31
-rw-r--r--src/validation.rs9
-rw-r--r--tests/dot.rs236
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(),
diff --git a/src/dot.rs b/src/dot.rs
index 5c1dc29..a6828b7 100644
--- a/src/dot.rs
+++ b/src/dot.rs
@@ -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()
}
}
diff --git a/src/lib.rs b/src/lib.rs
index a21d8cb..98c17d2 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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"))
+}