Skip to main content

oxigdal_dev_tools/
validator.rs

1//! Data validation utilities
2//!
3//! This module provides tools for validating geospatial data integrity,
4//! format compliance, and correctness.
5
6use colored::Colorize;
7use serde::{Deserialize, Serialize};
8use std::path::Path;
9
10/// Validation result
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ValidationResult {
13    /// Whether validation passed
14    pub passed: bool,
15    /// Validation errors
16    pub errors: Vec<ValidationError>,
17    /// Validation warnings
18    pub warnings: Vec<ValidationWarning>,
19    /// Validation info
20    pub info: Vec<String>,
21}
22
23/// Validation error
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct ValidationError {
26    /// Error category
27    pub category: ErrorCategory,
28    /// Error message
29    pub message: String,
30    /// Error location (optional)
31    pub location: Option<String>,
32}
33
34/// Validation warning
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct ValidationWarning {
37    /// Warning category
38    pub category: WarningCategory,
39    /// Warning message
40    pub message: String,
41}
42
43/// Error category
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
45pub enum ErrorCategory {
46    /// Format error
47    Format,
48    /// Data integrity error
49    Integrity,
50    /// Metadata error
51    Metadata,
52    /// Projection error
53    Projection,
54    /// Bounds error
55    Bounds,
56}
57
58/// Warning category
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
60pub enum WarningCategory {
61    /// Performance warning
62    Performance,
63    /// Compatibility warning
64    Compatibility,
65    /// Best practices warning
66    BestPractices,
67}
68
69impl ValidationResult {
70    /// Create a new validation result
71    pub fn new() -> Self {
72        Self {
73            passed: true,
74            errors: Vec::new(),
75            warnings: Vec::new(),
76            info: Vec::new(),
77        }
78    }
79
80    /// Add an error
81    pub fn add_error(&mut self, category: ErrorCategory, message: impl Into<String>) {
82        self.passed = false;
83        self.errors.push(ValidationError {
84            category,
85            message: message.into(),
86            location: None,
87        });
88    }
89
90    /// Add a warning
91    pub fn add_warning(&mut self, category: WarningCategory, message: impl Into<String>) {
92        self.warnings.push(ValidationWarning {
93            category,
94            message: message.into(),
95        });
96    }
97
98    /// Add info
99    pub fn add_info(&mut self, message: impl Into<String>) {
100        self.info.push(message.into());
101    }
102
103    /// Format as report
104    pub fn report(&self) -> String {
105        let mut report = String::new();
106        report.push_str(&format!("\n{}\n", "Validation Report".bold()));
107        report.push_str(&format!("{}\n\n", "=".repeat(60)));
108
109        let status = if self.passed {
110            "PASSED".green().bold()
111        } else {
112            "FAILED".red().bold()
113        };
114        report.push_str(&format!("Status: {}\n\n", status));
115
116        if !self.errors.is_empty() {
117            report.push_str(&format!(
118                "{} ({}):\n",
119                "Errors".red().bold(),
120                self.errors.len()
121            ));
122            for (i, error) in self.errors.iter().enumerate() {
123                report.push_str(&format!(
124                    "  {}. [{:?}] {}\n",
125                    i + 1,
126                    error.category,
127                    error.message
128                ));
129            }
130            report.push('\n');
131        }
132
133        if !self.warnings.is_empty() {
134            report.push_str(&format!(
135                "{} ({}):\n",
136                "Warnings".yellow().bold(),
137                self.warnings.len()
138            ));
139            for (i, warning) in self.warnings.iter().enumerate() {
140                report.push_str(&format!(
141                    "  {}. [{:?}] {}\n",
142                    i + 1,
143                    warning.category,
144                    warning.message
145                ));
146            }
147            report.push('\n');
148        }
149
150        if !self.info.is_empty() {
151            report.push_str(&format!("{}:\n", "Info".cyan()));
152            for info in &self.info {
153                report.push_str(&format!("  - {}\n", info));
154            }
155        }
156
157        report
158    }
159}
160
161impl Default for ValidationResult {
162    fn default() -> Self {
163        Self::new()
164    }
165}
166
167/// Data validator
168pub struct DataValidator;
169
170impl DataValidator {
171    /// Validate raster dimensions
172    pub fn validate_raster_dimensions(
173        width: usize,
174        height: usize,
175        bands: usize,
176    ) -> ValidationResult {
177        let mut result = ValidationResult::new();
178
179        if width == 0 {
180            result.add_error(ErrorCategory::Format, "Width cannot be zero");
181        }
182        if height == 0 {
183            result.add_error(ErrorCategory::Format, "Height cannot be zero");
184        }
185        if bands == 0 {
186            result.add_error(ErrorCategory::Format, "Bands cannot be zero");
187        }
188
189        if width > 100000 || height > 100000 {
190            result.add_warning(
191                WarningCategory::Performance,
192                format!("Large raster dimensions: {}x{}", width, height),
193            );
194        }
195
196        result.add_info(format!("Dimensions: {}x{}x{}", width, height, bands));
197
198        result
199    }
200
201    /// Validate bounds
202    pub fn validate_bounds(min_x: f64, min_y: f64, max_x: f64, max_y: f64) -> ValidationResult {
203        let mut result = ValidationResult::new();
204
205        if min_x >= max_x {
206            result.add_error(
207                ErrorCategory::Bounds,
208                format!("Invalid X bounds: min ({}) >= max ({})", min_x, max_x),
209            );
210        }
211
212        if min_y >= max_y {
213            result.add_error(
214                ErrorCategory::Bounds,
215                format!("Invalid Y bounds: min ({}) >= max ({})", min_y, max_y),
216            );
217        }
218
219        if !min_x.is_finite() || !min_y.is_finite() || !max_x.is_finite() || !max_y.is_finite() {
220            result.add_error(ErrorCategory::Bounds, "Bounds contain non-finite values");
221        }
222
223        result.add_info(format!(
224            "Bounds: [{}, {}] x [{}, {}]",
225            min_x, min_y, max_x, max_y
226        ));
227
228        result
229    }
230
231    /// Validate data range
232    pub fn validate_data_range(
233        data: &[f64],
234        expected_min: f64,
235        expected_max: f64,
236    ) -> ValidationResult {
237        let mut result = ValidationResult::new();
238
239        if data.is_empty() {
240            result.add_error(ErrorCategory::Integrity, "Empty data array");
241            return result;
242        }
243
244        let (actual_min, actual_max) = data
245            .iter()
246            .fold((f64::INFINITY, f64::NEG_INFINITY), |(min, max), &v| {
247                (min.min(v), max.max(v))
248            });
249
250        if actual_min < expected_min || actual_max > expected_max {
251            result.add_warning(
252                WarningCategory::BestPractices,
253                format!(
254                    "Data range [{}, {}] outside expected range [{}, {}]",
255                    actual_min, actual_max, expected_min, expected_max
256                ),
257            );
258        }
259
260        let nan_count = data.iter().filter(|v| !v.is_finite()).count();
261        if nan_count > 0 {
262            result.add_warning(
263                WarningCategory::BestPractices,
264                format!("Found {} non-finite values", nan_count),
265            );
266        }
267
268        result.add_info(format!("Data range: [{}, {}]", actual_min, actual_max));
269        result.add_info(format!("Data count: {}", data.len()));
270
271        result
272    }
273
274    /// Validate file path
275    pub fn validate_file_path(path: &Path) -> ValidationResult {
276        let mut result = ValidationResult::new();
277
278        if !path.exists() {
279            result.add_error(ErrorCategory::Format, "File does not exist");
280            return result;
281        }
282
283        if !path.is_file() {
284            result.add_error(ErrorCategory::Format, "Path is not a file");
285            return result;
286        }
287
288        if let Ok(metadata) = path.metadata() {
289            let size = metadata.len();
290            result.add_info(format!("File size: {} bytes", size));
291
292            if size == 0 {
293                result.add_error(ErrorCategory::Format, "File is empty");
294            }
295
296            if size > 1_000_000_000 {
297                result.add_warning(
298                    WarningCategory::Performance,
299                    format!("Large file size: {:.2} GB", size as f64 / 1e9),
300                );
301            }
302        }
303
304        result
305    }
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311
312    #[test]
313    fn test_validation_result_creation() {
314        let result = ValidationResult::new();
315        assert!(result.passed);
316        assert!(result.errors.is_empty());
317        assert!(result.warnings.is_empty());
318    }
319
320    #[test]
321    fn test_validation_error() {
322        let mut result = ValidationResult::new();
323        result.add_error(ErrorCategory::Format, "test error");
324
325        assert!(!result.passed);
326        assert_eq!(result.errors.len(), 1);
327    }
328
329    #[test]
330    fn test_validate_raster_dimensions() {
331        let result = DataValidator::validate_raster_dimensions(100, 100, 3);
332        assert!(result.passed);
333
334        let result = DataValidator::validate_raster_dimensions(0, 100, 3);
335        assert!(!result.passed);
336    }
337
338    #[test]
339    fn test_validate_bounds() {
340        let result = DataValidator::validate_bounds(0.0, 0.0, 100.0, 100.0);
341        assert!(result.passed);
342
343        let result = DataValidator::validate_bounds(100.0, 0.0, 0.0, 100.0);
344        assert!(!result.passed);
345    }
346
347    #[test]
348    fn test_validate_data_range() {
349        let data = vec![0.0, 50.0, 100.0];
350        let result = DataValidator::validate_data_range(&data, 0.0, 100.0);
351        assert!(result.passed);
352
353        let data = vec![0.0, 150.0, 100.0];
354        let result = DataValidator::validate_data_range(&data, 0.0, 100.0);
355        assert!(result.passed);
356        assert!(!result.warnings.is_empty());
357    }
358
359    #[test]
360    fn test_validate_empty_data() {
361        let data: Vec<f64> = vec![];
362        let result = DataValidator::validate_data_range(&data, 0.0, 100.0);
363        assert!(!result.passed);
364    }
365}