1use std::path::{Path, PathBuf};
18
19use clap::Subcommand;
20
21use crate::error::CliError;
22use crate::stubs::{self, render_template};
23
24#[derive(Subcommand, Debug)]
28pub enum MakeCommand {
29 #[command(name = "model")]
31 Model {
32 name: String,
34 },
35
36 #[command(name = "controller")]
38 Controller {
39 name: String,
41 #[arg(long)]
43 api: bool,
44 #[arg(long)]
46 plain: bool,
47 },
48
49 #[command(name = "migration")]
51 Migration {
52 name: String,
54 #[arg(short = 'p', long, default_value = "migrations")]
56 path: String,
57 },
58
59 #[command(name = "seeder")]
63 Seeder {
64 name: String,
66 #[arg(short = 'p', long, default_value = "seeds")]
68 path: String,
69 },
70
71 #[command(name = "guard")]
73 Guard {
74 name: String,
76 },
77
78 #[command(name = "validate")]
82 Validate {
83 name: String,
85 },
86
87 #[command(name = "event")]
91 Event {
92 name: String,
94 },
95
96 #[command(name = "listener")]
100 Listener {
101 name: String,
103 #[arg(long)]
105 event: Option<String>,
106 },
107
108 #[command(name = "command")]
112 Command {
113 name: String,
115 },
116
117 #[command(name = "service")]
121 Service {
122 name: String,
124 },
125
126 #[command(name = "middleware")]
131 Middleware {
132 name: String,
134 },
135
136 #[command(name = "scaffold")]
138 Scaffold {
139 name: String,
141 },
142
143 #[command(name = "plugin")]
149 Plugin {
150 #[arg(long)]
152 template: String,
153 #[arg(long)]
155 name: String,
156 #[arg(long)]
158 table: Option<String>,
159 #[arg(long)]
161 fields: Option<String>,
162 #[arg(long)]
164 force: bool,
165 #[arg(long)]
167 output: Option<String>,
168 #[arg(long)]
170 master: Option<String>,
171 #[arg(long)]
173 slave: Option<String>,
174 #[arg(long)]
176 master_fields: Option<String>,
177 #[arg(long)]
179 slave_fields: Option<String>,
180 #[arg(long)]
182 foreign_key: Option<String>,
183 },
184
185 #[command(name = "frontend")]
190 Frontend {
191 #[arg(long = "model")]
193 models: Vec<String>,
194 #[arg(long = "model-dir", default_value = "src/model/")]
196 model_dir: String,
197 #[arg(long, default_value = "vue")]
199 framework: String,
200 #[arg(long = "ui", default_value = "element_plus")]
202 ui: String,
203 #[arg(long, default_value = "./frontend/")]
205 output: String,
206 #[arg(long = "template-dir")]
208 template_dir: Option<String>,
209 #[arg(long = "override", default_value = "skip")]
211 override_strategy: String,
212 #[arg(long = "with-tests")]
214 with_tests: bool,
215 #[arg(long = "with-interceptors")]
217 with_interceptors: bool,
218 #[arg(long = "lazy-load", default_value_t = true)]
220 lazy_load: bool,
221 #[arg(long)]
223 force: bool,
224 },
225
226 #[command(name = "openapi")]
230 Openapi {
231 #[arg(short = 'o', long, default_value = "openapi.json")]
233 output: String,
234 #[arg(long, default_value = "SZ-Rust API")]
236 title: String,
237 #[arg(long, default_value = "1.0.0")]
239 version: String,
240 #[arg(long)]
242 force: bool,
243 },
244}
245
246pub async fn execute(cmd: &MakeCommand) -> Result<(), CliError> {
248 match cmd {
249 MakeCommand::Model { name } => execute_make_model(name),
250 MakeCommand::Controller { name, api, plain } => execute_make_controller(name, *api, *plain),
251 MakeCommand::Migration { name, path } => execute_make_migration(name, path),
252 MakeCommand::Seeder { name, path } => execute_make_seeder(name, path),
253 MakeCommand::Guard { name } => execute_make_guard(name),
254 MakeCommand::Validate { name } => execute_make_validate(name),
255 MakeCommand::Event { name } => execute_make_event(name),
256 MakeCommand::Listener { name, event } => execute_make_listener(name, event.as_deref()),
257 MakeCommand::Command { name } => execute_make_command(name),
258 MakeCommand::Service { name } => execute_make_service(name),
259 MakeCommand::Middleware { name } => execute_make_middleware(name),
260 MakeCommand::Scaffold { name } => execute_make_scaffold(name),
261 MakeCommand::Plugin {
262 template,
263 name,
264 table,
265 fields,
266 force,
267 output,
268 master,
269 slave,
270 master_fields,
271 slave_fields,
272 foreign_key,
273 } => {
274 execute_make_plugin(crate::context_builder::PluginCommandArgs {
275 template: template.clone(),
276 name: name.clone(),
277 table: table.clone(),
278 fields: fields.clone(),
279 force: *force,
280 output: output.clone(),
281 master: master.clone(),
282 slave: slave.clone(),
283 master_fields: master_fields.clone(),
284 slave_fields: slave_fields.clone(),
285 foreign_key: foreign_key.clone(),
286 })
287 .await
288 }
289 MakeCommand::Frontend {
290 models,
291 model_dir,
292 framework,
293 ui,
294 output,
295 template_dir,
296 override_strategy,
297 with_tests,
298 with_interceptors,
299 lazy_load,
300 force,
301 } => {
302 execute_make_frontend(
303 models,
304 model_dir,
305 framework,
306 ui,
307 output,
308 template_dir.as_deref(),
309 override_strategy,
310 *with_tests,
311 *with_interceptors,
312 *lazy_load,
313 *force,
314 )
315 .await
316 }
317 MakeCommand::Openapi {
318 output,
319 title,
320 version,
321 force,
322 } => execute_make_openapi(output, title, version, *force).await,
323 }
324}
325
326fn execute_make_model(name: &str) -> Result<(), CliError> {
330 let (class_name, module_path, file_path) = resolve_target(name, "model");
331 check_file_exists(&file_path)?;
332
333 let namespace = format!("app::{}", module_path);
334 let table_name = class_to_snake(&class_name);
335 let content = render_template(
336 stubs::MODEL_STUB,
337 &[
338 ("{%className%}", &class_name),
339 ("{%namespace%}", &namespace),
340 ("{%table_name%}", &table_name),
341 ],
342 );
343
344 write_file(&file_path, &content)?;
345 println!("Model created: {}", file_path.display());
346 Ok(())
347}
348
349fn execute_make_controller(name: &str, api: bool, plain: bool) -> Result<(), CliError> {
353 let (class_name, module_path, file_path) = resolve_target(name, "controller");
354 check_file_exists(&file_path)?;
355
356 let namespace = format!("app::{}", module_path);
357 let route = class_to_snake(&class_name);
358
359 let template = if plain {
360 stubs::CONTROLLER_PLAIN_STUB
361 } else if api {
362 stubs::CONTROLLER_API_STUB
363 } else {
364 stubs::CONTROLLER_STUB
365 };
366
367 let content = render_template(
368 template,
369 &[
370 ("{%className%}", &class_name),
371 ("{%namespace%}", &namespace),
372 ("{%route%}", &route),
373 ],
374 );
375
376 write_file(&file_path, &content)?;
377 println!("Controller created: {}", file_path.display());
378 Ok(())
379}
380
381fn execute_make_migration(name: &str, path: &str) -> Result<(), CliError> {
385 let dir = Path::new(path);
386 std::fs::create_dir_all(dir)?;
387
388 let timestamp = chrono::Utc::now().format("%Y%m%d%H%M%S").to_string();
389 let table_name = name_to_table(name);
390
391 let up_file = dir.join(format!("{}_{}_up.sql", timestamp, name));
392 let down_file = dir.join(format!("{}_{}_down.sql", timestamp, name));
393
394 check_file_exists(&up_file)?;
395 check_file_exists(&down_file)?;
396
397 let up_content = render_template(
398 stubs::MIGRATION_UP_STUB,
399 &[
400 ("{%name%}", name),
401 ("{%timestamp%}", ×tamp),
402 ("{%table_name%}", &table_name),
403 ],
404 );
405 let down_content = render_template(
406 stubs::MIGRATION_DOWN_STUB,
407 &[
408 ("{%name%}", name),
409 ("{%timestamp%}", ×tamp),
410 ("{%table_name%}", &table_name),
411 ],
412 );
413
414 write_file(&up_file, &up_content)?;
415 write_file(&down_file, &down_content)?;
416 println!(
417 "Migration created: {} & {}",
418 up_file.display(),
419 down_file.display()
420 );
421 Ok(())
422}
423
424fn execute_make_seeder(name: &str, path: &str) -> Result<(), CliError> {
428 let dir = Path::new(path);
429 std::fs::create_dir_all(dir)?;
430
431 let file_path = dir.join(format!("{}.sql", name));
432 check_file_exists(&file_path)?;
433
434 let timestamp = chrono::Utc::now()
435 .format("%Y-%m-%d %H:%M:%S UTC")
436 .to_string();
437 let content = render_template(
438 stubs::SEED_STUB,
439 &[("{%name%}", name), ("{%timestamp%}", ×tamp)],
440 );
441
442 write_file(&file_path, &content)?;
443 println!("Seeder created: {}", file_path.display());
444 Ok(())
445}
446
447fn execute_make_guard(name: &str) -> Result<(), CliError> {
449 let (class_name, _module_path, file_path) = resolve_target(name, "guard");
450 check_file_exists(&file_path)?;
451
452 let content = format!(
453 "//! Guard: {class_name}\n//!\n//! 由 `sz-rust make:guard` 生成。\n//!\n//! 对齐 NestJS Guard + Spring Security 模式。\n\nuse sz_rust_core::guard::Guard;\nuse sz_rust_core::request::Request;\n\n/// {class_name} Guard\npub struct {class_name};\n\nimpl Guard for {class_name} {{\n async fn can_activate(&self, _req: &Request) -> bool {{\n // 在此实现鉴权逻辑\n true\n }}\n}}\n"
454 );
455
456 write_file(&file_path, &content)?;
457 println!("Guard created: {}", file_path.display());
458 Ok(())
459}
460
461fn execute_make_validate(name: &str) -> Result<(), CliError> {
466 let (class_name, module_path, file_path) = resolve_target(name, "validate");
467 check_file_exists(&file_path)?;
468
469 let namespace = format!("app::{}", module_path);
470 let content = render_template(
471 stubs::VALIDATE_STUB,
472 &[
473 ("{%className%}", &class_name),
474 ("{%namespace%}", &namespace),
475 ],
476 );
477
478 write_file(&file_path, &content)?;
479 println!("Validator created: {}", file_path.display());
480 Ok(())
481}
482
483fn execute_make_event(name: &str) -> Result<(), CliError> {
487 let (class_name, module_path, file_path) = resolve_target(name, "event");
488 check_file_exists(&file_path)?;
489
490 let namespace = format!("app::{}", module_path);
491 let event_name = class_name.clone();
493 let content = render_template(
494 stubs::EVENT_STUB,
495 &[
496 ("{%className%}", &class_name),
497 ("{%namespace%}", &namespace),
498 ("{%event_name%}", &event_name),
499 ],
500 );
501
502 write_file(&file_path, &content)?;
503 println!("Event created: {}", file_path.display());
504 Ok(())
505}
506
507fn execute_make_listener(name: &str, event: Option<&str>) -> Result<(), CliError> {
512 let (class_name, module_path, file_path) = resolve_target(name, "listener");
513 check_file_exists(&file_path)?;
514
515 let namespace = format!("app::{}", module_path);
516 let event_name = event.unwrap_or(&class_name).to_string();
518 let content = render_template(
519 stubs::LISTENER_STUB,
520 &[
521 ("{%className%}", &class_name),
522 ("{%namespace%}", &namespace),
523 ("{%event_name%}", &event_name),
524 ],
525 );
526
527 write_file(&file_path, &content)?;
528 println!("Listener created: {}", file_path.display());
529 Ok(())
530}
531
532fn execute_make_command(name: &str) -> Result<(), CliError> {
537 let (class_name, module_path, file_path) = resolve_target(name, "command");
538 check_file_exists(&file_path)?;
539
540 let namespace = format!("app::{}", module_path);
541 let command_name = class_to_snake(&class_name);
543 let content = render_template(
544 stubs::COMMAND_STUB,
545 &[
546 ("{%className%}", &class_name),
547 ("{%namespace%}", &namespace),
548 ("{%command_name%}", &command_name),
549 ],
550 );
551
552 write_file(&file_path, &content)?;
553 println!("Command created: {}", file_path.display());
554 Ok(())
555}
556
557fn execute_make_service(name: &str) -> Result<(), CliError> {
561 let (class_name, module_path, file_path) = resolve_target(name, "service");
562 check_file_exists(&file_path)?;
563
564 let namespace = format!("app::{}", module_path);
565 let content = render_template(
566 stubs::SERVICE_STUB,
567 &[
568 ("{%className%}", &class_name),
569 ("{%namespace%}", &namespace),
570 ],
571 );
572
573 write_file(&file_path, &content)?;
574 println!("Service created: {}", file_path.display());
575 Ok(())
576}
577
578fn execute_make_middleware(name: &str) -> Result<(), CliError> {
583 let (class_name, module_path, file_path) = resolve_target(name, "middleware");
584 check_file_exists(&file_path)?;
585
586 let namespace = format!("app::{}", module_path);
587 let content = render_template(
588 stubs::MIDDLEWARE_STUB,
589 &[
590 ("{%className%}", &class_name),
591 ("{%namespace%}", &namespace),
592 ],
593 );
594
595 write_file(&file_path, &content)?;
596 println!("Middleware created: {}", file_path.display());
597 Ok(())
598}
599
600fn execute_make_scaffold(name: &str) -> Result<(), CliError> {
602 println!("Scaffolding for: {}", name);
603 execute_make_model(name)?;
604 execute_make_controller(name, false, false)?;
605 execute_make_migration(&class_to_snake(name), "migrations")?;
606 println!("Scaffold complete.");
607 Ok(())
608}
609
610fn resolve_target(name: &str, layer: &str) -> (String, String, PathBuf) {
622 let (app, class_part) = if let Some(idx) = name.find('@') {
624 (&name[..idx], &name[idx + 1..])
625 } else {
626 ("", name)
627 };
628
629 let segments: Vec<&str> = class_part.split('/').collect();
631 let class_name = segments.last().unwrap_or(&"").to_string();
632
633 let parent_segments: Vec<&str> = if segments.len() > 1 {
635 segments[..segments.len() - 1].to_vec()
636 } else {
637 Vec::new()
638 };
639
640 let module_path = if app.is_empty() {
641 if parent_segments.is_empty() {
642 layer.to_string()
643 } else {
644 format!("{}::{}", layer, parent_segments.join("::"))
645 }
646 } else if parent_segments.is_empty() {
647 format!("{}::{}", app, layer)
648 } else {
649 format!("{}::{}::{}", app, layer, parent_segments.join("::"))
650 };
651
652 let mut path = PathBuf::from("app");
654 if !app.is_empty() {
655 path.push(app);
656 }
657 path.push(layer);
659 for seg in &parent_segments {
661 path.push(seg);
662 }
663 path.push(format!("{}.rs", class_name));
665
666 (class_name, module_path, path)
667}
668
669fn check_file_exists(path: &Path) -> Result<(), CliError> {
673 if path.exists() {
674 return Err(CliError::FileExists(path.display().to_string()));
675 }
676 Ok(())
677}
678
679fn write_file(path: &Path, content: &str) -> Result<(), CliError> {
683 if let Some(parent) = path.parent() {
684 std::fs::create_dir_all(parent)?;
685 }
686 std::fs::write(path, content)?;
687 Ok(())
688}
689
690fn class_to_snake(s: &str) -> String {
694 let mut result = String::new();
695 for (i, ch) in s.chars().enumerate() {
696 if ch.is_uppercase() && i > 0 {
697 result.push('_');
698 }
699 result.push(ch.to_lowercase().next().unwrap_or(ch));
700 }
701 result
702}
703
704fn name_to_table(name: &str) -> String {
708 if let Some(rest) = name.strip_prefix("create_") {
710 return rest.to_string();
711 }
712 if let Some(rest) = name.strip_prefix("add_") {
713 if let Some(to_pos) = rest.find("_to_") {
715 return rest[to_pos + 4..].to_string();
716 }
717 return rest.to_string();
718 }
719 name.to_string()
720}
721
722pub async fn execute_make_plugin(
727 args: crate::context_builder::PluginCommandArgs,
728) -> Result<(), CliError> {
729 use crate::context_builder::TemplateContextBuilder;
730 use crate::template_engine::TemplateEngine;
731 use crate::validator::InputValidator;
732
733 InputValidator::validate_plugin_name(&args.name)?;
734
735 if let Some(ref table) = args.table {
736 InputValidator::validate_table_name(table)?;
737 }
738 if let Some(ref fields) = args.fields {
739 InputValidator::validate_fields(fields)?;
740 }
741 if let Some(ref master) = args.master {
742 InputValidator::validate_table_name(master)?;
743 }
744 if let Some(ref slave) = args.slave {
745 InputValidator::validate_table_name(slave)?;
746 }
747
748 let template_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("templates");
749 let engine = TemplateEngine::init(&template_dir).await?;
750
751 engine.validate_template_type(&args.template)?;
752
753 let is_master_slave = args.template == "master-slave";
754
755 let ctx = if is_master_slave {
756 TemplateContextBuilder::new(args.clone()).build_master_slave()?
757 } else {
758 TemplateContextBuilder::new(args.clone()).build()?
759 };
760
761 let output_dir = args
762 .output
763 .as_ref()
764 .map(PathBuf::from)
765 .unwrap_or_else(|| PathBuf::from("plugins").join(&args.name));
766
767 if output_dir.exists() && !args.force {
768 return Err(CliError::DirExists(output_dir));
769 }
770
771 let template_files: &[(&str, &str)] = match args.template.as_str() {
772 "master-slave" => &[
773 (
774 "plugin-master-slave/master_model.rs.tera",
775 "src/master_model.rs",
776 ),
777 (
778 "plugin-master-slave/slave_model.rs.tera",
779 "src/slave_model.rs",
780 ),
781 (
782 "plugin-master-slave/master_controller.rs.tera",
783 "src/master_controller.rs",
784 ),
785 (
786 "plugin-master-slave/slave_controller.rs.tera",
787 "src/slave_controller.rs",
788 ),
789 (
790 "plugin-master-slave/cascade_service.rs.tera",
791 "src/cascade_service.rs",
792 ),
793 (
794 "plugin-master-slave/datasource_config.rs.tera",
795 "src/datasource_config.rs",
796 ),
797 (
798 "plugin-master-slave/migration.sql.tera",
799 "migrations/master_slave.sql",
800 ),
801 ("plugin-master-slave/manifest.json.tera", "manifest.json"),
802 ],
803 "workflow" => &[
804 ("plugin-workflow/model.rs.tera", "src/model.rs"),
805 ("plugin-workflow/controller.rs.tera", "src/controller.rs"),
806 ("plugin-workflow/routes.rs.tera", "src/routes.rs"),
807 ("plugin-workflow/migration.sql.tera", "migrations/table.sql"),
808 ("plugin-workflow/manifest.json.tera", "manifest.json"),
809 ("plugin-workflow/tests.rs.tera", "tests/workflow_test.rs"),
810 ],
811 "report" => &[
812 ("plugin-report/model.rs.tera", "src/model.rs"),
813 ("plugin-report/controller.rs.tera", "src/controller.rs"),
814 ("plugin-report/routes.rs.tera", "src/routes.rs"),
815 ("plugin-report/migration.sql.tera", "migrations/table.sql"),
816 ("plugin-report/manifest.json.tera", "manifest.json"),
817 ("plugin-report/tests.rs.tera", "tests/report_test.rs"),
818 ],
819 _ => &[
820 ("plugin-crud/model.rs.tera", "src/model.rs"),
821 ("plugin-crud/controller.rs.tera", "src/controller.rs"),
822 ("plugin-crud/service.rs.tera", "src/service.rs"),
823 ("plugin-crud/repository.rs.tera", "src/repository.rs"),
824 ("plugin-crud/migration.sql.tera", "migrations/table.sql"),
825 ("plugin-crud/routes.rs.tera", "src/routes.rs"),
826 ("plugin-crud/manifest.json.tera", "manifest.json"),
827 ("plugin-crud/tests.rs.tera", "tests/crud_test.rs"),
828 ],
829 };
830
831 let mut rendered_files: Vec<(PathBuf, String)> = Vec::new();
832 for (template_name, output_path) in template_files {
833 let content = engine.render(template_name, &ctx)?;
834 rendered_files.push((output_dir.join(output_path), content));
835 }
836
837 let safety_files: Vec<(String, String)> = rendered_files
838 .iter()
839 .map(|(p, c)| (p.display().to_string(), c.clone()))
840 .collect();
841 let violations = crate::safety_validator::SafetyValidator::validate_files(&safety_files);
842 if !violations.is_empty() {
843 let report = crate::safety_validator::SafetyValidator::format_report(&violations);
844 eprintln!("{report}");
845 return Err(CliError::Generic(format!(
846 "安全检查失败:{} 个违规项,已阻止生成",
847 violations.len()
848 )));
849 }
850
851 if output_dir.exists() && args.force {
852 tokio::fs::remove_dir_all(&output_dir).await?;
853 }
854
855 for (file_path, content) in &rendered_files {
856 if let Some(parent) = file_path.parent() {
857 tokio::fs::create_dir_all(parent).await?;
858 }
859 tokio::fs::write(file_path, content).await?;
860 }
861
862 let written_paths: Vec<std::path::PathBuf> =
863 rendered_files.iter().map(|(p, _)| p.clone()).collect();
864
865 let check_result = crate::cargo_checker::CargoChecker::check(&output_dir).await;
866 match check_result {
867 Ok(result) if result.success => {
868 println!(
869 "Plugin '{}' created successfully at: {}",
870 args.name,
871 output_dir.display()
872 );
873 println!("Files generated:");
874 for (file_path, _) in &rendered_files {
875 println!(" - {}", file_path.display());
876 }
877 println!("cargo check: PASSED");
878 Ok(())
879 }
880 Ok(result) => {
881 eprintln!("cargo check: FAILED");
882 eprintln!("Compilation errors:");
883 for err in &result.errors {
884 eprintln!(" {err}");
885 }
886
887 let failures = crate::cargo_checker::CargoChecker::rollback(&written_paths).await;
888 if !failures.is_empty() {
889 eprintln!(
890 "Warning: {} files could not be removed during rollback",
891 failures.len()
892 );
893 }
894
895 Err(CliError::CompileFailed(result.errors))
896 }
897 Err(e) => {
898 eprintln!("cargo check could not be executed: {e}");
899 eprintln!("Rolling back generated files...");
900
901 let failures = crate::cargo_checker::CargoChecker::rollback(&written_paths).await;
902 if !failures.is_empty() {
903 eprintln!(
904 "Warning: {} files could not be removed during rollback",
905 failures.len()
906 );
907 }
908
909 Err(e)
910 }
911 }
912}
913
914#[allow(clippy::too_many_arguments)]
919async fn execute_make_frontend(
920 models: &[String],
921 model_dir: &str,
922 framework: &str,
923 ui: &str,
924 output: &str,
925 template_dir: Option<&str>,
926 override_strategy: &str,
927 with_tests: bool,
928 with_interceptors: bool,
929 lazy_load: bool,
930 force: bool,
931) -> Result<(), CliError> {
932 use sz_rust_frontend_codegen::{
933 CodegenService, Framework, GenerationConfig, OverrideStrategy, UiLibrary,
934 };
935
936 let fw = match framework.to_lowercase().as_str() {
937 "vue" => Framework::Vue,
938 "react" => Framework::React,
939 other => {
940 return Err(CliError::Generic(format!(
941 "不支持的前端框架: {other}(可选: vue, react)"
942 )));
943 }
944 };
945
946 let ui_lib = match ui.to_lowercase().as_str() {
947 "element_plus" | "element-plus" => UiLibrary::ElementPlus,
948 "ant_design_vue" | "ant-design-vue" => UiLibrary::AntDesignVue,
949 other => {
950 return Err(CliError::Generic(format!(
951 "不支持的 UI 库: {other}(可选: element_plus, ant_design_vue)"
952 )));
953 }
954 };
955
956 let strategy = match override_strategy.to_lowercase().as_str() {
957 "skip" => OverrideStrategy::Skip,
958 "overwrite" => OverrideStrategy::Overwrite,
959 "merge" => OverrideStrategy::Merge,
960 other => {
961 return Err(CliError::Generic(format!(
962 "不支持的覆盖策略: {other}(可选: skip, overwrite, merge)"
963 )));
964 }
965 };
966
967 let config = GenerationConfig {
968 models: models.to_vec(),
969 model_dir: PathBuf::from(model_dir),
970 framework: fw,
971 ui_library: ui_lib,
972 output_dir: PathBuf::from(output),
973 template_dir: template_dir.map(PathBuf::from),
974 override_strategy: strategy,
975 with_tests,
976 with_interceptors,
977 lazy_load,
978 force,
979 };
980
981 let service = CodegenService::new();
982 let report = service
983 .generate(config)
984 .await
985 .map_err(|e| CliError::Generic(e.to_string()))?;
986 println!("{}", report.format_cli());
987 Ok(())
988}
989
990async fn execute_make_openapi(
996 output: &str,
997 title: &str,
998 version: &str,
999 force: bool,
1000) -> Result<(), CliError> {
1001 let output_path = PathBuf::from(output);
1002 check_file_exists_with_force(&output_path, force)?;
1003
1004 let spec = generate_openapi_spec(title, version);
1005 let json = serde_json::to_string_pretty(&spec)
1006 .map_err(|e| CliError::Generation(format!("OpenAPI serialize failed: {e}")))?;
1007
1008 write_file(&output_path, &json)?;
1009 println!("OpenAPI spec created: {}", output_path.display());
1010
1011 run_post_generation_check()?;
1012 Ok(())
1013}
1014
1015#[derive(Debug, serde::Serialize)]
1017struct OpenApiSpec {
1018 openapi: String,
1019 info: OpenApiInfo,
1020 paths: serde_json::Value,
1021 components: serde_json::Value,
1022}
1023
1024#[derive(Debug, serde::Serialize)]
1025struct OpenApiInfo {
1026 title: String,
1027 version: String,
1028 description: String,
1029}
1030
1031fn generate_openapi_spec(title: &str, version: &str) -> OpenApiSpec {
1033 OpenApiSpec {
1034 openapi: "3.0.3".to_string(),
1035 info: OpenApiInfo {
1036 title: title.to_string(),
1037 version: version.to_string(),
1038 description: "Generated by sz-rust make:openapi".to_string(),
1039 },
1040 paths: serde_json::json!({
1041 "/health": {
1042 "get": {
1043 "summary": "Health check",
1044 "responses": {
1045 "200": {"description": "Service healthy"}
1046 }
1047 }
1048 }
1049 }),
1050 components: serde_json::json!({
1051 "schemas": {},
1052 "securitySchemes": {
1053 "bearerAuth": {
1054 "type": "http",
1055 "scheme": "bearer"
1056 }
1057 }
1058 }),
1059 }
1060}
1061
1062fn check_file_exists_with_force(path: &Path, force: bool) -> Result<(), CliError> {
1068 if path.exists() && !force {
1069 return Err(CliError::FileExists(path.display().to_string()));
1070 }
1071 Ok(())
1072}
1073
1074fn run_post_generation_check() -> Result<(), CliError> {
1076 let fmt_result = std::process::Command::new("cargo")
1077 .args(["fmt", "--check"])
1078 .output();
1079
1080 if let Ok(output) = fmt_result {
1081 if !output.status.success() {
1082 eprintln!("⚠️ cargo fmt --check failed, run `cargo fmt` to fix");
1083 }
1084 }
1085
1086 let check_result = std::process::Command::new("cargo").args(["check"]).output();
1087
1088 if let Ok(output) = check_result {
1089 if !output.status.success() {
1090 let stderr = String::from_utf8_lossy(&output.stderr);
1091 return Err(CliError::Generation(format!(
1092 "cargo check failed after generation:\n{stderr}"
1093 )));
1094 }
1095 }
1096
1097 let clippy_result = std::process::Command::new("cargo")
1098 .args(["clippy", "-D", "warnings"])
1099 .output();
1100
1101 if let Ok(output) = clippy_result {
1102 if !output.status.success() {
1103 let stderr = String::from_utf8_lossy(&output.stderr);
1104 eprintln!("⚠️ cargo clippy found warnings:\n{stderr}");
1105 eprintln!(" Run `cargo clippy --fix` to auto-fix.");
1106 }
1107 }
1108
1109 Ok(())
1110}
1111
1112#[cfg(test)]
1113mod tests {
1114 use super::*;
1115
1116 struct CwdGuard {
1122 original: Option<PathBuf>,
1123 _lock: std::sync::MutexGuard<'static, ()>,
1124 }
1125
1126 impl CwdGuard {
1127 fn switch(new_dir: &Path) -> std::io::Result<Self> {
1129 let lock = super::super::test_support::acquire_global_lock();
1130 let original = std::env::current_dir().ok();
1131 std::env::set_current_dir(new_dir)?;
1132 Ok(Self {
1133 original,
1134 _lock: lock,
1135 })
1136 }
1137 }
1138
1139 impl Drop for CwdGuard {
1140 fn drop(&mut self) {
1141 if let Some(ref orig) = self.original {
1142 let _ = std::env::set_current_dir(orig);
1143 }
1144 }
1145 }
1146
1147 #[test]
1148 fn test_resolve_target_simple_model() {
1149 let (class, module, path) = resolve_target("User", "model");
1150 assert_eq!(class, "User");
1151 assert_eq!(module, "model");
1152 assert_eq!(path, PathBuf::from("app/model/User.rs"));
1153 }
1154
1155 #[test]
1156 fn test_resolve_target_nested_controller() {
1157 let (class, module, path) = resolve_target("admin/User", "controller");
1158 assert_eq!(class, "User");
1159 assert_eq!(module, "controller::admin");
1160 assert_eq!(path, PathBuf::from("app/controller/admin/User.rs"));
1161 }
1162
1163 #[test]
1164 fn test_resolve_target_with_app() {
1165 let (class, module, _path) = resolve_target("admin@User", "model");
1166 assert_eq!(class, "User");
1167 assert_eq!(module, "admin::model");
1168 }
1169
1170 #[test]
1171 fn test_class_to_snake() {
1172 assert_eq!(class_to_snake("User"), "user");
1173 assert_eq!(class_to_snake("OrderItem"), "order_item");
1174 assert_eq!(class_to_snake("API"), "a_p_i");
1175 }
1176
1177 #[test]
1178 fn test_name_to_table_create() {
1179 assert_eq!(name_to_table("create_users"), "users");
1180 assert_eq!(name_to_table("create_orders"), "orders");
1181 }
1182
1183 #[test]
1184 fn test_name_to_table_add() {
1185 assert_eq!(name_to_table("add_index_to_orders"), "orders");
1186 assert_eq!(name_to_table("add_status"), "status");
1187 }
1188
1189 #[test]
1190 fn test_name_to_table_other() {
1191 assert_eq!(name_to_table("custom_migration"), "custom_migration");
1192 }
1193
1194 #[test]
1195 fn test_check_file_exists_nonexistent() {
1196 let result = check_file_exists(Path::new("/nonexistent/path/file.txt"));
1197 assert!(result.is_ok());
1198 }
1199
1200 #[test]
1201 fn test_check_file_exists_existing() {
1202 let temp = tempfile::NamedTempFile::new().unwrap();
1204 let result = check_file_exists(temp.path());
1205 assert!(matches!(result, Err(CliError::FileExists(_))));
1206 }
1207
1208 #[test]
1209 fn test_write_and_read_file() {
1210 let temp_dir = tempfile::tempdir().unwrap();
1211 let file_path = temp_dir.path().join("test_file.txt");
1212
1213 write_file(&file_path, "test content").unwrap();
1214 assert_eq!(std::fs::read_to_string(&file_path).unwrap(), "test content");
1215 }
1216
1217 #[test]
1218 fn test_execute_make_migration_creates_files() {
1219 let temp_dir = tempfile::tempdir().unwrap();
1220 let path = temp_dir.path().to_str().unwrap();
1221
1222 execute_make_migration("create_test_table", path).unwrap();
1223
1224 let entries: Vec<_> = std::fs::read_dir(path).unwrap().collect();
1225 assert_eq!(entries.len(), 2); let mut has_up = false;
1228 let mut has_down = false;
1229 for entry in entries {
1230 let name = entry.unwrap().file_name();
1231 let name = name.to_string_lossy();
1232 if name.ends_with("_up.sql") {
1233 has_up = true;
1234 }
1235 if name.ends_with("_down.sql") {
1236 has_down = true;
1237 }
1238 }
1239 assert!(has_up);
1240 assert!(has_down);
1241 }
1242
1243 #[test]
1244 fn test_execute_make_model_in_temp() {
1245 let temp_dir = tempfile::tempdir().unwrap();
1246 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
1247
1248 execute_make_model("TestUser").unwrap();
1249
1250 let model_path = temp_dir.path().join("app/model/TestUser.rs");
1251 assert!(model_path.exists());
1252
1253 let content = std::fs::read_to_string(&model_path).unwrap();
1254 assert!(content.contains("TestUser"));
1255 assert!(content.contains("test_user"));
1256 }
1257
1258 #[test]
1259 fn test_execute_make_seeder_creates_file() {
1260 let temp_dir = tempfile::tempdir().unwrap();
1261 let path = temp_dir.path().to_str().unwrap();
1262
1263 execute_make_seeder("001_test_seed", path).unwrap();
1264
1265 let seed_path = Path::new(path).join("001_test_seed.sql");
1266 assert!(seed_path.exists());
1267
1268 let content = std::fs::read_to_string(&seed_path).unwrap();
1269 assert!(content.contains("001_test_seed"));
1270 assert!(content.contains("-- Seed:"));
1271 assert!(!content.contains("{%"));
1273 }
1274
1275 #[test]
1276 fn test_execute_make_seeder_file_already_exists() {
1277 let temp_dir = tempfile::tempdir().unwrap();
1278 let path = temp_dir.path().to_str().unwrap();
1279
1280 execute_make_seeder("001_dup_seed", path).unwrap();
1282 let result = execute_make_seeder("001_dup_seed", path);
1284 assert!(matches!(result, Err(CliError::FileExists(_))));
1285 }
1286
1287 #[test]
1288 fn test_execute_make_seeder_creates_directory() {
1289 let temp_dir = tempfile::tempdir().unwrap();
1290 let nested = temp_dir.path().join("nested").join("seeds");
1291 let path = nested.to_str().unwrap();
1292
1293 execute_make_seeder("001_seed", path).unwrap();
1295 assert!(nested.exists());
1296 }
1297
1298 #[tokio::test]
1299 async fn test_make_validate_creates_file() {
1300 let temp_dir = tempfile::tempdir().unwrap();
1301 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
1302
1303 let cmd = MakeCommand::Validate {
1305 name: "Order".to_string(),
1306 };
1307 execute(&cmd).await.unwrap();
1308
1309 let validate_path = temp_dir.path().join("app/validate/Order.rs");
1310 assert!(validate_path.exists());
1311
1312 let content = std::fs::read_to_string(&validate_path).unwrap();
1313 assert!(content.contains("pub struct OrderValidate;"));
1315 assert!(content.contains("use sz_rust_core::validate::Validate"));
1316 assert!(content.contains("app::validate"));
1317 assert!(!content.contains("{%"));
1319 }
1320
1321 #[test]
1322 fn test_validate_stub_contains_required_elements() {
1323 assert!(stubs::VALIDATE_STUB.contains("pub struct {%className%}Validate;"));
1325 assert!(stubs::VALIDATE_STUB.contains("use sz_rust_core::validate::Validate"));
1326 assert!(stubs::VALIDATE_STUB.contains("impl {%className%}Validate"));
1327 assert!(stubs::VALIDATE_STUB.contains("pub fn new() -> Validate"));
1328 assert!(stubs::VALIDATE_STUB.contains("{%className%}"));
1329 assert!(stubs::VALIDATE_STUB.contains("{%namespace%}"));
1330 }
1331
1332 #[test]
1333 fn test_execute_make_validate_creates_file() {
1334 let temp_dir = tempfile::tempdir().unwrap();
1335 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
1336
1337 execute_make_validate("User").unwrap();
1338
1339 let validate_path = temp_dir.path().join("app/validate/User.rs");
1340 assert!(validate_path.exists());
1341
1342 let content = std::fs::read_to_string(&validate_path).unwrap();
1343 assert!(content.contains("UserValidate"));
1344 assert!(content.contains("use sz_rust_core::validate::Validate"));
1345 assert!(content.contains("app::validate"));
1346 assert!(!content.contains("{%"));
1348 }
1349
1350 #[test]
1351 fn test_execute_make_validate_file_already_exists() {
1352 let temp_dir = tempfile::tempdir().unwrap();
1353 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
1354
1355 execute_make_validate("User").unwrap();
1357 let result = execute_make_validate("User");
1359 assert!(matches!(result, Err(CliError::FileExists(_))));
1360 }
1361
1362 #[test]
1363 fn test_execute_make_validate_nested_path() {
1364 let temp_dir = tempfile::tempdir().unwrap();
1365 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
1366
1367 execute_make_validate("admin/User").unwrap();
1369
1370 let validate_path = temp_dir.path().join("app/validate/admin/User.rs");
1371 assert!(validate_path.exists());
1372
1373 let content = std::fs::read_to_string(&validate_path).unwrap();
1374 assert!(content.contains("UserValidate"));
1375 assert!(content.contains("app::validate::admin"));
1376 }
1377
1378 #[test]
1381 fn test_execute_make_event_creates_file() {
1382 let temp_dir = tempfile::tempdir().unwrap();
1383 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
1384
1385 execute_make_event("UserLogin").unwrap();
1386
1387 let event_path = temp_dir.path().join("app/event/UserLogin.rs");
1388 assert!(event_path.exists());
1389
1390 let content = std::fs::read_to_string(&event_path).unwrap();
1391 assert!(content.contains("pub struct UserLogin;"));
1392 assert!(content.contains("app::event"));
1393 assert!(content.contains("UserLogin"));
1394 assert!(content.contains("use serde_json::Value"));
1395 assert!(!content.contains("{%"));
1397 }
1398
1399 #[test]
1400 fn test_execute_make_event_file_already_exists() {
1401 let temp_dir = tempfile::tempdir().unwrap();
1402 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
1403
1404 execute_make_event("UserLogin").unwrap();
1405 let result = execute_make_event("UserLogin");
1406 assert!(matches!(result, Err(CliError::FileExists(_))));
1407 }
1408
1409 #[test]
1410 fn test_execute_make_event_nested_path() {
1411 let temp_dir = tempfile::tempdir().unwrap();
1412 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
1413
1414 execute_make_event("admin/UserLogin").unwrap();
1415
1416 let event_path = temp_dir.path().join("app/event/admin/UserLogin.rs");
1417 assert!(event_path.exists());
1418
1419 let content = std::fs::read_to_string(&event_path).unwrap();
1420 assert!(content.contains("app::event::admin"));
1421 }
1422
1423 #[test]
1424 fn test_execute_make_listener_default_event_name() {
1425 let temp_dir = tempfile::tempdir().unwrap();
1426 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
1427
1428 execute_make_listener("SendWelcomeEmail", None).unwrap();
1430
1431 let listener_path = temp_dir.path().join("app/listener/SendWelcomeEmail.rs");
1432 assert!(listener_path.exists());
1433
1434 let content = std::fs::read_to_string(&listener_path).unwrap();
1435 assert!(content.contains("pub struct SendWelcomeEmail;"));
1436 assert!(content.contains("app::listener"));
1437 assert!(content.contains("use sz_rust_core::event::{EventError, Listener}"));
1438 assert!(content.contains("impl Listener for SendWelcomeEmail"));
1439 assert!(content.contains(r#""SendWelcomeEmail""#));
1441 assert!(!content.contains("{%"));
1442 }
1443
1444 #[test]
1445 fn test_execute_make_listener_custom_event_name() {
1446 let temp_dir = tempfile::tempdir().unwrap();
1447 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
1448
1449 execute_make_listener("SendWelcomeEmail", Some("UserLogin")).unwrap();
1451
1452 let listener_path = temp_dir.path().join("app/listener/SendWelcomeEmail.rs");
1453 assert!(listener_path.exists());
1454
1455 let content = std::fs::read_to_string(&listener_path).unwrap();
1456 assert!(content.contains(r#""UserLogin""#));
1457 assert!(!content.contains(r#""SendWelcomeEmail""#));
1458 assert!(!content.contains("{%"));
1459 }
1460
1461 #[test]
1462 fn test_execute_make_listener_file_already_exists() {
1463 let temp_dir = tempfile::tempdir().unwrap();
1464 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
1465
1466 execute_make_listener("SendWelcomeEmail", None).unwrap();
1467 let result = execute_make_listener("SendWelcomeEmail", None);
1468 assert!(matches!(result, Err(CliError::FileExists(_))));
1469 }
1470
1471 #[test]
1472 fn test_execute_make_command_creates_file() {
1473 let temp_dir = tempfile::tempdir().unwrap();
1474 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
1475
1476 execute_make_command("SyncData").unwrap();
1477
1478 let command_path = temp_dir.path().join("app/command/SyncData.rs");
1479 assert!(command_path.exists());
1480
1481 let content = std::fs::read_to_string(&command_path).unwrap();
1482 assert!(content.contains("pub struct SyncData;"));
1483 assert!(content.contains("app::command"));
1484 assert!(content.contains("use sz_rust_cli::console::{Command, CommandSignature}"));
1485 assert!(content.contains("impl Command for SyncData"));
1486 assert!(content.contains(r#""sync_data""#));
1488 assert!(!content.contains("{%"));
1489 }
1490
1491 #[test]
1492 fn test_execute_make_command_file_already_exists() {
1493 let temp_dir = tempfile::tempdir().unwrap();
1494 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
1495
1496 execute_make_command("SyncData").unwrap();
1497 let result = execute_make_command("SyncData");
1498 assert!(matches!(result, Err(CliError::FileExists(_))));
1499 }
1500
1501 #[test]
1502 fn test_execute_make_command_nested_path() {
1503 let temp_dir = tempfile::tempdir().unwrap();
1504 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
1505
1506 execute_make_command("admin/SyncData").unwrap();
1507
1508 let command_path = temp_dir.path().join("app/command/admin/SyncData.rs");
1509 assert!(command_path.exists());
1510
1511 let content = std::fs::read_to_string(&command_path).unwrap();
1512 assert!(content.contains("app::command::admin"));
1513 }
1514
1515 #[test]
1516 fn test_execute_make_service_creates_file() {
1517 let temp_dir = tempfile::tempdir().unwrap();
1518 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
1519
1520 execute_make_service("UserService").unwrap();
1521
1522 let service_path = temp_dir.path().join("app/service/UserService.rs");
1523 assert!(service_path.exists());
1524
1525 let content = std::fs::read_to_string(&service_path).unwrap();
1526 assert!(content.contains("pub struct UserService;"));
1527 assert!(content.contains("app::service"));
1528 assert!(content.contains("impl Default for UserService"));
1529 assert!(content.contains("pub fn new() -> Self"));
1530 assert!(!content.contains("{%"));
1531 }
1532
1533 #[test]
1534 fn test_execute_make_service_file_already_exists() {
1535 let temp_dir = tempfile::tempdir().unwrap();
1536 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
1537
1538 execute_make_service("UserService").unwrap();
1539 let result = execute_make_service("UserService");
1540 assert!(matches!(result, Err(CliError::FileExists(_))));
1541 }
1542
1543 #[test]
1544 fn test_execute_make_service_nested_path() {
1545 let temp_dir = tempfile::tempdir().unwrap();
1546 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
1547
1548 execute_make_service("admin/UserService").unwrap();
1549
1550 let service_path = temp_dir.path().join("app/service/admin/UserService.rs");
1551 assert!(service_path.exists());
1552
1553 let content = std::fs::read_to_string(&service_path).unwrap();
1554 assert!(content.contains("app::service::admin"));
1555 }
1556
1557 #[test]
1560 fn test_execute_make_controller_creates_file() {
1561 let temp_dir = tempfile::tempdir().unwrap();
1562 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
1563
1564 execute_make_controller("User", false, false).unwrap();
1565
1566 let controller_path = temp_dir.path().join("app/controller/User.rs");
1567 assert!(controller_path.exists());
1568
1569 let content = std::fs::read_to_string(&controller_path).unwrap();
1570 assert!(content.contains("User"));
1571 assert!(content.contains("app::controller"));
1572 assert!(!content.contains("{%"));
1573 }
1574
1575 #[test]
1576 fn test_execute_make_controller_api() {
1577 let temp_dir = tempfile::tempdir().unwrap();
1578 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
1579
1580 execute_make_controller("User", true, false).unwrap();
1581
1582 let controller_path = temp_dir.path().join("app/controller/User.rs");
1583 assert!(controller_path.exists());
1584
1585 let content = std::fs::read_to_string(&controller_path).unwrap();
1586 assert!(!content.contains("{%"));
1587 }
1588
1589 #[test]
1590 fn test_execute_make_controller_plain() {
1591 let temp_dir = tempfile::tempdir().unwrap();
1592 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
1593
1594 execute_make_controller("User", false, true).unwrap();
1595
1596 let controller_path = temp_dir.path().join("app/controller/User.rs");
1597 assert!(controller_path.exists());
1598
1599 let content = std::fs::read_to_string(&controller_path).unwrap();
1600 assert!(!content.contains("{%"));
1601 }
1602
1603 #[test]
1604 fn test_execute_make_controller_file_already_exists() {
1605 let temp_dir = tempfile::tempdir().unwrap();
1606 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
1607
1608 execute_make_controller("User", false, false).unwrap();
1609 let result = execute_make_controller("User", false, false);
1610 assert!(matches!(result, Err(CliError::FileExists(_))));
1611 }
1612
1613 #[test]
1614 fn test_execute_make_guard_creates_file() {
1615 let temp_dir = tempfile::tempdir().unwrap();
1616 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
1617
1618 execute_make_guard("Admin").unwrap();
1619
1620 let guard_path = temp_dir.path().join("app/guard/Admin.rs");
1621 assert!(guard_path.exists());
1622
1623 let content = std::fs::read_to_string(&guard_path).unwrap();
1624 assert!(content.contains("pub struct Admin;"));
1625 assert!(content.contains("impl Guard for Admin"));
1626 assert!(content.contains("use sz_rust_core::guard::Guard"));
1627 }
1628
1629 #[test]
1630 fn test_execute_make_guard_file_already_exists() {
1631 let temp_dir = tempfile::tempdir().unwrap();
1632 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
1633
1634 execute_make_guard("Admin").unwrap();
1635 let result = execute_make_guard("Admin");
1636 assert!(matches!(result, Err(CliError::FileExists(_))));
1637 }
1638
1639 #[test]
1640 fn test_execute_make_middleware_creates_file() {
1641 let temp_dir = tempfile::tempdir().unwrap();
1642 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
1643
1644 execute_make_middleware("Cors").unwrap();
1645
1646 let middleware_path = temp_dir.path().join("app/middleware/Cors.rs");
1647 assert!(middleware_path.exists());
1648
1649 let content = std::fs::read_to_string(&middleware_path).unwrap();
1650 assert!(content.contains("Cors"));
1651 assert!(content.contains("app::middleware"));
1652 assert!(!content.contains("{%"));
1653 }
1654
1655 #[test]
1656 fn test_execute_make_middleware_file_already_exists() {
1657 let temp_dir = tempfile::tempdir().unwrap();
1658 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
1659
1660 execute_make_middleware("Cors").unwrap();
1661 let result = execute_make_middleware("Cors");
1662 assert!(matches!(result, Err(CliError::FileExists(_))));
1663 }
1664
1665 #[test]
1666 fn test_execute_make_scaffold_creates_files() {
1667 let temp_dir = tempfile::tempdir().unwrap();
1668 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
1669
1670 execute_make_scaffold("Post").unwrap();
1671
1672 let model_path = temp_dir.path().join("app/model/Post.rs");
1674 let controller_path = temp_dir.path().join("app/controller/Post.rs");
1675 assert!(model_path.exists(), "model should be created");
1676 assert!(controller_path.exists(), "controller should be created");
1677 let migrations_dir = temp_dir.path().join("migrations");
1679 assert!(migrations_dir.exists(), "migrations dir should be created");
1680 let migration_count = std::fs::read_dir(&migrations_dir).unwrap().count();
1681 assert_eq!(
1682 migration_count, 2,
1683 "should create up + down migration files"
1684 );
1685 }
1686
1687 #[tokio::test]
1688 async fn test_execute_dispatch_model() {
1689 let temp_dir = tempfile::tempdir().unwrap();
1690 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
1691
1692 let cmd = MakeCommand::Model {
1693 name: "User".to_string(),
1694 };
1695 execute(&cmd).await.unwrap();
1696
1697 let model_path = temp_dir.path().join("app/model/User.rs");
1698 assert!(model_path.exists());
1699 }
1700
1701 #[tokio::test]
1702 async fn test_execute_dispatch_controller() {
1703 let temp_dir = tempfile::tempdir().unwrap();
1704 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
1705
1706 let cmd = MakeCommand::Controller {
1707 name: "User".to_string(),
1708 api: false,
1709 plain: false,
1710 };
1711 execute(&cmd).await.unwrap();
1712
1713 let controller_path = temp_dir.path().join("app/controller/User.rs");
1714 assert!(controller_path.exists());
1715 }
1716
1717 #[tokio::test]
1718 async fn test_execute_dispatch_migration() {
1719 let temp_dir = tempfile::tempdir().unwrap();
1720 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
1721
1722 let migrations_path = temp_dir.path().join("migrations");
1723 let cmd = MakeCommand::Migration {
1724 name: "create_users".to_string(),
1725 path: migrations_path.to_string_lossy().to_string(),
1726 };
1727 execute(&cmd).await.unwrap();
1728
1729 let migration_count = std::fs::read_dir(&migrations_path).unwrap().count();
1730 assert_eq!(migration_count, 2);
1731 }
1732
1733 #[tokio::test]
1734 async fn test_execute_dispatch_seeder() {
1735 let temp_dir = tempfile::tempdir().unwrap();
1736 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
1737
1738 let seeds_path = temp_dir.path().join("seeds");
1739 let cmd = MakeCommand::Seeder {
1740 name: "001_users".to_string(),
1741 path: seeds_path.to_string_lossy().to_string(),
1742 };
1743 execute(&cmd).await.unwrap();
1744
1745 let seeder_path = seeds_path.join("001_users.sql");
1746 assert!(seeder_path.exists());
1747 }
1748
1749 #[tokio::test]
1750 async fn test_execute_dispatch_guard() {
1751 let temp_dir = tempfile::tempdir().unwrap();
1752 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
1753
1754 let cmd = MakeCommand::Guard {
1755 name: "Admin".to_string(),
1756 };
1757 execute(&cmd).await.unwrap();
1758
1759 let guard_path = temp_dir.path().join("app/guard/Admin.rs");
1760 assert!(guard_path.exists());
1761 }
1762
1763 #[tokio::test]
1764 async fn test_execute_dispatch_event() {
1765 let temp_dir = tempfile::tempdir().unwrap();
1766 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
1767
1768 let cmd = MakeCommand::Event {
1769 name: "UserLogin".to_string(),
1770 };
1771 execute(&cmd).await.unwrap();
1772
1773 let event_path = temp_dir.path().join("app/event/UserLogin.rs");
1774 assert!(event_path.exists());
1775 }
1776
1777 #[tokio::test]
1778 async fn test_execute_dispatch_listener() {
1779 let temp_dir = tempfile::tempdir().unwrap();
1780 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
1781
1782 let cmd = MakeCommand::Listener {
1783 name: "SendEmail".to_string(),
1784 event: None,
1785 };
1786 execute(&cmd).await.unwrap();
1787
1788 let listener_path = temp_dir.path().join("app/listener/SendEmail.rs");
1789 assert!(listener_path.exists());
1790 }
1791
1792 #[tokio::test]
1793 async fn test_execute_dispatch_command() {
1794 let temp_dir = tempfile::tempdir().unwrap();
1795 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
1796
1797 let cmd = MakeCommand::Command {
1798 name: "SyncData".to_string(),
1799 };
1800 execute(&cmd).await.unwrap();
1801
1802 let command_path = temp_dir.path().join("app/command/SyncData.rs");
1803 assert!(command_path.exists());
1804 }
1805
1806 #[tokio::test]
1807 async fn test_execute_dispatch_service() {
1808 let temp_dir = tempfile::tempdir().unwrap();
1809 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
1810
1811 let cmd = MakeCommand::Service {
1812 name: "UserService".to_string(),
1813 };
1814 execute(&cmd).await.unwrap();
1815
1816 let service_path = temp_dir.path().join("app/service/UserService.rs");
1817 assert!(service_path.exists());
1818 }
1819
1820 #[tokio::test]
1821 async fn test_execute_dispatch_middleware() {
1822 let temp_dir = tempfile::tempdir().unwrap();
1823 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
1824
1825 let cmd = MakeCommand::Middleware {
1826 name: "Cors".to_string(),
1827 };
1828 execute(&cmd).await.unwrap();
1829
1830 let middleware_path = temp_dir.path().join("app/middleware/Cors.rs");
1831 assert!(middleware_path.exists());
1832 }
1833
1834 #[tokio::test]
1835 async fn test_execute_dispatch_scaffold() {
1836 let temp_dir = tempfile::tempdir().unwrap();
1837 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
1838
1839 let cmd = MakeCommand::Scaffold {
1840 name: "Post".to_string(),
1841 };
1842 execute(&cmd).await.unwrap();
1843
1844 let model_path = temp_dir.path().join("app/model/Post.rs");
1845 assert!(model_path.exists());
1846 }
1847
1848 #[tokio::test]
1851 async fn test_execute_make_plugin_crud() {
1852 let temp_dir = tempfile::tempdir().unwrap();
1853 let args = crate::context_builder::PluginCommandArgs {
1854 template: "plugin-crud".to_string(),
1855 name: "test_plugin".to_string(),
1856 table: Some("test_table".to_string()),
1857 fields: Some("id:i32:pk,name:String".to_string()),
1858 force: false,
1859 output: Some(
1860 temp_dir
1861 .path()
1862 .join("myplugin")
1863 .to_string_lossy()
1864 .to_string(),
1865 ),
1866 master: None,
1867 slave: None,
1868 master_fields: None,
1869 slave_fields: None,
1870 foreign_key: None,
1871 };
1872 let result = execute_make_plugin(args).await;
1874 assert!(result.is_err(), "无 Cargo.toml 应失败");
1875 let err = format!("{}", result.unwrap_err());
1876 assert!(
1879 err.contains("Cargo.toml")
1880 || err.contains("CompileFailed")
1881 || err.contains("Compilation failed"),
1882 "应含编译失败: {err}"
1883 );
1884 }
1885
1886 #[tokio::test]
1887 async fn test_execute_make_plugin_workflow() {
1888 let temp_dir = tempfile::tempdir().unwrap();
1889 let args = crate::context_builder::PluginCommandArgs {
1890 template: "plugin-workflow".to_string(),
1891 name: "wf_plugin".to_string(),
1892 table: None,
1893 fields: Some("id:i32:pk,title:String".to_string()),
1894 force: false,
1895 output: Some(
1896 temp_dir
1897 .path()
1898 .join("wfplugin")
1899 .to_string_lossy()
1900 .to_string(),
1901 ),
1902 master: None,
1903 slave: None,
1904 master_fields: None,
1905 slave_fields: None,
1906 foreign_key: None,
1907 };
1908 let result = execute_make_plugin(args).await;
1909 assert!(result.is_err(), "无 Cargo.toml 应失败");
1910 }
1911
1912 #[tokio::test]
1913 async fn test_execute_make_plugin_report() {
1914 let temp_dir = tempfile::tempdir().unwrap();
1915 let args = crate::context_builder::PluginCommandArgs {
1916 template: "plugin-report".to_string(),
1917 name: "rpt_plugin".to_string(),
1918 table: None,
1919 fields: Some("id:i32:pk,data:String".to_string()),
1920 force: false,
1921 output: Some(
1922 temp_dir
1923 .path()
1924 .join("rptplugin")
1925 .to_string_lossy()
1926 .to_string(),
1927 ),
1928 master: None,
1929 slave: None,
1930 master_fields: None,
1931 slave_fields: None,
1932 foreign_key: None,
1933 };
1934 let result = execute_make_plugin(args).await;
1935 assert!(result.is_err(), "无 Cargo.toml 应失败");
1936 }
1937
1938 #[tokio::test]
1939 async fn test_execute_make_plugin_dir_exists_no_force() {
1940 let temp_dir = tempfile::tempdir().unwrap();
1941 let output_dir = temp_dir.path().join("existing_plugin");
1942 std::fs::create_dir_all(&output_dir).unwrap();
1943
1944 let args = crate::context_builder::PluginCommandArgs {
1945 template: "plugin-crud".to_string(),
1946 name: "existing".to_string(),
1947 table: None,
1948 fields: Some("id:i32:pk".to_string()),
1949 force: false,
1950 output: Some(output_dir.to_string_lossy().to_string()),
1951 master: None,
1952 slave: None,
1953 master_fields: None,
1954 slave_fields: None,
1955 foreign_key: None,
1956 };
1957 let result = execute_make_plugin(args).await;
1958 assert!(matches!(result, Err(CliError::DirExists(_))));
1959 }
1960
1961 #[tokio::test]
1962 async fn test_execute_make_plugin_force_overwrite() {
1963 let temp_dir = tempfile::tempdir().unwrap();
1964 let output_dir = temp_dir.path().join("force_plugin");
1965 std::fs::create_dir_all(&output_dir).unwrap();
1966
1967 let args = crate::context_builder::PluginCommandArgs {
1968 template: "plugin-crud".to_string(),
1969 name: "forced".to_string(),
1970 table: None,
1971 fields: Some("id:i32:pk".to_string()),
1972 force: true,
1973 output: Some(output_dir.to_string_lossy().to_string()),
1974 master: None,
1975 slave: None,
1976 master_fields: None,
1977 slave_fields: None,
1978 foreign_key: None,
1979 };
1980 let result = execute_make_plugin(args).await;
1981 assert!(result.is_err(), "无 Cargo.toml 应失败");
1982 }
1983
1984 #[tokio::test]
1985 async fn test_execute_make_plugin_invalid_name() {
1986 let temp_dir = tempfile::tempdir().unwrap();
1987 let args = crate::context_builder::PluginCommandArgs {
1988 template: "plugin-crud".to_string(),
1989 name: "InvalidName".to_string(),
1990 table: None,
1991 fields: None,
1992 force: false,
1993 output: Some(temp_dir.path().join("bad").to_string_lossy().to_string()),
1994 master: None,
1995 slave: None,
1996 master_fields: None,
1997 slave_fields: None,
1998 foreign_key: None,
1999 };
2000 let result = execute_make_plugin(args).await;
2001 assert!(result.is_err(), "大写插件名应失败");
2002 }
2003
2004 #[tokio::test]
2005 async fn test_execute_make_plugin_invalid_template() {
2006 let temp_dir = tempfile::tempdir().unwrap();
2007 let args = crate::context_builder::PluginCommandArgs {
2008 template: "nonexistent".to_string(),
2009 name: "test_plug".to_string(),
2010 table: None,
2011 fields: None,
2012 force: false,
2013 output: Some(temp_dir.path().join("bad").to_string_lossy().to_string()),
2014 master: None,
2015 slave: None,
2016 master_fields: None,
2017 slave_fields: None,
2018 foreign_key: None,
2019 };
2020 let result = execute_make_plugin(args).await;
2021 assert!(result.is_err(), "不存在的模板应失败");
2022 }
2023
2024 #[tokio::test]
2025 async fn test_execute_dispatch_plugin() {
2026 let temp_dir = tempfile::tempdir().unwrap();
2027 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
2028
2029 let cmd = MakeCommand::Plugin {
2030 template: "plugin-crud".to_string(),
2031 name: "dispatched".to_string(),
2032 table: None,
2033 fields: Some("id:i32:pk".to_string()),
2034 force: false,
2035 output: Some(temp_dir.path().join("disp").to_string_lossy().to_string()),
2036 master: None,
2037 slave: None,
2038 master_fields: None,
2039 slave_fields: None,
2040 foreign_key: None,
2041 };
2042 let result = execute(&cmd).await;
2043 assert!(result.is_err(), "无 Cargo.toml 应失败");
2044 }
2045
2046 #[tokio::test]
2047 async fn test_execute_make_frontend_invalid_framework() {
2048 let temp_dir = tempfile::tempdir().unwrap();
2049 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
2050 let result = execute_make_frontend(
2051 &["User".to_string()],
2052 "src/model/",
2053 "invalid_framework",
2054 "element_plus",
2055 "./frontend/",
2056 None,
2057 "skip",
2058 false,
2059 false,
2060 true,
2061 false,
2062 )
2063 .await;
2064 assert!(result.is_err());
2065 assert!(result.unwrap_err().to_string().contains("不支持的前端框架"));
2066 }
2067
2068 #[tokio::test]
2069 async fn test_execute_make_frontend_invalid_ui() {
2070 let temp_dir = tempfile::tempdir().unwrap();
2071 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
2072 let result = execute_make_frontend(
2073 &["User".to_string()],
2074 "src/model/",
2075 "vue",
2076 "invalid_ui",
2077 "./frontend/",
2078 None,
2079 "skip",
2080 false,
2081 false,
2082 true,
2083 false,
2084 )
2085 .await;
2086 assert!(result.is_err());
2087 assert!(result.unwrap_err().to_string().contains("不支持的 UI 库"));
2088 }
2089
2090 #[tokio::test]
2091 async fn test_execute_make_frontend_invalid_override_strategy() {
2092 let temp_dir = tempfile::tempdir().unwrap();
2093 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
2094 let result = execute_make_frontend(
2095 &["User".to_string()],
2096 "src/model/",
2097 "vue",
2098 "element_plus",
2099 "./frontend/",
2100 None,
2101 "invalid_strategy",
2102 false,
2103 false,
2104 true,
2105 false,
2106 )
2107 .await;
2108 assert!(result.is_err());
2109 assert!(result.unwrap_err().to_string().contains("不支持的覆盖策略"));
2110 }
2111
2112 #[tokio::test]
2113 async fn test_execute_make_frontend_vue_element_plus() {
2114 let temp_dir = tempfile::tempdir().unwrap();
2115 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
2116 let model_dir = temp_dir.path().join("src/model");
2117 std::fs::create_dir_all(&model_dir).unwrap();
2118 std::fs::write(
2119 model_dir.join("User.rs"),
2120 "#[derive(Model)]\npub struct User { pub id: i32, pub name: String }",
2121 )
2122 .unwrap();
2123 let output = temp_dir
2124 .path()
2125 .join("frontend")
2126 .to_string_lossy()
2127 .to_string();
2128 let result = execute_make_frontend(
2129 &["User".to_string()],
2130 &model_dir.to_string_lossy(),
2131 "vue",
2132 "element_plus",
2133 &output,
2134 None,
2135 "skip",
2136 false,
2137 false,
2138 true,
2139 false,
2140 )
2141 .await;
2142 assert!(result.is_ok(), "vue+element_plus 应成功: {:?}", result);
2143 }
2144
2145 #[tokio::test]
2146 async fn test_execute_make_frontend_react_ant_design() {
2147 let temp_dir = tempfile::tempdir().unwrap();
2148 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
2149 let model_dir = temp_dir.path().join("src/model");
2150 std::fs::create_dir_all(&model_dir).unwrap();
2151 std::fs::write(
2152 model_dir.join("Product.rs"),
2153 "#[derive(Model)]\npub struct Product { pub id: i32, pub name: String }",
2154 )
2155 .unwrap();
2156 let output = temp_dir
2157 .path()
2158 .join("frontend")
2159 .to_string_lossy()
2160 .to_string();
2161 let result = execute_make_frontend(
2162 &["Product".to_string()],
2163 &model_dir.to_string_lossy(),
2164 "react",
2165 "ant_design_vue",
2166 &output,
2167 None,
2168 "overwrite",
2169 true,
2170 true,
2171 false,
2172 true,
2173 )
2174 .await;
2175 assert!(result.is_ok(), "react+ant_design_vue 应成功: {:?}", result);
2176 }
2177
2178 #[tokio::test]
2179 async fn test_execute_make_frontend_element_plus_hyphen() {
2180 let temp_dir = tempfile::tempdir().unwrap();
2181 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
2182 let model_dir = temp_dir.path().join("src/model");
2183 std::fs::create_dir_all(&model_dir).unwrap();
2184 std::fs::write(
2185 model_dir.join("Order.rs"),
2186 "#[derive(Model)]\npub struct Order { pub id: i32 }",
2187 )
2188 .unwrap();
2189 let output = temp_dir
2190 .path()
2191 .join("frontend")
2192 .to_string_lossy()
2193 .to_string();
2194 let result = execute_make_frontend(
2195 &["Order".to_string()],
2196 &model_dir.to_string_lossy(),
2197 "vue",
2198 "element-plus",
2199 &output,
2200 None,
2201 "merge",
2202 false,
2203 false,
2204 true,
2205 false,
2206 )
2207 .await;
2208 assert!(result.is_ok(), "element-plus (hyphen) 应成功: {:?}", result);
2209 }
2210
2211 #[tokio::test]
2212 async fn test_execute_make_frontend_ant_design_hyphen() {
2213 let temp_dir = tempfile::tempdir().unwrap();
2214 let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
2215 let model_dir = temp_dir.path().join("src/model");
2216 std::fs::create_dir_all(&model_dir).unwrap();
2217 std::fs::write(
2218 model_dir.join("Item.rs"),
2219 "#[derive(Model)]\npub struct Item { pub id: i32 }",
2220 )
2221 .unwrap();
2222 let output = temp_dir
2223 .path()
2224 .join("frontend")
2225 .to_string_lossy()
2226 .to_string();
2227 let result = execute_make_frontend(
2228 &["Item".to_string()],
2229 &model_dir.to_string_lossy(),
2230 "vue",
2231 "ant-design-vue",
2232 &output,
2233 None,
2234 "skip",
2235 false,
2236 false,
2237 true,
2238 false,
2239 )
2240 .await;
2241 assert!(
2242 result.is_ok(),
2243 "ant-design-vue (hyphen) 应成功: {:?}",
2244 result
2245 );
2246 }
2247
2248 #[test]
2249 fn test_openapi_spec_generation() {
2250 let spec = generate_openapi_spec("Test API", "2.0.0");
2251 assert_eq!(spec.openapi, "3.0.3");
2252 assert_eq!(spec.info.title, "Test API");
2253 assert_eq!(spec.info.version, "2.0.0");
2254 }
2255
2256 #[test]
2257 fn test_openapi_spec_default() {
2258 let spec = generate_openapi_spec("SZ-Rust API", "1.0.0");
2259 assert_eq!(spec.info.title, "SZ-Rust API");
2260 assert_eq!(spec.info.version, "1.0.0");
2261 }
2262
2263 #[test]
2264 fn test_openapi_spec_serialize() {
2265 let spec = generate_openapi_spec("Test", "1.0");
2266 let json = serde_json::to_string(&spec).unwrap();
2267 assert!(json.contains("\"openapi\":\"3.0.3\""));
2268 assert!(json.contains("\"title\":\"Test\""));
2269 assert!(json.contains("bearerAuth"));
2270 }
2271
2272 #[test]
2273 fn test_check_file_exists_with_force() {
2274 let temp = tempfile::NamedTempFile::new().unwrap();
2275 let path = temp.path();
2276
2277 let result = check_file_exists_with_force(path, false);
2278 assert!(result.is_err(), "should error without force");
2279
2280 let result = check_file_exists_with_force(path, true);
2281 assert!(result.is_ok(), "should pass with force");
2282 }
2283
2284 #[test]
2285 fn test_check_file_exists_with_force_nonexistent() {
2286 let path = std::path::Path::new("nonexistent_file_12345.rs");
2287 let result = check_file_exists_with_force(path, false);
2288 assert!(result.is_ok(), "nonexistent file should pass");
2289 }
2290}