1use crate::commands::Command;
9use crate::commands::SupportedLanguage;
10use crate::commands::codegen::{GenContext, execute_codegen};
11use crate::error::{ActrCliError, Result};
12use crate::plugin_config::{load_protoc_plugin_config, version_is_at_least};
13use crate::utils::to_pascal_case;
14use async_trait::async_trait;
17use clap::Args;
18use std::path::{Path, PathBuf};
19use std::process::Command as StdCommand;
20use tracing::{debug, info, warn};
21
22#[derive(Args, Debug, Clone)]
23#[command(
24 about = "Generate code from proto files",
25 after_help = "Default output paths by language:
26 - rust: src/generated
27 - swift: {PascalName}/Generated (e.g., EchoApp/Generated)
28 - kotlin: app/src/main/java/{package}/generated
29 - python: generated"
30)]
31pub struct GenCommand {
32 #[arg(short, long, default_value = "protos")]
34 pub input: PathBuf,
35
36 #[arg(short, long)]
38 pub output: Option<PathBuf>,
39
40 #[arg(short, long, default_value = "Actr.toml")]
42 pub config: PathBuf,
43
44 #[arg(long = "clean")]
46 pub clean: bool,
47
48 #[arg(long = "no-scaffold")]
50 pub no_scaffold: bool,
51
52 #[arg(long)]
54 pub overwrite_user_code: bool,
55
56 #[arg(long = "no-format")]
58 pub no_format: bool,
59
60 #[arg(long)]
62 pub debug: bool,
63
64 #[arg(short, long, default_value = "rust")]
66 pub language: SupportedLanguage,
67}
68
69#[async_trait]
70impl Command for GenCommand {
71 async fn execute(&self) -> Result<()> {
72 self.check_lock_file()?;
74
75 let output = self.determine_output_path()?;
77
78 info!(
79 "🚀 Start code generation (language: {:?})...",
80 self.language
81 );
82 let config = actr_config::ConfigParser::from_file(&self.config)
83 .map_err(|e| ActrCliError::config_error(format!("Failed to parse Actr.toml: {e}")))?;
84
85 let proto_files = self.preprocess()?;
86 if self.language != SupportedLanguage::Rust {
87 let context = GenContext {
88 proto_files,
89 input_path: self.input.clone(),
90 output,
91 config_path: self.config.clone(),
92 config: config.clone(),
93 no_scaffold: self.no_scaffold,
94 overwrite_user_code: self.overwrite_user_code,
95 no_format: self.no_format,
96 debug: self.debug,
97 };
98 execute_codegen(self.language, &context).await?;
99 return Ok(());
100 }
101
102 self.generate_infrastructure_code(&proto_files, &config)
104 .await?;
105
106 if self.should_generate_scaffold() {
108 self.generate_user_code_scaffold(&proto_files).await?;
109 }
110
111 if self.should_format() {
113 self.format_generated_code().await?;
114 }
115
116 self.validate_generated_code().await?;
118
119 info!("✅ Code generation completed!");
120 self.set_generated_files_readonly()?;
122 self.print_next_steps();
123
124 Ok(())
125 }
126}
127
128impl GenCommand {
129 fn check_lock_file(&self) -> Result<()> {
131 let config_dir = self
132 .config
133 .parent()
134 .unwrap_or_else(|| std::path::Path::new("."));
135 let lock_file_path = config_dir.join("Actr.lock.toml");
136
137 if !lock_file_path.exists() {
138 return Err(ActrCliError::config_error(
139 "Actr.lock.toml not found\n\n\
140 The lock file is required for code generation. Please run:\n\n\
141 \x20\x20\x20\x20actr install\n\n\
142 This will generate Actr.lock.toml based on your Actr.toml configuration.",
143 ));
144 }
145
146 Ok(())
147 }
148
149 fn determine_output_path(&self) -> Result<PathBuf> {
151 if let Some(ref output) = self.output {
153 return Ok(output.clone());
154 }
155
156 match self.language {
158 SupportedLanguage::Swift => {
159 let config = actr_config::ConfigParser::from_file(&self.config).map_err(|e| {
161 ActrCliError::config_error(format!("Failed to parse Actr.toml: {e}"))
162 })?;
163 let project_name = &config.package.name;
164 let pascal_name = to_pascal_case(project_name);
166 Ok(PathBuf::from(format!("{}/Generated", pascal_name)))
167 }
168 SupportedLanguage::Kotlin => {
169 let config = actr_config::ConfigParser::from_file(&self.config).map_err(|e| {
172 ActrCliError::config_error(format!("Failed to parse Actr.toml: {e}"))
173 })?;
174 let clean_name: String = config
177 .package
178 .name
179 .chars()
180 .filter(|c| c.is_alphanumeric())
181 .collect::<String>()
182 .to_lowercase();
183 let package_path = format!("io/actr/{}", clean_name);
184 Ok(PathBuf::from(format!(
185 "app/src/main/java/{}/generated",
186 package_path
187 )))
188 }
189 SupportedLanguage::Python => {
190 Ok(PathBuf::from("generated"))
192 }
193 SupportedLanguage::Rust => {
194 Ok(PathBuf::from("src/generated"))
196 }
197 }
198 }
199
200 fn preprocess(&self) -> Result<Vec<PathBuf>> {
201 self.validate_inputs()?;
203
204 self.clean_generated_outputs()?;
206
207 self.prepare_output_dirs()?;
209
210 let proto_files = self.discover_proto_files()?;
212 info!("📁 Found {} proto files", proto_files.len());
213
214 Ok(proto_files)
215 }
216
217 fn should_generate_scaffold(&self) -> bool {
219 !self.no_scaffold
220 }
221
222 fn should_format(&self) -> bool {
224 !self.no_format
225 }
226
227 fn clean_generated_outputs(&self) -> Result<()> {
229 use std::fs;
230
231 if !self.clean {
232 return Ok(());
233 }
234
235 let output = self.determine_output_path()?;
236 if !output.exists() {
237 return Ok(());
238 }
239
240 info!("🧹 Cleaning old generation results: {:?}", output);
241
242 self.make_writable_recursive(&output)?;
243 fs::remove_dir_all(&output).map_err(|e| {
244 ActrCliError::config_error(format!("Failed to delete generation directory: {e}"))
245 })?;
246
247 Ok(())
248 }
249
250 #[allow(clippy::only_used_in_recursion)]
252 fn make_writable_recursive(&self, path: &Path) -> Result<()> {
253 use std::fs;
254
255 if path.is_file() {
256 let metadata = fs::metadata(path).map_err(|e| {
257 ActrCliError::config_error(format!("Failed to read file metadata: {e}"))
258 })?;
259 let mut permissions = metadata.permissions();
260
261 #[cfg(unix)]
262 {
263 use std::os::unix::fs::PermissionsExt;
264 let mode = permissions.mode();
265 permissions.set_mode(mode | 0o222);
266 }
267
268 #[cfg(not(unix))]
269 {
270 permissions.set_readonly(false);
271 }
272
273 fs::set_permissions(path, permissions).map_err(|e| {
274 ActrCliError::config_error(format!("Failed to reset file permissions: {e}"))
275 })?;
276 } else if path.is_dir() {
277 for entry in fs::read_dir(path)
278 .map_err(|e| ActrCliError::config_error(format!("Failed to read directory: {e}")))?
279 {
280 let entry = entry.map_err(|e| ActrCliError::config_error(e.to_string()))?;
281 self.make_writable_recursive(&entry.path())?;
282 }
283 }
284
285 Ok(())
286 }
287
288 fn validate_inputs(&self) -> Result<()> {
290 if !self.input.exists() {
291 return Err(ActrCliError::config_error(format!(
292 "Input path does not exist: {:?}",
293 self.input
294 )));
295 }
296
297 if self.input.is_file() && self.input.extension().unwrap_or_default() != "proto" {
298 warn!("Input file is not a .proto file: {:?}", self.input);
299 }
300
301 Ok(())
302 }
303
304 fn prepare_output_dirs(&self) -> Result<()> {
306 let output = self.determine_output_path()?;
307 std::fs::create_dir_all(&output).map_err(|e| {
308 ActrCliError::config_error(format!("Failed to create output directory: {e}"))
309 })?;
310
311 if self.should_generate_scaffold() {
312 let user_code_dir = output.join("../");
313 std::fs::create_dir_all(&user_code_dir).map_err(|e| {
314 ActrCliError::config_error(format!("Failed to create user code directory: {e}"))
315 })?;
316 }
317
318 Ok(())
319 }
320
321 fn discover_proto_files(&self) -> Result<Vec<PathBuf>> {
323 let mut proto_files = Vec::new();
324
325 if self.input.is_file() {
326 proto_files.push(self.input.clone());
327 } else {
328 self.collect_proto_files(&self.input, &mut proto_files)?;
329 }
330
331 if proto_files.is_empty() {
332 return Err(ActrCliError::config_error("No proto files found"));
333 }
334
335 Ok(proto_files)
336 }
337
338 #[allow(clippy::only_used_in_recursion)]
340 fn collect_proto_files(&self, dir: &PathBuf, proto_files: &mut Vec<PathBuf>) -> Result<()> {
341 for entry in std::fs::read_dir(dir)
342 .map_err(|e| ActrCliError::config_error(format!("Failed to read directory: {e}")))?
343 {
344 let entry = entry.map_err(|e| ActrCliError::config_error(e.to_string()))?;
345 let path = entry.path();
346
347 if path.is_file() && path.extension().unwrap_or_default() == "proto" {
348 proto_files.push(path);
349 } else if path.is_dir() {
350 self.collect_proto_files(&path, proto_files)?;
351 }
352 }
353 Ok(())
354 }
355
356 fn ensure_protoc_plugin(&self) -> Result<PathBuf> {
364 const EXPECTED_VERSION: &str = env!("CARGO_PKG_VERSION");
366 const PLUGIN_NAME: &str = "protoc-gen-actrframework";
367
368 let min_version = self.resolve_plugin_min_version(PLUGIN_NAME)?;
369 let require_exact = min_version.is_none();
370 let required_version = min_version.unwrap_or_else(|| EXPECTED_VERSION.to_string());
371
372 let installed_version = self.check_installed_plugin_version()?;
374
375 match installed_version {
376 Some(version) if self.version_satisfies(&version, &required_version, require_exact) => {
377 info!("✅ Using installed protoc-gen-actrframework v{}", version);
379 let output = StdCommand::new("which")
380 .arg(PLUGIN_NAME)
381 .output()
382 .map_err(|e| {
383 ActrCliError::command_error(format!("Failed to locate plugin: {e}"))
384 })?;
385
386 let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
387 Ok(PathBuf::from(path))
388 }
389 Some(version) => {
390 if require_exact {
392 info!(
393 "🔄 Version mismatch: installed v{}, need v{}",
394 version, required_version
395 );
396 } else {
397 info!(
398 "🔄 Version below minimum: installed v{}, need >= v{}",
399 version, required_version
400 );
401 }
402 info!("🔨 Upgrading plugin...");
403 let path = self.install_or_upgrade_plugin()?;
404 self.ensure_required_plugin_version(&required_version, require_exact)?;
405 Ok(path)
406 }
407 None => {
408 info!("📦 protoc-gen-actrframework not found, installing...");
410 let path = self.install_or_upgrade_plugin()?;
411 self.ensure_required_plugin_version(&required_version, require_exact)?;
412 Ok(path)
413 }
414 }
415 }
416
417 fn check_installed_plugin_version(&self) -> Result<Option<String>> {
419 let output = StdCommand::new("protoc-gen-actrframework")
420 .arg("--version")
421 .output();
422
423 match output {
424 Ok(output) if output.status.success() => {
425 let version_info = String::from_utf8_lossy(&output.stdout);
426 let version = version_info
428 .lines()
429 .next()
430 .and_then(|line| line.split_whitespace().nth(1))
431 .map(|v| v.to_string());
432
433 debug!("Detected installed version: {:?}", version);
434 Ok(version)
435 }
436 _ => {
437 debug!("Plugin not found in PATH");
438 Ok(None)
439 }
440 }
441 }
442
443 fn install_or_upgrade_plugin(&self) -> Result<PathBuf> {
445 let current_dir = std::env::current_dir()?;
447 let workspace_root = current_dir.ancestors().find(|p| {
448 let is_workspace =
449 p.join("Cargo.toml").exists() && p.join("crates/framework-protoc-codegen").exists();
450 if is_workspace {
451 debug!("Found workspace root: {:?}", p);
452 }
453 is_workspace
454 });
455
456 let workspace_root = workspace_root.ok_or_else(|| {
457 ActrCliError::config_error(
458 "Cannot find actr workspace.\n\
459 Please run this command from within an actr project or workspace.",
460 )
461 })?;
462
463 info!("🔍 Found actr workspace at: {}", workspace_root.display());
464
465 info!("🔨 Building protoc-gen-actrframework...");
467 let mut build_cmd = StdCommand::new("cargo");
468 build_cmd
469 .arg("build")
470 .arg("-p")
471 .arg("actr-framework-protoc-codegen")
472 .arg("--bin")
473 .arg("protoc-gen-actrframework")
474 .current_dir(workspace_root);
475
476 debug!("Running: {:?}", build_cmd);
477 let output = build_cmd
478 .output()
479 .map_err(|e| ActrCliError::command_error(format!("Failed to build plugin: {e}")))?;
480
481 if !output.status.success() {
482 let stderr = String::from_utf8_lossy(&output.stderr);
483 return Err(ActrCliError::command_error(format!(
484 "Failed to build plugin:\n{stderr}"
485 )));
486 }
487
488 info!("📦 Installing to ~/.cargo/bin/...");
490 let mut install_cmd = StdCommand::new("cargo");
491 install_cmd
492 .arg("install")
493 .arg("--path")
494 .arg(workspace_root.join("crates/framework-protoc-codegen"))
495 .arg("--bin")
496 .arg("protoc-gen-actrframework")
497 .arg("--force"); debug!("Running: {:?}", install_cmd);
500 let output = install_cmd
501 .output()
502 .map_err(|e| ActrCliError::command_error(format!("Failed to install plugin: {e}")))?;
503
504 if !output.status.success() {
505 let stderr = String::from_utf8_lossy(&output.stderr);
506 return Err(ActrCliError::command_error(format!(
507 "Failed to install plugin:\n{stderr}"
508 )));
509 }
510
511 info!("✅ Plugin installed successfully");
512
513 let which_output = StdCommand::new("which")
515 .arg("protoc-gen-actrframework")
516 .output()
517 .map_err(|e| {
518 ActrCliError::command_error(format!("Failed to locate installed plugin: {e}"))
519 })?;
520
521 let path = String::from_utf8_lossy(&which_output.stdout)
522 .trim()
523 .to_string();
524 Ok(PathBuf::from(path))
525 }
526
527 fn resolve_plugin_min_version(&self, plugin_name: &str) -> Result<Option<String>> {
528 let config = load_protoc_plugin_config(&self.config)?;
529 if let Some(config) = config
530 && let Some(min_version) = config.min_version(plugin_name)
531 {
532 info!(
533 "🔧 Using minimum version for {} from {}",
534 plugin_name,
535 config.path().display()
536 );
537 return Ok(Some(min_version.to_string()));
538 }
539 Ok(None)
540 }
541
542 fn version_satisfies(&self, installed: &str, required: &str, strict_equal: bool) -> bool {
543 if strict_equal {
544 installed == required
545 } else {
546 version_is_at_least(installed, required)
547 }
548 }
549
550 fn ensure_required_plugin_version(
551 &self,
552 required_version: &str,
553 strict_equal: bool,
554 ) -> Result<()> {
555 let installed_version = self.check_installed_plugin_version()?;
556 let Some(installed_version) = installed_version else {
557 return Err(ActrCliError::command_error(
558 "Failed to determine installed protoc-gen-actrframework version after install"
559 .to_string(),
560 ));
561 };
562
563 if self.version_satisfies(&installed_version, required_version, strict_equal) {
564 return Ok(());
565 }
566
567 if strict_equal {
568 Err(ActrCliError::command_error(format!(
569 "protoc-gen-actrframework version {} does not match required version {}",
570 installed_version, required_version
571 )))
572 } else {
573 Err(ActrCliError::command_error(format!(
574 "protoc-gen-actrframework version {} is lower than minimum version {}",
575 installed_version, required_version
576 )))
577 }
578 }
579
580 async fn generate_infrastructure_code(
582 &self,
583 proto_files: &[PathBuf],
584 config: &actr_config::Config,
585 ) -> Result<()> {
586 info!("🔧 Generating infrastructure code...");
587
588 let plugin_path = self.ensure_protoc_plugin()?;
590
591 let manufacturer = config.package.actr_type.manufacturer.clone();
592 debug!("Using manufacturer from Actr.toml: {}", manufacturer);
593
594 let output = self.determine_output_path()?;
596
597 for proto_file in proto_files {
598 debug!("Processing proto file: {:?}", proto_file);
599
600 let mut cmd = StdCommand::new("protoc");
602 cmd.arg(format!("--proto_path={}", self.input.display()))
603 .arg("--prost_opt=flat_output_dir")
604 .arg(format!("--prost_out={}", output.display()))
605 .arg(proto_file);
606
607 debug!("Executing protoc (prost): {:?}", cmd);
608 let output_cmd = cmd.output().map_err(|e| {
609 ActrCliError::command_error(format!("Failed to execute protoc (prost): {e}"))
610 })?;
611
612 if !output_cmd.status.success() {
613 let stderr = String::from_utf8_lossy(&output_cmd.stderr);
614 return Err(ActrCliError::command_error(format!(
615 "protoc (prost) execution failed: {stderr}"
616 )));
617 }
618
619 let mut cmd = StdCommand::new("protoc");
621 cmd.arg(format!("--proto_path={}", self.input.display()))
622 .arg(format!(
623 "--plugin=protoc-gen-actrframework={}",
624 plugin_path.display()
625 ))
626 .arg(format!("--actrframework_opt=manufacturer={manufacturer}"))
627 .arg(format!("--actrframework_out={}", output.display()))
628 .arg(proto_file);
629
630 debug!("Executing protoc (actrframework): {:?}", cmd);
631 let output_cmd = cmd.output().map_err(|e| {
632 ActrCliError::command_error(format!(
633 "Failed to execute protoc (actrframework): {e}"
634 ))
635 })?;
636
637 if !output_cmd.status.success() {
638 let stderr = String::from_utf8_lossy(&output_cmd.stderr);
639 return Err(ActrCliError::command_error(format!(
640 "protoc (actrframework) execution failed: {stderr}"
641 )));
642 }
643
644 let stdout = String::from_utf8_lossy(&output_cmd.stdout);
645 if !stdout.is_empty() {
646 debug!("protoc output: {}", stdout);
647 }
648 }
649
650 self.generate_mod_rs(proto_files).await?;
652
653 info!("✅ Infrastructure code generation completed");
654 Ok(())
655 }
656
657 async fn generate_mod_rs(&self, _proto_files: &[PathBuf]) -> Result<()> {
659 let output = self.determine_output_path()?;
660 let mod_path = output.join("mod.rs");
661
662 let mut proto_modules = Vec::new();
664 let mut service_modules = Vec::new();
665
666 use std::fs;
667 for entry in fs::read_dir(&output).map_err(|e| {
668 ActrCliError::config_error(format!("Failed to read output directory: {e}"))
669 })? {
670 let entry = entry.map_err(|e| ActrCliError::config_error(e.to_string()))?;
671 let path = entry.path();
672
673 if path.is_file()
674 && path.extension().unwrap_or_default() == "rs"
675 && let Some(file_name) = path.file_stem().and_then(|s| s.to_str())
676 {
677 if file_name == "mod" {
679 continue;
680 }
681
682 if file_name.ends_with("_service_actor") {
684 service_modules.push(format!("pub mod {file_name};"));
685 } else {
686 proto_modules.push(format!("pub mod {file_name};"));
687 }
688 }
689 }
690
691 proto_modules.sort();
693 service_modules.sort();
694
695 let mod_content = format!(
696 r#"//! Automatically generated code module
697//!
698//! This module is automatically generated by the `actr gen` command, including:
699//! - protobuf message type definitions
700//! - Actor framework code (router, traits)
701//!
702//! ⚠️ Do not manually modify files in this directory
703
704// Protobuf message types (generated by prost)
705{}
706
707// Actor framework code (generated by protoc-gen-actrframework)
708{}
709
710// Common types are defined in their respective modules, please import as needed
711"#,
712 proto_modules.join("\n"),
713 service_modules.join("\n"),
714 );
715
716 std::fs::write(&mod_path, mod_content)
717 .map_err(|e| ActrCliError::config_error(format!("Failed to write mod.rs: {e}")))?;
718
719 debug!("Generated mod.rs: {:?}", mod_path);
720 Ok(())
721 }
722
723 fn set_generated_files_readonly(&self) -> Result<()> {
725 use std::fs;
726
727 let output = self.determine_output_path()?;
728 for entry in fs::read_dir(&output).map_err(|e| {
729 ActrCliError::config_error(format!("Failed to read output directory: {e}"))
730 })? {
731 let entry = entry.map_err(|e| ActrCliError::config_error(e.to_string()))?;
732 let path = entry.path();
733
734 if path.is_file() && path.extension().unwrap_or_default() == "rs" {
735 let metadata = fs::metadata(&path).map_err(|e| {
737 ActrCliError::config_error(format!("Failed to get file metadata: {e}"))
738 })?;
739 let mut permissions = metadata.permissions();
740
741 #[cfg(unix)]
743 {
744 use std::os::unix::fs::PermissionsExt;
745 let mode = permissions.mode();
746 permissions.set_mode(mode & !0o222); }
748
749 #[cfg(not(unix))]
750 {
751 permissions.set_readonly(true);
752 }
753
754 fs::set_permissions(&path, permissions).map_err(|e| {
755 ActrCliError::config_error(format!("Failed to set file permissions: {e}"))
756 })?;
757
758 debug!("Set read-only attribute: {:?}", path);
759 }
760 }
761
762 Ok(())
763 }
764
765 async fn generate_user_code_scaffold(&self, proto_files: &[PathBuf]) -> Result<()> {
767 info!("📝 Generating user code scaffold...");
768
769 for proto_file in proto_files {
770 let service_name = proto_file
771 .file_stem()
772 .and_then(|s| s.to_str())
773 .ok_or_else(|| ActrCliError::config_error("Invalid proto file name"))?;
774
775 self.generate_service_scaffold(service_name).await?;
776 }
777
778 info!("✅ User code scaffold generation completed");
779 Ok(())
780 }
781
782 async fn generate_service_scaffold(&self, service_name: &str) -> Result<()> {
784 let output = self.determine_output_path()?;
785 let user_file_path = output
786 .parent()
787 .unwrap_or_else(|| Path::new("src"))
788 .join(format!("{}_service.rs", service_name.to_lowercase()));
789
790 if user_file_path.exists() && !self.overwrite_user_code {
792 info!("⏭️ Skipping existing user code file: {:?}", user_file_path);
793 return Ok(());
794 }
795
796 let scaffold_content = self.generate_scaffold_content(service_name);
797
798 std::fs::write(&user_file_path, scaffold_content).map_err(|e| {
799 ActrCliError::config_error(format!("Failed to write user code scaffold: {e}"))
800 })?;
801
802 info!("📄 Generated user code scaffold: {:?}", user_file_path);
803 Ok(())
804 }
805
806 fn generate_scaffold_content(&self, service_name: &str) -> String {
808 let service_name_pascal = service_name
809 .split('_')
810 .map(|s| {
811 let mut chars = s.chars();
812 match chars.next() {
813 None => String::new(),
814 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
815 }
816 })
817 .collect::<String>();
818
819 let template = format!(
820 r#"//! # {service_name_pascal} user business logic implementation
821//!
822//! This file is a user code scaffold automatically generated by the `actr gen` command.
823//! Please implement your specific business logic here.
824
825use crate::generated::{{{service_name_pascal}Handler, {service_name_pascal}Actor}};
826// 只导入必要的类型,避免拉入不需要的依赖如 sqlite
827// use actr_framework::prelude::*;
828use std::sync::Arc;
829
830/// Specific implementation of the {service_name_pascal} service
831///
832/// TODO: Add state fields you need, for example:
833/// - Database connection pool
834/// - Configuration information
835/// - Cache client
836/// - Logger, etc.
837pub struct My{service_name_pascal}Service {{
838 // TODO: Add your service state fields
839 // For example:
840 // pub db_pool: Arc<DatabasePool>,
841 // pub config: Arc<ServiceConfig>,
842 // pub metrics: Arc<Metrics>,
843}}
844
845impl My{service_name_pascal}Service {{
846 /// Create a new service instance
847 ///
848 /// TODO: Modify constructor parameters as needed
849 pub fn new(/* TODO: Add necessary dependencies */) -> Self {{
850 Self {{
851 // TODO: Initialize your fields
852 }}
853 }}
854
855 /// Create a service instance with default configuration (for testing)
856 pub fn default_for_testing() -> Self {{
857 Self {{
858 // TODO: Provide default values for testing
859 }}
860 }}
861}}
862
863// TODO: Implement all methods of the {service_name_pascal}Handler trait
864// Note: The impl_user_code_scaffold! macro has generated a basic scaffold for you,
865// you need to replace it with real business logic implementation.
866//
867// Example:
868// #[async_trait]
869// impl {service_name_pascal}Handler for My{service_name_pascal}Service {{
870// async fn method_name(&self, req: RequestType) -> ActorResult<ResponseType> {{
871// // 1. Validate input
872// // 2. Execute business logic
873// // 3. Return result
874// todo!("Implement your business logic")
875// }}
876// }}
877
878#[cfg(test)]
879mod tests {{
880 use super::*;
881
882 #[tokio::test]
883 async fn test_service_creation() {{
884 let _service = My{service_name_pascal}Service::default_for_testing();
885 // TODO: Add your tests
886 }}
887
888 // TODO: Add more test cases
889}}
890
891/*
892📚 User Guide
893
894## 🚀 Quick Start
895
8961. **Implement business logic**:
897 Implement all methods of the `{service_name_pascal}Handler` trait in `My{service_name_pascal}Service`
898
8992. **Add dependencies**:
900 Add dependencies you need in `Cargo.toml`, such as database clients, HTTP clients, etc.
901
9023. **Configure service**:
903 Modify the `new()` constructor to inject necessary dependencies
904
9054. **Start service**:
906 ```rust
907 #[tokio::main]
908 async fn main() -> ActorResult<()> {{
909 let service = My{service_name_pascal}Service::new(/* dependencies */);
910
911 ActorSystem::new()
912 .attach(service)
913 .start()
914 .await
915 }}
916 ```
917
918## 🔧 Development Tips
919
920- Use `tracing` crate for logging
921- Implement error handling and retry logic
922- Add unit and integration tests
923- Consider using configuration files for environment variables
924- Implement health checks and metrics collection
925
926## 📖 More Resources
927
928- Actor-RTC Documentation: [Link]
929- API Reference: [Link]
930- Example Projects: [Link]
931*/
932"# );
934
935 template
936 }
937
938 async fn format_generated_code(&self) -> Result<()> {
940 info!("🎨 Formatting generated code...");
941
942 let mut cmd = StdCommand::new("rustfmt");
943 cmd.arg("--edition")
944 .arg("2024")
945 .arg("--config")
946 .arg("max_width=100");
947
948 let output = self.determine_output_path()?;
950 for entry in std::fs::read_dir(&output).map_err(|e| {
951 ActrCliError::config_error(format!("Failed to read output directory: {e}"))
952 })? {
953 let entry = entry.map_err(|e| ActrCliError::config_error(e.to_string()))?;
954 let path = entry.path();
955
956 if path.extension().unwrap_or_default() == "rs" {
957 cmd.arg(&path);
958 }
959 }
960
961 let output = cmd
962 .output()
963 .map_err(|e| ActrCliError::command_error(format!("Failed to execute rustfmt: {e}")))?;
964
965 if !output.status.success() {
966 let stderr = String::from_utf8_lossy(&output.stderr);
967 warn!("rustfmt execution warning: {}", stderr);
968 } else {
969 info!("✅ Code formatting completed");
970 }
971
972 Ok(())
973 }
974
975 async fn validate_generated_code(&self) -> Result<()> {
977 info!("🔍 Validating generated code...");
978
979 let project_root = self.find_project_root()?;
981
982 let mut cmd = StdCommand::new("cargo");
983 cmd.arg("check").arg("--quiet").current_dir(&project_root);
984
985 let output = cmd.output().map_err(|e| {
986 ActrCliError::command_error(format!("Failed to execute cargo check: {e}"))
987 })?;
988
989 if !output.status.success() {
990 let stderr = String::from_utf8_lossy(&output.stderr);
991 warn!(
992 "Generated code has compilation warnings or errors:\n{}",
993 stderr
994 );
995 info!("💡 This is usually normal because the user code scaffold contains TODO markers");
996 } else {
997 info!("✅ Code validation passed");
998 }
999
1000 Ok(())
1001 }
1002
1003 fn find_project_root(&self) -> Result<PathBuf> {
1005 let mut current = std::env::current_dir().map_err(ActrCliError::Io)?;
1006
1007 loop {
1008 if current.join("Cargo.toml").exists() {
1009 return Ok(current);
1010 }
1011
1012 match current.parent() {
1013 Some(parent) => current = parent.to_path_buf(),
1014 None => break,
1015 }
1016 }
1017
1018 std::env::current_dir().map_err(ActrCliError::Io)
1020 }
1021
1022 fn print_next_steps(&self) {
1024 println!("\n🎉 Code generation completed!");
1025 println!("\n📋 Next steps:");
1026 let output = self
1027 .determine_output_path()
1028 .unwrap_or_else(|_| PathBuf::from("src/generated"));
1029 println!("1. 📖 View generated code: {:?}", output);
1030 if self.should_generate_scaffold() {
1031 println!(
1032 "2. ✏️ Implement business logic: in the *_service.rs files in the src/ directory"
1033 );
1034 println!("3. 🔧 Add dependencies: add required packages in Cargo.toml");
1035 println!("4. 🏗️ Build project: cargo build");
1036 println!("5. 🧪 Run tests: cargo test");
1037 println!("6. 🚀 Start service: cargo run");
1038 } else {
1039 println!("2. 🏗️ Build project: cargo build");
1040 println!("3. 🧪 Run tests: cargo test");
1041 println!("4. 🚀 Start service: cargo run");
1042 }
1043 println!("\n💡 Tip: Check the detailed user guide in the generated user code files");
1044 }
1045}