murgamu 0.7.3

Murgamü is an NestJS-inspired web framework for Rust
Documentation
use super::parse_duration;
use super::parse_size;
use super::MurConfigBuilder;
use super::MurConfigError;
use super::MurConfigResult;
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;

#[derive(Debug, Clone)]
pub struct MurConfig {
	pub values: HashMap<String, String>,
	metadata: ConfigMetadata,
}

#[derive(Debug, Clone, Default)]
pub struct ConfigMetadata {
	pub loaded_files: Vec<String>,
	pub secret_keys: Vec<String>,
	pub environment: Option<String>,
}

impl MurConfig {
	pub fn new() -> Self {
		Self {
			values: HashMap::new(),
			metadata: ConfigMetadata::default(),
		}
	}

	pub fn from_map(values: HashMap<String, String>) -> Self {
		Self {
			values,
			metadata: ConfigMetadata::default(),
		}
	}

	pub fn from_env() -> Self {
		let mut config = Self::new();
		config.load_env();
		config.try_load_dotenv();
		config
	}

	pub fn builder() -> MurConfigBuilder {
		MurConfigBuilder::new()
	}

	pub fn load_env(&mut self) {
		for (key, value) in std::env::vars() {
			self.values.insert(key, value);
		}
	}

	fn try_load_dotenv(&mut self) {
		if Path::new(".env").exists() {
			let _ = self.load_file(".env");
		}

		let env = self.detect_environment();
		self.metadata.environment = Some(env.clone());

		let env_file = format!(".env.{}", env);
		if Path::new(&env_file).exists() {
			let _ = self.load_file(&env_file);
		}

		if Path::new(".env.local").exists() {
			let _ = self.load_file(".env.local");
		}

		for (key, value) in std::env::vars() {
			self.values.insert(key, value);
		}
	}

	fn detect_environment(&self) -> String {
		self.values
			.get("MUR_ENV")
			.or_else(|| self.values.get("APP_ENV"))
			.or_else(|| self.values.get("RUST_ENV"))
			.or_else(|| self.values.get("NODE_ENV"))
			.cloned()
			.unwrap_or_else(|| "development".to_string())
			.to_lowercase()
	}

	pub fn load_file(&mut self, path: &str) -> MurConfigResult<()> {
		let content = std::fs::read_to_string(path).map_err(|e| MurConfigError::FileError {
			path: path.to_string(),
			message: e.to_string(),
		})?;

		self.parse_dotenv(&content);
		self.metadata.loaded_files.push(path.to_string());
		Ok(())
	}

	pub fn parse_dotenv(&mut self, content: &str) {
		for line in content.lines() {
			let line = line.trim();

			if line.is_empty() || line.starts_with('#') {
				continue;
			}

			if let Some((key, value)) = line.split_once('=') {
				let key = key.trim().to_string();
				let value = Self::parse_value(value.trim());
				self.values.insert(key, value);
			}
		}
	}

	fn parse_value(value: &str) -> String {
		let value = value.trim();

		if (value.starts_with('"') && value.ends_with('"'))
			|| (value.starts_with('\'') && value.ends_with('\''))
		{
			let inner = &value[1..value.len() - 1];

			if value.starts_with('"') {
				return inner
					.replace("\\n", "\n")
					.replace("\\t", "\t")
					.replace("\\r", "\r")
					.replace("\\\"", "\"")
					.replace("\\\\", "\\");
			}
			return inner.to_string();
		}

		if let Some(idx) = value.find(" #") {
			return value[..idx].trim().to_string();
		}

		value.to_string()
	}

	pub fn get(&self, key: &str) -> Option<&String> {
		self.values.get(key)
	}

	pub fn get_or_string(&self, key: &str, default: &str) -> String {
		self.values
			.get(key)
			.cloned()
			.unwrap_or_else(|| default.to_string())
	}

	pub fn get_or<T: std::str::FromStr>(&self, key: &str, default: T) -> T {
		self.values
			.get(key)
			.and_then(|v| v.parse().ok())
			.unwrap_or(default)
	}

	pub fn get_required(&self, key: &str) -> MurConfigResult<String> {
		self.values
			.get(key)
			.cloned()
			.ok_or_else(|| MurConfigError::MissingKey(key.to_string()))
	}

	pub fn get_required_as<T: std::str::FromStr>(&self, key: &str) -> MurConfigResult<T> {
		let value = self.get_required(key)?;
		value.parse().map_err(|_| MurConfigError::ParseError {
			key: key.to_string(),
			message: format!(
				"Failed to parse '{}' as {}",
				value,
				std::any::type_name::<T>()
			),
		})
	}

	pub fn get_bool(&self, key: &str) -> Option<bool> {
		self.values.get(key).map(|v| {
			let v = v.to_lowercase();
			v == "1" || v == "true" || v == "yes" || v == "on"
		})
	}

	pub fn get_bool_or(&self, key: &str, default: bool) -> bool {
		self.get_bool(key).unwrap_or(default)
	}

	pub fn get_list(&self, key: &str) -> Option<Vec<String>> {
		self.values.get(key).map(|v| {
			v.split(',')
				.map(|s| s.trim().to_string())
				.filter(|s| !s.is_empty())
				.collect()
		})
	}

	pub fn get_list_or(&self, key: &str, default: Vec<String>) -> Vec<String> {
		self.get_list(key).unwrap_or(default)
	}

	pub fn get_duration(&self, key: &str) -> Option<std::time::Duration> {
		self.values.get(key).and_then(|v| parse_duration(v))
	}

	pub fn get_duration_or(&self, key: &str, default: std::time::Duration) -> std::time::Duration {
		self.get_duration(key).unwrap_or(default)
	}

	pub fn get_size_bytes(&self, key: &str) -> Option<u64> {
		self.values.get(key).and_then(|v| parse_size(v))
	}

	pub fn get_size_bytes_or(&self, key: &str, default: u64) -> u64 {
		self.get_size_bytes(key).unwrap_or(default)
	}

	pub fn set(&mut self, key: impl Into<String>, value: impl Into<String>) {
		self.values.insert(key.into(), value.into());
	}

	pub fn set_default(&mut self, key: impl Into<String>, value: impl Into<String>) {
		self.values.entry(key.into()).or_insert(value.into());
	}

	pub fn remove(&mut self, key: &str) -> Option<String> {
		self.values.remove(key)
	}

	pub fn has(&self, key: &str) -> bool {
		self.values.contains_key(key)
	}

	pub fn validate_required(&self, keys: &[&str]) -> MurConfigResult<()> {
		let missing: Vec<_> = keys.iter().filter(|k| !self.has(k)).collect();

		if missing.is_empty() {
			Ok(())
		} else if missing.len() == 1 {
			Err(MurConfigError::MissingKey(missing[0].to_string()))
		} else {
			Err(MurConfigError::Multiple(
				missing
					.into_iter()
					.map(|k| MurConfigError::MissingKey(k.to_string()))
					.collect(),
			))
		}
	}

	pub fn mark_secret(&mut self, key: &str) {
		if !self.metadata.secret_keys.contains(&key.to_string()) {
			self.metadata.secret_keys.push(key.to_string());
		}
	}

	pub fn is_secret(&self, key: &str) -> bool {
		self.metadata.secret_keys.contains(&key.to_string())
	}

	pub fn loaded_files(&self) -> &[String] {
		&self.metadata.loaded_files
	}

	pub fn environment(&self) -> Option<&String> {
		self.metadata.environment.as_ref()
	}

	pub fn keys(&self) -> impl Iterator<Item = &String> {
		self.values.keys()
	}

	pub fn len(&self) -> usize {
		self.values.len()
	}

	pub fn is_empty(&self) -> bool {
		self.values.is_empty()
	}

	pub fn into_arc(self) -> Arc<Self> {
		Arc::new(self)
	}

	pub fn subset(&self, prefix: &str) -> MurConfig {
		let values: HashMap<String, String> = self
			.values
			.iter()
			.filter_map(|(k, v)| {
				k.strip_prefix(prefix)
					.map(|stripped| (stripped.to_string(), v.clone()))
			})
			.collect();

		MurConfig {
			values,
			metadata: ConfigMetadata::default(),
		}
	}
}

impl Default for MurConfig {
	fn default() -> Self {
		Self::new()
	}
}