1use std::fmt;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7#[non_exhaustive]
8pub enum ValidationError {
9 InvalidNodeId {
11 value: String,
13 reason: &'static str,
15 },
16
17 DuplicateNodeId {
19 id: String,
21 },
22
23 DanglingEdgeReference {
25 id: String,
27 field: &'static str,
29 },
30
31 EmptyTier {
33 id: String,
35 },
36
37 NestingTooDeep {
39 max_depth: usize,
41 actual_depth: usize,
43 },
44
45 MissingField {
47 field: &'static str,
49 },
50}
51
52impl fmt::Display for ValidationError {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 match self {
55 Self::InvalidNodeId { value, reason } => {
56 write!(f, "invalid node ID {value:?}: {reason}")
57 }
58 Self::DuplicateNodeId { id } => {
59 write!(f, "duplicate node ID: {id:?}")
60 }
61 Self::DanglingEdgeReference { id, field } => {
62 write!(f, "edge {field} references unknown node: {id:?}")
63 }
64 Self::EmptyTier { id } => {
65 write!(f, "tier {id:?} has neither nodes nor a container")
66 }
67 Self::NestingTooDeep {
68 max_depth,
69 actual_depth,
70 } => {
71 write!(
72 f,
73 "container nesting depth {actual_depth} exceeds maximum {max_depth}"
74 )
75 }
76 Self::MissingField { field } => {
77 write!(f, "missing required field: {field}")
78 }
79 }
80 }
81}
82
83impl std::error::Error for ValidationError {}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[test]
90 fn test_display_invalid_node_id() {
91 let err = ValidationError::InvalidNodeId {
92 value: "Bad ID".to_owned(),
93 reason: "contains spaces",
94 };
95 let msg = format!("{err}");
96 assert!(msg.contains("invalid node ID"));
97 assert!(msg.contains("Bad ID"));
98 assert!(msg.contains("contains spaces"));
99 }
100
101 #[test]
102 fn test_display_duplicate_node_id() {
103 let err = ValidationError::DuplicateNodeId {
104 id: "app".to_owned(),
105 };
106 let msg = format!("{err}");
107 assert!(msg.contains("duplicate node ID"));
108 assert!(msg.contains("app"));
109 }
110
111 #[test]
112 fn test_display_dangling_edge_reference() {
113 let err = ValidationError::DanglingEdgeReference {
114 id: "ghost".to_owned(),
115 field: "from",
116 };
117 let msg = format!("{err}");
118 assert!(msg.contains("edge from references unknown node"));
119 assert!(msg.contains("ghost"));
120 }
121
122 #[test]
123 fn test_display_empty_tier() {
124 let err = ValidationError::EmptyTier {
125 id: "tier-1".to_owned(),
126 };
127 let msg = format!("{err}");
128 assert!(msg.contains("tier"));
129 assert!(msg.contains("tier-1"));
130 assert!(msg.contains("neither nodes nor a container"));
131 }
132
133 #[test]
134 fn test_display_nesting_too_deep() {
135 let err = ValidationError::NestingTooDeep {
136 max_depth: 3,
137 actual_depth: 5,
138 };
139 let msg = format!("{err}");
140 assert!(msg.contains("nesting depth 5"));
141 assert!(msg.contains("maximum 3"));
142 }
143
144 #[test]
145 fn test_display_missing_field() {
146 let err = ValidationError::MissingField { field: "title" };
147 let msg = format!("{err}");
148 assert!(msg.contains("missing required field"));
149 assert!(msg.contains("title"));
150 }
151
152 #[test]
153 fn test_debug_format() {
154 let err = ValidationError::MissingField { field: "id" };
155 let debug = format!("{err:?}");
156 assert!(debug.contains("MissingField"));
157 }
158
159 #[test]
160 fn test_clone_and_eq() {
161 let err = ValidationError::DuplicateNodeId {
162 id: "app".to_owned(),
163 };
164 let cloned = err.clone();
165 assert_eq!(err, cloned);
166 }
167
168 #[test]
169 fn test_error_trait_source_is_none() {
170 let err = ValidationError::MissingField { field: "id" };
171 let source = std::error::Error::source(&err);
172 assert!(source.is_none());
173 }
174}