use std::{ collections::HashSet, path::{Path, PathBuf}, sync::mpsc::Sender, }; use once_cell::sync::Lazy; use pasetors::{keys::SymmetricKey, version4::V4}; use crate::{error::AppError, services::UserConfirmationMessage}; static REQUIRED_ENV_VARS: Lazy> = Lazy::new(|| { HashSet::::from_iter( ["TOKEN_KEY", "DATABASE_URL", "ASSETS_DIR"] .into_iter() .map(ToString::to_string), ) }); #[derive(Clone)] pub struct Environment { token_key: SymmetricKey, database_url: String, email_sender: Sender, assets_dir: PathBuf, } impl Environment { pub fn init(email_sender: Sender) -> Result { let mut builder = EnvironmentObjectBuilder::new(email_sender); dotenvy::dotenv_iter() .expect("Missing .env file") .filter_map(|item| item.ok()) .filter(|(key, _)| REQUIRED_ENV_VARS.contains(key)) .for_each(|(key, value)| match key.as_str() { "TOKEN_KEY" => builder.with_token_key(value), "DATABASE_URL" => builder.with_database_url(value), "ASSETS_DIR" => builder.with_assets_dir(value), _ => {} }); if let Some(missing_vars) = builder.uninitialized_variables() { Err(AppError::missing_environment_variables(missing_vars)) } else { Ok(Environment::from(builder)) } } pub fn token_key(&self) -> &SymmetricKey { &self.token_key } pub fn db_connection_uri(&self) -> &str { self.database_url.as_str() } pub fn assets_dir(&self) -> &Path { self.assets_dir.as_path() } pub fn email_sender(&self) -> &Sender { &self.email_sender } } impl From for Environment { fn from(builder: EnvironmentObjectBuilder) -> Self { let EnvironmentObjectBuilder { token_key, database_url, email_sender, assets_dir, } = builder; Self { token_key: token_key.unwrap(), database_url: database_url.unwrap(), email_sender: email_sender.unwrap(), assets_dir: assets_dir.unwrap(), } } } #[derive(Default)] pub struct EnvironmentObjectBuilder { pub token_key: Option>, pub database_url: Option, pub email_sender: Option>, pub assets_dir: Option, } impl EnvironmentObjectBuilder { pub fn new(email_sender: Sender) -> Self { Self { email_sender: Some(email_sender), ..Default::default() } } pub fn uninitialized_variables(&self) -> Option> { let mut missing_vars = Vec::with_capacity(3); if self.token_key.is_none() { missing_vars.push("TOKEN_KEY"); } if self.database_url.is_none() { missing_vars.push("DATABASE_URL"); } if self.assets_dir.is_none() { missing_vars.push("ASSETS_DIR"); } if missing_vars.is_empty() { None } else { Some(missing_vars) } } pub fn with_token_key(&mut self, key: String) { match SymmetricKey::::try_from(key.as_str()).map_err(|_| AppError::token_key()) { Ok(key) => self.token_key = Some(key), Err(err) => panic!("{err}"), }; } pub fn with_database_url(&mut self, url: String) { self.database_url = Some(url); } pub fn with_assets_dir(&mut self, assets_dir_path: String) { let assets_dir = PathBuf::from(assets_dir_path); if !assets_dir.try_exists().unwrap_or_default() { panic!( "The 'ASSETS_DIR' environment variable points to a directory that doesn't exist." ); } if assets_dir.is_file() { panic!("The 'ASSETS_DIR' environment variable must be set to a directory, not a file."); } self.assets_dir = Some(assets_dir); } }