1use 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;
13use 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 #[arg(short, long, default_value = "protos")]
33 pub input: PathBuf,
34
35 #[arg(short, long)]
37 pub output: Option<PathBuf>,
38
39 #[arg(short, long, default_value = "Actr.toml")]
41 pub config: PathBuf,
42
43 #[arg(long = "clean")]
45 pub clean: bool,
46
47 #[arg(long = "no-scaffold")]
49 pub no_scaffold: bool,
50
51 #[arg(long)]
53 pub overwrite_user_code: bool,
54
55 #[arg(long = "no-format")]
57 pub no_format: bool,
58
59 #[arg(long)]
61 pub debug: bool,
62
63 #[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 self.check_lock_file()?;
73
74 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 self.generate_infrastructure_code(&proto_files, &config)
102 .await?;
103
104 if self.should_generate_scaffold() {
106 self.generate_user_code_scaffold(&proto_files).await?;
107 }
108
109 if self.should_format() {
111 self.format_generated_code().await?;
112 }
113
114 self.validate_generated_code().await?;
116
117 info!("✅ Code generation completed!");
118 self.set_generated_files_readonly()?;
120 self.print_next_steps();
121
122 Ok(())
123 }
124}
125
126impl GenCommand {
127 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 fn determine_output_path(&self) -> Result<PathBuf> {
149 if let Some(ref output) = self.output {
151 return Ok(output.clone());
152 }
153
154 match self.language {
156 SupportedLanguage::Swift => {
157 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 let pascal_name = to_pascal_case(project_name);
164 Ok(PathBuf::from(format!("{}/Generated", pascal_name)))
165 }
166 SupportedLanguage::Kotlin => {
167 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 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 Ok(PathBuf::from("generated"))
190 }
191 SupportedLanguage::Rust => {
192 Ok(PathBuf::from("src/generated"))
194 }
195 }
196 }
197
198 fn preprocess(&self) -> Result<Vec<PathBuf>> {
199 self.validate_inputs()?;
201
202 self.clean_generated_outputs()?;
204
205 self.prepare_output_dirs()?;
207
208 let proto_files = self.discover_proto_files()?;
210 info!("📁 Found {} proto files", proto_files.len());
211
212 Ok(proto_files)
213 }
214
215 fn should_generate_scaffold(&self) -> bool {
217 !self.no_scaffold
218 }
219
220 fn should_format(&self) -> bool {
222 !self.no_format
223 }
224
225 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 #[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 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 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 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 #[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 fn ensure_protoc_plugin(&self) -> Result<PathBuf> {
366 const EXPECTED_VERSION: &str = env!("CARGO_PKG_VERSION");
368
369 let installed_version = self.check_installed_plugin_version()?;
371
372 match installed_version {
373 Some(version) if version == EXPECTED_VERSION => {
374 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 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 info!("📦 protoc-gen-actrframework not found, installing...");
398 self.install_or_upgrade_plugin()
399 }
400 }
401 }
402
403 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 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 fn install_or_upgrade_plugin(&self) -> Result<PathBuf> {
431 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 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 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"); 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 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 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 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 let output = self.determine_output_path()?;
529
530 for proto_file in proto_files {
531 debug!("Processing proto file: {:?}", proto_file);
532
533 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 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 self.generate_mod_rs(proto_files).await?;
585
586 info!("✅ Infrastructure code generation completed");
587 Ok(())
588 }
589
590 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 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 if file_name == "mod" {
612 continue;
613 }
614
615 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 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 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 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 #[cfg(unix)]
676 {
677 use std::os::unix::fs::PermissionsExt;
678 let mode = permissions.mode();
679 permissions.set_mode(mode & !0o222); }
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 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 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 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 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"# );
867
868 template
869 }
870
871 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 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 async fn validate_generated_code(&self) -> Result<()> {
910 info!("🔍 Validating generated code...");
911
912 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 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 std::env::current_dir().map_err(ActrCliError::Io)
953 }
954
955 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}