Skip to main content

tideway_cli/templates/
mod.rs

1//! Template engine for generating frontend and backend components.
2//!
3//! Uses Handlebars templates embedded at compile time.
4
5use anyhow::{Result, anyhow};
6use handlebars::Handlebars;
7use include_dir::{Dir, include_dir};
8use serde::Serialize;
9
10use crate::cli::{BackendPreset, Style};
11
12// Embed all templates at compile time
13static TEMPLATES_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates");
14
15/// Context for template rendering
16#[derive(Serialize, Clone)]
17pub struct TemplateContext {
18    pub api_base_url: String,
19    pub style: Style,
20}
21
22/// Template engine using Handlebars
23pub struct TemplateEngine {
24    handlebars: Handlebars<'static>,
25    context: TemplateContext,
26}
27
28impl TemplateEngine {
29    /// Create a new template engine with the given context
30    pub fn new(context: TemplateContext) -> Result<Self> {
31        let mut handlebars = Handlebars::new();
32        handlebars.set_strict_mode(true);
33
34        // Register all templates from embedded directory
35        register_templates(&mut handlebars, &TEMPLATES_DIR, "")?;
36
37        Ok(Self {
38            handlebars,
39            context,
40        })
41    }
42
43    /// Render a template by name
44    pub fn render(&self, template_name: &str) -> Result<String> {
45        // Build the full template path based on style
46        let style_suffix = match self.context.style {
47            Style::Shadcn => "shadcn",
48            Style::Tailwind => "tailwind",
49            Style::Unstyled => "unstyled",
50        };
51
52        // Try style-specific template first, fall back to default
53        let styled_name = format!("vue/{}.{}", template_name, style_suffix);
54        let default_name = format!("vue/{}", template_name);
55
56        let template_key = if self.handlebars.has_template(&styled_name) {
57            styled_name
58        } else if self.handlebars.has_template(&default_name) {
59            default_name
60        } else {
61            return Err(anyhow!("Template not found: {}", template_name));
62        };
63
64        self.handlebars
65            .render(&template_key, &self.context)
66            .map_err(|e| anyhow!("Failed to render template {}: {}", template_name, e))
67    }
68}
69
70/// Recursively register templates from the embedded directory
71fn register_templates(
72    handlebars: &mut Handlebars<'static>,
73    dir: &'static Dir<'static>,
74    prefix: &str,
75) -> Result<()> {
76    for entry in dir.entries() {
77        match entry {
78            include_dir::DirEntry::Dir(subdir) => {
79                let new_prefix = if prefix.is_empty() {
80                    subdir.path().to_string_lossy().to_string()
81                } else {
82                    format!(
83                        "{}/{}",
84                        prefix,
85                        subdir.path().file_name().unwrap().to_string_lossy()
86                    )
87                };
88                register_templates(handlebars, subdir, &new_prefix)?;
89            }
90            include_dir::DirEntry::File(file) => {
91                let path = file.path();
92                if path.extension().map_or(false, |ext| ext == "hbs") {
93                    // Remove .hbs extension for template name
94                    let name = path.file_stem().unwrap().to_string_lossy();
95                    let template_key = if prefix.is_empty() {
96                        name.to_string()
97                    } else {
98                        format!("{}/{}", prefix, name)
99                    };
100
101                    let content = file
102                        .contents_utf8()
103                        .ok_or_else(|| anyhow!("Invalid UTF-8 in template: {}", path.display()))?;
104
105                    handlebars.register_template_string(&template_key, content)?;
106                }
107            }
108        }
109    }
110    Ok(())
111}
112
113// Make Style serializable for templates
114impl Serialize for Style {
115    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
116    where
117        S: serde::Serializer,
118    {
119        serializer.serialize_str(&self.to_string())
120    }
121}
122
123/// Context for backend template rendering
124#[derive(Serialize, Clone)]
125pub struct BackendTemplateContext {
126    /// Project name in snake_case (e.g., "my_app")
127    pub project_name: String,
128    /// Project name in PascalCase (e.g., "MyApp")
129    pub project_name_pascal: String,
130    /// Whether the preset includes organizations (B2B)
131    pub has_organizations: bool,
132    /// Database type ("postgres" or "sqlite")
133    pub database: String,
134    /// Tideway crate version for scaffolding
135    pub tideway_version: String,
136    /// Tideway feature list for starter templates
137    pub tideway_features: Vec<String>,
138    /// Whether any Tideway features were requested
139    pub has_tideway_features: bool,
140    /// Whether auth is requested (starter templates)
141    pub has_auth_feature: bool,
142    /// Whether database is requested (starter templates)
143    pub has_database_feature: bool,
144    /// Whether openapi is requested (starter templates)
145    pub has_openapi_feature: bool,
146    /// Whether starter templates need Arc
147    pub needs_arc: bool,
148    /// Whether starter templates should include config/error modules
149    pub has_config: bool,
150}
151
152/// Template engine for backend scaffolding
153pub struct BackendTemplateEngine {
154    handlebars: Handlebars<'static>,
155    context: BackendTemplateContext,
156}
157
158impl BackendTemplateEngine {
159    /// Create a new backend template engine with the given context
160    pub fn new(context: BackendTemplateContext) -> Result<Self> {
161        let mut handlebars = Handlebars::new();
162        handlebars.set_strict_mode(true);
163
164        // Register all templates from embedded directory
165        register_templates(&mut handlebars, &TEMPLATES_DIR, "")?;
166
167        Ok(Self {
168            handlebars,
169            context,
170        })
171    }
172
173    /// Render a backend template by name
174    pub fn render(&self, template_name: &str) -> Result<String> {
175        let template_key = format!("backend/{}", template_name);
176
177        if !self.handlebars.has_template(&template_key) {
178            return Err(anyhow!("Backend template not found: {}", template_name));
179        }
180
181        self.handlebars
182            .render(&template_key, &self.context)
183            .map_err(|e| anyhow!("Failed to render template {}: {}", template_name, e))
184    }
185
186    /// Check if a template exists
187    pub fn has_template(&self, template_name: &str) -> bool {
188        let template_key = format!("backend/{}", template_name);
189        self.handlebars.has_template(&template_key)
190    }
191}
192
193// Make BackendPreset serializable for templates
194impl Serialize for BackendPreset {
195    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
196    where
197        S: serde::Serializer,
198    {
199        serializer.serialize_str(&self.to_string())
200    }
201}