mod config;
pub mod error;
use crate::network::ForgeApiKind;
pub use config::*;
use error::Error;
use std::env;
use std::str::FromStr;
use std::sync::OnceLock;
use url::Url;
const CERTS_DIR_KEY: &str = "CERTS_DIR";
const FORGE_AUTH_TOKEN_KEY: &str = "FORGE_AUTH_TOKEN";
const FORGE_LIST_USERS_KEY: &str = "FORGE_LIST_USERS";
const FORGE_TYPE_KEY: &str = "FORGE_TYPE";
const FORGE_URL_KEY: &str = "FORGE_URL";
const PORT_KEY: &str = "PORT";
trait QueryEnvironment {
fn get(&self, key: &str) -> Result<String, std::env::VarError>;
}
struct RealEnv {
_private: (),
}
impl RealEnv {
fn new() -> Self {
#[cfg(debug_assertions)]
let _e = dotenvy::dotenv();
Self { _private: () }
}
}
impl QueryEnvironment for RealEnv {
fn get(&self, key: &str) -> Result<String, std::env::VarError> {
env::var(key)
}
}
fn config_with_env(environment: &dyn QueryEnvironment) -> Result<Config, Error> {
let default = Config::default();
let certs_dir: String = environment.get(CERTS_DIR_KEY).unwrap_or(default.certs_dir);
let forge_kind: ForgeApiKind = match environment.get(FORGE_TYPE_KEY) {
Err(_) => default.forge_kind,
Ok(forge_type) => ForgeApiKind::from_str(&forge_type.to_lowercase())?,
};
let forge_url: Url = match environment.get(FORGE_URL_KEY) {
Err(_) => default.forge_url,
Ok(forge_url_string) => Url::parse(&forge_url_string)?,
};
let forge_secret_key: Option<SecretHeader> = match environment.get(FORGE_AUTH_TOKEN_KEY) {
Err(_) => default.forge_secret_key,
Ok(val) => Some(SecretHeader::from(ZeroizedHeaderValue::try_from(val)?)),
};
let forge_list_users: bool = match environment.get(FORGE_LIST_USERS_KEY) {
Err(_) => default.forge_list_users,
Ok(val) => val.parse().unwrap_or(default.forge_list_users),
};
let port: u16 = match environment.get(PORT_KEY) {
Err(_) => default.port,
Ok(val) => val.parse().unwrap_or(default.port),
};
Ok(Config {
certs_dir,
forge_kind,
forge_list_users,
forge_secret_key,
forge_url,
port,
})
}
fn config_or_error() -> &'static Result<Config, Error> {
static CONFIG: OnceLock<Result<Config, Error>> = OnceLock::new();
CONFIG.get_or_init(|| {
println!("Initializing Config");
config_with_env(&RealEnv::new())
})
}
pub fn config() -> &'static Config {
config_or_error().as_ref().expect("Config is invalid")
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::hash_map::HashMap;
impl QueryEnvironment for HashMap<&str, String> {
fn get(&self, key: &str) -> Result<String, std::env::VarError> {
return match self.get(key) {
Some(val) => Ok(val.clone()),
None => Err(std::env::VarError::NotPresent),
};
}
}
#[test]
fn test_error_if_malformed_forge_url() {
let mut mock_env: HashMap<&str, String> = HashMap::new();
mock_env.insert(FORGE_URL_KEY, String::from("foobar"));
let result: Result<Config, Error> = config_with_env(&mock_env);
let Err(Error::BadUrl(_)) = result else {
panic!("Result should be an error");
};
}
#[test]
fn test_error_if_unknown_forge_type() {
let mut mock_env: HashMap<&str, String> = HashMap::new();
mock_env.insert(FORGE_URL_KEY, String::from("http://localhost"));
mock_env.insert(FORGE_TYPE_KEY, String::from("foobar"));
let result: Result<Config, Error> = config_with_env(&mock_env);
let Err(Error::BadPlatformType(_)) = result else {
panic!("Result should be an error");
};
}
#[test]
fn test_defaul_config_with_minimal_env() {
let mut mock_env: HashMap<&str, String> = HashMap::new();
mock_env.insert(FORGE_URL_KEY, String::from("http://localhost"));
let result: Result<Config, Error> = config_with_env(&mock_env);
let Ok(config) = result else {
panic!("Result should be a Config instance");
};
assert_eq!(config.certs_dir, ".certs");
if let ForgeApiKind::Forgejo = config.forge_kind {
} else {
unreachable!("Expected Forgejo API type");
}
}
}