ricecoder_generation/templates/
engine.rs1use crate::models::{RenderResult, TemplateContext};
10use crate::templates::error::TemplateError;
11use crate::templates::parser::{TemplateElement, TemplateParser};
12use crate::templates::resolver::{CaseTransform, PlaceholderResolver};
13use std::collections::HashMap;
14
15pub struct TemplateEngine {
17 resolver: PlaceholderResolver,
19}
20
21impl TemplateEngine {
22 pub fn new() -> Self {
24 Self {
25 resolver: PlaceholderResolver::new(),
26 }
27 }
28
29 pub fn with_resolver(resolver: PlaceholderResolver) -> Self {
31 Self { resolver }
32 }
33
34 pub fn add_value(&mut self, name: impl Into<String>, value: impl Into<String>) {
36 self.resolver.add_value(name, value);
37 }
38
39 pub fn add_values(&mut self, values: HashMap<String, String>) {
41 self.resolver.add_values(values);
42 }
43
44 pub fn require(&mut self, name: impl Into<String>) {
46 self.resolver.require(name);
47 }
48
49 pub fn render(
58 &self,
59 template_content: &str,
60 context: &TemplateContext,
61 ) -> Result<RenderResult, TemplateError> {
62 let parsed = TemplateParser::parse(template_content)?;
64
65 self.resolver.validate()?;
67
68 let rendered = self.render_elements(&parsed.elements, context)?;
70
71 Ok(RenderResult {
72 content: rendered,
73 warnings: Vec::new(),
74 placeholders_used: self.resolver.provided_names(),
75 })
76 }
77
78 fn render_elements(
80 &self,
81 elements: &[TemplateElement],
82 context: &TemplateContext,
83 ) -> Result<String, TemplateError> {
84 let mut result = String::new();
85
86 for element in elements {
87 match element {
88 TemplateElement::Text(text) => {
89 result.push_str(text);
90 }
91 TemplateElement::Placeholder(placeholder_name) => {
92 let rendered = self.render_placeholder(placeholder_name, context)?;
93 result.push_str(&rendered);
94 }
95 TemplateElement::Conditional { condition, content } => {
96 if self.evaluate_condition(condition, context)? {
97 let rendered = self.render_elements(content, context)?;
98 result.push_str(&rendered);
99 }
100 }
101 TemplateElement::Loop { variable, content } => {
102 let rendered = self.render_loop(variable, content, context)?;
103 result.push_str(&rendered);
104 }
105 TemplateElement::Include(partial_name) => {
106 return Err(TemplateError::RenderError(format!(
109 "Includes not yet supported: {}",
110 partial_name
111 )));
112 }
113 }
114 }
115
116 Ok(result)
117 }
118
119 fn render_placeholder(
121 &self,
122 placeholder_name: &str,
123 _context: &TemplateContext,
124 ) -> Result<String, TemplateError> {
125 let (name, case_transform) = self.parse_placeholder_syntax(placeholder_name)?;
127
128 self.resolver.resolve(&name, case_transform)
130 }
131
132 fn parse_placeholder_syntax(
134 &self,
135 content: &str,
136 ) -> Result<(String, CaseTransform), TemplateError> {
137 let content = content.trim();
138
139 if content.ends_with("_snake") {
141 let name = content.trim_end_matches("_snake").to_lowercase();
142 Ok((name, CaseTransform::SnakeCase))
143 } else if content.ends_with("-kebab") {
144 let name = content.trim_end_matches("-kebab").to_lowercase();
145 Ok((name, CaseTransform::KebabCase))
146 } else if content.ends_with("Camel") {
147 let name = content.trim_end_matches("Camel").to_lowercase();
148 Ok((name, CaseTransform::CamelCase))
149 } else if content.chars().all(|c| c.is_uppercase() || c == '_') && content.len() > 1 {
150 let name = content.to_lowercase();
152 Ok((name, CaseTransform::UpperCase))
153 } else if content.chars().next().is_some_and(|c| c.is_uppercase()) {
154 let name = content.to_lowercase();
156 Ok((name, CaseTransform::PascalCase))
157 } else {
158 Ok((content.to_string(), CaseTransform::LowerCase))
160 }
161 }
162
163 fn evaluate_condition(
165 &self,
166 condition: &str,
167 _context: &TemplateContext,
168 ) -> Result<bool, TemplateError> {
169 let condition = condition.trim();
172
173 if self.resolver.has_value(condition) {
175 Ok(true)
176 } else {
177 Ok(false)
178 }
179 }
180
181 fn render_loop(
183 &self,
184 variable: &str,
185 _content: &[TemplateElement],
186 _context: &TemplateContext,
187 ) -> Result<String, TemplateError> {
188 Err(TemplateError::RenderError(format!(
191 "Loops not yet fully supported: {}",
192 variable
193 )))
194 }
195
196 pub fn render_simple(&self, template_content: &str) -> Result<String, TemplateError> {
201 let mut result = template_content.to_string();
202
203 while let Some(start) = result.find("{{") {
205 if let Some(end_offset) = result[start..].find("}}") {
206 let end = start + end_offset + 1; let placeholder_content = &result[start + 2..start + end_offset];
208
209 let (name, case_transform) = self.parse_placeholder_syntax(placeholder_content)?;
211
212 let resolved = self.resolver.resolve(&name, case_transform)?;
214
215 result.replace_range(start..=end, &resolved);
217 } else {
218 break;
219 }
220 }
221
222 Ok(result)
223 }
224}
225
226impl Default for TemplateEngine {
227 fn default() -> Self {
228 Self::new()
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235 use crate::models::RenderOptions;
236
237 #[test]
238 fn test_template_engine_creation() {
239 let engine = TemplateEngine::new();
240 assert_eq!(engine.resolver.provided_names().len(), 0);
241 }
242
243 #[test]
244 fn test_add_value() {
245 let mut engine = TemplateEngine::new();
246 engine.add_value("name", "my_project");
247 assert_eq!(engine.resolver.provided_names().len(), 1);
248 }
249
250 #[test]
251 fn test_render_simple_placeholder() {
252 let mut engine = TemplateEngine::new();
253 engine.add_value("name", "my_project");
254 let result = engine.render_simple("Hello {{name}}");
255 assert_eq!(result.unwrap(), "Hello my_project");
256 }
257
258 #[test]
259 fn test_render_simple_pascal_case() {
260 let mut engine = TemplateEngine::new();
261 engine.add_value("name", "my_project");
262 let result = engine.render_simple("struct {{Name}} {}");
263 assert_eq!(result.unwrap(), "struct MyProject {}");
264 }
265
266 #[test]
267 fn test_render_simple_snake_case() {
268 let mut engine = TemplateEngine::new();
269 engine.add_value("name", "MyProject");
270 let result = engine.render_simple("let {{name_snake}} = 42;");
271 assert_eq!(result.unwrap(), "let my_project = 42;");
272 }
273
274 #[test]
275 fn test_render_simple_kebab_case() {
276 let mut engine = TemplateEngine::new();
277 engine.add_value("name", "MyProject");
278 let result = engine.render_simple("package-name: {{name-kebab}}");
279 assert_eq!(result.unwrap(), "package-name: my-project");
280 }
281
282 #[test]
283 fn test_render_simple_uppercase() {
284 let mut engine = TemplateEngine::new();
285 engine.add_value("name", "my_project");
286 let result = engine.render_simple("const {{NAME}} = 1;");
287 assert_eq!(result.unwrap(), "const MY_PROJECT = 1;");
288 }
289
290 #[test]
291 fn test_render_simple_camel_case() {
292 let mut engine = TemplateEngine::new();
293 engine.add_value("name", "my_project");
294 let result = engine.render_simple("function {{nameCamel}}() {}");
295 assert_eq!(result.unwrap(), "function myProject() {}");
296 }
297
298 #[test]
299 fn test_render_simple_multiple_placeholders() {
300 let mut engine = TemplateEngine::new();
301 engine.add_value("name", "my_project");
302 engine.add_value("author", "john_doe");
303 let result = engine.render_simple("Project: {{Name}}, Author: {{author}}");
304 assert_eq!(result.unwrap(), "Project: MyProject, Author: john_doe");
305 }
306
307 #[test]
308 fn test_render_simple_missing_placeholder() {
309 let engine = TemplateEngine::new();
310 let result = engine.render_simple("Hello {{name}}");
311 assert!(result.is_err());
312 }
313
314 #[test]
315 fn test_parse_placeholder_syntax_pascal_case() {
316 let engine = TemplateEngine::new();
317 let (name, transform) = engine.parse_placeholder_syntax("Name").unwrap();
318 assert_eq!(name, "name");
319 assert_eq!(transform, CaseTransform::PascalCase);
320 }
321
322 #[test]
323 fn test_parse_placeholder_syntax_snake_case() {
324 let engine = TemplateEngine::new();
325 let (name, transform) = engine.parse_placeholder_syntax("name_snake").unwrap();
326 assert_eq!(name, "name");
327 assert_eq!(transform, CaseTransform::SnakeCase);
328 }
329
330 #[test]
331 fn test_parse_placeholder_syntax_kebab_case() {
332 let engine = TemplateEngine::new();
333 let (name, transform) = engine.parse_placeholder_syntax("name-kebab").unwrap();
334 assert_eq!(name, "name");
335 assert_eq!(transform, CaseTransform::KebabCase);
336 }
337
338 #[test]
339 fn test_parse_placeholder_syntax_uppercase() {
340 let engine = TemplateEngine::new();
341 let (name, transform) = engine.parse_placeholder_syntax("NAME").unwrap();
342 assert_eq!(name, "name");
343 assert_eq!(transform, CaseTransform::UpperCase);
344 }
345
346 #[test]
347 fn test_parse_placeholder_syntax_camel_case() {
348 let engine = TemplateEngine::new();
349 let (name, transform) = engine.parse_placeholder_syntax("nameCamel").unwrap();
350 assert_eq!(name, "name");
351 assert_eq!(transform, CaseTransform::CamelCase);
352 }
353
354 #[test]
355 fn test_parse_placeholder_syntax_lowercase() {
356 let engine = TemplateEngine::new();
357 let (name, transform) = engine.parse_placeholder_syntax("name").unwrap();
358 assert_eq!(name, "name");
359 assert_eq!(transform, CaseTransform::LowerCase);
360 }
361
362 #[test]
363 fn test_render_with_context() {
364 let mut engine = TemplateEngine::new();
365 engine.add_value("name", "my_project");
366
367 let context = TemplateContext {
368 values: Default::default(),
369 options: RenderOptions::default(),
370 };
371
372 let result = engine.render("Hello {{name}}", &context);
373 assert!(result.is_ok());
374 assert_eq!(result.unwrap().content, "Hello my_project");
375 }
376
377 #[test]
378 fn test_render_result_includes_placeholders_used() {
379 let mut engine = TemplateEngine::new();
380 engine.add_value("name", "my_project");
381
382 let context = TemplateContext {
383 values: Default::default(),
384 options: RenderOptions::default(),
385 };
386
387 let result = engine.render("Hello {{name}}", &context).unwrap();
388 assert!(result.placeholders_used.contains(&"name".to_string()));
389 }
390}