use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ValidationError {
InvalidNodeId {
value: String,
reason: &'static str,
},
DuplicateNodeId {
id: String,
},
DanglingEdgeReference {
id: String,
field: &'static str,
},
EmptyTier {
id: String,
},
NestingTooDeep {
max_depth: usize,
actual_depth: usize,
},
MissingField {
field: &'static str,
},
}
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidNodeId { value, reason } => {
write!(f, "invalid node ID {value:?}: {reason}")
}
Self::DuplicateNodeId { id } => {
write!(f, "duplicate node ID: {id:?}")
}
Self::DanglingEdgeReference { id, field } => {
write!(f, "edge {field} references unknown node: {id:?}")
}
Self::EmptyTier { id } => {
write!(f, "tier {id:?} has neither nodes nor a container")
}
Self::NestingTooDeep {
max_depth,
actual_depth,
} => {
write!(
f,
"container nesting depth {actual_depth} exceeds maximum {max_depth}"
)
}
Self::MissingField { field } => {
write!(f, "missing required field: {field}")
}
}
}
}
impl std::error::Error for ValidationError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_display_invalid_node_id() {
let err = ValidationError::InvalidNodeId {
value: "Bad ID".to_owned(),
reason: "contains spaces",
};
let msg = format!("{err}");
assert!(msg.contains("invalid node ID"));
assert!(msg.contains("Bad ID"));
assert!(msg.contains("contains spaces"));
}
#[test]
fn test_display_duplicate_node_id() {
let err = ValidationError::DuplicateNodeId {
id: "app".to_owned(),
};
let msg = format!("{err}");
assert!(msg.contains("duplicate node ID"));
assert!(msg.contains("app"));
}
#[test]
fn test_display_dangling_edge_reference() {
let err = ValidationError::DanglingEdgeReference {
id: "ghost".to_owned(),
field: "from",
};
let msg = format!("{err}");
assert!(msg.contains("edge from references unknown node"));
assert!(msg.contains("ghost"));
}
#[test]
fn test_display_empty_tier() {
let err = ValidationError::EmptyTier {
id: "tier-1".to_owned(),
};
let msg = format!("{err}");
assert!(msg.contains("tier"));
assert!(msg.contains("tier-1"));
assert!(msg.contains("neither nodes nor a container"));
}
#[test]
fn test_display_nesting_too_deep() {
let err = ValidationError::NestingTooDeep {
max_depth: 3,
actual_depth: 5,
};
let msg = format!("{err}");
assert!(msg.contains("nesting depth 5"));
assert!(msg.contains("maximum 3"));
}
#[test]
fn test_display_missing_field() {
let err = ValidationError::MissingField { field: "title" };
let msg = format!("{err}");
assert!(msg.contains("missing required field"));
assert!(msg.contains("title"));
}
#[test]
fn test_debug_format() {
let err = ValidationError::MissingField { field: "id" };
let debug = format!("{err:?}");
assert!(debug.contains("MissingField"));
}
#[test]
fn test_clone_and_eq() {
let err = ValidationError::DuplicateNodeId {
id: "app".to_owned(),
};
let cloned = err.clone();
assert_eq!(err, cloned);
}
#[test]
fn test_error_trait_source_is_none() {
let err = ValidationError::MissingField { field: "id" };
let source = std::error::Error::source(&err);
assert!(source.is_none());
}
}