use std::num::{ParseFloatError, ParseIntError};
use std::str::ParseBoolError;
use thiserror::Error;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ConfigError {
#[error("Configuration key not found: {key}")]
ConfigKeyNotFound {
key: String,
},
#[error(
"Failed to convert configuration value for key '{key}' to type {target_type}: {source}"
)]
TypeConversionError {
key: String,
target_type: String,
source: Box<dyn std::error::Error + Send + Sync>,
},
#[error("Configuration source '{source_name}' error: {message}")]
SourceError {
source_name: String,
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("Failed to parse configuration: {message}")]
ParseError {
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("Configuration watcher error: {message}")]
WatcherError {
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),
}
impl ConfigError {
pub fn from_parse_int_error(key: String, err: ParseIntError) -> Self {
ConfigError::TypeConversionError {
key,
target_type: "integer".to_string(),
source: Box::new(err),
}
}
pub fn from_parse_float_error(key: String, err: ParseFloatError) -> Self {
ConfigError::TypeConversionError {
key,
target_type: "float".to_string(),
source: Box::new(err),
}
}
pub fn from_parse_bool_error(key: String, err: ParseBoolError) -> Self {
ConfigError::TypeConversionError {
key,
target_type: "boolean".to_string(),
source: Box::new(err),
}
}
}
pub type Result<T> = std::result::Result<T, ConfigError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_key_not_found_error() {
let error = ConfigError::ConfigKeyNotFound {
key: "test.key".to_string(),
};
assert_eq!(error.to_string(), "Configuration key not found: test.key");
}
#[test]
fn test_type_conversion_error() {
let source_error = "invalid value".parse::<i32>().unwrap_err();
let error = ConfigError::TypeConversionError {
key: "test.key".to_string(),
target_type: "i32".to_string(),
source: Box::new(source_error),
};
assert!(error.to_string().contains("test.key"));
assert!(error.to_string().contains("i32"));
}
#[test]
fn test_source_error() {
let error = ConfigError::SourceError {
source_name: "env".to_string(),
message: "Failed to read environment".to_string(),
source: None,
};
assert_eq!(
error.to_string(),
"Configuration source 'env' error: Failed to read environment"
);
}
#[test]
fn test_parse_error() {
let error = ConfigError::ParseError {
message: "Invalid YAML".to_string(),
source: None,
};
assert_eq!(
error.to_string(),
"Failed to parse configuration: Invalid YAML"
);
}
#[test]
fn test_watcher_error() {
let error = ConfigError::WatcherError {
message: "File watcher failed".to_string(),
source: None,
};
assert_eq!(
error.to_string(),
"Configuration watcher error: File watcher failed"
);
}
#[test]
fn test_io_error_conversion() {
let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let error = ConfigError::from(io_error);
assert!(matches!(error, ConfigError::IoError(_)));
}
#[test]
fn test_from_parse_int_error() {
let parse_err = "not_a_number".parse::<i32>().unwrap_err();
let error = ConfigError::from_parse_int_error("test.key".to_string(), parse_err);
assert!(matches!(error, ConfigError::TypeConversionError { .. }));
assert!(error.to_string().contains("integer"));
}
#[test]
fn test_from_parse_float_error() {
let parse_err = "not_a_float".parse::<f64>().unwrap_err();
let error = ConfigError::from_parse_float_error("test.key".to_string(), parse_err);
assert!(matches!(error, ConfigError::TypeConversionError { .. }));
assert!(error.to_string().contains("float"));
}
#[test]
fn test_from_parse_bool_error() {
let parse_err = "not_a_bool".parse::<bool>().unwrap_err();
let error = ConfigError::from_parse_bool_error("test.key".to_string(), parse_err);
assert!(matches!(error, ConfigError::TypeConversionError { .. }));
assert!(error.to_string().contains("boolean"));
}
}