actr_cli/templates/
mod.rs1use 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 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 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 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 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 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 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 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 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 start_of_word = true;
293 continue;
294 }
295
296 if c.is_uppercase() {
297 result.push(c);
299 start_of_word = false;
300 } else if start_of_word {
301 result.push(c.to_uppercase().next().unwrap());
303 start_of_word = false;
304 } else {
305 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}