bq_schema_gen/validate/
error.rs

1//! Validation error types for schema validation.
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6/// Types of validation errors that can occur.
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "snake_case")]
9pub enum ValidationErrorType {
10    /// A required field is missing or null
11    MissingRequired,
12    /// Value type doesn't match expected schema type
13    TypeMismatch { expected: String, actual: String },
14    /// Field exists in data but not in schema
15    UnknownField,
16}
17
18impl fmt::Display for ValidationErrorType {
19    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20        match self {
21            ValidationErrorType::MissingRequired => write!(f, "missing required field"),
22            ValidationErrorType::TypeMismatch { expected, actual } => {
23                write!(f, "expected {}, got {}", expected, actual)
24            }
25            ValidationErrorType::UnknownField => write!(f, "unknown field"),
26        }
27    }
28}
29
30/// A single validation error.
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct ValidationError {
33    /// Line number where the error occurred (1-indexed)
34    pub line: usize,
35    /// Path to the field (e.g., "user.address.city")
36    pub path: String,
37    /// Type of validation error
38    pub error_type: ValidationErrorType,
39    /// Human-readable error message
40    pub message: String,
41}
42
43impl ValidationError {
44    /// Create a new validation error for a missing required field.
45    pub fn missing_required(line: usize, path: &str) -> Self {
46        Self {
47            line,
48            path: path.to_string(),
49            error_type: ValidationErrorType::MissingRequired,
50            message: format!("Field '{}' is REQUIRED but missing", path),
51        }
52    }
53
54    /// Create a new validation error for a type mismatch.
55    pub fn type_mismatch(
56        line: usize,
57        path: &str,
58        expected: &str,
59        actual: &str,
60        value: &str,
61    ) -> Self {
62        Self {
63            line,
64            path: path.to_string(),
65            error_type: ValidationErrorType::TypeMismatch {
66                expected: expected.to_string(),
67                actual: actual.to_string(),
68            },
69            message: format!(
70                "Field '{}' expected {}, got {} (\"{}\")",
71                path, expected, actual, value
72            ),
73        }
74    }
75
76    /// Create a new validation error for an unknown field.
77    pub fn unknown_field(line: usize, path: &str) -> Self {
78        Self {
79            line,
80            path: path.to_string(),
81            error_type: ValidationErrorType::UnknownField,
82            message: format!("Unknown field '{}' not in schema", path),
83        }
84    }
85}
86
87impl fmt::Display for ValidationError {
88    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89        write!(f, "Line {}: {}", self.line, self.message)
90    }
91}
92
93/// Result of validating data against a schema.
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct ValidationResult {
96    /// Whether all data is valid
97    pub valid: bool,
98    /// Number of errors found
99    pub error_count: usize,
100    /// List of validation errors
101    pub errors: Vec<ValidationError>,
102    /// List of warnings (e.g., unknown fields when --allow-unknown is set)
103    #[serde(skip_serializing_if = "Vec::is_empty")]
104    pub warnings: Vec<ValidationError>,
105}
106
107impl ValidationResult {
108    /// Create a new empty validation result (valid).
109    pub fn new() -> Self {
110        Self {
111            valid: true,
112            error_count: 0,
113            errors: Vec::new(),
114            warnings: Vec::new(),
115        }
116    }
117
118    /// Add an error to the result.
119    pub fn add_error(&mut self, error: ValidationError) {
120        self.valid = false;
121        self.error_count += 1;
122        self.errors.push(error);
123    }
124
125    /// Add a warning to the result.
126    pub fn add_warning(&mut self, warning: ValidationError) {
127        self.warnings.push(warning);
128    }
129
130    /// Check if max errors has been reached.
131    pub fn reached_max_errors(&self, max_errors: usize) -> bool {
132        self.error_count >= max_errors
133    }
134}
135
136impl Default for ValidationResult {
137    fn default() -> Self {
138        Self::new()
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn test_validation_error_display() {
148        let err = ValidationError::missing_required(42, "user_id");
149        assert!(err.to_string().contains("Line 42"));
150        assert!(err.to_string().contains("REQUIRED"));
151
152        let err = ValidationError::type_mismatch(10, "age", "INTEGER", "STRING", "thirty");
153        assert!(err.to_string().contains("expected INTEGER"));
154        assert!(err.to_string().contains("got STRING"));
155
156        let err = ValidationError::unknown_field(5, "legacy_field");
157        assert!(err.to_string().contains("Unknown field"));
158    }
159
160    #[test]
161    fn test_validation_result() {
162        let mut result = ValidationResult::new();
163        assert!(result.valid);
164        assert_eq!(result.error_count, 0);
165
166        result.add_error(ValidationError::missing_required(1, "id"));
167        assert!(!result.valid);
168        assert_eq!(result.error_count, 1);
169
170        result.add_warning(ValidationError::unknown_field(2, "extra"));
171        assert_eq!(result.warnings.len(), 1);
172    }
173
174    // ===== Additional Coverage Tests =====
175
176    #[test]
177    fn test_validation_error_type_display() {
178        let missing = ValidationErrorType::MissingRequired;
179        assert_eq!(missing.to_string(), "missing required field");
180
181        let type_mismatch = ValidationErrorType::TypeMismatch {
182            expected: "INTEGER".to_string(),
183            actual: "STRING".to_string(),
184        };
185        assert_eq!(type_mismatch.to_string(), "expected INTEGER, got STRING");
186
187        let unknown = ValidationErrorType::UnknownField;
188        assert_eq!(unknown.to_string(), "unknown field");
189    }
190
191    #[test]
192    fn test_validation_error_missing_required() {
193        let err = ValidationError::missing_required(10, "user.name");
194
195        assert_eq!(err.line, 10);
196        assert_eq!(err.path, "user.name");
197        assert_eq!(err.error_type, ValidationErrorType::MissingRequired);
198        assert!(err.message.contains("REQUIRED"));
199        assert!(err.message.contains("user.name"));
200    }
201
202    #[test]
203    fn test_validation_error_type_mismatch() {
204        let err = ValidationError::type_mismatch(5, "age", "INTEGER", "STRING", "twenty");
205
206        assert_eq!(err.line, 5);
207        assert_eq!(err.path, "age");
208        match &err.error_type {
209            ValidationErrorType::TypeMismatch { expected, actual } => {
210                assert_eq!(expected, "INTEGER");
211                assert_eq!(actual, "STRING");
212            }
213            _ => panic!("Expected TypeMismatch"),
214        }
215        assert!(err.message.contains("expected INTEGER"));
216        assert!(err.message.contains("got STRING"));
217        assert!(err.message.contains("twenty"));
218    }
219
220    #[test]
221    fn test_validation_error_unknown_field() {
222        let err = ValidationError::unknown_field(3, "legacy_data.old_field");
223
224        assert_eq!(err.line, 3);
225        assert_eq!(err.path, "legacy_data.old_field");
226        assert_eq!(err.error_type, ValidationErrorType::UnknownField);
227        assert!(err.message.contains("Unknown field"));
228        assert!(err.message.contains("not in schema"));
229    }
230
231    #[test]
232    fn test_validation_error_display_format() {
233        let err = ValidationError::missing_required(42, "required_field");
234        let display_str = err.to_string();
235
236        assert!(display_str.contains("Line 42"));
237        assert!(display_str.contains("required_field"));
238    }
239
240    #[test]
241    fn test_validation_result_default() {
242        let result = ValidationResult::default();
243
244        assert!(result.valid);
245        assert_eq!(result.error_count, 0);
246        assert!(result.errors.is_empty());
247        assert!(result.warnings.is_empty());
248    }
249
250    #[test]
251    fn test_validation_result_reached_max_errors() {
252        let mut result = ValidationResult::new();
253
254        assert!(!result.reached_max_errors(5));
255
256        for i in 0..5 {
257            result.add_error(ValidationError::missing_required(i, "field"));
258        }
259
260        assert!(result.reached_max_errors(5));
261        assert!(!result.reached_max_errors(6));
262    }
263
264    #[test]
265    fn test_validation_result_multiple_errors() {
266        let mut result = ValidationResult::new();
267
268        result.add_error(ValidationError::missing_required(1, "a"));
269        result.add_error(ValidationError::type_mismatch(2, "b", "INT", "STR", "x"));
270        result.add_error(ValidationError::unknown_field(3, "c"));
271
272        assert!(!result.valid);
273        assert_eq!(result.error_count, 3);
274        assert_eq!(result.errors.len(), 3);
275    }
276
277    #[test]
278    fn test_validation_result_multiple_warnings() {
279        let mut result = ValidationResult::new();
280
281        result.add_warning(ValidationError::unknown_field(1, "extra1"));
282        result.add_warning(ValidationError::unknown_field(2, "extra2"));
283
284        // Warnings don't affect validity
285        assert!(result.valid);
286        assert_eq!(result.error_count, 0);
287        assert_eq!(result.warnings.len(), 2);
288    }
289
290    #[test]
291    fn test_validation_error_serialization() {
292        let err = ValidationError::type_mismatch(1, "field", "INTEGER", "BOOLEAN", "true");
293        let json = serde_json::to_string(&err).unwrap();
294
295        assert!(json.contains("\"line\":1"));
296        assert!(json.contains("\"path\":\"field\""));
297        assert!(json.contains("type_mismatch"));
298    }
299
300    #[test]
301    fn test_validation_result_serialization() {
302        let mut result = ValidationResult::new();
303        result.add_error(ValidationError::missing_required(1, "id"));
304
305        let json = serde_json::to_string(&result).unwrap();
306
307        assert!(json.contains("\"valid\":false"));
308        assert!(json.contains("\"error_count\":1"));
309        // warnings should be skipped if empty
310    }
311
312    #[test]
313    fn test_validation_error_type_equality() {
314        let a = ValidationErrorType::MissingRequired;
315        let b = ValidationErrorType::MissingRequired;
316        assert_eq!(a, b);
317
318        let c = ValidationErrorType::TypeMismatch {
319            expected: "A".to_string(),
320            actual: "B".to_string(),
321        };
322        let d = ValidationErrorType::TypeMismatch {
323            expected: "A".to_string(),
324            actual: "B".to_string(),
325        };
326        assert_eq!(c, d);
327
328        let e = ValidationErrorType::TypeMismatch {
329            expected: "A".to_string(),
330            actual: "C".to_string(),
331        };
332        assert_ne!(c, e);
333    }
334}