use regex::Regex;
use std::collections::BTreeMap;
use std::env::VarError;
use std::fmt::Display;
use std::str::FromStr;
pub trait GetEnv {
fn get(&self, key: &str) -> Result<String, VarError>;
fn all(&self) -> BTreeMap<String, String>;
}
#[derive(Default)]
pub struct StdEnvGetter {}
impl GetEnv for StdEnvGetter {
fn get(&self, key: &str) -> Result<String, VarError> {
std::env::var(key)
}
fn all(&self) -> BTreeMap<String, String> {
std::env::vars().into_iter().collect()
}
}
pub struct Env<EnvGetter: GetEnv = StdEnvGetter> {
getter: EnvGetter,
message: String,
}
pub trait EnvValue: FromStr + Display + Default + PartialEq {}
impl<T: FromStr + Display + Default + PartialEq> EnvValue for T {}
impl Default for Env {
fn default() -> Self {
Env::new(StdEnvGetter::default())
}
}
impl<EnvGetter: GetEnv> Env<EnvGetter> {
pub fn new(getter: EnvGetter) -> Env<EnvGetter> {
Env::<EnvGetter> {
getter,
message: String::new(),
}
}
pub fn get<T: EnvValue>(&mut self, key: &str, default_value: T) -> T {
self.get_impl(key, default_value, false)
}
pub fn get_masked<T: EnvValue>(&mut self, key: &str, default_value: T) -> T {
self.get_impl(key, default_value, true)
}
pub fn get_optional<T: EnvValue>(&mut self, key: &str) -> Option<T> {
let mut additional = String::new();
let value = match self.getter.get(key) {
Ok(value) => match T::from_str(&value) {
Ok(value) => Ok(value),
Err(_) => {
additional.push_str("(invalid)");
Err(value)
}
},
Err(_) => Err(String::new()),
};
let print_value = match &value {
Ok(value) => value.to_string(),
Err(e) => e.clone(),
};
if !print_value.is_empty() {
self.message
.push_str(&format!("\t{} = {} {}\n", key, print_value, additional));
}
value.ok()
}
fn get_impl<T: EnvValue>(&mut self, key: &str, default_value: T, masked: bool) -> T {
let mut additional = String::new();
let value: T = match self.getter.get(key) {
Ok(value) => match value.parse() {
Ok(value) => value,
Err(_) => {
additional.push_str("(invalid)");
default_value
}
},
Err(_) => {
additional.push_str("(default)");
default_value
}
};
if value != T::default() {
if masked {
self.message.push_str(&format!(
"\t{} = {} {}\n",
key,
"*".repeat(value.to_string().len()),
additional
));
} else {
self.message
.push_str(&format!("\t{} = {} {}\n", key, value, additional));
}
}
return value;
}
pub fn matches<T: FromStr + Display + Default + PartialEq>(
&mut self,
pattern: &str,
) -> BTreeMap<String, T> {
let mut matches = BTreeMap::new();
let pattern = Regex::new(pattern).unwrap();
for (key, _) in self.getter.all() {
if let Some(result) = pattern.captures(&key) {
matches.insert(
result.get(1).unwrap().as_str().to_string(),
self.get(&key, T::default()),
);
}
}
matches
}
pub fn message(&self) -> &String {
&self.message
}
}
#[cfg(test)]
mod tests {
use crate::core::env::Env;
use rstest::{fixture, rstest};
use std::thread::sleep;
#[rstest]
fn make_env(_env: Env) {
let env = Env::default();
assert_eq!(env.message(), "");
}
#[rstest]
fn default_values(mut env: Env) {
let value = env.get("TEST__", String::from("default"));
assert_eq!(value, "default");
assert_eq!(env.message(), "\tTEST__ = default (default)\n");
}
#[rstest]
fn masked_values(mut env: Env) {
std::env::set_var("TEST", "123");
let value = env.get_masked("TEST", String::from("default"));
assert_eq!(value, "123");
assert_eq!(env.message(), "\tTEST = *** \n");
}
#[rstest]
fn test_matches(mut env: Env) {
std::env::set_var("RS_BUCKET_TEST_QUOTA_TYPE", "test");
std::env::set_var("RS_BUCKET_TEST2_QUOTA_TYPE", "test2");
let matches = env.matches::<String>("RS_BUCKET_(.+)_QUOTA_TYPE");
assert_eq!(matches.len(), 2);
assert_eq!(matches.get("TEST").unwrap(), "test");
assert_eq!(matches.get("TEST2").unwrap(), "test2");
}
#[fixture]
fn env() -> Env {
std::env::remove_var("TEST");
sleep(std::time::Duration::from_millis(100));
Env::default()
}
}