1use handlebars::{Handlebars, TemplateError};
2use std::collections::HashMap;
3use thiserror::Error;
4use walkdir::WalkDir;
5
6const PARTIAL_TEMPLATE_PREFIX: &str = "_";
7const HANDLEBARS_TEMPLATE_EXTENSION: &str = ".hbs";
8
9#[derive(Error, Debug)]
10pub enum RenderError {
11 #[error("Template error: `{0}`.")]
12 Template(#[from] TemplateError),
13 #[error("Walkdir error: `{0}`.")]
14 Walkdir(#[from] walkdir::Error),
15 #[error("2 main templates were found : `{0}` and `{1}`. There should be only one in all the template directories.")]
16 TwoMainTemp(String, String),
17 #[error("2 partial templates are named `{0}` they should have unique names.")]
18 UniqueNameTemp(String),
19 #[error("No main template has been detected, we don't know what to execute.")]
20 NoMainTemp,
21}
22
23#[derive(Debug, Clone, PartialEq)]
24pub struct TemplateCollection {
25 main: Template,
26 partials: HashMap<String, Template>,
27}
28
29impl TemplateCollection {
30 pub fn from_list(templates: impl IntoIterator<Item = Template>) -> Result<TemplateCollection, RenderError> {
31 let mut main: Option<Template> = None;
32 let mut partials = HashMap::<String, Template>::new();
33
34 for t in templates {
35 match t.template_type() {
36 TemplateType::Main => {
37 if let Some(existing) = main.as_ref() {
38 return Err(RenderError::TwoMainTemp(
39 existing.template_name().to_string(),
40 t.template_name().to_string(),
41 ));
42 };
43 main = Some(t);
44 }
45 TemplateType::Partial => {
46 if let Some(existing) = partials.get(t.template_name()) {
47 return Err(RenderError::UniqueNameTemp(existing.template_name().into()));
48 };
49 partials.insert(t.template_name().into(), t);
50 }
51 }
52 }
53
54 let main = main.ok_or(RenderError::NoMainTemp)?;
55
56 Ok(Self { main, partials })
57 }
58
59 pub fn setup_handlebars(&self, handlebars: &mut Handlebars) -> Result<(), RenderError> {
60 handlebars.register_template_file(self.main.template_name(), self.main.file_path())?;
61 for (_, value) in self.partials.iter() {
62 handlebars.register_template_file(value.template_name(), value.file_path())?
63 }
64 Ok(())
65 }
66
67 pub fn main_template_name(&self) -> &str {
68 self.main.template_name()
69 }
70}
71
72#[derive(Debug, Copy, Clone, PartialEq)]
73pub enum TemplateType {
74 Main,
75 Partial,
76}
77
78#[derive(Debug, Clone, PartialEq)]
79pub struct Template {
80 template_type: TemplateType,
81 file_name: String,
82 file_path: String,
83}
84
85impl Template {
86 pub fn new(template_type: TemplateType, file_name: impl Into<String>, file_path: impl Into<String>) -> Self {
87 Self {
88 template_type,
89 file_name: file_name.into(),
90 file_path: file_path.into(),
91 }
92 }
93
94 pub fn file_path(&self) -> &str {
95 &self.file_path
96 }
97
98 pub fn template_name(&self) -> &str {
99 self.file_name.trim_start_matches('_').trim_end_matches(".hbs")
100 }
101
102 pub fn template_type(&self) -> TemplateType {
103 self.template_type
104 }
105}
106
107pub fn get_templates_from_directory(dir_path: &str) -> Result<Vec<Template>, RenderError> {
108 let mut result = vec![];
109 for entry in WalkDir::new(dir_path) {
110 let entry = entry?;
111
112 let file_path = entry.path().to_str();
113 let file_path = match file_path {
114 Some(f) => f,
115 None => continue,
116 };
117
118 let file_name = entry.file_name().to_str();
119 if let Some(file_name) = file_name {
120 if !file_name.ends_with(HANDLEBARS_TEMPLATE_EXTENSION) {
121 continue;
122 }
123 let t = if file_name.starts_with(PARTIAL_TEMPLATE_PREFIX) {
124 TemplateType::Partial
125 } else {
126 TemplateType::Main
127 };
128 result.push(Template::new(t, file_name, file_path));
129 }
130 }
131 Ok(result)
132}
133
134#[cfg(test)]
135mod test {
136 use super::*;
137 use crate::helpers::handlebars_setup;
138
139 #[test]
140 fn handlebars_loading_test() -> Result<(), anyhow::Error> {
141 let list = get_templates_from_directory("_samples/render/test_denis")?;
142 let collection = TemplateCollection::from_list(list)?;
143 let mut h = Handlebars::new();
144 handlebars_setup(&mut h, Default::default());
145 let result = collection.setup_handlebars(&mut h)?;
146 dbg!(result);
147 Ok(())
148 }
149
150 #[test]
151 fn get_templates_from_directory_test() -> Result<(), anyhow::Error> {
152 let mut templates = get_templates_from_directory("./_samples/render/templates")?;
153 templates.sort_by_key(|t| t.file_path.clone());
154 let expected: Vec<Template>;
155 if cfg!(windows) {
156 expected = vec![
157 Template::new(
158 TemplateType::Partial,
159 "_other_partial.hbs",
160 "./_samples/render/templates\\_other_partial.hbs",
161 ),
162 Template::new(TemplateType::Partial, "_partial.hbs", "./_samples/render/templates\\_partial.hbs"),
163 Template::new(TemplateType::Partial, "_plop.hbs", "./_samples/render/templates\\sub\\_plop.hbs"),
164 Template::new(TemplateType::Main, "plop.hbs", "./_samples/render/templates\\sub\\plop.hbs"),
165 Template::new(TemplateType::Main, "test.hbs", "./_samples/render/templates\\test.hbs"),
166 ];
167 } else {
168 expected = vec![
169 Template::new(
170 TemplateType::Partial,
171 "_other_partial.hbs",
172 "./_samples/render/templates/_other_partial.hbs",
173 ),
174 Template::new(TemplateType::Partial, "_partial.hbs", "./_samples/render/templates/_partial.hbs"),
175 Template::new(TemplateType::Partial, "_plop.hbs", "./_samples/render/templates/sub/_plop.hbs"),
176 Template::new(TemplateType::Main, "plop.hbs", "./_samples/render/templates/sub/plop.hbs"),
177 Template::new(TemplateType::Main, "test.hbs", "./_samples/render/templates/test.hbs"),
178 ];
179 }
180
181 assert_eq!(templates, expected);
183
184 let first = templates.get(0).expect("?");
185 assert_eq!(first.template_name(), "other_partial");
186 Ok(())
187 }
188
189 #[test]
190 fn from_list_fails_with_double_main() {
191 let list = vec![
192 Template::new(TemplateType::Main, "plop.hbs", "./_samples/render/templates/sub/plop.hbs"),
193 Template::new(TemplateType::Main, "test.hbs", "./_samples/render/templates/test.hbs"),
194 ];
195
196 let test = TemplateCollection::from_list(list);
197 let err = test.expect_err("Should be an error");
198 assert_eq!(
199 err.to_string(),
200 "2 main templates were found : `plop` and `test`. There should be only one in all the template directories."
201 );
202 }
203
204 #[test]
205 fn from_list_fails_with_same_names_partials() {
206 let list = vec![
207 Template::new(
208 TemplateType::Partial,
209 "_other_partial.hbs",
210 "./_samples/render/templates/_other_partial.hbs",
211 ),
212 Template::new(TemplateType::Partial, "_partial.hbs", "./_samples/render/templates/_partial.hbs"),
213 Template::new(TemplateType::Partial, "_plop.hbs", "./_samples/render/templates/sub/_plop.hbs"),
214 Template::new(TemplateType::Partial, "_plop.hbs", "./_samples/render/templates/sub2/_plop.hbs"),
215 ];
216
217 let test = TemplateCollection::from_list(list);
218 let err = test.expect_err("Should be an error");
219 assert_eq!(
220 err.to_string(),
221 "2 partial templates are named `plop` they should have unique names."
222 );
223 }
224
225 #[test]
226 fn from_list_fails_with_no_main_found() {
227 let list = vec![
228 Template::new(
229 TemplateType::Partial,
230 "_other_partial.hbs",
231 "./_samples/render/templates/_other_partial.hbs",
232 ),
233 Template::new(TemplateType::Partial, "_partial.hbs", "./_samples/render/templates/_partial.hbs"),
234 Template::new(TemplateType::Partial, "_plop.hbs", "./_samples/render/templates/sub/_plop.hbs"),
235 ];
236
237 let test = TemplateCollection::from_list(list);
238 let err = test.expect_err("Should be an error");
239 assert!(matches!(err, RenderError::NoMainTemp));
240 }
241
242 #[test]
243 fn from_list_success() {
244 let list = vec![
245 Template::new(TemplateType::Main, "plop.hbs", "./_samples/render/templates/sub/plop.hbs"),
246 Template::new(TemplateType::Partial, "_partial.hbs", "./_samples/render/templates/_partial.hbs"),
247 ];
248 let test = TemplateCollection::from_list(list).expect("?");
249 let mut map = HashMap::new();
250 map.insert(
251 "partial".into(),
252 Template::new(TemplateType::Partial, "_partial.hbs", "./_samples/render/templates/_partial.hbs"),
253 );
254 let expected = TemplateCollection {
255 main: Template::new(TemplateType::Main, "plop.hbs", "./_samples/render/templates/sub/plop.hbs"),
256 partials: map,
257 };
258 assert_eq!(test, expected);
259 }
260}