diff --git a/src/email/templating.rs b/src/email/templating.rs index 1d0141b..9fe10a5 100644 --- a/src/email/templating.rs +++ b/src/email/templating.rs @@ -1,19 +1,25 @@ use crate::http::structs::AybConfigEmail; - -const CLI_CONFIRM_TMPL: &str = "To complete your registration, type\n\tayb client confirm {token}"; -const WEB_CONFIRM_TMPL: &str = "To complete your registration, visit\n\t {url}"; +use crate::templating::TemplateString; pub fn render_confirmation_template(config: &AybConfigEmail, token: &str) -> String { + let cli_confirm_tmpl: TemplateString = + "To complete your registration, type\n\tayb client confirm {token}" + .to_string() + .into(); + let web_confirm_tmpl: TemplateString = "To complete your registration, visit\n\t {url}" + .to_string() + .into(); + if let Some(tmpl_conf) = &config.templates { if let Some(confirm_conf) = &tmpl_conf.confirm { - return WEB_CONFIRM_TMPL.replace( - "{url}", + return web_confirm_tmpl.execute(vec![( + "url", &confirm_conf .confirmation_url - .replace("{token}", &urlencoding::encode(token)), - ); + .execute(vec![("token", &urlencoding::encode(token))]), + )]); } } - CLI_CONFIRM_TMPL.replace("{token}", token) + cli_confirm_tmpl.execute(vec![("token", token)]) } diff --git a/src/http/structs.rs b/src/http/structs.rs index f6a35b5..6c206d1 100644 --- a/src/http/structs.rs +++ b/src/http/structs.rs @@ -2,6 +2,7 @@ use crate::ayb_db::models::{ DBType, EntityType, InstantiatedDatabase as PersistedDatabase, InstantiatedDatabase, InstantiatedEntity as PersistedEntity, }; +use crate::templating::TemplateString; use prettytable::{format, Cell, Row, Table}; use serde::{Deserialize, Serialize}; @@ -18,7 +19,7 @@ pub struct AybConfigAuthentication { #[derive(Clone, Serialize, Deserialize)] pub struct AybConfigEmailTemplatesConfirm { - pub confirmation_url: String, + pub confirmation_url: TemplateString, } #[derive(Clone, Serialize, Deserialize)] diff --git a/src/lib.rs b/src/lib.rs index ecc8570..313b69c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,3 +7,4 @@ pub mod email; pub mod error; pub mod hosted_db; pub mod http; +pub mod templating; diff --git a/src/templating.rs b/src/templating.rs new file mode 100644 index 0000000..60598b2 --- /dev/null +++ b/src/templating.rs @@ -0,0 +1,98 @@ +use serde::de::{Error, Visitor}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::fmt::Formatter; + +#[derive(Clone)] +pub struct TemplateString { + string: String, +} + +impl TemplateString { + pub fn execute(&self, values: Vec<(&str, &str)>) -> String { + let mut string = self.string.clone(); + values + .iter() + .for_each(|(k, v)| string = string.replace(&format!("{{{}}}", k), v)); + string + } +} + +impl Serialize for TemplateString { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.string) + } +} + +struct TmplStrVisitor; + +impl<'de> Visitor<'de> for TmplStrVisitor { + type Value = TemplateString; + + fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + formatter.write_str("a valid template string") + } + + fn visit_string(self, v: String) -> Result + where + E: Error, + { + Ok(TemplateString { string: v }) + } + + fn visit_str(self, v: &str) -> Result + where + E: Error, + { + Ok(TemplateString { string: v.into() }) + } +} + +impl<'de> Deserialize<'de> for TemplateString { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(TmplStrVisitor) + } +} + +impl From for TemplateString { + fn from(value: String) -> Self { + Self { string: value } + } +} + +#[cfg(test)] +mod tests { + use crate::templating::TemplateString; + + #[test] + fn deserializes_properly() { + let str = TemplateString { + string: "Hi {name}!".into(), + }; + + assert_eq!( + serde_json::to_string(&str).unwrap(), + "\"Hi {name}!\"".to_string() + ); + } + + #[test] + fn serializes_properly() { + let str: TemplateString = serde_json::from_str("\"Hi {name}!\"").unwrap(); + + assert_eq!(str.string, "Hi {name}!".to_string()); + } + + #[test] + fn executes_properly() { + let str = TemplateString::from("Hello, {name1} and {name2}!".to_string()); + let result = str.execute(vec![("name1", "Alice"), ("name2", "Bob")]); + + assert_eq!(result, "Hello, Alice and Bob!".to_string()) + } +}