use crate::error::{Error, Result};
use serde::Serialize;
use serde_json::{Map, Value};
pub(crate) fn serialize_data_map<T>(data: T) -> Result<Map<String, Value>>
where
T: Serialize,
{
match serde_json::to_value(data).map_err(Error::TemplateDataSerialization)? {
Value::Object(map) => Ok(map),
_ => Err(Error::TemplateDataMustBeObject),
}
}
pub(crate) fn normalize_email(value: String) -> Result<String> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(Error::InvalidEmailAddress {
reason: "email cannot be empty",
value,
});
}
if trimmed.contains(char::is_whitespace) {
return Err(Error::InvalidEmailAddress {
reason: "email cannot contain whitespace",
value,
});
}
let mut parts = trimmed.split('@');
let local = parts.next().unwrap_or_default();
let domain = parts.next().unwrap_or_default();
if local.is_empty() || domain.is_empty() || parts.next().is_some() {
return Err(Error::InvalidEmailAddress {
reason: "email must contain exactly one @ with non-empty local and domain parts",
value,
});
}
if !domain.contains('.') {
return Err(Error::InvalidEmailAddress {
reason: "email domain must contain a dot",
value,
});
}
Ok(trimmed.to_string())
}
pub(crate) fn normalize_non_empty(value: String, error: Error) -> Result<String> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(error);
}
Ok(trimmed.to_string())
}
pub(crate) fn normalize_template_id(value: String) -> Result<String> {
let trimmed = normalize_non_empty(value, Error::InvalidTemplateId)?;
if is_uuid_like(&trimmed) {
Ok(trimmed)
} else {
Err(Error::InvalidTemplateIdFormat)
}
}
fn is_uuid_like(value: &str) -> bool {
let bytes = value.as_bytes();
if bytes.len() != 36 {
return false;
}
for (idx, byte) in bytes.iter().enumerate() {
match idx {
8 | 13 | 18 | 23 => {
if *byte != b'-' {
return false;
}
}
_ => {
if !byte.is_ascii_hexdigit() {
return false;
}
}
}
}
true
}
pub(crate) fn normalize_header_key(value: String) -> Result<String> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(Error::InvalidHeaderName);
}
if trimmed.contains(':') {
return Err(Error::InvalidHeaderName);
}
Ok(trimmed.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalize_template_id_accepts_uuid() {
let id = normalize_template_id("550e8400-e29b-41d4-a716-446655440000".to_string()).unwrap();
assert_eq!(id, "550e8400-e29b-41d4-a716-446655440000");
}
#[test]
fn normalize_template_id_rejects_name() {
let error = normalize_template_id("free-trial".to_string()).unwrap_err();
assert!(matches!(error, Error::InvalidTemplateIdFormat));
}
}