use serde::Deserialize;
use std::fs;
use std::path::PathBuf;
#[derive(Deserialize, Debug, Default, Clone)]
pub struct Config {
#[serde(default)]
pub location: Location,
#[serde(default)]
pub hide_hud: bool,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Location {
#[serde(default = "default_latitude")]
pub latitude: f64,
#[serde(default = "default_longitude")]
pub longitude: f64,
#[serde(default)]
pub auto: bool,
#[serde(default)]
pub hide: bool,
}
fn default_latitude() -> f64 {
52.52
}
fn default_longitude() -> f64 {
13.41
}
impl Default for Location {
fn default() -> Self {
Self {
latitude: default_latitude(),
longitude: default_longitude(),
auto: true,
hide: false,
}
}
}
impl Config {
pub fn load() -> Result<Self, String> {
if let Ok(cwd) = std::env::current_dir() {
let local_config = cwd.join("config.toml");
if local_config.exists() {
let config = Self::load_from_path(&local_config)?;
config.validate()?;
return Ok(config);
}
}
let config_path = Self::get_config_path()?;
if !config_path.exists() {
eprintln!("Config file not found at {:?}", config_path);
eprintln!("Auto-detecting location via IP...");
eprintln!("(Set auto = false in config to use Berlin as default)");
return Ok(Self::default());
}
let config = Self::load_from_path(&config_path)?;
config.validate()?;
Ok(config)
}
fn validate(&self) -> Result<(), String> {
if self.location.latitude < -90.0 || self.location.latitude > 90.0 {
return Err(format!(
"Invalid latitude: {}. Must be between -90.0 and 90.0",
self.location.latitude
));
}
if self.location.longitude < -180.0 || self.location.longitude > 180.0 {
return Err(format!(
"Invalid longitude: {}. Must be between -180.0 and 180.0",
self.location.longitude
));
}
Ok(())
}
pub fn load_from_path(path: &PathBuf) -> Result<Self, String> {
let content = fs::read_to_string(path)
.map_err(|e| format!("Failed to read config file at {:?}: {}", path, e))?;
toml::from_str(&content).map_err(|e| format!("Failed to parse config: {}", e))
}
fn get_config_path() -> Result<PathBuf, String> {
let config_dir = if let Ok(xdg_config) = std::env::var("XDG_CONFIG_HOME") {
PathBuf::from(xdg_config)
} else {
dirs::config_dir().ok_or("Could not determine config directory")?
};
Ok(config_dir.join("weathr").join("config.toml"))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn test_config_deserialize_valid() {
let toml_content = r#"
[location]
latitude = 52.52
longitude = 13.41
"#;
let config: Config = toml::from_str(toml_content).unwrap();
assert_eq!(config.location.latitude, 52.52);
assert_eq!(config.location.longitude, 13.41);
}
#[test]
fn test_config_deserialize_negative_coordinates() {
let toml_content = r#"
[location]
latitude = -33.8688
longitude = 151.2093
"#;
let config: Config = toml::from_str(toml_content).unwrap();
assert_eq!(config.location.latitude, -33.8688);
assert_eq!(config.location.longitude, 151.2093);
}
#[test]
fn test_config_load_from_path_success() {
let temp_dir = std::env::temp_dir();
let test_config_path = temp_dir.join("weathr_test_config.toml");
let mut file = fs::File::create(&test_config_path).unwrap();
writeln!(file, "[location]").unwrap();
writeln!(file, "latitude = 40.7128").unwrap();
writeln!(file, "longitude = -74.0060").unwrap();
let config = Config::load_from_path(&test_config_path).unwrap();
assert_eq!(config.location.latitude, 40.7128);
assert_eq!(config.location.longitude, -74.0060);
fs::remove_file(test_config_path).ok();
}
#[test]
fn test_config_load_from_path_file_not_found() {
let nonexistent_path = PathBuf::from("/tmp/nonexistent_weathr_config_12345.toml");
let result = Config::load_from_path(&nonexistent_path);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Failed to read config file"));
}
#[test]
fn test_config_load_from_path_invalid_toml() {
let temp_dir = std::env::temp_dir();
let test_config_path = temp_dir.join("weathr_test_invalid.toml");
let mut file = fs::File::create(&test_config_path).unwrap();
writeln!(file, "this is not valid toml {{{{").unwrap();
let result = Config::load_from_path(&test_config_path);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Failed to parse config"));
fs::remove_file(test_config_path).ok();
}
#[test]
fn test_config_missing_latitude() {
let toml_content = r#"
[location]
longitude = 13.41
"#;
let config: Config = toml::from_str(toml_content).unwrap();
assert_eq!(config.location.latitude, 52.52);
assert_eq!(config.location.longitude, 13.41);
}
#[test]
fn test_config_missing_longitude() {
let toml_content = r#"
[location]
latitude = 52.52
"#;
let config: Config = toml::from_str(toml_content).unwrap();
assert_eq!(config.location.latitude, 52.52);
assert_eq!(config.location.longitude, 13.41);
}
#[test]
fn test_location_boundary_values() {
let toml_content = r#"
[location]
latitude = 90.0
longitude = 180.0
"#;
let config: Config = toml::from_str(toml_content).unwrap();
assert_eq!(config.location.latitude, 90.0);
assert_eq!(config.location.longitude, 180.0);
}
#[test]
fn test_location_zero_coordinates() {
let toml_content = r#"
[location]
latitude = 0.0
longitude = 0.0
"#;
let config: Config = toml::from_str(toml_content).unwrap();
assert_eq!(config.location.latitude, 0.0);
assert_eq!(config.location.longitude, 0.0);
}
#[test]
fn test_validation_invalid_latitude_high() {
let config = Config {
location: Location {
latitude: 91.0,
longitude: 0.0,
auto: false,
hide: false,
},
hide_hud: false,
};
let result = config.validate();
assert!(result.is_err());
assert!(result.unwrap_err().contains("latitude"));
}
#[test]
fn test_validation_invalid_latitude_low() {
let config = Config {
location: Location {
latitude: -91.0,
longitude: 0.0,
auto: false,
hide: false,
},
hide_hud: false,
};
let result = config.validate();
assert!(result.is_err());
assert!(result.unwrap_err().contains("latitude"));
}
#[test]
fn test_validation_invalid_longitude_high() {
let config = Config {
location: Location {
latitude: 0.0,
longitude: 181.0,
auto: false,
hide: false,
},
hide_hud: false,
};
let result = config.validate();
assert!(result.is_err());
assert!(result.unwrap_err().contains("longitude"));
}
#[test]
fn test_validation_invalid_longitude_low() {
let config = Config {
location: Location {
latitude: 0.0,
longitude: -181.0,
auto: false,
hide: false,
},
hide_hud: false,
};
let result = config.validate();
assert!(result.is_err());
assert!(result.unwrap_err().contains("longitude"));
}
#[test]
fn test_validation_valid_config() {
let config = Config {
location: Location {
latitude: 52.52,
longitude: 13.41,
auto: false,
hide: false,
},
hide_hud: false,
};
let result = config.validate();
assert!(result.is_ok());
}
}