actr_cli/commands/
generate.rs

1//! # Code Generation Command
2//!
3//! Generate Rust Actor code from proto files, including:
4//! 1. Protobuf message types
5//! 2. Actor infrastructure code
6//! 3. User business logic scaffolds (with TODO comments)
7
8use crate::commands::Command;
9use crate::commands::SupportedLanguage;
10use crate::commands::codegen::{GenContext, execute_codegen};
11use crate::error::{ActrCliError, Result};
12use crate::utils::to_pascal_case;
13// 只导入必要的类型,避免拉入不需要的依赖如 sqlite
14// use actr_framework::prelude::*;
15use async_trait::async_trait;
16use clap::Args;
17use std::path::{Path, PathBuf};
18use std::process::Command as StdCommand;
19use tracing::{debug, info, warn};
20
21#[derive(Args, Debug, Clone)]
22#[command(
23    about = "Generate code from proto files",
24    after_help = "Default output paths by language:
25  - rust:   src/generated
26  - swift:  {PascalName}/Generated (e.g., EchoApp/Generated)
27  - kotlin: app/src/main/java/{package}/generated
28  - python: generated"
29)]
30pub struct GenCommand {
31    /// Input proto file or directory
32    #[arg(short, long, default_value = "protos")]
33    pub input: PathBuf,
34
35    /// Output directory for generated code (use -o to override language defaults)
36    #[arg(short, long)]
37    pub output: Option<PathBuf>,
38
39    /// Path to Actr.toml config file
40    #[arg(short, long, default_value = "Actr.toml")]
41    pub config: PathBuf,
42
43    /// Clean generated outputs before regenerating
44    #[arg(long = "clean")]
45    pub clean: bool,
46
47    /// Skip user code scaffold generation
48    #[arg(long = "no-scaffold")]
49    pub no_scaffold: bool,
50
51    /// Whether to overwrite existing user code files
52    #[arg(long)]
53    pub overwrite_user_code: bool,
54
55    /// Skip formatting
56    #[arg(long = "no-format")]
57    pub no_format: bool,
58
59    /// Debug mode: keep intermediate generated files
60    #[arg(long)]
61    pub debug: bool,
62
63    /// Target language for generation
64    #[arg(short, long, default_value = "rust")]
65    pub language: SupportedLanguage,
66}
67
68#[async_trait]
69impl Command for GenCommand {
70    async fn execute(&self) -> Result<()> {
71        // Check if Actr.lock.toml exists
72        self.check_lock_file()?;
73
74        // Determine output path based on language
75        let output = self.determine_output_path()?;
76
77        info!(
78            "🚀 Start code generation (language: {:?})...",
79            self.language
80        );
81        let config = actr_config::ConfigParser::from_file(&self.config)
82            .map_err(|e| ActrCliError::config_error(format!("Failed to parse Actr.toml: {e}")))?;
83
84        let proto_files = self.preprocess()?;
85        if self.language != SupportedLanguage::Rust {
86            let context = GenContext {
87                proto_files,
88                input_path: self.input.clone(),
89                output,
90                config: config.clone(),
91                no_scaffold: self.no_scaffold,
92                overwrite_user_code: self.overwrite_user_code,
93                no_format: self.no_format,
94                debug: self.debug,
95            };
96            execute_codegen(self.language, &context).await?;
97            return Ok(());
98        }
99
100        // Step 5: Generate infrastructure code
101        self.generate_infrastructure_code(&proto_files, &config)
102            .await?;
103
104        // Step 6: Generate user code scaffold
105        if self.should_generate_scaffold() {
106            self.generate_user_code_scaffold(&proto_files).await?;
107        }
108
109        // Step 7: Format code
110        if self.should_format() {
111            self.format_generated_code().await?;
112        }
113
114        // Step 8: Validate generated code
115        self.validate_generated_code().await?;
116
117        info!("✅ Code generation completed!");
118        // Set all generated files to read-only only after generation, formatting, and validation are complete, to not interfere with rustfmt or other steps.
119        self.set_generated_files_readonly()?;
120        self.print_next_steps();
121
122        Ok(())
123    }
124}
125
126impl GenCommand {
127    /// Check if Actr.lock.toml exists and provide helpful error message if not
128    fn check_lock_file(&self) -> Result<()> {
129        let config_dir = self
130            .config
131            .parent()
132            .unwrap_or_else(|| std::path::Path::new("."));
133        let lock_file_path = config_dir.join("Actr.lock.toml");
134
135        if !lock_file_path.exists() {
136            return Err(ActrCliError::config_error(
137                "Actr.lock.toml not found\n\n\
138                The lock file is required for code generation. Please run:\n\n\
139                \x20\x20\x20\x20actr install\n\n\
140                This will generate Actr.lock.toml based on your Actr.toml configuration.",
141            ));
142        }
143
144        Ok(())
145    }
146
147    /// Determine output path based on language if not explicitly specified
148    fn determine_output_path(&self) -> Result<PathBuf> {
149        // If user specified a custom output, use it
150        if let Some(ref output) = self.output {
151            return Ok(output.clone());
152        }
153
154        // Determine language-specific default output path
155        match self.language {
156            SupportedLanguage::Swift => {
157                // Read package name from config for Swift
158                let config = actr_config::ConfigParser::from_file(&self.config).map_err(|e| {
159                    ActrCliError::config_error(format!("Failed to parse Actr.toml: {e}"))
160                })?;
161                let project_name = &config.package.name;
162                // Convert to PascalCase for Swift module name
163                let pascal_name = to_pascal_case(project_name);
164                Ok(PathBuf::from(format!("{}/Generated", pascal_name)))
165            }
166            SupportedLanguage::Kotlin => {
167                // Kotlin default: app/src/main/java/{package_path}/generated
168                // Package name follows the pattern: io.actr.{project_name_cleaned}
169                let config = actr_config::ConfigParser::from_file(&self.config).map_err(|e| {
170                    ActrCliError::config_error(format!("Failed to parse Actr.toml: {e}"))
171                })?;
172                // Convert project name to valid Android package name
173                // e.g., "my-app" -> "io.actr.myapp"
174                let clean_name: String = config
175                    .package
176                    .name
177                    .chars()
178                    .filter(|c| c.is_alphanumeric())
179                    .collect::<String>()
180                    .to_lowercase();
181                let package_path = format!("io/actr/{}", clean_name);
182                Ok(PathBuf::from(format!(
183                    "app/src/main/java/{}/generated",
184                    package_path
185                )))
186            }
187            SupportedLanguage::Python => {
188                // Python default: generated
189                Ok(PathBuf::from("generated"))
190            }
191            SupportedLanguage::Rust => {
192                // Rust default: src/generated
193                Ok(PathBuf::from("src/generated"))
194            }
195        }
196    }
197
198    fn preprocess(&self) -> Result<Vec<PathBuf>> {
199        // Step 1: Validate inputs
200        self.validate_inputs()?;
201
202        // Step 2: Clean old generation outputs (optional)
203        self.clean_generated_outputs()?;
204
205        // Step 3: Prepare output directories
206        self.prepare_output_dirs()?;
207
208        // Step 4: Discover proto files
209        let proto_files = self.discover_proto_files()?;
210        info!("📁 Found {} proto files", proto_files.len());
211
212        Ok(proto_files)
213    }
214
215    /// Whether user code scaffold should be generated
216    fn should_generate_scaffold(&self) -> bool {
217        !self.no_scaffold
218    }
219
220    /// Whether formatting should run
221    fn should_format(&self) -> bool {
222        !self.no_format
223    }
224
225    /// Remove previously generated files when --clean is used
226    fn clean_generated_outputs(&self) -> Result<()> {
227        use std::fs;
228
229        if !self.clean {
230            return Ok(());
231        }
232
233        let output = self.determine_output_path()?;
234        if !output.exists() {
235            return Ok(());
236        }
237
238        info!("🧹 Cleaning old generation results: {:?}", output);
239
240        self.make_writable_recursive(&output)?;
241        fs::remove_dir_all(&output).map_err(|e| {
242            ActrCliError::config_error(format!("Failed to delete generation directory: {e}"))
243        })?;
244
245        Ok(())
246    }
247
248    /// Ensure all files are writable so removal works across platforms
249    #[allow(clippy::only_used_in_recursion)]
250    fn make_writable_recursive(&self, path: &Path) -> Result<()> {
251        use std::fs;
252
253        if path.is_file() {
254            let metadata = fs::metadata(path).map_err(|e| {
255                ActrCliError::config_error(format!("Failed to read file metadata: {e}"))
256            })?;
257            let mut permissions = metadata.permissions();
258
259            #[cfg(unix)]
260            {
261                use std::os::unix::fs::PermissionsExt;
262                let mode = permissions.mode();
263                permissions.set_mode(mode | 0o222);
264            }
265
266            #[cfg(not(unix))]
267            {
268                permissions.set_readonly(false);
269            }
270
271            fs::set_permissions(path, permissions).map_err(|e| {
272                ActrCliError::config_error(format!("Failed to reset file permissions: {e}"))
273            })?;
274        } else if path.is_dir() {
275            for entry in fs::read_dir(path)
276                .map_err(|e| ActrCliError::config_error(format!("Failed to read directory: {e}")))?
277            {
278                let entry = entry.map_err(|e| ActrCliError::config_error(e.to_string()))?;
279                self.make_writable_recursive(&entry.path())?;
280            }
281        }
282
283        Ok(())
284    }
285
286    /// 验证输入参数
287    fn validate_inputs(&self) -> Result<()> {
288        if !self.input.exists() {
289            return Err(ActrCliError::config_error(format!(
290                "Input path does not exist: {:?}",
291                self.input
292            )));
293        }
294
295        if self.input.is_file() && self.input.extension().unwrap_or_default() != "proto" {
296            warn!("Input file is not a .proto file: {:?}", self.input);
297        }
298
299        Ok(())
300    }
301
302    /// 准备输出目录
303    fn prepare_output_dirs(&self) -> Result<()> {
304        let output = self.determine_output_path()?;
305        std::fs::create_dir_all(&output).map_err(|e| {
306            ActrCliError::config_error(format!("Failed to create output directory: {e}"))
307        })?;
308
309        if self.should_generate_scaffold() {
310            let user_code_dir = output.join("../");
311            std::fs::create_dir_all(&user_code_dir).map_err(|e| {
312                ActrCliError::config_error(format!("Failed to create user code directory: {e}"))
313            })?;
314        }
315
316        Ok(())
317    }
318
319    /// Find proto files recursively
320    fn discover_proto_files(&self) -> Result<Vec<PathBuf>> {
321        let mut proto_files = Vec::new();
322
323        if self.input.is_file() {
324            proto_files.push(self.input.clone());
325        } else {
326            self.collect_proto_files(&self.input, &mut proto_files)?;
327        }
328
329        if proto_files.is_empty() {
330            return Err(ActrCliError::config_error("No proto files found"));
331        }
332
333        Ok(proto_files)
334    }
335
336    /// Collect proto files recursively
337    #[allow(clippy::only_used_in_recursion)]
338    fn collect_proto_files(&self, dir: &PathBuf, proto_files: &mut Vec<PathBuf>) -> Result<()> {
339        for entry in std::fs::read_dir(dir)
340            .map_err(|e| ActrCliError::config_error(format!("Failed to read directory: {e}")))?
341        {
342            let entry = entry.map_err(|e| ActrCliError::config_error(e.to_string()))?;
343            let path = entry.path();
344
345            if path.is_file() && path.extension().unwrap_or_default() == "proto" {
346                proto_files.push(path);
347            } else if path.is_dir() {
348                self.collect_proto_files(&path, proto_files)?;
349            }
350        }
351        Ok(())
352    }
353
354    /// 确保 protoc-gen-actrframework 插件可用
355    ///
356    /// 版本管理策略:
357    /// 1. 检查系统已安装版本
358    /// 2. 如果版本匹配 → 直接使用
359    /// 3. 如果版本不匹配或未安装 → 自动安装/升级
360    ///
361    /// 这种策略确保:
362    /// - 版本一致性:插件版本始终与 CLI 匹配
363    /// - 自动管理:无需手动安装或升级
364    /// - 简单明确:只看版本,不区分开发/生产环境
365    fn ensure_protoc_plugin(&self) -> Result<PathBuf> {
366        // Expected version (same as actr-framework-protoc-codegen)
367        const EXPECTED_VERSION: &str = env!("CARGO_PKG_VERSION");
368
369        // 1. Check installed version
370        let installed_version = self.check_installed_plugin_version()?;
371
372        match installed_version {
373            Some(version) if version == EXPECTED_VERSION => {
374                // Version matches, use it directly
375                info!("✅ Using installed protoc-gen-actrframework v{}", version);
376                let output = StdCommand::new("which")
377                    .arg("protoc-gen-actrframework")
378                    .output()
379                    .map_err(|e| {
380                        ActrCliError::command_error(format!("Failed to locate plugin: {e}"))
381                    })?;
382
383                let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
384                Ok(PathBuf::from(path))
385            }
386            Some(version) => {
387                // Version mismatch, upgrade needed
388                info!(
389                    "🔄 Version mismatch: installed v{}, need v{}",
390                    version, EXPECTED_VERSION
391                );
392                info!("🔨 Upgrading plugin...");
393                self.install_or_upgrade_plugin()
394            }
395            None => {
396                // Not installed, install it
397                info!("📦 protoc-gen-actrframework not found, installing...");
398                self.install_or_upgrade_plugin()
399            }
400        }
401    }
402
403    /// Check installed plugin version
404    fn check_installed_plugin_version(&self) -> Result<Option<String>> {
405        let output = StdCommand::new("protoc-gen-actrframework")
406            .arg("--version")
407            .output();
408
409        match output {
410            Ok(output) if output.status.success() => {
411                let version_info = String::from_utf8_lossy(&output.stdout);
412                // Parse "protoc-gen-actrframework 0.1.0"
413                let version = version_info
414                    .lines()
415                    .next()
416                    .and_then(|line| line.split_whitespace().nth(1))
417                    .map(|v| v.to_string());
418
419                debug!("Detected installed version: {:?}", version);
420                Ok(version)
421            }
422            _ => {
423                debug!("Plugin not found in PATH");
424                Ok(None)
425            }
426        }
427    }
428
429    /// Install or upgrade plugin from workspace
430    fn install_or_upgrade_plugin(&self) -> Result<PathBuf> {
431        // Find actr workspace
432        let current_dir = std::env::current_dir()?;
433        let workspace_root = current_dir.ancestors().find(|p| {
434            let is_workspace =
435                p.join("Cargo.toml").exists() && p.join("crates/framework-protoc-codegen").exists();
436            if is_workspace {
437                debug!("Found workspace root: {:?}", p);
438            }
439            is_workspace
440        });
441
442        let workspace_root = workspace_root.ok_or_else(|| {
443            ActrCliError::config_error(
444                "Cannot find actr workspace.\n\
445                 Please run this command from within an actr project or workspace.",
446            )
447        })?;
448
449        info!("🔍 Found actr workspace at: {}", workspace_root.display());
450
451        // Step 1: Build the plugin
452        info!("🔨 Building protoc-gen-actrframework...");
453        let mut build_cmd = StdCommand::new("cargo");
454        build_cmd
455            .arg("build")
456            .arg("-p")
457            .arg("actr-framework-protoc-codegen")
458            .arg("--bin")
459            .arg("protoc-gen-actrframework")
460            .current_dir(workspace_root);
461
462        debug!("Running: {:?}", build_cmd);
463        let output = build_cmd
464            .output()
465            .map_err(|e| ActrCliError::command_error(format!("Failed to build plugin: {e}")))?;
466
467        if !output.status.success() {
468            let stderr = String::from_utf8_lossy(&output.stderr);
469            return Err(ActrCliError::command_error(format!(
470                "Failed to build plugin:\n{stderr}"
471            )));
472        }
473
474        // Step 2: Install to ~/.cargo/bin/
475        info!("📦 Installing to ~/.cargo/bin/...");
476        let mut install_cmd = StdCommand::new("cargo");
477        install_cmd
478            .arg("install")
479            .arg("--path")
480            .arg(workspace_root.join("crates/framework-protoc-codegen"))
481            .arg("--bin")
482            .arg("protoc-gen-actrframework")
483            .arg("--force"); // Overwrite existing version
484
485        debug!("Running: {:?}", install_cmd);
486        let output = install_cmd
487            .output()
488            .map_err(|e| ActrCliError::command_error(format!("Failed to install plugin: {e}")))?;
489
490        if !output.status.success() {
491            let stderr = String::from_utf8_lossy(&output.stderr);
492            return Err(ActrCliError::command_error(format!(
493                "Failed to install plugin:\n{stderr}"
494            )));
495        }
496
497        info!("✅ Plugin installed successfully");
498
499        // Return the installed path
500        let which_output = StdCommand::new("which")
501            .arg("protoc-gen-actrframework")
502            .output()
503            .map_err(|e| {
504                ActrCliError::command_error(format!("Failed to locate installed plugin: {e}"))
505            })?;
506
507        let path = String::from_utf8_lossy(&which_output.stdout)
508            .trim()
509            .to_string();
510        Ok(PathBuf::from(path))
511    }
512
513    /// 生成基础设施代码
514    async fn generate_infrastructure_code(
515        &self,
516        proto_files: &[PathBuf],
517        config: &actr_config::Config,
518    ) -> Result<()> {
519        info!("🔧 Generating infrastructure code...");
520
521        // 确保 protoc 插件可用
522        let plugin_path = self.ensure_protoc_plugin()?;
523
524        let manufacturer = config.package.actr_type.manufacturer.clone();
525        debug!("Using manufacturer from Actr.toml: {}", manufacturer);
526
527        // 确定输出路径
528        let output = self.determine_output_path()?;
529
530        for proto_file in proto_files {
531            debug!("Processing proto file: {:?}", proto_file);
532
533            // 第一步:使用 prost 生成基础 protobuf 消息类型
534            let mut cmd = StdCommand::new("protoc");
535            cmd.arg(format!("--proto_path={}", self.input.display()))
536                .arg("--prost_opt=flat_output_dir")
537                .arg(format!("--prost_out={}", output.display()))
538                .arg(proto_file);
539
540            debug!("Executing protoc (prost): {:?}", cmd);
541            let output_cmd = cmd.output().map_err(|e| {
542                ActrCliError::command_error(format!("Failed to execute protoc (prost): {e}"))
543            })?;
544
545            if !output_cmd.status.success() {
546                let stderr = String::from_utf8_lossy(&output_cmd.stderr);
547                return Err(ActrCliError::command_error(format!(
548                    "protoc (prost) execution failed: {stderr}"
549                )));
550            }
551
552            // 第二步:使用 actrframework 插件生成 Actor 框架代码
553            let mut cmd = StdCommand::new("protoc");
554            cmd.arg(format!("--proto_path={}", self.input.display()))
555                .arg(format!(
556                    "--plugin=protoc-gen-actrframework={}",
557                    plugin_path.display()
558                ))
559                .arg(format!("--actrframework_opt=manufacturer={manufacturer}"))
560                .arg(format!("--actrframework_out={}", output.display()))
561                .arg(proto_file);
562
563            debug!("Executing protoc (actrframework): {:?}", cmd);
564            let output_cmd = cmd.output().map_err(|e| {
565                ActrCliError::command_error(format!(
566                    "Failed to execute protoc (actrframework): {e}"
567                ))
568            })?;
569
570            if !output_cmd.status.success() {
571                let stderr = String::from_utf8_lossy(&output_cmd.stderr);
572                return Err(ActrCliError::command_error(format!(
573                    "protoc (actrframework) execution failed: {stderr}"
574                )));
575            }
576
577            let stdout = String::from_utf8_lossy(&output_cmd.stdout);
578            if !stdout.is_empty() {
579                debug!("protoc output: {}", stdout);
580            }
581        }
582
583        // 生成 mod.rs
584        self.generate_mod_rs(proto_files).await?;
585
586        info!("✅ Infrastructure code generation completed");
587        Ok(())
588    }
589
590    /// 生成 mod.rs 文件
591    async fn generate_mod_rs(&self, _proto_files: &[PathBuf]) -> Result<()> {
592        let output = self.determine_output_path()?;
593        let mod_path = output.join("mod.rs");
594
595        // 扫描实际生成的文件,而不是根据 proto 文件名猜测
596        let mut proto_modules = Vec::new();
597        let mut service_modules = Vec::new();
598
599        use std::fs;
600        for entry in fs::read_dir(&output).map_err(|e| {
601            ActrCliError::config_error(format!("Failed to read output directory: {e}"))
602        })? {
603            let entry = entry.map_err(|e| ActrCliError::config_error(e.to_string()))?;
604            let path = entry.path();
605
606            if path.is_file()
607                && path.extension().unwrap_or_default() == "rs"
608                && let Some(file_name) = path.file_stem().and_then(|s| s.to_str())
609            {
610                // 跳过 mod.rs 本身
611                if file_name == "mod" {
612                    continue;
613                }
614
615                // 区分 service_actor 文件和 proto 文件
616                if file_name.ends_with("_service_actor") {
617                    service_modules.push(format!("pub mod {file_name};"));
618                } else {
619                    proto_modules.push(format!("pub mod {file_name};"));
620                }
621            }
622        }
623
624        // 排序以保证生成的 mod.rs 内容稳定
625        proto_modules.sort();
626        service_modules.sort();
627
628        let mod_content = format!(
629            r#"//! Automatically generated code module
630//!
631//! This module is automatically generated by the `actr gen` command, including:
632//! - protobuf message type definitions
633//! - Actor framework code (router, traits)
634//!
635//! ⚠️ Do not manually modify files in this directory
636
637// Protobuf message types (generated by prost)
638{}
639
640// Actor framework code (generated by protoc-gen-actrframework)
641{}
642
643// Common types are defined in their respective modules, please import as needed
644"#,
645            proto_modules.join("\n"),
646            service_modules.join("\n"),
647        );
648
649        std::fs::write(&mod_path, mod_content)
650            .map_err(|e| ActrCliError::config_error(format!("Failed to write mod.rs: {e}")))?;
651
652        debug!("Generated mod.rs: {:?}", mod_path);
653        Ok(())
654    }
655
656    /// 将生成目录中的文件设置为只读
657    fn set_generated_files_readonly(&self) -> Result<()> {
658        use std::fs;
659
660        let output = self.determine_output_path()?;
661        for entry in fs::read_dir(&output).map_err(|e| {
662            ActrCliError::config_error(format!("Failed to read output directory: {e}"))
663        })? {
664            let entry = entry.map_err(|e| ActrCliError::config_error(e.to_string()))?;
665            let path = entry.path();
666
667            if path.is_file() && path.extension().unwrap_or_default() == "rs" {
668                // 获取当前权限
669                let metadata = fs::metadata(&path).map_err(|e| {
670                    ActrCliError::config_error(format!("Failed to get file metadata: {e}"))
671                })?;
672                let mut permissions = metadata.permissions();
673
674                // 设置只读(移除写权限)
675                #[cfg(unix)]
676                {
677                    use std::os::unix::fs::PermissionsExt;
678                    let mode = permissions.mode();
679                    permissions.set_mode(mode & !0o222); // 移除所有写权限
680                }
681
682                #[cfg(not(unix))]
683                {
684                    permissions.set_readonly(true);
685                }
686
687                fs::set_permissions(&path, permissions).map_err(|e| {
688                    ActrCliError::config_error(format!("Failed to set file permissions: {e}"))
689                })?;
690
691                debug!("Set read-only attribute: {:?}", path);
692            }
693        }
694
695        Ok(())
696    }
697
698    /// Generate user code scaffold
699    async fn generate_user_code_scaffold(&self, proto_files: &[PathBuf]) -> Result<()> {
700        info!("📝 Generating user code scaffold...");
701
702        for proto_file in proto_files {
703            let service_name = proto_file
704                .file_stem()
705                .and_then(|s| s.to_str())
706                .ok_or_else(|| ActrCliError::config_error("Invalid proto file name"))?;
707
708            self.generate_service_scaffold(service_name).await?;
709        }
710
711        info!("✅ User code scaffold generation completed");
712        Ok(())
713    }
714
715    /// Generate scaffold for a specific service
716    async fn generate_service_scaffold(&self, service_name: &str) -> Result<()> {
717        let output = self.determine_output_path()?;
718        let user_file_path = output
719            .parent()
720            .unwrap_or_else(|| Path::new("src"))
721            .join(format!("{}_service.rs", service_name.to_lowercase()));
722
723        // If file exists and overwrite is not forced, skip
724        if user_file_path.exists() && !self.overwrite_user_code {
725            info!("⏭️  Skipping existing user code file: {:?}", user_file_path);
726            return Ok(());
727        }
728
729        let scaffold_content = self.generate_scaffold_content(service_name);
730
731        std::fs::write(&user_file_path, scaffold_content).map_err(|e| {
732            ActrCliError::config_error(format!("Failed to write user code scaffold: {e}"))
733        })?;
734
735        info!("📄 Generated user code scaffold: {:?}", user_file_path);
736        Ok(())
737    }
738
739    /// 生成用户代码框架内容
740    fn generate_scaffold_content(&self, service_name: &str) -> String {
741        let service_name_pascal = service_name
742            .split('_')
743            .map(|s| {
744                let mut chars = s.chars();
745                match chars.next() {
746                    None => String::new(),
747                    Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
748                }
749            })
750            .collect::<String>();
751
752        let template = format!(
753            r#"//! # {service_name_pascal} user business logic implementation
754//!
755//! This file is a user code scaffold automatically generated by the `actr gen` command.
756//! Please implement your specific business logic here.
757
758use crate::generated::{{{service_name_pascal}Handler, {service_name_pascal}Actor}};
759// 只导入必要的类型,避免拉入不需要的依赖如 sqlite
760// use actr_framework::prelude::*;
761use std::sync::Arc;
762
763/// Specific implementation of the {service_name_pascal} service
764/// 
765/// TODO: Add state fields you need, for example:
766/// - Database connection pool
767/// - Configuration information
768/// - Cache client
769/// - Logger, etc.
770pub struct My{service_name_pascal}Service {{
771    // TODO: Add your service state fields
772    // For example:
773    // pub db_pool: Arc<DatabasePool>,
774    // pub config: Arc<ServiceConfig>,
775    // pub metrics: Arc<Metrics>,
776}}
777
778impl My{service_name_pascal}Service {{
779    /// Create a new service instance
780    /// 
781    /// TODO: Modify constructor parameters as needed
782    pub fn new(/* TODO: Add necessary dependencies */) -> Self {{
783        Self {{
784            // TODO: Initialize your fields
785        }}
786    }}
787    
788    /// Create a service instance with default configuration (for testing)
789    pub fn default_for_testing() -> Self {{
790        Self {{
791            // TODO: Provide default values for testing
792        }}
793    }}
794}}
795
796// TODO: Implement all methods of the {service_name_pascal}Handler trait
797// Note: The impl_user_code_scaffold! macro has generated a basic scaffold for you,
798// you need to replace it with real business logic implementation.
799//
800// Example:
801// #[async_trait]
802// impl {service_name_pascal}Handler for My{service_name_pascal}Service {{
803//     async fn method_name(&self, req: RequestType) -> ActorResult<ResponseType> {{
804//         // 1. Validate input
805//         // 2. Execute business logic
806//         // 3. Return result
807//         todo!("Implement your business logic")
808//     }}
809// }}
810
811#[cfg(test)]
812mod tests {{
813    use super::*;
814    
815    #[tokio::test]
816    async fn test_service_creation() {{
817        let _service = My{service_name_pascal}Service::default_for_testing();
818        // TODO: Add your tests
819    }}
820    
821    // TODO: Add more test cases
822}}
823
824/*
825📚 User Guide
826
827## 🚀 Quick Start
828
8291. **Implement business logic**:
830   Implement all methods of the `{service_name_pascal}Handler` trait in `My{service_name_pascal}Service`
831
8322. **Add dependencies**:
833   Add dependencies you need in `Cargo.toml`, such as database clients, HTTP clients, etc.
834
8353. **Configure service**:
836   Modify the `new()` constructor to inject necessary dependencies
837
8384. **Start service**:
839   ```rust
840   #[tokio::main]
841   async fn main() -> ActorResult<()> {{
842       let service = My{service_name_pascal}Service::new(/* dependencies */);
843       
844       ActorSystem::new()
845           .attach(service)
846           .start()
847           .await
848   }}
849   ```
850
851## 🔧 Development Tips
852
853- Use `tracing` crate for logging
854- Implement error handling and retry logic
855- Add unit and integration tests
856- Consider using configuration files for environment variables
857- Implement health checks and metrics collection
858
859## 📖 More Resources
860
861- Actor-RTC Documentation: [Link]
862- API Reference: [Link]
863- Example Projects: [Link]
864*/
865"# // Service in example code
866        );
867
868        template
869    }
870
871    /// 格式化生成的代码
872    async fn format_generated_code(&self) -> Result<()> {
873        info!("🎨 Formatting generated code...");
874
875        let mut cmd = StdCommand::new("rustfmt");
876        cmd.arg("--edition")
877            .arg("2024")
878            .arg("--config")
879            .arg("max_width=100");
880
881        // 格式化生成目录中的所有 .rs 文件
882        let output = self.determine_output_path()?;
883        for entry in std::fs::read_dir(&output).map_err(|e| {
884            ActrCliError::config_error(format!("Failed to read output directory: {e}"))
885        })? {
886            let entry = entry.map_err(|e| ActrCliError::config_error(e.to_string()))?;
887            let path = entry.path();
888
889            if path.extension().unwrap_or_default() == "rs" {
890                cmd.arg(&path);
891            }
892        }
893
894        let output = cmd
895            .output()
896            .map_err(|e| ActrCliError::command_error(format!("Failed to execute rustfmt: {e}")))?;
897
898        if !output.status.success() {
899            let stderr = String::from_utf8_lossy(&output.stderr);
900            warn!("rustfmt execution warning: {}", stderr);
901        } else {
902            info!("✅ Code formatting completed");
903        }
904
905        Ok(())
906    }
907
908    /// 验证生成的代码
909    async fn validate_generated_code(&self) -> Result<()> {
910        info!("🔍 Validating generated code...");
911
912        // 查找项目根目录(包含 Cargo.toml 的目录)
913        let project_root = self.find_project_root()?;
914
915        let mut cmd = StdCommand::new("cargo");
916        cmd.arg("check").arg("--quiet").current_dir(&project_root);
917
918        let output = cmd.output().map_err(|e| {
919            ActrCliError::command_error(format!("Failed to execute cargo check: {e}"))
920        })?;
921
922        if !output.status.success() {
923            let stderr = String::from_utf8_lossy(&output.stderr);
924            warn!(
925                "Generated code has compilation warnings or errors:\n{}",
926                stderr
927            );
928            info!("💡 This is usually normal because the user code scaffold contains TODO markers");
929        } else {
930            info!("✅ Code validation passed");
931        }
932
933        Ok(())
934    }
935
936    /// 查找项目根目录(包含 Cargo.toml 的目录)
937    fn find_project_root(&self) -> Result<PathBuf> {
938        let mut current = std::env::current_dir().map_err(ActrCliError::Io)?;
939
940        loop {
941            if current.join("Cargo.toml").exists() {
942                return Ok(current);
943            }
944
945            match current.parent() {
946                Some(parent) => current = parent.to_path_buf(),
947                None => break,
948            }
949        }
950
951        // 如果找不到 Cargo.toml,回退到当前目录
952        std::env::current_dir().map_err(ActrCliError::Io)
953    }
954
955    /// 打印后续步骤提示
956    fn print_next_steps(&self) {
957        println!("\n🎉 Code generation completed!");
958        println!("\n📋 Next steps:");
959        let output = self
960            .determine_output_path()
961            .unwrap_or_else(|_| PathBuf::from("src/generated"));
962        println!("1. 📖 View generated code: {:?}", output);
963        if self.should_generate_scaffold() {
964            println!(
965                "2. ✏️  Implement business logic: in the *_service.rs files in the src/ directory"
966            );
967            println!("3. 🔧 Add dependencies: add required packages in Cargo.toml");
968            println!("4. 🏗️  Build project: cargo build");
969            println!("5. 🧪 Run tests: cargo test");
970            println!("6. 🚀 Start service: cargo run");
971        } else {
972            println!("2. 🏗️  Build project: cargo build");
973            println!("3. 🧪 Run tests: cargo test");
974            println!("4. 🚀 Start service: cargo run");
975        }
976        println!("\n💡 Tip: Check the detailed user guide in the generated user code files");
977    }
978}