use anyhow::{Context, Result}; use serde::Deserialize; use std::{env, fs::File, io::BufReader}; /// Email server credentials. #[derive(Deserialize)] pub struct EmailCredentials { pub domain: String, pub username: String, pub password: String, } #[derive(Deserialize)] pub struct Email { pub from: String, pub to: String, pub credentials: EmailCredentials, } /// UTC offset for time formatting. #[derive(Deserialize)] pub struct UtcOffset { pub hours: i8, pub minutes: i8, } #[derive(Deserialize)] #[serde(tag = "type")] pub enum CustomFieldType { Text, Textarea { rows: u8 }, } #[derive(Deserialize)] pub struct CustomField { pub key: String, pub label: String, #[serde(default)] pub required_feedback: Option, pub field_type: CustomFieldType, } /// Error messages for localization. #[derive(Deserialize)] pub struct ErrorMessages { pub captcha_error: String, pub email_error: String, } fn default_name_label() -> String { "Name".to_string() } fn default_name_required_feedback() -> String { "Please enter your name".to_string() } #[derive(Deserialize)] pub struct NameField { #[serde(default = "default_name_label")] pub label: String, #[serde(default = "default_name_required_feedback")] pub required_feedback: String, } fn default_email_label() -> String { "Email".to_string() } fn default_email_required_feedback() -> String { "Please enter your email".to_string() } #[derive(Deserialize)] pub struct EmailField { #[serde(default = "default_email_label")] pub label: String, #[serde(default = "default_email_required_feedback")] pub required_feedback: String, } fn default_captcha_label() -> String { "Enter the code above".to_string() } fn default_captcha_required_feedback() -> String { "Please enter code from the image above".to_string() } #[derive(Deserialize)] pub struct CaptchaField { #[serde(default = "default_captcha_label")] pub label: String, #[serde(default = "default_captcha_required_feedback")] pub required_feedback: String, } /// Localization strings. #[derive(Deserialize)] pub struct Strings { pub description: String, pub title: String, pub optional: String, pub submit: String, pub success: String, pub message_from: String, pub name_field: NameField, pub email_field: EmailField, pub captcha_field: CaptchaField, pub error_messages: ErrorMessages, } fn default_lang() -> String { "en".to_string() } #[derive(Deserialize)] pub struct StateConfig { /// The path prefix of all routes. pub path_prefix: String, pub custom_fields: Vec, /// The language tag of the HTML file. #[serde(default = "default_lang")] pub lang: String, pub strings: Strings, } /// Configuration. #[derive(Deserialize)] pub struct Config { /// The server socket address including port. pub socket_address: String, pub email: Email, pub log_file: String, pub utc_offset: UtcOffset, #[serde(flatten)] pub state_config: StateConfig, } impl Config { /// Parses the configuration from the config path in the environment variable. pub fn build() -> Result { // The environment variable with the path to the config file. let config_file_var = "CF_CONFIG_FILE"; let config_path = env::var(config_file_var) .with_context(|| format!("Environment variable {config_file_var} missing!"))?; let file = File::open(&config_path) .with_context(|| format!("Can not open the config file at the path {config_path}"))?; let reader = BufReader::new(file); let mut config: Self = serde_yaml::from_reader(reader).context("Can not parse the YAML config file!")?; // Add a space at the end for the email subject. config.state_config.strings.message_from = config.state_config.strings.message_from.trim().to_string() + " "; // Add the optional word to fields without a required_feedback. config .state_config .custom_fields .iter_mut() .filter(|field| field.required_feedback.is_none()) .for_each(|field| { field.label = format!( "{} ({})", field.label.trim(), config.state_config.strings.optional ); }); Ok(config) } }