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 #[serde(rename = "REALM_ID")]
77 pub realm_id: u64,
78 #[serde(rename = "STUN_URLS")]
79 pub stun_urls: String,
80 #[serde(rename = "TURN_URLS")]
81 pub turn_urls: String,
82}
83
84impl TemplateContext {
85 pub fn new(project_name: &str, signaling_url: &str, service_name: &str) -> Self {
86 let project_name_pascal = to_pascal_case(project_name);
87 Self {
88 project_name: project_name.to_string(),
89 project_name_snake: to_snake_case(project_name),
90 project_name_pascal: project_name_pascal.clone(),
91 signaling_url: signaling_url.to_string(),
92 manufacturer: "unknown".to_string(),
93 service_name: service_name.to_string(),
94 workload_name: format!("{}Workload", project_name_pascal),
95 actr_swift_version: DEFAULT_ACTR_SWIFT_VERSION.to_string(),
96 actr_protocols_version: DEFAULT_ACTR_PROTOCOLS_VERSION.to_string(),
97 actr_local_path: std::env::var("ACTR_SWIFT_LOCAL_PATH").ok(),
98 realm_id: 2368266035,
99 stun_urls: r#"["stun:actrix1.develenv.com:3478"]"#.to_string(),
100 turn_urls: r#"["turn:actrix1.develenv.com:3478"]"#.to_string(),
101 }
102 }
103
104 pub async fn new_with_versions(
105 project_name: &str,
106 signaling_url: &str,
107 service_name: &str,
108 ) -> Self {
109 let mut ctx = Self::new(project_name, signaling_url, service_name);
110
111 let swift_task = crate::utils::fetch_latest_git_tag(
113 "https://github.com/actor-rtc/actr-swift",
114 &ctx.actr_swift_version,
115 );
116 let protocols_task = crate::utils::fetch_latest_git_tag(
117 "https://github.com/actor-rtc/actr-protocols-swift",
118 &ctx.actr_protocols_version,
119 );
120
121 let (swift_v, protocols_v) = tokio::join!(swift_task, protocols_task);
122
123 ctx.actr_swift_version = swift_v;
124 ctx.actr_protocols_version = protocols_v;
125
126 ctx
127 }
128}
129
130pub trait LangTemplate: Send + Sync {
131 fn load_files(&self, template_name: ProjectTemplateName) -> Result<HashMap<String, String>>;
132}
133
134pub struct ProjectTemplate {
135 name: ProjectTemplateName,
136 lang_template: Box<dyn LangTemplate>,
137}
138
139impl ProjectTemplate {
140 pub fn new(template_name: ProjectTemplateName, language: SupportedLanguage) -> Self {
141 let lang_template: Box<dyn LangTemplate> = match language {
142 SupportedLanguage::Swift => Box::new(SwiftTemplate),
143 SupportedLanguage::Kotlin => Box::new(KotlinTemplate),
144 SupportedLanguage::Python => Box::new(PythonTemplate),
145 SupportedLanguage::Rust => Box::new(RustTemplate),
146 };
147
148 Self {
149 name: template_name,
150 lang_template,
151 }
152 }
153
154 pub fn load_file(
155 fixture_path: &Path,
156 files: &mut HashMap<String, String>,
157 key: &str,
158 ) -> Result<()> {
159 let content = std::fs::read_to_string(fixture_path)?;
160 files.insert(key.to_string(), content);
161 Ok(())
162 }
163
164 pub fn generate(&self, project_path: &Path, context: &TemplateContext) -> Result<()> {
165 let files = self.lang_template.load_files(self.name)?;
166 let handlebars = Handlebars::new();
167
168 for (file_path, content) in &files {
169 let rendered_path = handlebars.render_template(file_path, context)?;
170 let rendered_content = handlebars.render_template(content, context)?;
171
172 let full_path = project_path.join(&rendered_path);
173
174 if let Some(parent) = full_path.parent() {
176 std::fs::create_dir_all(parent)?;
177 }
178
179 std::fs::write(full_path, rendered_content)?;
180 }
181
182 Ok(())
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189 use tempfile::TempDir;
190
191 #[test]
192 fn test_template_context() {
193 let ctx = TemplateContext::new("my-chat-service", "ws://localhost:8080", "echo-service");
194 assert_eq!(ctx.project_name, "my-chat-service");
195 assert_eq!(ctx.project_name_snake, "my_chat_service");
196 assert_eq!(ctx.project_name_pascal, "MyChatService");
197 assert_eq!(ctx.workload_name, "MyChatServiceWorkload");
198 assert_eq!(ctx.signaling_url, "ws://localhost:8080");
199 assert_eq!(ctx.actr_swift_version, DEFAULT_ACTR_SWIFT_VERSION);
200 assert_eq!(ctx.actr_protocols_version, DEFAULT_ACTR_PROTOCOLS_VERSION);
201 }
202
203 #[test]
204 fn test_project_template_new() {
205 let template = ProjectTemplate::new(ProjectTemplateName::Echo, SupportedLanguage::Swift);
206 assert_eq!(template.name, ProjectTemplateName::Echo);
207 }
208
209 #[test]
210 fn test_project_template_generation() {
211 let temp_dir = TempDir::new().unwrap();
212 let template = ProjectTemplate::new(ProjectTemplateName::Echo, SupportedLanguage::Swift);
213 let context = TemplateContext::new("test-app", "ws://localhost:8080", "echo-service");
214
215 template
216 .generate(temp_dir.path(), &context)
217 .expect("Failed to generate");
218
219 assert!(temp_dir.path().join("project.yml").exists());
221 assert!(temp_dir.path().join("Actr.toml").exists());
223 assert!(temp_dir.path().join(".gitignore").exists());
225 assert!(
228 temp_dir
229 .path()
230 .join("TestApp")
231 .join("TestApp.swift")
232 .exists()
233 );
234 }
235
236 #[test]
237 fn test_project_template_load_files() {
238 let template = ProjectTemplate::new(ProjectTemplateName::Echo, SupportedLanguage::Swift);
239 let result = template.lang_template.load_files(ProjectTemplateName::Echo);
240 assert!(result.is_ok());
241 }
242}