pub mod error;
use error::Error;
use url::Url;
use std::env;
use std::sync::OnceLock;
const CERTS_DIR_KEY: &'static str = "CERTS_DIR";
const FORGE_TYPE_KEY: &'static str = "PLATFORM_TYPE";
const FORGE_URL_KEY: &'static str = "FORGE_URL";
const CRATE_VERSION: &'static str = env!("CARGO_PKG_VERSION", "Package version unknown");
#[derive(Clone, Debug)]
pub enum ForgeApi {
Forgejo(Url),
}
#[derive(Clone, Debug)]
pub struct Config {
pub crate_version: &'static str,
pub forge_url: Url,
pub forge_api: ForgeApi,
pub certs_dir: String,
}
trait QueryEnvironment {
fn get(&self, key: &str) -> Result<String, std::env::VarError>;
}
struct RealEnv;
impl QueryEnvironment for RealEnv {
fn get(&self, key: &str) -> Result<String, std::env::VarError> {
return env::var(key);
}
}
fn config_with_env(environment: &dyn QueryEnvironment) -> Result<Config, Error> {
let default_certs_dir: &str = ".certs";
let certs_dir: String = environment.get(CERTS_DIR_KEY)
.unwrap_or(String::from(default_certs_dir));
let default_type: &str = "forgejo";
let forge_type: String = environment.get(FORGE_TYPE_KEY)
.unwrap_or(String::from(default_type))
.to_lowercase();
if forge_type != default_type {
return Err(Error::BadPlatformType(forge_type));
}
let forge_url_string: String = environment.get(FORGE_URL_KEY)
.unwrap_or(String::from(""));
if forge_url_string.len() == 0 {
return Err(Error::MissingUrl);
}
return match Url::parse(&forge_url_string) {
Err(parse_error) => Err(Error::BadUrl(parse_error)),
Ok(forge_url) => {
let forge_api_url: Url = forge_url.join("api/v1/").unwrap(); println!("Forgejo API URL is {forge_api_url}");
let forge_api: ForgeApi = ForgeApi::Forgejo(forge_api_url);
return Ok(Config {
crate_version: CRATE_VERSION,
forge_url,
forge_api,
certs_dir
})
},
};
}
pub fn config_or_error() -> &'static Result<Config, Error> {
static CONFIG: OnceLock<Result<Config, Error>> = OnceLock::new();
return CONFIG.get_or_init(|| {
println!("Initializing Config");
return config_with_env(&RealEnv);
})
}
pub fn config() -> &'static Config {
return config_or_error().as_ref().unwrap();
}
#[cfg(test)]
mod tests {
use std::collections::hash_map::HashMap;
use super::*;
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_no_forge_url() {
let result: Result<Config, Error> = config_with_env(&HashMap::new());
let Err(Error::MissingUrl) = result else {
panic!("Result should be an error");
};
}
#[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(platform_type)) = result else {
panic!("Result should be an error");
};
assert_eq!(platform_type, "foobar");
}
#[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.crate_version, CRATE_VERSION);
assert_eq!(config.certs_dir, ".certs");
assert_eq!(config.forge_url, Url::parse("http://localhost").unwrap());
if let ForgeApi::Forgejo(_) = config.forge_api {} else {
unreachable!("Expected Forgejo API type");
}
}
}