actr_cli/commands/
generate.rs1use crate::commands::Command;
9use crate::error::{ActrCliError, Result};
10use async_trait::async_trait;
13use clap::Parser;
14use std::path::{Path, PathBuf};
15use std::process::Command as StdCommand;
16use tracing::{debug, info, warn};
17
18#[derive(Parser, Debug, Clone)]
19pub struct GenCommand {
20 #[arg(short, long, default_value = "proto")]
22 pub input: PathBuf,
23
24 #[arg(short, long, default_value = "src/generated")]
26 pub output: PathBuf,
27
28 #[arg(long, default_value = "true")]
30 pub generate_scaffold: bool,
31
32 #[arg(long)]
34 pub overwrite_user_code: bool,
35
36 #[arg(long, default_value = "true")]
38 pub format: bool,
39
40 #[arg(long)]
42 pub debug: bool,
43}
44
45#[async_trait]
46impl Command for GenCommand {
47 async fn execute(&self) -> Result<()> {
48 info!("🚀 开始代码生成...");
49
50 self.validate_inputs()?;
52
53 self.prepare_output_dirs()?;
55
56 let proto_files = self.discover_proto_files()?;
58 info!("📁 发现 {} 个 proto 文件", proto_files.len());
59
60 self.generate_infrastructure_code(&proto_files).await?;
62
63 if self.generate_scaffold {
65 self.generate_user_code_scaffold(&proto_files).await?;
66 }
67
68 if self.format {
70 self.format_generated_code().await?;
71 }
72
73 self.validate_generated_code().await?;
75
76 info!("✅ 代码生成完成!");
77 self.print_next_steps();
78
79 Ok(())
80 }
81}
82
83impl GenCommand {
84 fn read_manufacturer(&self) -> Result<String> {
86 use std::fs;
87
88 let config_path = PathBuf::from("Actr.toml");
90 if !config_path.exists() {
91 warn!("Actr.toml not found, using default manufacturer 'acme'");
92 return Ok("acme".to_string());
93 }
94
95 let content = fs::read_to_string(&config_path)
97 .map_err(|e| ActrCliError::config_error(format!("Failed to read Actr.toml: {e}")))?;
98
99 let raw_config: actr::config::RawConfig = toml::from_str(&content)
100 .map_err(|e| ActrCliError::config_error(format!("Failed to parse Actr.toml: {e}")))?;
101
102 Ok(raw_config.package.manufacturer)
103 }
104
105 fn validate_inputs(&self) -> Result<()> {
107 if !self.input.exists() {
108 return Err(ActrCliError::config_error(format!(
109 "输入路径不存在: {:?}",
110 self.input
111 )));
112 }
113
114 if self.input.is_file() && self.input.extension().unwrap_or_default() != "proto" {
115 warn!("输入文件不是 .proto 文件: {:?}", self.input);
116 }
117
118 Ok(())
119 }
120
121 fn prepare_output_dirs(&self) -> Result<()> {
123 std::fs::create_dir_all(&self.output)
124 .map_err(|e| ActrCliError::config_error(format!("创建输出目录失败: {e}")))?;
125
126 if self.generate_scaffold {
127 let user_code_dir = self.output.join("../");
128 std::fs::create_dir_all(&user_code_dir)
129 .map_err(|e| ActrCliError::config_error(format!("创建用户代码目录失败: {e}")))?;
130 }
131
132 Ok(())
133 }
134
135 fn discover_proto_files(&self) -> Result<Vec<PathBuf>> {
137 let mut proto_files = Vec::new();
138
139 if self.input.is_file() {
140 proto_files.push(self.input.clone());
141 } else {
142 for entry in std::fs::read_dir(&self.input)
144 .map_err(|e| ActrCliError::config_error(format!("读取输入目录失败: {e}")))?
145 {
146 let entry = entry.map_err(|e| ActrCliError::config_error(e.to_string()))?;
147 let path = entry.path();
148
149 if path.extension().unwrap_or_default() == "proto" {
150 proto_files.push(path);
151 }
152 }
153 }
154
155 if proto_files.is_empty() {
156 return Err(ActrCliError::config_error("未找到 proto 文件"));
157 }
158
159 Ok(proto_files)
160 }
161
162 fn ensure_protoc_plugin(&self) -> Result<PathBuf> {
174 const EXPECTED_VERSION: &str = env!("CARGO_PKG_VERSION");
176
177 let installed_version = self.check_installed_plugin_version()?;
179
180 match installed_version {
181 Some(version) if version == EXPECTED_VERSION => {
182 info!("✅ Using installed protoc-gen-actrframework v{}", version);
184 let output = StdCommand::new("which")
185 .arg("protoc-gen-actrframework")
186 .output()
187 .map_err(|e| {
188 ActrCliError::command_error(format!("Failed to locate plugin: {e}"))
189 })?;
190
191 let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
192 Ok(PathBuf::from(path))
193 }
194 Some(version) => {
195 info!(
197 "🔄 Version mismatch: installed v{}, need v{}",
198 version, EXPECTED_VERSION
199 );
200 info!("🔨 Upgrading plugin...");
201 self.install_or_upgrade_plugin()
202 }
203 None => {
204 info!("📦 protoc-gen-actrframework not found, installing...");
206 self.install_or_upgrade_plugin()
207 }
208 }
209 }
210
211 fn check_installed_plugin_version(&self) -> Result<Option<String>> {
213 let output = StdCommand::new("protoc-gen-actrframework")
214 .arg("--version")
215 .output();
216
217 match output {
218 Ok(output) if output.status.success() => {
219 let version_info = String::from_utf8_lossy(&output.stdout);
220 let version = version_info
222 .lines()
223 .next()
224 .and_then(|line| line.split_whitespace().nth(1))
225 .map(|v| v.to_string());
226
227 debug!("Detected installed version: {:?}", version);
228 Ok(version)
229 }
230 _ => {
231 debug!("Plugin not found in PATH");
232 Ok(None)
233 }
234 }
235 }
236
237 fn install_or_upgrade_plugin(&self) -> Result<PathBuf> {
239 let current_dir = std::env::current_dir()?;
241 let workspace_root = current_dir.ancestors().find(|p| {
242 let is_workspace =
243 p.join("Cargo.toml").exists() && p.join("crates/framework-protoc-codegen").exists();
244 if is_workspace {
245 debug!("Found workspace root: {:?}", p);
246 }
247 is_workspace
248 });
249
250 let workspace_root = workspace_root.ok_or_else(|| {
251 ActrCliError::config_error(
252 "Cannot find actr workspace.\n\
253 Please run this command from within an actr project or workspace.",
254 )
255 })?;
256
257 info!("🔍 Found actr workspace at: {}", workspace_root.display());
258
259 info!("🔨 Building protoc-gen-actrframework...");
261 let mut build_cmd = StdCommand::new("cargo");
262 build_cmd
263 .arg("build")
264 .arg("-p")
265 .arg("actr-framework-protoc-codegen")
266 .arg("--bin")
267 .arg("protoc-gen-actrframework")
268 .current_dir(workspace_root);
269
270 debug!("Running: {:?}", build_cmd);
271 let output = build_cmd
272 .output()
273 .map_err(|e| ActrCliError::command_error(format!("Failed to build plugin: {e}")))?;
274
275 if !output.status.success() {
276 let stderr = String::from_utf8_lossy(&output.stderr);
277 return Err(ActrCliError::command_error(format!(
278 "Failed to build plugin:\n{stderr}"
279 )));
280 }
281
282 info!("📦 Installing to ~/.cargo/bin/...");
284 let mut install_cmd = StdCommand::new("cargo");
285 install_cmd
286 .arg("install")
287 .arg("--path")
288 .arg(workspace_root.join("crates/framework-protoc-codegen"))
289 .arg("--bin")
290 .arg("protoc-gen-actrframework")
291 .arg("--force"); debug!("Running: {:?}", install_cmd);
294 let output = install_cmd
295 .output()
296 .map_err(|e| ActrCliError::command_error(format!("Failed to install plugin: {e}")))?;
297
298 if !output.status.success() {
299 let stderr = String::from_utf8_lossy(&output.stderr);
300 return Err(ActrCliError::command_error(format!(
301 "Failed to install plugin:\n{stderr}"
302 )));
303 }
304
305 info!("✅ Plugin installed successfully");
306
307 let which_output = StdCommand::new("which")
309 .arg("protoc-gen-actrframework")
310 .output()
311 .map_err(|e| {
312 ActrCliError::command_error(format!("Failed to locate installed plugin: {e}"))
313 })?;
314
315 let path = String::from_utf8_lossy(&which_output.stdout)
316 .trim()
317 .to_string();
318 Ok(PathBuf::from(path))
319 }
320
321 async fn generate_infrastructure_code(&self, proto_files: &[PathBuf]) -> Result<()> {
323 info!("🔧 生成基础设施代码...");
324
325 let plugin_path = self.ensure_protoc_plugin()?;
327
328 let manufacturer = self.read_manufacturer()?;
330 debug!("Using manufacturer from Actr.toml: {}", manufacturer);
331
332 for proto_file in proto_files {
333 debug!("处理 proto 文件: {:?}", proto_file);
334
335 let mut cmd = StdCommand::new("protoc");
337 cmd.arg(format!("--proto_path={}", self.input.display()))
338 .arg(format!("--prost_out={}", self.output.display()))
339 .arg(proto_file);
340
341 debug!("执行 protoc (prost): {:?}", cmd);
342 let output = cmd.output().map_err(|e| {
343 ActrCliError::command_error(format!("执行 protoc (prost) 失败: {e}"))
344 })?;
345
346 if !output.status.success() {
347 let stderr = String::from_utf8_lossy(&output.stderr);
348 return Err(ActrCliError::command_error(format!(
349 "protoc (prost) 执行失败: {stderr}"
350 )));
351 }
352
353 let mut cmd = StdCommand::new("protoc");
355 cmd.arg(format!("--proto_path={}", self.input.display()))
356 .arg(format!(
357 "--plugin=protoc-gen-actrframework={}",
358 plugin_path.display()
359 ))
360 .arg(format!("--actrframework_opt=manufacturer={manufacturer}"))
361 .arg(format!("--actrframework_out={}", self.output.display()))
362 .arg(proto_file);
363
364 debug!("执行 protoc (actrframework): {:?}", cmd);
365 let output = cmd.output().map_err(|e| {
366 ActrCliError::command_error(format!("执行 protoc (actrframework) 失败: {e}"))
367 })?;
368
369 if !output.status.success() {
370 let stderr = String::from_utf8_lossy(&output.stderr);
371 return Err(ActrCliError::command_error(format!(
372 "protoc (actrframework) 执行失败: {stderr}"
373 )));
374 }
375
376 let stdout = String::from_utf8_lossy(&output.stdout);
377 if !stdout.is_empty() {
378 debug!("protoc 输出: {}", stdout);
379 }
380 }
381
382 self.generate_mod_rs(proto_files).await?;
384
385 self.set_generated_files_readonly()?;
387
388 info!("✅ 基础设施代码生成完成");
389 Ok(())
390 }
391
392 async fn generate_mod_rs(&self, _proto_files: &[PathBuf]) -> Result<()> {
394 let mod_path = self.output.join("mod.rs");
395
396 let mut proto_modules = Vec::new();
398 let mut service_modules = Vec::new();
399
400 use std::fs;
401 for entry in fs::read_dir(&self.output)
402 .map_err(|e| ActrCliError::config_error(format!("读取输出目录失败: {e}")))?
403 {
404 let entry = entry.map_err(|e| ActrCliError::config_error(e.to_string()))?;
405 let path = entry.path();
406
407 if path.is_file() && path.extension().unwrap_or_default() == "rs" {
408 if let Some(file_name) = path.file_stem().and_then(|s| s.to_str()) {
409 if file_name == "mod" {
411 continue;
412 }
413
414 if file_name.ends_with("_service_actor") {
416 service_modules.push(format!("pub mod {file_name};"));
417 } else {
418 proto_modules.push(format!("pub mod {file_name};"));
419 }
420 }
421 }
422 }
423
424 proto_modules.sort();
426 service_modules.sort();
427
428 let mod_content = format!(
429 r#"//! 自动生成的代码模块
430//!
431//! 此模块由 `actr gen` 命令自动生成,包括:
432//! - protobuf 消息类型定义
433//! - Actor 框架代码(路由器、trait)
434//!
435//! ⚠️ 请勿手动修改此目录中的文件
436
437// Protobuf 消息类型(由 prost 生成)
438{}
439
440// Actor 框架代码(由 protoc-gen-actrframework 生成)
441{}
442
443// 常用类型会在各自的模块中定义,请按需导入
444"#,
445 proto_modules.join("\n"),
446 service_modules.join("\n"),
447 );
448
449 std::fs::write(&mod_path, mod_content)
450 .map_err(|e| ActrCliError::config_error(format!("写入 mod.rs 失败: {e}")))?;
451
452 debug!("生成 mod.rs: {:?}", mod_path);
453 Ok(())
454 }
455
456 fn set_generated_files_readonly(&self) -> Result<()> {
458 use std::fs;
459
460 for entry in fs::read_dir(&self.output)
461 .map_err(|e| ActrCliError::config_error(format!("读取输出目录失败: {e}")))?
462 {
463 let entry = entry.map_err(|e| ActrCliError::config_error(e.to_string()))?;
464 let path = entry.path();
465
466 if path.is_file() && path.extension().unwrap_or_default() == "rs" {
467 let metadata = fs::metadata(&path)
469 .map_err(|e| ActrCliError::config_error(format!("获取文件元数据失败: {e}")))?;
470 let mut permissions = metadata.permissions();
471
472 #[cfg(unix)]
474 {
475 use std::os::unix::fs::PermissionsExt;
476 let mode = permissions.mode();
477 permissions.set_mode(mode & !0o222); }
479
480 #[cfg(not(unix))]
481 {
482 permissions.set_readonly(true);
483 }
484
485 fs::set_permissions(&path, permissions)
486 .map_err(|e| ActrCliError::config_error(format!("设置文件权限失败: {e}")))?;
487
488 debug!("设置只读属性: {:?}", path);
489 }
490 }
491
492 Ok(())
493 }
494
495 async fn generate_user_code_scaffold(&self, proto_files: &[PathBuf]) -> Result<()> {
497 info!("📝 生成用户代码框架...");
498
499 for proto_file in proto_files {
500 let service_name = proto_file
501 .file_stem()
502 .and_then(|s| s.to_str())
503 .ok_or_else(|| ActrCliError::config_error("无效的 proto 文件名"))?;
504
505 self.generate_service_scaffold(service_name).await?;
506 }
507
508 info!("✅ 用户代码框架生成完成");
509 Ok(())
510 }
511
512 async fn generate_service_scaffold(&self, service_name: &str) -> Result<()> {
514 let user_file_path = self
515 .output
516 .parent()
517 .unwrap_or_else(|| Path::new("src"))
518 .join(format!("{}_service.rs", service_name.to_lowercase()));
519
520 if user_file_path.exists() && !self.overwrite_user_code {
522 info!("⏭️ 跳过已存在的用户代码文件: {:?}", user_file_path);
523 return Ok(());
524 }
525
526 let scaffold_content = self.generate_scaffold_content(service_name);
527
528 std::fs::write(&user_file_path, scaffold_content)
529 .map_err(|e| ActrCliError::config_error(format!("写入用户代码框架失败: {e}")))?;
530
531 info!("📄 生成用户代码框架: {:?}", user_file_path);
532 Ok(())
533 }
534
535 fn generate_scaffold_content(&self, service_name: &str) -> String {
537 let service_name_pascal = service_name
538 .split('_')
539 .map(|s| {
540 let mut chars = s.chars();
541 match chars.next() {
542 None => String::new(),
543 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
544 }
545 })
546 .collect::<String>();
547
548 let template = format!(
549 r#"//! # {service_name_pascal} 用户业务逻辑实现
550//!
551//! 这个文件是由 `actr gen` 命令自动生成的用户代码框架。
552//! 请在这里实现您的具体业务逻辑。
553
554use crate::generated::{{{service_name_pascal}Handler, {service_name_pascal}Actor}};
555// 只导入必要的类型,避免拉入不需要的依赖如 sqlite
556// use actr_framework::prelude::*;
557use std::sync::Arc;
558
559/// {service_name_pascal} 服务的具体实现
560///
561/// TODO: 添加您需要的状态字段,例如:
562/// - 数据库连接池
563/// - 配置信息
564/// - 缓存客户端
565/// - 日志记录器等
566pub struct My{service_name_pascal}Service {{
567 // TODO: 添加您的服务状态字段
568 // 例如:
569 // pub db_pool: Arc<DatabasePool>,
570 // pub config: Arc<ServiceConfig>,
571 // pub metrics: Arc<Metrics>,
572}}
573
574impl My{service_name_pascal}Service {{
575 /// 创建新的服务实例
576 ///
577 /// TODO: 根据您的需要修改构造函数参数
578 pub fn new(/* TODO: 添加必要的依赖 */) -> Self {{
579 Self {{
580 // TODO: 初始化您的字段
581 }}
582 }}
583
584 /// 使用默认配置创建服务实例(用于测试)
585 pub fn default_for_testing() -> Self {{
586 Self {{
587 // TODO: 提供测试用的默认值
588 }}
589 }}
590}}
591
592// TODO: 实现 {service_name_pascal}Handler trait 的所有方法
593// 注意:impl_user_code_scaffold! 宏已经为您生成了基础框架,
594// 您需要将其替换为真实的业务逻辑实现。
595//
596// 示例:
597// #[async_trait]
598// impl {service_name_pascal}Handler for My{service_name_pascal}Service {{
599// async fn method_name(&self, req: RequestType) -> ActorResult<ResponseType> {{
600// // 1. 验证输入
601// // 2. 执行业务逻辑
602// // 3. 返回结果
603// todo!("实现您的业务逻辑")
604// }}
605// }}
606
607#[cfg(test)]
608mod tests {{
609 use super::*;
610
611 #[tokio::test]
612 async fn test_service_creation() {{
613 let _service = My{service_name_pascal}Service::default_for_testing();
614 // TODO: 添加您的测试
615 }}
616
617 // TODO: 添加更多测试用例
618}}
619
620/*
621📚 使用指南
622
623## 🚀 快速开始
624
6251. **实现业务逻辑**:
626 在 `My{service_name_pascal}Service` 中实现 `{service_name_pascal}Handler` trait 的所有方法
627
6282. **添加依赖**:
629 在 `Cargo.toml` 中添加您需要的依赖,例如数据库客户端、HTTP 客户端等
630
6313. **配置服务**:
632 修改 `new()` 构造函数,注入必要的依赖
633
6344. **启动服务**:
635 ```rust
636 #[tokio::main]
637 async fn main() -> ActorResult<()> {{
638 let service = My{service_name_pascal}Service::new(/* 依赖 */);
639
640 ActorSystem::new()
641 .attach(service)
642 .start()
643 .await
644 }}
645 ```
646
647## 🔧 开发提示
648
649- 使用 `tracing` crate 进行日志记录
650- 实现错误处理和重试逻辑
651- 添加单元测试和集成测试
652- 考虑使用配置文件管理环境变量
653- 实现健康检查和指标收集
654
655## 📖 更多资源
656
657- Actor-RTC 文档: [链接]
658- API 参考: [链接]
659- 示例项目: [链接]
660*/
661"# );
663
664 template
665 }
666
667 async fn format_generated_code(&self) -> Result<()> {
669 info!("🎨 格式化生成的代码...");
670
671 let mut cmd = StdCommand::new("rustfmt");
672 cmd.arg("--edition")
673 .arg("2024")
674 .arg("--config")
675 .arg("max_width=100");
676
677 for entry in std::fs::read_dir(&self.output)
679 .map_err(|e| ActrCliError::config_error(format!("读取输出目录失败: {e}")))?
680 {
681 let entry = entry.map_err(|e| ActrCliError::config_error(e.to_string()))?;
682 let path = entry.path();
683
684 if path.extension().unwrap_or_default() == "rs" {
685 cmd.arg(&path);
686 }
687 }
688
689 let output = cmd
690 .output()
691 .map_err(|e| ActrCliError::command_error(format!("执行 rustfmt 失败: {e}")))?;
692
693 if !output.status.success() {
694 let stderr = String::from_utf8_lossy(&output.stderr);
695 warn!("rustfmt 执行警告: {}", stderr);
696 } else {
697 info!("✅ 代码格式化完成");
698 }
699
700 Ok(())
701 }
702
703 async fn validate_generated_code(&self) -> Result<()> {
705 info!("🔍 验证生成的代码...");
706
707 let project_root = self.find_project_root()?;
709
710 let mut cmd = StdCommand::new("cargo");
711 cmd.arg("check").arg("--quiet").current_dir(&project_root);
712
713 let output = cmd
714 .output()
715 .map_err(|e| ActrCliError::command_error(format!("执行 cargo check 失败: {e}")))?;
716
717 if !output.status.success() {
718 let stderr = String::from_utf8_lossy(&output.stderr);
719 warn!("生成的代码存在编译警告或错误:\n{}", stderr);
720 info!("💡 这通常是正常的,因为用户代码框架包含 TODO 标记");
721 } else {
722 info!("✅ 代码验证通过");
723 }
724
725 Ok(())
726 }
727
728 fn find_project_root(&self) -> Result<PathBuf> {
730 let mut current = std::env::current_dir().map_err(ActrCliError::Io)?;
731
732 loop {
733 if current.join("Cargo.toml").exists() {
734 return Ok(current);
735 }
736
737 match current.parent() {
738 Some(parent) => current = parent.to_path_buf(),
739 None => break,
740 }
741 }
742
743 std::env::current_dir().map_err(ActrCliError::Io)
745 }
746
747 fn print_next_steps(&self) {
749 println!("\n🎉 代码生成完成!");
750 println!("\n📋 后续步骤:");
751 println!("1. 📖 查看生成的代码: {:?}", self.output);
752 if self.generate_scaffold {
753 println!("2. ✏️ 实现业务逻辑: 在 src/ 目录下的 *_service.rs 文件中");
754 println!("3. 🔧 添加依赖: 在 Cargo.toml 中添加需要的依赖包");
755 }
756 println!("4. 🏗️ 编译项目: cargo build");
757 println!("5. 🧪 运行测试: cargo test");
758 println!("6. 🚀 启动服务: cargo run");
759 println!("\n💡 提示: 查看生成的用户代码文件中的详细使用指南");
760 }
761}