Skip to main content

sz_rust_cli/cmd/
make.rs

1//! `make:*` 代码生成命令 — 对齐 PHP `think\console\command\make\*`
2//!
3//! ## PHP 对齐
4//!
5//! PHP `Make::execute()` 流程:
6//! 1. `getArgument('name')` 获取类名
7//! 2. `getClassName(name)` 处理 `@` 分隔应用名 + `/` 转 `\`
8//! 3. `getPathName(className)` 剥离 `app\` 前缀 + `/` 替换 + `.php` 后缀
9//! 4. 检查文件存在 → `mkdir` → `file_put_contents(buildClass())`
10//! 5. `buildClass(name)` 读取 stub,替换占位符
11//!
12//! Rust 端对齐上述流程,但生成 `.rs` 文件而非 `.php` 文件。
13
14use std::path::{Path, PathBuf};
15
16use clap::Subcommand;
17
18use crate::error::CliError;
19use crate::stubs::{self, render_template};
20
21/// `make` 子命令枚举
22///
23/// 对齐 PHP `think make:*` 命令组。
24#[derive(Subcommand, Debug)]
25pub enum MakeCommand {
26    /// 生成 Model(对齐 `php think make:model User`)
27    #[command(name = "model")]
28    Model {
29        /// 类名(如 `User` 或 `admin/User`)
30        name: String,
31    },
32
33    /// 生成 Controller(对齐 `php think make:controller User`)
34    #[command(name = "controller")]
35    Controller {
36        /// 类名
37        name: String,
38        /// 生成 API 风格控制器(5 方法,无 create/edit)
39        #[arg(long)]
40        api: bool,
41        /// 生成空控制器
42        #[arg(long)]
43        plain: bool,
44    },
45
46    /// 生成迁移文件(对齐 Phinx `make:migration`)
47    #[command(name = "migration")]
48    Migration {
49        /// 迁移名称(如 `create_users`)
50        name: String,
51        /// 迁移目录(默认 `migrations`)
52        #[arg(short = 'p', long, default_value = "migrations")]
53        path: String,
54    },
55
56    /// 生成填充文件(对齐 PHP `make:seeder`)
57    ///
58    /// 在 `seeds/` 目录下生成 `<name>.sql` 文件骨架。
59    #[command(name = "seeder")]
60    Seeder {
61        /// 填充文件名称(如 `001_users_seed`)
62        name: String,
63        /// 填充目录(默认 `seeds`)
64        #[arg(short = 'p', long, default_value = "seeds")]
65        path: String,
66    },
67
68    /// 生成 Guard(sz-rust 自研,无 PHP 对应)
69    #[command(name = "guard")]
70    Guard {
71        /// Guard 名称(如 `Admin`)
72        name: String,
73    },
74
75    /// 生成验证器(对齐 PHP `make:validate`)
76    ///
77    /// 在 `app/validate/` 目录下生成 `<Name>.rs` 验证器骨架。
78    #[command(name = "validate")]
79    Validate {
80        /// 验证器类名(如 `User` 或 `admin/User`)
81        name: String,
82    },
83
84    /// 生成事件类(对齐 PHP `make:event`)
85    ///
86    /// 在 `app/event/` 目录下生成 `<Name>.rs` 事件骨架,包含事件名与负载构造方法。
87    #[command(name = "event")]
88    Event {
89        /// 事件类名(如 `UserLogin` 或 `admin/UserLogin`)
90        name: String,
91    },
92
93    /// 生成监听器(对齐 PHP `make:listener`)
94    ///
95    /// 在 `app/listener/` 目录下生成 `<Name>.rs` 监听器骨架,实现 `Listener` trait。
96    #[command(name = "listener")]
97    Listener {
98        /// 监听器类名(如 `SendWelcomeEmail`)
99        name: String,
100        /// 监听的事件名(可选,默认使用类名)
101        #[arg(long)]
102        event: Option<String>,
103    },
104
105    /// 生成自定义命令(对齐 PHP `make:command`)
106    ///
107    /// 在 `app/command/` 目录下生成 `<Name>.rs` 命令骨架,实现 `Command` trait。
108    #[command(name = "command")]
109    Command {
110        /// 命令类名(如 `SyncData`)
111        name: String,
112    },
113
114    /// 生成服务类(对齐 PHP `make:service`)
115    ///
116    /// 在 `app/service/` 目录下生成 `<Name>.rs` 服务骨架,可注入容器。
117    #[command(name = "service")]
118    Service {
119        /// 服务类名(如 `UserService`)
120        name: String,
121    },
122
123    /// 生成中间件(sz-rust 自研,对齐 NestJS `make:middleware`)
124    ///
125    /// 在 `app/middleware/` 目录下生成 `<Name>.rs` 中间件骨架,
126    /// 实现 `SzMiddleware` trait,可注册到中间件链。
127    #[command(name = "middleware")]
128    Middleware {
129        /// 中间件类名(如 `CorsMiddleware` 或 `admin/RateLimit`)
130        name: String,
131    },
132
133    /// 生成脚手架(Model + Controller + Migration)
134    #[command(name = "scaffold")]
135    Scaffold {
136        /// 资源名称(如 `User`)
137        name: String,
138    },
139}
140
141/// 执行 make 子命令
142pub fn execute(cmd: &MakeCommand) -> Result<(), CliError> {
143    match cmd {
144        MakeCommand::Model { name } => execute_make_model(name),
145        MakeCommand::Controller { name, api, plain } => execute_make_controller(name, *api, *plain),
146        MakeCommand::Migration { name, path } => execute_make_migration(name, path),
147        MakeCommand::Seeder { name, path } => execute_make_seeder(name, path),
148        MakeCommand::Guard { name } => execute_make_guard(name),
149        MakeCommand::Validate { name } => execute_make_validate(name),
150        MakeCommand::Event { name } => execute_make_event(name),
151        MakeCommand::Listener { name, event } => execute_make_listener(name, event.as_deref()),
152        MakeCommand::Command { name } => execute_make_command(name),
153        MakeCommand::Service { name } => execute_make_service(name),
154        MakeCommand::Middleware { name } => execute_make_middleware(name),
155        MakeCommand::Scaffold { name } => execute_make_scaffold(name),
156    }
157}
158
159/// 生成 Model 文件
160///
161/// 对齐 PHP `make:model`:读取 `model.stub`,替换占位符,写入 `app/model/{name}.rs`。
162fn execute_make_model(name: &str) -> Result<(), CliError> {
163    let (class_name, module_path, file_path) = resolve_target(name, "model");
164    check_file_exists(&file_path)?;
165
166    let namespace = format!("app::{}", module_path);
167    let table_name = class_to_snake(&class_name);
168    let content = render_template(
169        stubs::MODEL_STUB,
170        &[
171            ("{%className%}", &class_name),
172            ("{%namespace%}", &namespace),
173            ("{%table_name%}", &table_name),
174        ],
175    );
176
177    write_file(&file_path, &content)?;
178    println!("Model created: {}", file_path.display());
179    Ok(())
180}
181
182/// 生成 Controller 文件
183///
184/// 对齐 PHP `make:controller`:根据 `--api` / `--plain` 选择不同 stub。
185fn execute_make_controller(name: &str, api: bool, plain: bool) -> Result<(), CliError> {
186    let (class_name, module_path, file_path) = resolve_target(name, "controller");
187    check_file_exists(&file_path)?;
188
189    let namespace = format!("app::{}", module_path);
190    let route = class_to_snake(&class_name);
191
192    let template = if plain {
193        stubs::CONTROLLER_PLAIN_STUB
194    } else if api {
195        stubs::CONTROLLER_API_STUB
196    } else {
197        stubs::CONTROLLER_STUB
198    };
199
200    let content = render_template(
201        template,
202        &[
203            ("{%className%}", &class_name),
204            ("{%namespace%}", &namespace),
205            ("{%route%}", &route),
206        ],
207    );
208
209    write_file(&file_path, &content)?;
210    println!("Controller created: {}", file_path.display());
211    Ok(())
212}
213
214/// 生成迁移文件
215///
216/// 对齐 Phinx 风格:生成 `{timestamp}_{name}_up.sql` 和 `{timestamp}_{name}_down.sql`。
217fn execute_make_migration(name: &str, path: &str) -> Result<(), CliError> {
218    let dir = Path::new(path);
219    std::fs::create_dir_all(dir)?;
220
221    let timestamp = chrono::Utc::now().format("%Y%m%d%H%M%S").to_string();
222    let table_name = name_to_table(name);
223
224    let up_file = dir.join(format!("{}_{}_up.sql", timestamp, name));
225    let down_file = dir.join(format!("{}_{}_down.sql", timestamp, name));
226
227    check_file_exists(&up_file)?;
228    check_file_exists(&down_file)?;
229
230    let up_content = render_template(
231        stubs::MIGRATION_UP_STUB,
232        &[
233            ("{%name%}", name),
234            ("{%timestamp%}", &timestamp),
235            ("{%table_name%}", &table_name),
236        ],
237    );
238    let down_content = render_template(
239        stubs::MIGRATION_DOWN_STUB,
240        &[
241            ("{%name%}", name),
242            ("{%timestamp%}", &timestamp),
243            ("{%table_name%}", &table_name),
244        ],
245    );
246
247    write_file(&up_file, &up_content)?;
248    write_file(&down_file, &down_content)?;
249    println!(
250        "Migration created: {} & {}",
251        up_file.display(),
252        down_file.display()
253    );
254    Ok(())
255}
256
257/// 生成填充文件
258///
259/// 对齐 PHP `make:seeder`:在 `seeds/` 目录下生成 `<name>.sql` 文件骨架。
260fn execute_make_seeder(name: &str, path: &str) -> Result<(), CliError> {
261    let dir = Path::new(path);
262    std::fs::create_dir_all(dir)?;
263
264    let file_path = dir.join(format!("{}.sql", name));
265    check_file_exists(&file_path)?;
266
267    let timestamp = chrono::Utc::now()
268        .format("%Y-%m-%d %H:%M:%S UTC")
269        .to_string();
270    let content = render_template(
271        stubs::SEED_STUB,
272        &[("{%name%}", name), ("{%timestamp%}", &timestamp)],
273    );
274
275    write_file(&file_path, &content)?;
276    println!("Seeder created: {}", file_path.display());
277    Ok(())
278}
279
280/// 生成 Guard 文件(sz-rust 自研)
281fn execute_make_guard(name: &str) -> Result<(), CliError> {
282    let (class_name, _module_path, file_path) = resolve_target(name, "guard");
283    check_file_exists(&file_path)?;
284
285    let content = format!(
286        "//! 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"
287    );
288
289    write_file(&file_path, &content)?;
290    println!("Guard created: {}", file_path.display());
291    Ok(())
292}
293
294/// 生成验证器文件
295///
296/// 对齐 PHP `make:validate`:在 `app/validate/` 目录下生成 `<Name>.rs` 验证器骨架,
297/// 包含 `Validate` 结构体初始化与常见规则示例。
298fn execute_make_validate(name: &str) -> Result<(), CliError> {
299    let (class_name, module_path, file_path) = resolve_target(name, "validate");
300    check_file_exists(&file_path)?;
301
302    let namespace = format!("app::{}", module_path);
303    let content = render_template(
304        stubs::VALIDATE_STUB,
305        &[
306            ("{%className%}", &class_name),
307            ("{%namespace%}", &namespace),
308        ],
309    );
310
311    write_file(&file_path, &content)?;
312    println!("Validator created: {}", file_path.display());
313    Ok(())
314}
315
316/// 生成事件类文件
317///
318/// 对齐 PHP `make:event`:读取 `event.stub`,替换占位符,写入 `app/event/{name}.rs`。
319fn execute_make_event(name: &str) -> Result<(), CliError> {
320    let (class_name, module_path, file_path) = resolve_target(name, "event");
321    check_file_exists(&file_path)?;
322
323    let namespace = format!("app::{}", module_path);
324    // 事件名默认使用类名(对齐 PHP `make:event` 默认行为)
325    let event_name = class_name.clone();
326    let content = render_template(
327        stubs::EVENT_STUB,
328        &[
329            ("{%className%}", &class_name),
330            ("{%namespace%}", &namespace),
331            ("{%event_name%}", &event_name),
332        ],
333    );
334
335    write_file(&file_path, &content)?;
336    println!("Event created: {}", file_path.display());
337    Ok(())
338}
339
340/// 生成监听器文件
341///
342/// 对齐 PHP `make:listener`:读取 `listener.stub`,替换占位符,写入 `app/listener/{name}.rs`。
343/// 若未指定 `--event`,事件名默认使用监听器类名。
344fn execute_make_listener(name: &str, event: Option<&str>) -> Result<(), CliError> {
345    let (class_name, module_path, file_path) = resolve_target(name, "listener");
346    check_file_exists(&file_path)?;
347
348    let namespace = format!("app::{}", module_path);
349    // 事件名:优先使用 --event 参数,否则默认使用类名
350    let event_name = event.unwrap_or(&class_name).to_string();
351    let content = render_template(
352        stubs::LISTENER_STUB,
353        &[
354            ("{%className%}", &class_name),
355            ("{%namespace%}", &namespace),
356            ("{%event_name%}", &event_name),
357        ],
358    );
359
360    write_file(&file_path, &content)?;
361    println!("Listener created: {}", file_path.display());
362    Ok(())
363}
364
365/// 生成自定义命令文件
366///
367/// 对齐 PHP `make:command`:读取 `command.stub`,替换占位符,写入 `app/command/{name}.rs`。
368/// 命令名默认为类名的 snake_case 形式(如 `SyncData` → `sync_data`)。
369fn execute_make_command(name: &str) -> Result<(), CliError> {
370    let (class_name, module_path, file_path) = resolve_target(name, "command");
371    check_file_exists(&file_path)?;
372
373    let namespace = format!("app::{}", module_path);
374    // 命令名:snake_case(类名)(对齐 PHP `make:command` 默认行为)
375    let command_name = class_to_snake(&class_name);
376    let content = render_template(
377        stubs::COMMAND_STUB,
378        &[
379            ("{%className%}", &class_name),
380            ("{%namespace%}", &namespace),
381            ("{%command_name%}", &command_name),
382        ],
383    );
384
385    write_file(&file_path, &content)?;
386    println!("Command created: {}", file_path.display());
387    Ok(())
388}
389
390/// 生成服务类文件
391///
392/// 对齐 PHP `make:service`:读取 `service.stub`,替换占位符,写入 `app/service/{name}.rs`。
393fn execute_make_service(name: &str) -> Result<(), CliError> {
394    let (class_name, module_path, file_path) = resolve_target(name, "service");
395    check_file_exists(&file_path)?;
396
397    let namespace = format!("app::{}", module_path);
398    let content = render_template(
399        stubs::SERVICE_STUB,
400        &[
401            ("{%className%}", &class_name),
402            ("{%namespace%}", &namespace),
403        ],
404    );
405
406    write_file(&file_path, &content)?;
407    println!("Service created: {}", file_path.display());
408    Ok(())
409}
410
411/// 生成中间件文件(sz-rust 自研)
412///
413/// 在 `app/middleware/` 目录下生成 `<Name>.rs` 中间件骨架,
414/// 实现 `SzMiddleware` trait,可注册到中间件链。
415fn execute_make_middleware(name: &str) -> Result<(), CliError> {
416    let (class_name, module_path, file_path) = resolve_target(name, "middleware");
417    check_file_exists(&file_path)?;
418
419    let namespace = format!("app::{}", module_path);
420    let content = render_template(
421        stubs::MIDDLEWARE_STUB,
422        &[
423            ("{%className%}", &class_name),
424            ("{%namespace%}", &namespace),
425        ],
426    );
427
428    write_file(&file_path, &content)?;
429    println!("Middleware created: {}", file_path.display());
430    Ok(())
431}
432
433/// 生成脚手架(Model + Controller + Migration)
434fn execute_make_scaffold(name: &str) -> Result<(), CliError> {
435    println!("Scaffolding for: {}", name);
436    execute_make_model(name)?;
437    execute_make_controller(name, false, false)?;
438    execute_make_migration(&class_to_snake(name), "migrations")?;
439    println!("Scaffold complete.");
440    Ok(())
441}
442
443// ============================================================================
444// 辅助函数(对齐 PHP Make 基类方法)
445// ============================================================================
446
447/// 解析目标(类名 + 模块路径 + 文件路径)
448///
449/// 对齐 PHP `Make::getClassName()` + `getPathName()`:
450///
451/// - `User` → (`User`, `model`, `app/model/User.rs`)
452/// - `admin/User` → (`User`, `controller::admin`, `app/controller/admin/User.rs`)
453/// - `admin@User` → (`User`, `admin::model`, `app/admin/model/User.rs`)
454fn resolve_target(name: &str, layer: &str) -> (String, String, PathBuf) {
455    // 处理 @ 分隔应用名(对齐 PHP getClassName)
456    let (app, class_part) = if let Some(idx) = name.find('@') {
457        (&name[..idx], &name[idx + 1..])
458    } else {
459        ("", name)
460    };
461
462    // 处理 / 分隔子目录(对齐 PHP / → \)
463    let segments: Vec<&str> = class_part.split('/').collect();
464    let class_name = segments.last().unwrap_or(&"").to_string();
465
466    // 构建模块路径
467    let parent_segments: Vec<&str> = if segments.len() > 1 {
468        segments[..segments.len() - 1].to_vec()
469    } else {
470        Vec::new()
471    };
472
473    let module_path = if app.is_empty() {
474        if parent_segments.is_empty() {
475            layer.to_string()
476        } else {
477            format!("{}::{}", layer, parent_segments.join("::"))
478        }
479    } else if parent_segments.is_empty() {
480        format!("{}::{}", app, layer)
481    } else {
482        format!("{}::{}::{}", app, layer, parent_segments.join("::"))
483    };
484
485    // 构建文件路径(对齐 PHP getPathName:app\ → app/ + / 替换)
486    let mut path = PathBuf::from("app");
487    if !app.is_empty() {
488        path.push(app);
489    }
490    // 添加层目录
491    path.push(layer);
492    // 添加子目录
493    for seg in &parent_segments {
494        path.push(seg);
495    }
496    // 添加文件名
497    path.push(format!("{}.rs", class_name));
498
499    (class_name, module_path, path)
500}
501
502/// 检查文件是否已存在
503///
504/// 对齐 PHP `Make::execute()` 中 `already exists!` 提示。
505fn check_file_exists(path: &Path) -> Result<(), CliError> {
506    if path.exists() {
507        return Err(CliError::FileExists(path.display().to_string()));
508    }
509    Ok(())
510}
511
512/// 写入文件(自动创建父目录)
513///
514/// 对齐 PHP `mkdir` + `file_put_contents`。
515fn write_file(path: &Path, content: &str) -> Result<(), CliError> {
516    if let Some(parent) = path.parent() {
517        std::fs::create_dir_all(parent)?;
518    }
519    std::fs::write(path, content)?;
520    Ok(())
521}
522
523/// 类名转 snake_case 表名
524///
525/// `User` → `user`,`OrderItem` → `order_item`
526fn class_to_snake(s: &str) -> String {
527    let mut result = String::new();
528    for (i, ch) in s.chars().enumerate() {
529        if ch.is_uppercase() && i > 0 {
530            result.push('_');
531        }
532        result.push(ch.to_lowercase().next().unwrap_or(ch));
533    }
534    result
535}
536
537/// 迁移名转表名
538///
539/// `create_users` → `users`,`add_index_to_orders` → `orders`
540fn name_to_table(name: &str) -> String {
541    // 简单提取:create_xxx → xxx
542    if let Some(rest) = name.strip_prefix("create_") {
543        return rest.to_string();
544    }
545    if let Some(rest) = name.strip_prefix("add_") {
546        // add_index_to_orders → orders
547        if let Some(to_pos) = rest.find("_to_") {
548            return rest[to_pos + 4..].to_string();
549        }
550        return rest.to_string();
551    }
552    name.to_string()
553}
554
555#[cfg(test)]
556mod tests {
557    use super::*;
558
559    /// RAII 守卫:在作用域结束时恢复原始工作目录。
560    ///
561    /// 即使测试 panic 也能保证恢复,避免污染后续测试。
562    /// 配合 `super::super::test_support::acquire_global_lock()` 使用,
563    /// 确保与 optimize 模块测试的 set_current_dir 互斥。
564    struct CwdGuard {
565        original: Option<PathBuf>,
566    }
567
568    impl CwdGuard {
569        /// 切换到 `new_dir` 并返回守卫。守卫 drop 时恢复原目录。
570        fn switch(new_dir: &Path) -> std::io::Result<Self> {
571            let original = std::env::current_dir().ok();
572            std::env::set_current_dir(new_dir)?;
573            Ok(Self { original })
574        }
575    }
576
577    impl Drop for CwdGuard {
578        fn drop(&mut self) {
579            if let Some(ref orig) = self.original {
580                let _ = std::env::set_current_dir(orig);
581            }
582        }
583    }
584
585    #[test]
586    fn test_resolve_target_simple_model() {
587        let (class, module, path) = resolve_target("User", "model");
588        assert_eq!(class, "User");
589        assert_eq!(module, "model");
590        assert_eq!(path, PathBuf::from("app/model/User.rs"));
591    }
592
593    #[test]
594    fn test_resolve_target_nested_controller() {
595        let (class, module, path) = resolve_target("admin/User", "controller");
596        assert_eq!(class, "User");
597        assert_eq!(module, "controller::admin");
598        assert_eq!(path, PathBuf::from("app/controller/admin/User.rs"));
599    }
600
601    #[test]
602    fn test_resolve_target_with_app() {
603        let (class, module, _path) = resolve_target("admin@User", "model");
604        assert_eq!(class, "User");
605        assert_eq!(module, "admin::model");
606    }
607
608    #[test]
609    fn test_class_to_snake() {
610        assert_eq!(class_to_snake("User"), "user");
611        assert_eq!(class_to_snake("OrderItem"), "order_item");
612        assert_eq!(class_to_snake("API"), "a_p_i");
613    }
614
615    #[test]
616    fn test_name_to_table_create() {
617        assert_eq!(name_to_table("create_users"), "users");
618        assert_eq!(name_to_table("create_orders"), "orders");
619    }
620
621    #[test]
622    fn test_name_to_table_add() {
623        assert_eq!(name_to_table("add_index_to_orders"), "orders");
624        assert_eq!(name_to_table("add_status"), "status");
625    }
626
627    #[test]
628    fn test_name_to_table_other() {
629        assert_eq!(name_to_table("custom_migration"), "custom_migration");
630    }
631
632    #[test]
633    fn test_check_file_exists_nonexistent() {
634        let result = check_file_exists(Path::new("/nonexistent/path/file.txt"));
635        assert!(result.is_ok());
636    }
637
638    #[test]
639    fn test_check_file_exists_existing() {
640        // 使用临时文件确保文件确实存在
641        let temp = tempfile::NamedTempFile::new().unwrap();
642        let result = check_file_exists(temp.path());
643        assert!(matches!(result, Err(CliError::FileExists(_))));
644    }
645
646    #[test]
647    fn test_write_and_read_file() {
648        let temp_dir = tempfile::tempdir().unwrap();
649        let file_path = temp_dir.path().join("test_file.txt");
650
651        write_file(&file_path, "test content").unwrap();
652        assert_eq!(std::fs::read_to_string(&file_path).unwrap(), "test content");
653    }
654
655    #[test]
656    fn test_execute_make_migration_creates_files() {
657        let temp_dir = tempfile::tempdir().unwrap();
658        let path = temp_dir.path().to_str().unwrap();
659
660        execute_make_migration("create_test_table", path).unwrap();
661
662        let entries: Vec<_> = std::fs::read_dir(path).unwrap().collect();
663        assert_eq!(entries.len(), 2); // up + down
664
665        let mut has_up = false;
666        let mut has_down = false;
667        for entry in entries {
668            let name = entry.unwrap().file_name();
669            let name = name.to_string_lossy();
670            if name.ends_with("_up.sql") {
671                has_up = true;
672            }
673            if name.ends_with("_down.sql") {
674                has_down = true;
675            }
676        }
677        assert!(has_up);
678        assert!(has_down);
679    }
680
681    #[test]
682    fn test_execute_make_model_in_temp() {
683        let temp_dir = tempfile::tempdir().unwrap();
684        let _lock = super::super::test_support::acquire_global_lock();
685        let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
686
687        execute_make_model("TestUser").unwrap();
688
689        let model_path = temp_dir.path().join("app/model/TestUser.rs");
690        assert!(model_path.exists());
691
692        let content = std::fs::read_to_string(&model_path).unwrap();
693        assert!(content.contains("TestUser"));
694        assert!(content.contains("test_user"));
695    }
696
697    #[test]
698    fn test_execute_make_seeder_creates_file() {
699        let temp_dir = tempfile::tempdir().unwrap();
700        let path = temp_dir.path().to_str().unwrap();
701
702        execute_make_seeder("001_test_seed", path).unwrap();
703
704        let seed_path = Path::new(path).join("001_test_seed.sql");
705        assert!(seed_path.exists());
706
707        let content = std::fs::read_to_string(&seed_path).unwrap();
708        assert!(content.contains("001_test_seed"));
709        assert!(content.contains("-- Seed:"));
710        // 渲染后不应残留占位符
711        assert!(!content.contains("{%"));
712    }
713
714    #[test]
715    fn test_execute_make_seeder_file_already_exists() {
716        let temp_dir = tempfile::tempdir().unwrap();
717        let path = temp_dir.path().to_str().unwrap();
718
719        // 第一次创建应成功
720        execute_make_seeder("001_dup_seed", path).unwrap();
721        // 第二次创建同名文件应失败
722        let result = execute_make_seeder("001_dup_seed", path);
723        assert!(matches!(result, Err(CliError::FileExists(_))));
724    }
725
726    #[test]
727    fn test_execute_make_seeder_creates_directory() {
728        let temp_dir = tempfile::tempdir().unwrap();
729        let nested = temp_dir.path().join("nested").join("seeds");
730        let path = nested.to_str().unwrap();
731
732        // 目录不存在时应自动创建
733        execute_make_seeder("001_seed", path).unwrap();
734        assert!(nested.exists());
735    }
736
737    #[test]
738    fn test_make_validate_creates_file() {
739        let temp_dir = tempfile::tempdir().unwrap();
740        let _lock = super::super::test_support::acquire_global_lock();
741        let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
742
743        // 通过顶层 execute 入口调用 make validate(验证命令分发正常)
744        let cmd = MakeCommand::Validate {
745            name: "Order".to_string(),
746        };
747        execute(&cmd).unwrap();
748
749        let validate_path = temp_dir.path().join("app/validate/Order.rs");
750        assert!(validate_path.exists());
751
752        let content = std::fs::read_to_string(&validate_path).unwrap();
753        // 验证器 struct 与 Validate 引用
754        assert!(content.contains("pub struct OrderValidate;"));
755        assert!(content.contains("use sz_rust_core::validate::Validate"));
756        assert!(content.contains("app::validate"));
757        // 渲染后不应残留占位符
758        assert!(!content.contains("{%"));
759    }
760
761    #[test]
762    fn test_validate_stub_contains_required_elements() {
763        // 验证 stub 模板包含必要元素:struct 定义、Validate 引用、占位符
764        assert!(stubs::VALIDATE_STUB.contains("pub struct {%className%}Validate;"));
765        assert!(stubs::VALIDATE_STUB.contains("use sz_rust_core::validate::Validate"));
766        assert!(stubs::VALIDATE_STUB.contains("impl {%className%}Validate"));
767        assert!(stubs::VALIDATE_STUB.contains("pub fn new() -> Validate"));
768        assert!(stubs::VALIDATE_STUB.contains("{%className%}"));
769        assert!(stubs::VALIDATE_STUB.contains("{%namespace%}"));
770    }
771
772    #[test]
773    fn test_execute_make_validate_creates_file() {
774        let temp_dir = tempfile::tempdir().unwrap();
775        let _lock = super::super::test_support::acquire_global_lock();
776        let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
777
778        execute_make_validate("User").unwrap();
779
780        let validate_path = temp_dir.path().join("app/validate/User.rs");
781        assert!(validate_path.exists());
782
783        let content = std::fs::read_to_string(&validate_path).unwrap();
784        assert!(content.contains("UserValidate"));
785        assert!(content.contains("use sz_rust_core::validate::Validate"));
786        assert!(content.contains("app::validate"));
787        // 渲染后不应残留占位符
788        assert!(!content.contains("{%"));
789    }
790
791    #[test]
792    fn test_execute_make_validate_file_already_exists() {
793        let temp_dir = tempfile::tempdir().unwrap();
794        let _lock = super::super::test_support::acquire_global_lock();
795        let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
796
797        // 第一次创建应成功
798        execute_make_validate("User").unwrap();
799        // 第二次创建同名文件应失败
800        let result = execute_make_validate("User");
801        assert!(matches!(result, Err(CliError::FileExists(_))));
802    }
803
804    #[test]
805    fn test_execute_make_validate_nested_path() {
806        let temp_dir = tempfile::tempdir().unwrap();
807        let _lock = super::super::test_support::acquire_global_lock();
808        let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
809
810        // 嵌套路径:admin/User → app/validate/admin/User.rs
811        execute_make_validate("admin/User").unwrap();
812
813        let validate_path = temp_dir.path().join("app/validate/admin/User.rs");
814        assert!(validate_path.exists());
815
816        let content = std::fs::read_to_string(&validate_path).unwrap();
817        assert!(content.contains("UserValidate"));
818        assert!(content.contains("app::validate::admin"));
819    }
820
821    // ---------- make event/listener/command/service 测试 ----------
822
823    #[test]
824    fn test_execute_make_event_creates_file() {
825        let temp_dir = tempfile::tempdir().unwrap();
826        let _lock = super::super::test_support::acquire_global_lock();
827        let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
828
829        execute_make_event("UserLogin").unwrap();
830
831        let event_path = temp_dir.path().join("app/event/UserLogin.rs");
832        assert!(event_path.exists());
833
834        let content = std::fs::read_to_string(&event_path).unwrap();
835        assert!(content.contains("pub struct UserLogin;"));
836        assert!(content.contains("app::event"));
837        assert!(content.contains("UserLogin"));
838        assert!(content.contains("use serde_json::Value"));
839        // 渲染后不应残留占位符
840        assert!(!content.contains("{%"));
841    }
842
843    #[test]
844    fn test_execute_make_event_file_already_exists() {
845        let temp_dir = tempfile::tempdir().unwrap();
846        let _lock = super::super::test_support::acquire_global_lock();
847        let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
848
849        execute_make_event("UserLogin").unwrap();
850        let result = execute_make_event("UserLogin");
851        assert!(matches!(result, Err(CliError::FileExists(_))));
852    }
853
854    #[test]
855    fn test_execute_make_event_nested_path() {
856        let temp_dir = tempfile::tempdir().unwrap();
857        let _lock = super::super::test_support::acquire_global_lock();
858        let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
859
860        execute_make_event("admin/UserLogin").unwrap();
861
862        let event_path = temp_dir.path().join("app/event/admin/UserLogin.rs");
863        assert!(event_path.exists());
864
865        let content = std::fs::read_to_string(&event_path).unwrap();
866        assert!(content.contains("app::event::admin"));
867    }
868
869    #[test]
870    fn test_execute_make_listener_default_event_name() {
871        let temp_dir = tempfile::tempdir().unwrap();
872        let _lock = super::super::test_support::acquire_global_lock();
873        let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
874
875        // 未指定 --event,事件名默认使用类名
876        execute_make_listener("SendWelcomeEmail", None).unwrap();
877
878        let listener_path = temp_dir.path().join("app/listener/SendWelcomeEmail.rs");
879        assert!(listener_path.exists());
880
881        let content = std::fs::read_to_string(&listener_path).unwrap();
882        assert!(content.contains("pub struct SendWelcomeEmail;"));
883        assert!(content.contains("app::listener"));
884        assert!(content.contains("use sz_rust_core::event::{EventError, Listener}"));
885        assert!(content.contains("impl Listener for SendWelcomeEmail"));
886        // 默认事件名 = 类名
887        assert!(content.contains(r#""SendWelcomeEmail""#));
888        assert!(!content.contains("{%"));
889    }
890
891    #[test]
892    fn test_execute_make_listener_custom_event_name() {
893        let temp_dir = tempfile::tempdir().unwrap();
894        let _lock = super::super::test_support::acquire_global_lock();
895        let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
896
897        // 指定 --event UserLogin
898        execute_make_listener("SendWelcomeEmail", Some("UserLogin")).unwrap();
899
900        let listener_path = temp_dir.path().join("app/listener/SendWelcomeEmail.rs");
901        assert!(listener_path.exists());
902
903        let content = std::fs::read_to_string(&listener_path).unwrap();
904        assert!(content.contains(r#""UserLogin""#));
905        assert!(!content.contains(r#""SendWelcomeEmail""#));
906        assert!(!content.contains("{%"));
907    }
908
909    #[test]
910    fn test_execute_make_listener_file_already_exists() {
911        let temp_dir = tempfile::tempdir().unwrap();
912        let _lock = super::super::test_support::acquire_global_lock();
913        let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
914
915        execute_make_listener("SendWelcomeEmail", None).unwrap();
916        let result = execute_make_listener("SendWelcomeEmail", None);
917        assert!(matches!(result, Err(CliError::FileExists(_))));
918    }
919
920    #[test]
921    fn test_execute_make_command_creates_file() {
922        let temp_dir = tempfile::tempdir().unwrap();
923        let _lock = super::super::test_support::acquire_global_lock();
924        let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
925
926        execute_make_command("SyncData").unwrap();
927
928        let command_path = temp_dir.path().join("app/command/SyncData.rs");
929        assert!(command_path.exists());
930
931        let content = std::fs::read_to_string(&command_path).unwrap();
932        assert!(content.contains("pub struct SyncData;"));
933        assert!(content.contains("app::command"));
934        assert!(content.contains("use sz_rust_cli::console::{Command, CommandSignature}"));
935        assert!(content.contains("impl Command for SyncData"));
936        // 命令名应为 snake_case:sync_data
937        assert!(content.contains(r#""sync_data""#));
938        assert!(!content.contains("{%"));
939    }
940
941    #[test]
942    fn test_execute_make_command_file_already_exists() {
943        let temp_dir = tempfile::tempdir().unwrap();
944        let _lock = super::super::test_support::acquire_global_lock();
945        let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
946
947        execute_make_command("SyncData").unwrap();
948        let result = execute_make_command("SyncData");
949        assert!(matches!(result, Err(CliError::FileExists(_))));
950    }
951
952    #[test]
953    fn test_execute_make_command_nested_path() {
954        let temp_dir = tempfile::tempdir().unwrap();
955        let _lock = super::super::test_support::acquire_global_lock();
956        let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
957
958        execute_make_command("admin/SyncData").unwrap();
959
960        let command_path = temp_dir.path().join("app/command/admin/SyncData.rs");
961        assert!(command_path.exists());
962
963        let content = std::fs::read_to_string(&command_path).unwrap();
964        assert!(content.contains("app::command::admin"));
965    }
966
967    #[test]
968    fn test_execute_make_service_creates_file() {
969        let temp_dir = tempfile::tempdir().unwrap();
970        let _lock = super::super::test_support::acquire_global_lock();
971        let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
972
973        execute_make_service("UserService").unwrap();
974
975        let service_path = temp_dir.path().join("app/service/UserService.rs");
976        assert!(service_path.exists());
977
978        let content = std::fs::read_to_string(&service_path).unwrap();
979        assert!(content.contains("pub struct UserService;"));
980        assert!(content.contains("app::service"));
981        assert!(content.contains("impl Default for UserService"));
982        assert!(content.contains("pub fn new() -> Self"));
983        assert!(!content.contains("{%"));
984    }
985
986    #[test]
987    fn test_execute_make_service_file_already_exists() {
988        let temp_dir = tempfile::tempdir().unwrap();
989        let _lock = super::super::test_support::acquire_global_lock();
990        let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
991
992        execute_make_service("UserService").unwrap();
993        let result = execute_make_service("UserService");
994        assert!(matches!(result, Err(CliError::FileExists(_))));
995    }
996
997    #[test]
998    fn test_execute_make_service_nested_path() {
999        let temp_dir = tempfile::tempdir().unwrap();
1000        let _lock = super::super::test_support::acquire_global_lock();
1001        let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
1002
1003        execute_make_service("admin/UserService").unwrap();
1004
1005        let service_path = temp_dir.path().join("app/service/admin/UserService.rs");
1006        assert!(service_path.exists());
1007
1008        let content = std::fs::read_to_string(&service_path).unwrap();
1009        assert!(content.contains("app::service::admin"));
1010    }
1011}