1pub mod kotlin;
4pub mod python;
5pub mod rust;
6pub mod swift;
7
8use self::kotlin::KotlinTemplate;
9use self::python::PythonTemplate;
10use self::rust::RustTemplate;
11use self::swift::SwiftTemplate;
12use crate::error::Result;
13use crate::utils::{to_pascal_case, to_snake_case};
14use clap::ValueEnum;
15use handlebars::Handlebars;
16use serde::Serialize;
17use std::collections::HashMap;
18use std::path::Path;
19
20pub use crate::commands::SupportedLanguage;
21
22pub const DEFAULT_ACTR_SWIFT_VERSION: &str = "0.1.15";
23pub const DEFAULT_ACTR_PROTOCOLS_VERSION: &str = "0.1.2";
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, ValueEnum, Serialize)]
27#[value(rename_all = "lowercase")]
28pub enum ProjectTemplateName {
29 #[default]
30 Echo,
31 #[value(name = "data-stream")]
32 DataStream,
33}
34
35impl ProjectTemplateName {
36 pub fn to_service_name(self) -> &'static str {
38 match self {
39 ProjectTemplateName::Echo => "echo-service",
40 ProjectTemplateName::DataStream => "data-stream-service",
41 }
42 }
43}
44
45impl std::fmt::Display for ProjectTemplateName {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 let pv = self
48 .to_possible_value()
49 .expect("ValueEnum variant must have a possible value");
50 write!(f, "{}", pv.get_name())
51 }
52}
53
54#[derive(Debug, Clone, Serialize)]
55pub struct TemplateContext {
56 #[serde(rename = "PROJECT_NAME")]
57 pub project_name: String,
58 #[serde(rename = "PROJECT_NAME_SNAKE")]
59 pub project_name_snake: String,
60 #[serde(rename = "PROJECT_NAME_PASCAL")]
61 pub project_name_pascal: String,
62 #[serde(rename = "SIGNALING_URL")]
63 pub signaling_url: String,
64 #[serde(rename = "MANUFACTURER")]
65 pub manufacturer: String,
66 #[serde(rename = "SERVICE_NAME")]
67 pub service_name: String,
68 #[serde(rename = "WORKLOAD_NAME")]
69 pub workload_name: String,
70 #[serde(rename = "ACTR_SWIFT_VERSION")]
71 pub actr_swift_version: String,
72 #[serde(rename = "ACTR_PROTOCOLS_VERSION")]
73 pub actr_protocols_version: String,
74 #[serde(rename = "ACTR_LOCAL_PATH")]
75 pub actr_local_path: Option<String>,
76}
77
78impl TemplateContext {
79 pub fn new(project_name: &str, signaling_url: &str, service_name: &str) -> Self {
80 let project_name_pascal = to_pascal_case(project_name);
81 Self {
82 project_name: project_name.to_string(),
83 project_name_snake: to_snake_case(project_name),
84 project_name_pascal: project_name_pascal.clone(),
85 signaling_url: signaling_url.to_string(),
86 manufacturer: "unknown".to_string(),
87 service_name: service_name.to_string(),
88 workload_name: format!("{}Workload", project_name_pascal),
89 actr_swift_version: DEFAULT_ACTR_SWIFT_VERSION.to_string(),
90 actr_protocols_version: DEFAULT_ACTR_PROTOCOLS_VERSION.to_string(),
91 actr_local_path: std::env::var("ACTR_SWIFT_LOCAL_PATH").ok(),
92 }
93 }
94
95 pub async fn new_with_versions(
96 project_name: &str,
97 signaling_url: &str,
98 service_name: &str,
99 ) -> Self {
100 let mut ctx = Self::new(project_name, signaling_url, service_name);
101
102 let swift_task = crate::utils::fetch_latest_git_tag(
104 "https://github.com/actor-rtc/actr-swift",
105 &ctx.actr_swift_version,
106 );
107 let protocols_task = crate::utils::fetch_latest_git_tag(
108 "https://github.com/actor-rtc/actr-protocols-swift",
109 &ctx.actr_protocols_version,
110 );
111
112 let (swift_v, protocols_v) = tokio::join!(swift_task, protocols_task);
113
114 ctx.actr_swift_version = swift_v;
115 ctx.actr_protocols_version = protocols_v;
116
117 ctx
118 }
119}
120
121pub trait LangTemplate: Send + Sync {
122 fn load_files(&self, template_name: ProjectTemplateName) -> Result<HashMap<String, String>>;
123}
124
125pub struct ProjectTemplate {
126 name: ProjectTemplateName,
127 lang_template: Box<dyn LangTemplate>,
128}
129
130impl ProjectTemplate {
131 pub fn new(template_name: ProjectTemplateName, language: SupportedLanguage) -> Self {
132 let lang_template: Box<dyn LangTemplate> = match language {
133 SupportedLanguage::Swift => Box::new(SwiftTemplate),
134 SupportedLanguage::Kotlin => Box::new(KotlinTemplate),
135 SupportedLanguage::Python => Box::new(PythonTemplate),
136 SupportedLanguage::Rust => Box::new(RustTemplate),
137 };
138
139 Self {
140 name: template_name,
141 lang_template,
142 }
143 }
144
145 pub fn load_file(
146 fixture_path: &Path,
147 files: &mut HashMap<String, String>,
148 key: &str,
149 ) -> Result<()> {
150 let content = std::fs::read_to_string(fixture_path)?;
151 files.insert(key.to_string(), content);
152 Ok(())
153 }
154
155 pub fn generate(&self, project_path: &Path, context: &TemplateContext) -> Result<()> {
156 let files = self.lang_template.load_files(self.name)?;
157 let handlebars = Handlebars::new();
158
159 for (file_path, content) in &files {
160 let rendered_path = handlebars.render_template(file_path, context)?;
161 let rendered_content = handlebars.render_template(content, context)?;
162
163 let full_path = project_path.join(&rendered_path);
164
165 if let Some(parent) = full_path.parent() {
167 std::fs::create_dir_all(parent)?;
168 }
169
170 std::fs::write(full_path, rendered_content)?;
171 }
172
173 Ok(())
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use tempfile::TempDir;
181
182 #[test]
183 fn test_template_context() {
184 let ctx = TemplateContext::new("my-chat-service", "ws://localhost:8080", "echo-service");
185 assert_eq!(ctx.project_name, "my-chat-service");
186 assert_eq!(ctx.project_name_snake, "my_chat_service");
187 assert_eq!(ctx.project_name_pascal, "MyChatService");
188 assert_eq!(ctx.workload_name, "MyChatServiceWorkload");
189 assert_eq!(ctx.signaling_url, "ws://localhost:8080");
190 assert_eq!(ctx.actr_swift_version, DEFAULT_ACTR_SWIFT_VERSION);
191 assert_eq!(ctx.actr_protocols_version, DEFAULT_ACTR_PROTOCOLS_VERSION);
192 }
193
194 #[test]
195 fn test_project_template_new() {
196 let template = ProjectTemplate::new(ProjectTemplateName::Echo, SupportedLanguage::Swift);
197 assert_eq!(template.name, ProjectTemplateName::Echo);
198 }
199
200 #[test]
201 fn test_project_template_generation() {
202 let temp_dir = TempDir::new().unwrap();
203 let template = ProjectTemplate::new(ProjectTemplateName::Echo, SupportedLanguage::Swift);
204 let context = TemplateContext::new("test-app", "ws://localhost:8080", "echo-service");
205
206 template
207 .generate(temp_dir.path(), &context)
208 .expect("Failed to generate");
209
210 assert!(temp_dir.path().join("project.yml").exists());
212 assert!(temp_dir.path().join("Actr.toml").exists());
214 assert!(temp_dir.path().join(".gitignore").exists());
216 assert!(
219 temp_dir
220 .path()
221 .join("TestApp")
222 .join("TestApp.swift")
223 .exists()
224 );
225 }
226
227 #[test]
228 fn test_project_template_load_files() {
229 let template = ProjectTemplate::new(ProjectTemplateName::Echo, SupportedLanguage::Swift);
230 let result = template.lang_template.load_files(ProjectTemplateName::Echo);
231 assert!(result.is_ok());
232 }
233}