contact-form/src/config.rs
2023-02-26 17:02:09 +01:00

221 lines
5.7 KiB
Rust

use anyhow::{Context, Result};
use serde::Deserialize;
use std::{fs::File, io::BufReader, path::Path};
/// 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, Default)]
#[serde(default)]
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<String>,
pub field_type: CustomFieldType,
}
/// Error messages for localization.
#[derive(Deserialize)]
#[serde(default)]
pub struct ErrorMessages {
pub captcha_error: String,
pub email_error: String,
}
impl Default for ErrorMessages {
fn default() -> Self {
Self {
captcha_error: "You did enter the wrong code at the end of the form. Please try again."
.to_string(),
email_error:
"An internal error occurred while sending your request. Please try again later."
.to_string(),
}
}
}
#[derive(Deserialize)]
#[serde(default)]
pub struct NameField {
pub label: String,
pub required_feedback: String,
}
impl Default for NameField {
fn default() -> Self {
Self {
label: "Name".to_string(),
required_feedback: "Please enter your name".to_string(),
}
}
}
#[derive(Deserialize)]
#[serde(default)]
pub struct EmailField {
pub label: String,
pub required_feedback: String,
}
impl Default for EmailField {
fn default() -> Self {
Self {
label: "Email".to_string(),
required_feedback: "Please enter your email".to_string(),
}
}
}
#[derive(Deserialize)]
#[serde(default)]
pub struct CaptchaField {
pub label: String,
pub required_feedback: String,
}
impl Default for CaptchaField {
fn default() -> Self {
Self {
label: "Enter the code above".to_string(),
required_feedback: "Please enter the code from the image above".to_string(),
}
}
}
/// Localization strings.
#[derive(Deserialize)]
#[serde(default)]
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,
}
impl Default for Strings {
fn default() -> Self {
Self {
description: String::default(),
title: "Contact form".to_string(),
optional: "optional".to_string(),
submit: "Submit".to_string(),
success: "Your request has been successfully submitted. We will get back to you soon."
.to_string(),
message_from: "Message from".to_string(),
name_field: NameField::default(),
email_field: EmailField::default(),
captcha_field: CaptchaField::default(),
error_messages: ErrorMessages::default(),
}
}
}
#[derive(Deserialize)]
pub struct StateConfig {
/// The path prefix of all routes.
#[serde(default)]
pub path_prefix: String,
pub custom_fields: Vec<CustomField>,
/// The language tag of the HTML file.
#[serde(default = "default_lang")]
pub lang: String,
#[serde(default)]
pub strings: Strings,
}
fn default_lang() -> String {
"en".to_string()
}
/// Configuration.
#[derive(Deserialize)]
pub struct Config {
/// The server socket address including port.
#[serde(default = "default_socket_address")]
pub socket_address: String,
pub email: Email,
#[serde(default)]
pub utc_offset: UtcOffset,
#[serde(flatten)]
pub state_config: StateConfig,
}
fn default_socket_address() -> String {
"0.0.0.0:80".to_string()
}
impl Config {
/// Parses the configuration in the given data directory.
pub fn build(data_dir: &Path) -> Result<Self> {
let reader = {
let mut buf = data_dir.to_path_buf();
buf.push("config.yaml");
let file = File::open(&buf).with_context(|| {
format!("Can not open the config file at the path {}", buf.display())
})?;
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
);
});
// Trim path prefix and set it to empty if it is /.
config.state_config.path_prefix = {
let path_prefix = config.state_config.path_prefix.trim();
if path_prefix == "/" {
String::default()
} else {
path_prefix.to_string()
}
};
Ok(config)
}
}