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