Skip to main content

actr_cli/templates/
mod.rs

1//! Project template system
2
3pub 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/// Project template options
26#[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    /// Maps template name to remote service name
37    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        // Fetch latest versions in parallel with 5s timeout
112        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            // Create parent directories if they don't exist
175            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        // Verify project.yml exists
220        assert!(temp_dir.path().join("project.yml").exists());
221        // Verify Actr.toml exists
222        assert!(temp_dir.path().join("Actr.toml").exists());
223        // Verify .gitignore exists
224        assert!(temp_dir.path().join(".gitignore").exists());
225        // Note: proto files are no longer created during init, they will be pulled via actr install
226        // Verify app directory exists
227        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}