Skip to main content

webserver_base/templates/
template_registry.rs

1use chrono::{DateTime, Utc};
2use handlebars::{Handlebars, handlebars_helper};
3use serde::Serialize;
4use serde_json::Value;
5use std::path::PathBuf;
6use std::{env, fs};
7use tracing::instrument;
8
9use super::error::TemplateRegistryError;
10
11// comma-delimits a list of strings
12handlebars_helper!(
13    join: |list: Vec<String>| list.join(",")
14);
15
16// prints a UTC date like "January 15, 1990"
17handlebars_helper!(
18    pretty_date: |date: DateTime<Utc>| date.format("%B %e, %Y").to_string()
19);
20
21// returns true if the object has the key
22handlebars_helper!(has_key: |obj: Value, key: str| {
23    obj.as_object()
24        .is_some_and(|o| o.contains_key(key))
25});
26
27#[derive(Clone)]
28pub struct TemplateRegistry<'a> {
29    handlebars: Handlebars<'a>,
30}
31
32impl Default for TemplateRegistry<'_> {
33    fn default() -> Self {
34        let mut html_path: PathBuf = env::current_dir().expect("failed to get current directory");
35        html_path.push("html");
36
37        let template_files: Vec<PathBuf> =
38            TemplateRegistry::find_all_template_files(&mut html_path)
39                .expect("failed to retrieve template files");
40
41        Self::new(template_files).expect("failed to create TemplateRegistry")
42    }
43}
44
45impl TemplateRegistry<'_> {
46    /// # Errors
47    ///
48    /// Will return `Error` if it encounters any `FileIO` or Handlebars Template errors.
49    ///
50    /// # Panics
51    ///
52    /// Panics if it encounters any `FileIO` errors.
53    #[instrument(skip_all)]
54    pub fn new(template_files: Vec<PathBuf>) -> Result<Self, TemplateRegistryError> {
55        // initialize Handlebars
56        let mut handlebars = Handlebars::new();
57
58        // register all helpers
59        handlebars.register_helper("join", Box::new(join));
60        handlebars.register_helper("pretty_date", Box::new(pretty_date));
61        handlebars.register_helper("has_key", Box::new(has_key));
62
63        // enforce strict templates
64        handlebars.set_strict_mode(true);
65
66        // find all HTML files at this hard-coded path
67        for template_file in template_files {
68            let file_name = template_file
69                .file_stem()
70                .expect("failed to extract stem (non-file-extension) part of template file name")
71                .to_string_lossy();
72            handlebars.register_template_file(file_name.as_ref(), &template_file)?;
73        }
74
75        Ok(Self { handlebars })
76    }
77
78    #[instrument(skip_all)]
79    fn find_all_template_files(
80        html_path: &mut PathBuf,
81    ) -> Result<Vec<PathBuf>, TemplateRegistryError> {
82        let mut template_files: Vec<PathBuf> = vec![];
83
84        for directory in &["layouts", "pages", "partials"] {
85            html_path.push(directory);
86            template_files.extend(TemplateRegistry::get_all_files(html_path)?);
87            html_path.pop();
88        }
89
90        Ok(template_files)
91    }
92
93    #[instrument(skip_all)]
94    fn get_all_files(current_dir: &PathBuf) -> Result<Vec<PathBuf>, TemplateRegistryError> {
95        let mut files: Vec<PathBuf> = vec![];
96
97        for entry in fs::read_dir(current_dir)? {
98            let entry = entry?;
99            let path = entry.path();
100            files.push(path);
101        }
102
103        Ok(files)
104    }
105
106    /// # Errors
107    ///
108    /// Will return `Error` if the template cannot be rendered.
109    #[instrument(skip_all)]
110    pub fn render<T>(&self, name: &str, data: &T) -> Result<String, TemplateRegistryError>
111    where
112        T: Serialize,
113    {
114        let rendered_template: String = self.handlebars.render(name, data)?;
115        Ok(rendered_template)
116    }
117}