runbeam_sdk/validation/
error.rs

1//! Validation error types for TOML configuration validation.
2//!
3//! This module defines error types used when validating TOML configurations
4//! against schema definitions.
5
6use std::fmt;
7
8/// Errors that can occur during TOML validation.
9#[derive(Debug, Clone)]
10pub enum ValidationError {
11    /// A required field is missing from the configuration.
12    MissingRequiredField {
13        /// The path to the missing field (e.g., "proxy.id" or "network.default.bind_address").
14        field_path: String,
15    },
16
17    /// A field has an invalid type.
18    InvalidType {
19        /// The path to the field with the wrong type.
20        field_path: String,
21        /// The expected type according to the schema.
22        expected: String,
23        /// The actual type found in the TOML.
24        found: String,
25    },
26
27    /// A field value is not one of the allowed enum values.
28    InvalidEnumValue {
29        /// The path to the field with the invalid value.
30        field_path: String,
31        /// The value that was found.
32        value: String,
33        /// The list of allowed values.
34        allowed: Vec<String>,
35    },
36
37    /// A numeric value is outside the allowed range.
38    OutOfRange {
39        /// The path to the field that is out of range.
40        field_path: String,
41        /// The value that was found.
42        value: String,
43        /// The minimum allowed value, if specified.
44        min: Option<String>,
45        /// The maximum allowed value, if specified.
46        max: Option<String>,
47    },
48
49    /// An array has an invalid number of items.
50    InvalidArrayLength {
51        /// The path to the array field.
52        field_path: String,
53        /// The actual length of the array.
54        length: usize,
55        /// The minimum required length, if specified.
56        min: Option<usize>,
57        /// The maximum allowed length, if specified.
58        max: Option<usize>,
59    },
60
61    /// A field value does not match the required pattern.
62    PatternMismatch {
63        /// The path to the field with the pattern mismatch.
64        field_path: String,
65        /// The pattern that should have been matched.
66        pattern: String,
67    },
68
69    /// A conditionally required field is missing.
70    ConditionalRequirementFailed {
71        /// The path to the missing field.
72        field_path: String,
73        /// The condition that triggered the requirement.
74        condition: String,
75    },
76
77    /// Failed to parse the schema TOML.
78    SchemaParseError(String),
79
80    /// Failed to parse the content TOML.
81    TomlParseError(String),
82
83    /// Multiple validation errors occurred.
84    Multiple(Vec<ValidationError>),
85
86    /// An unexpected table was found.
87    UnexpectedTable {
88        /// The path to the unexpected table.
89        table_path: String,
90    },
91
92    /// An unexpected field was found.
93    UnexpectedField {
94        /// The path to the unexpected field.
95        field_path: String,
96    },
97}
98
99impl fmt::Display for ValidationError {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        match self {
102            ValidationError::MissingRequiredField { field_path } => {
103                write!(f, "Missing required field: {}", field_path)
104            }
105            ValidationError::InvalidType {
106                field_path,
107                expected,
108                found,
109            } => {
110                write!(
111                    f,
112                    "Invalid type for field '{}': expected {}, found {}",
113                    field_path, expected, found
114                )
115            }
116            ValidationError::InvalidEnumValue {
117                field_path,
118                value,
119                allowed,
120            } => {
121                write!(
122                    f,
123                    "Invalid value '{}' for field '{}': must be one of [{}]",
124                    value,
125                    field_path,
126                    allowed.join(", ")
127                )
128            }
129            ValidationError::OutOfRange {
130                field_path,
131                value,
132                min,
133                max,
134            } => {
135                let range = match (min, max) {
136                    (Some(min), Some(max)) => format!("between {} and {}", min, max),
137                    (Some(min), None) => format!("at least {}", min),
138                    (None, Some(max)) => format!("at most {}", max),
139                    (None, None) => "within valid range".to_string(),
140                };
141                write!(
142                    f,
143                    "Value '{}' for field '{}' is out of range: must be {}",
144                    value, field_path, range
145                )
146            }
147            ValidationError::InvalidArrayLength {
148                field_path,
149                length,
150                min,
151                max,
152            } => {
153                let constraint = match (min, max) {
154                    (Some(min), Some(max)) => format!("between {} and {} items", min, max),
155                    (Some(min), None) => format!("at least {} items", min),
156                    (None, Some(max)) => format!("at most {} items", max),
157                    (None, None) => "valid length".to_string(),
158                };
159                write!(
160                    f,
161                    "Array '{}' has {} items, but must have {}",
162                    field_path, length, constraint
163                )
164            }
165            ValidationError::PatternMismatch {
166                field_path,
167                pattern,
168            } => {
169                write!(
170                    f,
171                    "Field '{}' does not match required pattern: {}",
172                    field_path, pattern
173                )
174            }
175            ValidationError::ConditionalRequirementFailed {
176                field_path,
177                condition,
178            } => {
179                write!(
180                    f,
181                    "Field '{}' is required when condition '{}' is met",
182                    field_path, condition
183                )
184            }
185            ValidationError::SchemaParseError(msg) => {
186                write!(f, "Failed to parse schema: {}", msg)
187            }
188            ValidationError::TomlParseError(msg) => {
189                write!(f, "Failed to parse TOML: {}", msg)
190            }
191            ValidationError::Multiple(errors) => {
192                writeln!(f, "Multiple validation errors occurred:")?;
193                for (i, error) in errors.iter().enumerate() {
194                    writeln!(f, "  {}. {}", i + 1, error)?;
195                }
196                Ok(())
197            }
198            ValidationError::UnexpectedTable { table_path } => {
199                write!(f, "Unexpected table: {}", table_path)
200            }
201            ValidationError::UnexpectedField { field_path } => {
202                write!(f, "Unexpected field: {}", field_path)
203            }
204        }
205    }
206}
207
208impl std::error::Error for ValidationError {}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn test_missing_required_field_display() {
216        let error = ValidationError::MissingRequiredField {
217            field_path: "proxy.id".to_string(),
218        };
219        let message = error.to_string();
220        assert!(message.contains("proxy.id"));
221        assert!(message.contains("Missing required field"));
222    }
223
224    #[test]
225    fn test_invalid_type_display() {
226        let error = ValidationError::InvalidType {
227            field_path: "network.default.bind_address".to_string(),
228            expected: "string".to_string(),
229            found: "integer".to_string(),
230        };
231        let message = error.to_string();
232        assert!(message.contains("network.default.bind_address"));
233        assert!(message.contains("string"));
234        assert!(message.contains("integer"));
235    }
236
237    #[test]
238    fn test_invalid_enum_value_display() {
239        let error = ValidationError::InvalidEnumValue {
240            field_path: "proxy.log_level".to_string(),
241            value: "invalid".to_string(),
242            allowed: vec!["trace".to_string(), "debug".to_string(), "info".to_string()],
243        };
244        let message = error.to_string();
245        assert!(message.contains("proxy.log_level"));
246        assert!(message.contains("invalid"));
247        assert!(message.contains("trace"));
248        assert!(message.contains("debug"));
249        assert!(message.contains("info"));
250    }
251
252    #[test]
253    fn test_out_of_range_display() {
254        let error = ValidationError::OutOfRange {
255            field_path: "proxy.jwks_cache_duration_hours".to_string(),
256            value: "200".to_string(),
257            min: Some("1".to_string()),
258            max: Some("168".to_string()),
259        };
260        let message = error.to_string();
261        assert!(message.contains("proxy.jwks_cache_duration_hours"));
262        assert!(message.contains("200"));
263        assert!(message.contains("1"));
264        assert!(message.contains("168"));
265    }
266
267    #[test]
268    fn test_invalid_array_length_display() {
269        let error = ValidationError::InvalidArrayLength {
270            field_path: "pipelines.example.endpoints".to_string(),
271            length: 0,
272            min: Some(1),
273            max: None,
274        };
275        let message = error.to_string();
276        assert!(message.contains("pipelines.example.endpoints"));
277        assert!(message.contains("0 items"));
278        assert!(message.contains("at least 1"));
279    }
280
281    #[test]
282    fn test_pattern_mismatch_display() {
283        let error = ValidationError::PatternMismatch {
284            field_path: "network.invalid-name".to_string(),
285            pattern: "^[a-z0-9_-]+$".to_string(),
286        };
287        let message = error.to_string();
288        assert!(message.contains("network.invalid-name"));
289        assert!(message.contains("^[a-z0-9_-]+$"));
290    }
291
292    #[test]
293    fn test_conditional_requirement_failed_display() {
294        let error = ValidationError::ConditionalRequirementFailed {
295            field_path: "management.network".to_string(),
296            condition: "management.enabled == true".to_string(),
297        };
298        let message = error.to_string();
299        assert!(message.contains("management.network"));
300        assert!(message.contains("management.enabled == true"));
301    }
302
303    #[test]
304    fn test_multiple_errors_display() {
305        let errors = vec![
306            ValidationError::MissingRequiredField {
307                field_path: "proxy.id".to_string(),
308            },
309            ValidationError::InvalidType {
310                field_path: "proxy.port".to_string(),
311                expected: "integer".to_string(),
312                found: "string".to_string(),
313            },
314        ];
315        let error = ValidationError::Multiple(errors);
316        let message = error.to_string();
317        assert!(message.contains("Multiple validation errors"));
318        assert!(message.contains("proxy.id"));
319        assert!(message.contains("proxy.port"));
320    }
321}