Skip to main content

dendryform_core/
error.rs

1//! Error types for the dendryform-core crate.
2
3use std::fmt;
4
5/// Errors that occur when constructing or validating core types.
6#[derive(Debug, Clone, PartialEq, Eq)]
7#[non_exhaustive]
8pub enum ValidationError {
9    /// A node ID contains invalid characters or is empty.
10    InvalidNodeId {
11        /// The invalid ID value that was rejected.
12        value: String,
13        /// What was wrong with it.
14        reason: &'static str,
15    },
16
17    /// Two or more nodes share the same ID.
18    DuplicateNodeId {
19        /// The duplicated ID.
20        id: String,
21    },
22
23    /// An edge references a node ID that does not exist in the diagram.
24    DanglingEdgeReference {
25        /// The edge's source or target that doesn't exist.
26        id: String,
27        /// Which field was invalid ("from" or "to").
28        field: &'static str,
29    },
30
31    /// A tier has neither nodes nor a container.
32    EmptyTier {
33        /// The tier's ID.
34        id: String,
35    },
36
37    /// Container nesting exceeds the maximum depth.
38    NestingTooDeep {
39        /// The maximum allowed depth.
40        max_depth: usize,
41        /// The actual depth found.
42        actual_depth: usize,
43    },
44
45    /// A required field is missing.
46    MissingField {
47        /// The name of the missing field.
48        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}