actr_cli/templates/
mod.rs1pub mod kotlin;
4pub mod python;
5pub mod rust;
6pub mod swift;
7
8pub use crate::commands::SupportedLanguage;
9use crate::error::Result;
10use crate::utils::{to_pascal_case, to_snake_case};
11use clap::ValueEnum;
12use handlebars::Handlebars;
13use serde::Serialize;
14use std::collections::HashMap;
15use std::path::Path;
16
17use self::kotlin::KotlinTemplate;
18use self::python::PythonTemplate;
19use self::rust::RustTemplate;
20use self::swift::SwiftTemplate;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, ValueEnum, Serialize)]
24#[value(rename_all = "lowercase")]
25pub enum ProjectTemplateName {
26 #[default]
27 Echo,
28}
29
30impl std::fmt::Display for ProjectTemplateName {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 let pv = self
33 .to_possible_value()
34 .expect("ValueEnum variant must have a possible value");
35 write!(f, "{}", pv.get_name())
36 }
37}
38
39#[derive(Debug, Clone, Serialize)]
40pub struct TemplateContext {
41 #[serde(rename = "PROJECT_NAME")]
42 pub project_name: String,
43 #[serde(rename = "PROJECT_NAME_SNAKE")]
44 pub project_name_snake: String,
45 #[serde(rename = "PROJECT_NAME_PASCAL")]
46 pub project_name_pascal: String,
47 #[serde(rename = "SIGNALING_URL")]
48 pub signaling_url: String,
49 #[serde(rename = "MANUFACTURER")]
50 pub manufacturer: String,
51 #[serde(rename = "SERVICE_NAME")]
52 pub service_name: String,
53}
54
55impl TemplateContext {
56 pub fn new(project_name: &str, signaling_url: &str) -> Self {
57 Self {
58 project_name: project_name.to_string(),
59 project_name_snake: to_snake_case(project_name),
60 project_name_pascal: to_pascal_case(project_name),
61 signaling_url: signaling_url.to_string(),
62 manufacturer: "unknown".to_string(),
63 service_name: "UnknownService".to_string(),
64 }
65 }
66}
67
68pub trait LangTemplate: Send + Sync {
69 fn load_files(&self, template_name: ProjectTemplateName) -> Result<HashMap<String, String>>;
70}
71
72pub struct ProjectTemplate {
73 name: ProjectTemplateName,
74 lang_template: Box<dyn LangTemplate>,
75}
76
77impl ProjectTemplate {
78 pub fn new(template_name: ProjectTemplateName, language: SupportedLanguage) -> Self {
79 let lang_template: Box<dyn LangTemplate> = match language {
80 SupportedLanguage::Swift => Box::new(SwiftTemplate),
81 SupportedLanguage::Kotlin => Box::new(KotlinTemplate),
82 SupportedLanguage::Python => Box::new(PythonTemplate),
83 SupportedLanguage::Rust => Box::new(RustTemplate),
84 };
85
86 Self {
87 name: template_name,
88 lang_template,
89 }
90 }
91
92 pub fn load_file(
93 fixture_path: &Path,
94 files: &mut HashMap<String, String>,
95 key: &str,
96 ) -> Result<()> {
97 let content = std::fs::read_to_string(fixture_path)?;
98 files.insert(key.to_string(), content);
99 Ok(())
100 }
101
102 pub fn generate(&self, project_path: &Path, context: &TemplateContext) -> Result<()> {
103 let files = self.lang_template.load_files(self.name)?;
104 let handlebars = Handlebars::new();
105
106 for (file_path, content) in &files {
107 let rendered_path = handlebars.render_template(file_path, context)?;
108 let rendered_content = handlebars.render_template(content, context)?;
109
110 let full_path = project_path.join(&rendered_path);
111
112 if let Some(parent) = full_path.parent() {
114 std::fs::create_dir_all(parent)?;
115 }
116
117 std::fs::write(full_path, rendered_content)?;
118 }
119
120 Ok(())
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use tempfile::TempDir;
128
129 #[test]
130 fn test_template_context() {
131 let ctx = TemplateContext::new("my-chat-service", "ws://localhost:8080");
132 assert_eq!(ctx.project_name, "my-chat-service");
133 assert_eq!(ctx.project_name_snake, "my_chat_service");
134 assert_eq!(ctx.project_name_pascal, "MyChatService");
135 assert_eq!(ctx.signaling_url, "ws://localhost:8080");
136 }
137
138 #[test]
139 fn test_project_template_new() {
140 let template = ProjectTemplate::new(ProjectTemplateName::Echo, SupportedLanguage::Swift);
141 assert_eq!(template.name, ProjectTemplateName::Echo);
142 }
143
144 #[test]
145 fn test_project_template_generation() {
146 let temp_dir = TempDir::new().unwrap();
147 let template = ProjectTemplate::new(ProjectTemplateName::Echo, SupportedLanguage::Swift);
148 let context = TemplateContext::new("test-app", "ws://localhost:8080");
149
150 template
151 .generate(temp_dir.path(), &context)
152 .expect("Failed to generate");
153
154 assert!(temp_dir.path().join("project.yml").exists());
156 assert!(temp_dir.path().join("Actr.toml").exists());
158 assert!(temp_dir.path().join(".gitignore").exists());
160 assert!(temp_dir.path().join("protos/echo.proto").exists());
162 assert!(
164 temp_dir
165 .path()
166 .join("TestApp")
167 .join("TestAppApp.swift")
168 .exists()
169 );
170 }
171
172 #[test]
173 fn test_project_template_load_files() {
174 let template = ProjectTemplate::new(ProjectTemplateName::Echo, SupportedLanguage::Swift);
175 let result = template.lang_template.load_files(ProjectTemplateName::Echo);
176 assert!(result.is_ok());
177 }
178}