actr_cli/templates/
mod.rs

1//! Project template system
2
3use crate::error::{ActrCliError, Result};
4use handlebars::Handlebars;
5use serde::Serialize;
6use std::collections::HashMap;
7use std::path::Path;
8
9#[derive(Debug, Clone, Serialize)]
10pub struct TemplateContext {
11    pub project_name: String,
12    pub project_name_snake: String,
13    pub project_name_pascal: String,
14}
15
16impl TemplateContext {
17    pub fn new(project_name: &str) -> Self {
18        Self {
19            project_name: project_name.to_string(),
20            project_name_snake: to_snake_case(project_name),
21            project_name_pascal: to_pascal_case(project_name),
22        }
23    }
24}
25
26pub struct ProjectTemplate {
27    #[allow(dead_code)]
28    name: String,
29    files: HashMap<String, String>,
30}
31
32impl ProjectTemplate {
33    pub fn load(template_name: &str) -> Result<Self> {
34        match template_name {
35            "basic" => Ok(Self::basic_template()),
36            "echo" => Ok(Self::echo_template()),
37            _ => Err(ActrCliError::InvalidProject(format!(
38                "Unknown template: {template_name}"
39            ))),
40        }
41    }
42
43    pub fn generate(&self, project_path: &Path, context: &TemplateContext) -> Result<()> {
44        let handlebars = Handlebars::new();
45
46        for (file_path, content) in &self.files {
47            let rendered_path = handlebars.render_template(file_path, context)?;
48            let rendered_content = handlebars.render_template(content, context)?;
49
50            let full_path = project_path.join(&rendered_path);
51
52            // Create parent directories if they don't exist
53            if let Some(parent) = full_path.parent() {
54                std::fs::create_dir_all(parent)?;
55            }
56
57            std::fs::write(full_path, rendered_content)?;
58        }
59
60        Ok(())
61    }
62
63    fn basic_template() -> Self {
64        let mut files = HashMap::new();
65
66        // Cargo.toml
67        files.insert(
68            "Cargo.toml".to_string(),
69            r#"[package]
70name = "{{project_name}}"
71version = "0.1.0"
72edition = "2021"
73
74[dependencies]
75# Actor-RTC framework
76actor-rtc-framework = { path = "../../actor-rtc-framework" }  # Adjust path as needed
77
78# Async runtime
79tokio = { version = "1.0", features = ["full"] }
80async-trait = "0.1"
81
82# Protocol definitions
83tonic = "0.10"
84prost = "0.12"
85
86# Logging
87tracing = "0.1"
88tracing-subscriber = "0.3"
89
90# Error handling
91anyhow = "1.0"
92
93[build-dependencies]
94tonic-build = "0.10"
95"#
96            .to_string(),
97        );
98
99        // src/lib.rs for auto-runner mode
100        files.insert(
101            "src/lib.rs".to_string(),
102            r#"//! {{project_name}} - Actor-RTC service implementation
103
104use actor_rtc_framework::prelude::*;
105use async_trait::async_trait;
106use std::sync::Arc;
107use tracing::info;
108
109// Include generated proto code
110pub mod greeter {
111    tonic::include_proto!("greeter");
112}
113
114// Include generated actor code
115include!(concat!(env!("OUT_DIR"), "/greeter_service_actor.rs"));
116
117use greeter::{GreetRequest, GreetResponse};
118
119/// Main actor implementation
120#[derive(Default)]
121pub struct {{project_name_pascal}}Actor {
122    greeting_count: std::sync::atomic::AtomicU64,
123}
124
125#[async_trait]
126impl IGreeterService for {{project_name_pascal}}Actor {
127    async fn greet(
128        &self, 
129        request: GreetRequest,
130        _context: Arc<Context>
131    ) -> Result<GreetResponse, tonic::Status> {
132        let count = self.greeting_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
133        
134        info!("Received greeting request #{}: Hello {}", count, request.name);
135        
136        let message = format!("Hello, {}! This is greeting #{}", request.name, count);
137        
138        Ok(GreetResponse { message })
139    }
140}
141
142#[async_trait]
143impl ILifecycle for {{project_name_pascal}}Actor {
144    async fn on_start(&self, _ctx: Arc<Context>) {
145        info!("{{project_name_pascal}}Actor started successfully");
146    }
147
148    async fn on_stop(&self, _ctx: Arc<Context>) {
149        info!("{{project_name_pascal}}Actor shutting down");
150    }
151
152    async fn on_actor_discovered(&self, _ctx: Arc<Context>, _actor_id: &ActorId) -> bool {
153        // Accept connections from any actor
154        true
155    }
156}
157"#
158            .to_string(),
159        );
160
161        // proto/greeter.proto
162        files.insert(
163            "protos/greeter.proto".to_string(),
164            r#"syntax = "proto3";
165
166package greeter;
167
168// Greeting request message
169message GreetRequest {
170    string name = 1;
171}
172
173// Greeting response message
174message GreetResponse {
175    string message = 1;
176}
177
178// Greeter service definition
179service GreeterService {
180    rpc Greet(GreetRequest) returns (GreetResponse);
181}
182"#
183            .to_string(),
184        );
185
186        // build.rs
187        files.insert(
188            "build.rs".to_string(),
189            r#"fn main() -> Result<(), Box<dyn std::error::Error>> {
190    let proto_files = ["protos/greeter.proto"];
191
192    // Build with protoc-gen-actrframework plugin if available
193    let plugin_path = std::env::current_dir()?
194        .parent()
195        .unwrap()
196        .join("target/debug/protoc-gen-actrframework");
197    
198    let mut config = tonic_build::configure()
199        .build_server(false)  // We generate our own server-side traits
200        .build_client(true);
201
202    if plugin_path.exists() {
203        config = config
204            .protoc_arg(format!("--plugin=protoc-gen-actrframework={}", plugin_path.display()))
205            .protoc_arg("--actrframework_out=.");
206        println!("Using protoc-gen-actrframework plugin");
207    } else {
208        println!("Warning: protoc-gen-actrframework plugin not found, using standard tonic generation only");
209    }
210
211    config.compile(&proto_files, &["protos/"])?;
212
213    // Re-run if proto files change
214    for proto_file in &proto_files {
215        println!("cargo:rerun-if-changed={}", proto_file);
216    }
217
218    Ok(())
219}
220"#.to_string(),
221        );
222
223        // README.md
224        files.insert(
225            "README.md".to_string(),
226            r#"# {{project_name}}
227
228An Actor-RTC service implementation.
229
230## Building
231
232```bash
233actr gen --input proto --output src/generated
234```
235
236## Running
237
238```bash
239actr run
240```
241
242## Development
243
244This project uses the Actor-RTC framework's auto-runner mode. The main logic is implemented in `src/lib.rs`, and the framework automatically generates the necessary startup code.
245
246The service definition is in `protos/greeter.proto`, and the implementation is in the `{{project_name_pascal}}Actor` struct.
247"#.to_string(),
248        );
249
250        Self {
251            name: "basic".to_string(),
252            files,
253        }
254    }
255
256    fn echo_template() -> Self {
257        // Similar to basic but with echo-specific content
258        // For now, just return the basic template
259        Self::basic_template()
260    }
261}
262
263fn to_snake_case(s: &str) -> String {
264    let mut result = String::new();
265    let chars = s.chars().peekable();
266
267    for c in chars {
268        if c.is_uppercase() {
269            if !result.is_empty() {
270                result.push('_');
271            }
272            result.push(c.to_lowercase().next().unwrap());
273        } else if c.is_alphanumeric() {
274            result.push(c);
275        } else {
276            result.push('_');
277        }
278    }
279
280    result
281}
282
283fn to_pascal_case(s: &str) -> String {
284    // Handle already PascalCase or camelCase strings by detecting uppercase transitions
285    let mut result = String::new();
286    let chars = s.chars().peekable();
287    let mut start_of_word = true;
288
289    for c in chars {
290        if !c.is_alphanumeric() {
291            // Separator - next char starts a new word
292            start_of_word = true;
293            continue;
294        }
295
296        if c.is_uppercase() {
297            // Uppercase marks start of a word
298            result.push(c);
299            start_of_word = false;
300        } else if start_of_word {
301            // First char of a word - capitalize it
302            result.push(c.to_uppercase().next().unwrap());
303            start_of_word = false;
304        } else {
305            // Middle of a word - keep as lowercase
306            result.push(c.to_lowercase().next().unwrap());
307        }
308    }
309
310    result
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316
317    #[test]
318    fn test_to_snake_case() {
319        assert_eq!(to_snake_case("MyProject"), "my_project");
320        assert_eq!(to_snake_case("my-project"), "my_project");
321        assert_eq!(to_snake_case("my_project"), "my_project");
322    }
323
324    #[test]
325    fn test_to_pascal_case() {
326        assert_eq!(to_pascal_case("my-project"), "MyProject");
327        assert_eq!(to_pascal_case("my_project"), "MyProject");
328        assert_eq!(to_pascal_case("MyProject"), "MyProject");
329    }
330}