use lazy_static::lazy_static;
use parking_lot::{Mutex, MutexGuard};
use serde::Deserialize;
use std::{
env::{remove_var, set_var, var_os},
ffi::{OsStr, OsString},
ops::Deref,
path::Path,
sync::Arc,
};
use crate::{latitude::Latitude, longitude::Longitude, ApiStringType, Error, StringType};
#[derive(Default, Debug, Deserialize, PartialEq, Eq)]
pub struct ConfigInner {
pub api_key: Option<ApiStringType>,
#[serde(default = "default_api_endpoint")]
pub api_endpoint: StringType,
#[serde(default = "default_api_path")]
pub api_path: StringType,
pub zipcode: Option<u64>,
pub country_code: Option<StringType>,
pub city_name: Option<StringType>,
pub lat: Option<Latitude>,
pub lon: Option<Longitude>,
}
fn default_api_endpoint() -> StringType {
"api.openweathermap.org".into()
}
fn default_api_path() -> StringType {
"data/2.5/".into()
}
#[derive(Default, Debug, Clone, PartialEq, Eq)]
pub struct Config(Arc<ConfigInner>);
impl Config {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn init_config(config_path: Option<&Path>) -> Result<Self, Error> {
let fname = config_path.unwrap_or_else(|| Path::new("config.env"));
let config_dir = dirs::config_dir().unwrap_or_else(|| "./".into());
let default_fname = config_dir.join("weather_util").join("config.env");
let env_file = if fname.exists() {
fname
} else {
&default_fname
};
dotenvy::dotenv().ok();
if env_file.exists() {
dotenvy::from_path(env_file).ok();
}
let conf: ConfigInner = envy::from_env()?;
Ok(Self(Arc::new(conf)))
}
}
impl Deref for Config {
type Target = ConfigInner;
fn deref(&self) -> &Self::Target {
&self.0
}
}
lazy_static! {
static ref TEST_MUTEX: Mutex<()> = Mutex::new(());
}
pub struct TestEnvs<'a> {
_guard: MutexGuard<'a, ()>,
envs: Vec<(OsString, Option<OsString>)>,
}
impl<'a> TestEnvs<'a> {
#[allow(dead_code)]
pub fn new(keys: &[impl AsRef<OsStr>]) -> Self {
let guard = TEST_MUTEX.lock();
let envs = keys
.iter()
.map(|k| (k.as_ref().to_os_string(), var_os(k)))
.collect();
Self {
_guard: guard,
envs,
}
}
}
impl<'a> Drop for TestEnvs<'a> {
fn drop(&mut self) {
for (key, val) in &self.envs {
if let Some(val) = val {
set_var(key, val);
} else {
remove_var(key);
}
}
}
}
#[cfg(test)]
mod tests {
use log::info;
use std::{
env::{remove_var, set_var},
fs::write,
};
use tempfile::NamedTempFile;
use crate::{
config::{Config, TestEnvs},
Error,
};
#[test]
fn test_config() -> Result<(), Error> {
assert_eq!(Config::new(), Config::default());
let _env = TestEnvs::new(&["API_KEY", "API_ENDPOINT", "ZIPCODE", "API_PATH"]);
set_var("API_KEY", "fb2380d74189c9983ea52f55914da824");
set_var("API_ENDPOINT", "test.local");
set_var("ZIPCODE", "8675309");
set_var("API_PATH", "weather/");
let conf = Config::init_config(None)?;
drop(_env);
info!("{}", conf.api_key.as_ref().unwrap());
assert_eq!(
conf.api_key.as_ref().unwrap().as_str(),
"fb2380d74189c9983ea52f55914da824"
);
#[cfg(feature = "stackstring")]
assert!(conf.api_key.as_ref().unwrap().is_inline());
assert_eq!(&conf.api_endpoint, "test.local");
assert_eq!(conf.zipcode, Some(8675309));
assert_eq!(&conf.api_path, "weather/");
Ok(())
}
#[test]
fn test_config_file() -> Result<(), Error> {
let _env = TestEnvs::new(&["API_KEY", "API_ENDPOINT", "ZIPCODE", "API_PATH"]);
remove_var("API_KEY");
remove_var("API_ENDPOINT");
remove_var("ZIPCODE");
remove_var("API_PATH");
let config_data = include_bytes!("../tests/config.env");
let config_file = NamedTempFile::new()?;
let config_path = config_file.path();
write(config_file.path(), config_data)?;
let conf = Config::init_config(Some(config_path))?;
assert_eq!(
conf.api_key.as_ref().unwrap().as_str(),
"fb2380d74189c9983ea52f55914da824"
);
#[cfg(feature = "stackstring")]
assert!(conf.api_key.as_ref().unwrap().is_inline());
assert_eq!(&conf.api_endpoint, "test.local");
assert_eq!(conf.zipcode, Some(8675309));
assert_eq!(&conf.api_path, "weather/");
Ok(())
}
}