use colored::Colorize;
use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationResult {
pub passed: bool,
pub errors: Vec<ValidationError>,
pub warnings: Vec<ValidationWarning>,
pub info: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationError {
pub category: ErrorCategory,
pub message: String,
pub location: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationWarning {
pub category: WarningCategory,
pub message: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ErrorCategory {
Format,
Integrity,
Metadata,
Projection,
Bounds,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum WarningCategory {
Performance,
Compatibility,
BestPractices,
}
impl ValidationResult {
pub fn new() -> Self {
Self {
passed: true,
errors: Vec::new(),
warnings: Vec::new(),
info: Vec::new(),
}
}
pub fn add_error(&mut self, category: ErrorCategory, message: impl Into<String>) {
self.passed = false;
self.errors.push(ValidationError {
category,
message: message.into(),
location: None,
});
}
pub fn add_warning(&mut self, category: WarningCategory, message: impl Into<String>) {
self.warnings.push(ValidationWarning {
category,
message: message.into(),
});
}
pub fn add_info(&mut self, message: impl Into<String>) {
self.info.push(message.into());
}
pub fn report(&self) -> String {
let mut report = String::new();
report.push_str(&format!("\n{}\n", "Validation Report".bold()));
report.push_str(&format!("{}\n\n", "=".repeat(60)));
let status = if self.passed {
"PASSED".green().bold()
} else {
"FAILED".red().bold()
};
report.push_str(&format!("Status: {}\n\n", status));
if !self.errors.is_empty() {
report.push_str(&format!(
"{} ({}):\n",
"Errors".red().bold(),
self.errors.len()
));
for (i, error) in self.errors.iter().enumerate() {
report.push_str(&format!(
" {}. [{:?}] {}\n",
i + 1,
error.category,
error.message
));
}
report.push('\n');
}
if !self.warnings.is_empty() {
report.push_str(&format!(
"{} ({}):\n",
"Warnings".yellow().bold(),
self.warnings.len()
));
for (i, warning) in self.warnings.iter().enumerate() {
report.push_str(&format!(
" {}. [{:?}] {}\n",
i + 1,
warning.category,
warning.message
));
}
report.push('\n');
}
if !self.info.is_empty() {
report.push_str(&format!("{}:\n", "Info".cyan()));
for info in &self.info {
report.push_str(&format!(" - {}\n", info));
}
}
report
}
}
impl Default for ValidationResult {
fn default() -> Self {
Self::new()
}
}
pub struct DataValidator;
impl DataValidator {
pub fn validate_raster_dimensions(
width: usize,
height: usize,
bands: usize,
) -> ValidationResult {
let mut result = ValidationResult::new();
if width == 0 {
result.add_error(ErrorCategory::Format, "Width cannot be zero");
}
if height == 0 {
result.add_error(ErrorCategory::Format, "Height cannot be zero");
}
if bands == 0 {
result.add_error(ErrorCategory::Format, "Bands cannot be zero");
}
if width > 100000 || height > 100000 {
result.add_warning(
WarningCategory::Performance,
format!("Large raster dimensions: {}x{}", width, height),
);
}
result.add_info(format!("Dimensions: {}x{}x{}", width, height, bands));
result
}
pub fn validate_bounds(min_x: f64, min_y: f64, max_x: f64, max_y: f64) -> ValidationResult {
let mut result = ValidationResult::new();
if min_x >= max_x {
result.add_error(
ErrorCategory::Bounds,
format!("Invalid X bounds: min ({}) >= max ({})", min_x, max_x),
);
}
if min_y >= max_y {
result.add_error(
ErrorCategory::Bounds,
format!("Invalid Y bounds: min ({}) >= max ({})", min_y, max_y),
);
}
if !min_x.is_finite() || !min_y.is_finite() || !max_x.is_finite() || !max_y.is_finite() {
result.add_error(ErrorCategory::Bounds, "Bounds contain non-finite values");
}
result.add_info(format!(
"Bounds: [{}, {}] x [{}, {}]",
min_x, min_y, max_x, max_y
));
result
}
pub fn validate_data_range(
data: &[f64],
expected_min: f64,
expected_max: f64,
) -> ValidationResult {
let mut result = ValidationResult::new();
if data.is_empty() {
result.add_error(ErrorCategory::Integrity, "Empty data array");
return result;
}
let (actual_min, actual_max) = data
.iter()
.fold((f64::INFINITY, f64::NEG_INFINITY), |(min, max), &v| {
(min.min(v), max.max(v))
});
if actual_min < expected_min || actual_max > expected_max {
result.add_warning(
WarningCategory::BestPractices,
format!(
"Data range [{}, {}] outside expected range [{}, {}]",
actual_min, actual_max, expected_min, expected_max
),
);
}
let nan_count = data.iter().filter(|v| !v.is_finite()).count();
if nan_count > 0 {
result.add_warning(
WarningCategory::BestPractices,
format!("Found {} non-finite values", nan_count),
);
}
result.add_info(format!("Data range: [{}, {}]", actual_min, actual_max));
result.add_info(format!("Data count: {}", data.len()));
result
}
pub fn validate_file_path(path: &Path) -> ValidationResult {
let mut result = ValidationResult::new();
if !path.exists() {
result.add_error(ErrorCategory::Format, "File does not exist");
return result;
}
if !path.is_file() {
result.add_error(ErrorCategory::Format, "Path is not a file");
return result;
}
if let Ok(metadata) = path.metadata() {
let size = metadata.len();
result.add_info(format!("File size: {} bytes", size));
if size == 0 {
result.add_error(ErrorCategory::Format, "File is empty");
}
if size > 1_000_000_000 {
result.add_warning(
WarningCategory::Performance,
format!("Large file size: {:.2} GB", size as f64 / 1e9),
);
}
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validation_result_creation() {
let result = ValidationResult::new();
assert!(result.passed);
assert!(result.errors.is_empty());
assert!(result.warnings.is_empty());
}
#[test]
fn test_validation_error() {
let mut result = ValidationResult::new();
result.add_error(ErrorCategory::Format, "test error");
assert!(!result.passed);
assert_eq!(result.errors.len(), 1);
}
#[test]
fn test_validate_raster_dimensions() {
let result = DataValidator::validate_raster_dimensions(100, 100, 3);
assert!(result.passed);
let result = DataValidator::validate_raster_dimensions(0, 100, 3);
assert!(!result.passed);
}
#[test]
fn test_validate_bounds() {
let result = DataValidator::validate_bounds(0.0, 0.0, 100.0, 100.0);
assert!(result.passed);
let result = DataValidator::validate_bounds(100.0, 0.0, 0.0, 100.0);
assert!(!result.passed);
}
#[test]
fn test_validate_data_range() {
let data = vec![0.0, 50.0, 100.0];
let result = DataValidator::validate_data_range(&data, 0.0, 100.0);
assert!(result.passed);
let data = vec![0.0, 150.0, 100.0];
let result = DataValidator::validate_data_range(&data, 0.0, 100.0);
assert!(result.passed);
assert!(!result.warnings.is_empty());
}
#[test]
fn test_validate_empty_data() {
let data: Vec<f64> = vec![];
let result = DataValidator::validate_data_range(&data, 0.0, 100.0);
assert!(!result.passed);
}
}