use std::collections::HashMap;
#[cfg(feature = "session")]
use crate::session::{FlashKind, FlashMessage};
#[derive(Debug, Clone, Default)]
pub struct TemplateContext {
#[cfg(feature = "session")]
pub flash_messages: Vec<FlashMessage>,
pub csrf_token: Option<String>,
pub current_path: String,
pub is_authenticated: bool,
pub user_id: Option<String>,
pub meta: HashMap<String, String>,
}
impl TemplateContext {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_path(mut self, path: impl Into<String>) -> Self {
self.current_path = path.into();
self
}
#[must_use]
pub fn with_auth(mut self, user_id: Option<String>) -> Self {
self.is_authenticated = user_id.is_some();
self.user_id = user_id;
self
}
#[must_use]
pub fn with_meta(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.meta.insert(key.into(), value.into());
self
}
#[must_use]
pub fn with_csrf(mut self, token: impl Into<String>) -> Self {
self.csrf_token = Some(token.into());
self
}
#[cfg(feature = "session")]
#[must_use]
pub fn with_flash(mut self, messages: Vec<FlashMessage>) -> Self {
self.flash_messages = messages;
self
}
#[cfg(feature = "session")]
#[must_use]
pub fn has_flash(&self) -> bool {
!self.flash_messages.is_empty()
}
#[cfg(feature = "session")]
#[must_use]
pub fn success_messages(&self) -> Vec<&FlashMessage> {
self.flash_messages
.iter()
.filter(|m| m.kind == FlashKind::Success)
.collect()
}
#[cfg(feature = "session")]
#[must_use]
pub fn error_messages(&self) -> Vec<&FlashMessage> {
self.flash_messages
.iter()
.filter(|m| m.kind == FlashKind::Error)
.collect()
}
#[cfg(feature = "session")]
#[must_use]
pub fn warning_messages(&self) -> Vec<&FlashMessage> {
self.flash_messages
.iter()
.filter(|m| m.kind == FlashKind::Warning)
.collect()
}
#[cfg(feature = "session")]
#[must_use]
pub fn info_messages(&self) -> Vec<&FlashMessage> {
self.flash_messages
.iter()
.filter(|m| m.kind == FlashKind::Info)
.collect()
}
#[must_use]
pub fn csrf_field(&self) -> String {
match &self.csrf_token {
Some(token) => format!(
r#"<input type="hidden" name="_csrf" value="{}">"#,
html_escape(token)
),
None => String::new(),
}
}
#[must_use]
pub fn csrf_meta(&self) -> String {
match &self.csrf_token {
Some(token) => format!(
r#"<meta name="csrf-token" content="{}">"#,
html_escape(token)
),
None => String::new(),
}
}
#[must_use]
pub fn is_active(&self, path: &str) -> bool {
self.current_path == path
}
#[must_use]
pub fn is_active_prefix(&self, prefix: &str) -> bool {
self.current_path.starts_with(prefix)
}
}
fn html_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_template_context_builder() {
let ctx = TemplateContext::new()
.with_path("/dashboard")
.with_auth(Some("user123".to_string()))
.with_meta("theme", "dark")
.with_csrf("token123");
assert_eq!(ctx.current_path, "/dashboard");
assert!(ctx.is_authenticated);
assert_eq!(ctx.user_id, Some("user123".to_string()));
assert_eq!(ctx.meta.get("theme"), Some(&"dark".to_string()));
assert_eq!(ctx.csrf_token, Some("token123".to_string()));
}
#[test]
fn test_csrf_field() {
let ctx = TemplateContext::new().with_csrf("test-token");
let field = ctx.csrf_field();
assert!(field.contains("test-token"));
assert!(field.contains("name=\"_csrf\""));
}
#[test]
fn test_csrf_meta() {
let ctx = TemplateContext::new().with_csrf("test-token");
let meta = ctx.csrf_meta();
assert!(meta.contains("test-token"));
assert!(meta.contains("csrf-token"));
}
#[test]
fn test_is_active() {
let ctx = TemplateContext::new().with_path("/users/123");
assert!(!ctx.is_active("/users"));
assert!(ctx.is_active("/users/123"));
assert!(ctx.is_active_prefix("/users"));
}
#[test]
fn test_html_escape() {
assert_eq!(html_escape("test"), "test");
assert_eq!(html_escape("<script>"), "<script>");
assert_eq!(html_escape("\"hello\""), ""hello"");
}
}