Skip to main content

sz_rust_cli/cmd/
make.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2024-2026 SZ-Rust Team
3//
4//! `make:*` 代码生成命令 — 对齐 PHP `think\console\command\make\*`
5//!
6//! ## PHP 对齐
7//!
8//! PHP `Make::execute()` 流程:
9//! 1. `getArgument('name')` 获取类名
10//! 2. `getClassName(name)` 处理 `@` 分隔应用名 + `/` 转 `\`
11//! 3. `getPathName(className)` 剥离 `app\` 前缀 + `/` 替换 + `.php` 后缀
12//! 4. 检查文件存在 → `mkdir` → `file_put_contents(buildClass())`
13//! 5. `buildClass(name)` 读取 stub,替换占位符
14//!
15//! Rust 端对齐上述流程,但生成 `.rs` 文件而非 `.php` 文件。
16
17use std::path::{Path, PathBuf};
18
19use clap::Subcommand;
20
21use crate::error::CliError;
22use crate::stubs::{self, render_template};
23
24/// `make` 子命令枚举
25///
26/// 对齐 PHP `think make:*` 命令组。
27#[derive(Subcommand, Debug)]
28pub enum MakeCommand {
29    /// 生成 Model(对齐 `php think make:model User`)
30    #[command(name = "model")]
31    Model {
32        /// 类名(如 `User` 或 `admin/User`)
33        name: String,
34    },
35
36    /// 生成 Controller(对齐 `php think make:controller User`)
37    #[command(name = "controller")]
38    Controller {
39        /// 类名
40        name: String,
41        /// 生成 API 风格控制器(5 方法,无 create/edit)
42        #[arg(long)]
43        api: bool,
44        /// 生成空控制器
45        #[arg(long)]
46        plain: bool,
47    },
48
49    /// 生成迁移文件(对齐 Phinx `make:migration`)
50    #[command(name = "migration")]
51    Migration {
52        /// 迁移名称(如 `create_users`)
53        name: String,
54        /// 迁移目录(默认 `migrations`)
55        #[arg(short = 'p', long, default_value = "migrations")]
56        path: String,
57    },
58
59    /// 生成填充文件(对齐 PHP `make:seeder`)
60    ///
61    /// 在 `seeds/` 目录下生成 `<name>.sql` 文件骨架。
62    #[command(name = "seeder")]
63    Seeder {
64        /// 填充文件名称(如 `001_users_seed`)
65        name: String,
66        /// 填充目录(默认 `seeds`)
67        #[arg(short = 'p', long, default_value = "seeds")]
68        path: String,
69    },
70
71    /// 生成 Guard(sz-rust 自研,无 PHP 对应)
72    #[command(name = "guard")]
73    Guard {
74        /// Guard 名称(如 `Admin`)
75        name: String,
76    },
77
78    /// 生成验证器(对齐 PHP `make:validate`)
79    ///
80    /// 在 `app/validate/` 目录下生成 `<Name>.rs` 验证器骨架。
81    #[command(name = "validate")]
82    Validate {
83        /// 验证器类名(如 `User` 或 `admin/User`)
84        name: String,
85    },
86
87    /// 生成事件类(对齐 PHP `make:event`)
88    ///
89    /// 在 `app/event/` 目录下生成 `<Name>.rs` 事件骨架,包含事件名与负载构造方法。
90    #[command(name = "event")]
91    Event {
92        /// 事件类名(如 `UserLogin` 或 `admin/UserLogin`)
93        name: String,
94    },
95
96    /// 生成监听器(对齐 PHP `make:listener`)
97    ///
98    /// 在 `app/listener/` 目录下生成 `<Name>.rs` 监听器骨架,实现 `Listener` trait。
99    #[command(name = "listener")]
100    Listener {
101        /// 监听器类名(如 `SendWelcomeEmail`)
102        name: String,
103        /// 监听的事件名(可选,默认使用类名)
104        #[arg(long)]
105        event: Option<String>,
106    },
107
108    /// 生成自定义命令(对齐 PHP `make:command`)
109    ///
110    /// 在 `app/command/` 目录下生成 `<Name>.rs` 命令骨架,实现 `Command` trait。
111    #[command(name = "command")]
112    Command {
113        /// 命令类名(如 `SyncData`)
114        name: String,
115    },
116
117    /// 生成服务类(对齐 PHP `make:service`)
118    ///
119    /// 在 `app/service/` 目录下生成 `<Name>.rs` 服务骨架,可注入容器。
120    #[command(name = "service")]
121    Service {
122        /// 服务类名(如 `UserService`)
123        name: String,
124    },
125
126    /// 生成中间件(sz-rust 自研,对齐 NestJS `make:middleware`)
127    ///
128    /// 在 `app/middleware/` 目录下生成 `<Name>.rs` 中间件骨架,
129    /// 实现 `SzMiddleware` trait,可注册到中间件链。
130    #[command(name = "middleware")]
131    Middleware {
132        /// 中间件类名(如 `CorsMiddleware` 或 `admin/RateLimit`)
133        name: String,
134    },
135
136    /// 生成脚手架(Model + Controller + Migration)
137    #[command(name = "scaffold")]
138    Scaffold {
139        /// 资源名称(如 `User`)
140        name: String,
141    },
142
143    /// 生成插件骨架(P1-T3 插件模板库)
144    ///
145    /// 基于 Tera 模板引擎生成完整插件结构,含 Model、Controller、Service、
146    /// Repository、Migration、Routes、Manifest、Tests。
147    /// 生成后自动执行 `cargo check` 验证,失败时回滚已写入文件。
148    #[command(name = "plugin")]
149    Plugin {
150        /// 模板类型(如 `crud` 或 `master-slave`)
151        #[arg(long)]
152        template: String,
153        /// 插件名称(如 `user-management`)
154        #[arg(long)]
155        name: String,
156        /// 表名(可选,默认取插件名 snake_case)
157        #[arg(long)]
158        table: Option<String>,
159        /// 字段定义(如 `id:i32:pk,name:String,age:i32`)
160        #[arg(long)]
161        fields: Option<String>,
162        /// 强制覆盖已存在目录
163        #[arg(long)]
164        force: bool,
165        /// 输出目录(可选,默认 `plugins/<name>/`)
166        #[arg(long)]
167        output: Option<String>,
168        /// 主表名(主从模板专用)
169        #[arg(long)]
170        master: Option<String>,
171        /// 从表名(主从模板专用)
172        #[arg(long)]
173        slave: Option<String>,
174        /// 主表字段定义(主从模板专用)
175        #[arg(long)]
176        master_fields: Option<String>,
177        /// 从表字段定义(主从模板专用)
178        #[arg(long)]
179        slave_fields: Option<String>,
180        /// 外键字段名(主从模板专用)
181        #[arg(long)]
182        foreign_key: Option<String>,
183    },
184
185    /// 生成前端代码(P4-T1 前端代码生成)
186    ///
187    /// 根据 ORM 模型自动生成 Vue/React 组件、路由、权限、API 客户端。
188    /// 对齐 Laravel Artisan `make:frontend` 风格。
189    #[command(name = "frontend")]
190    Frontend {
191        /// 要生成的模型名(可多次指定)
192        #[arg(long = "model")]
193        models: Vec<String>,
194        /// 模型目录(默认 `src/model/`)
195        #[arg(long = "model-dir", default_value = "src/model/")]
196        model_dir: String,
197        /// 前端框架(vue / react)
198        #[arg(long, default_value = "vue")]
199        framework: String,
200        /// UI 组件库(element_plus / ant_design_vue)
201        #[arg(long = "ui", default_value = "element_plus")]
202        ui: String,
203        /// 输出目录(默认 `./frontend/`)
204        #[arg(long, default_value = "./frontend/")]
205        output: String,
206        /// 自定义模板目录
207        #[arg(long = "template-dir")]
208        template_dir: Option<String>,
209        /// 覆盖策略(skip / overwrite / merge)
210        #[arg(long = "override", default_value = "skip")]
211        override_strategy: String,
212        /// 生成测试骨架
213        #[arg(long = "with-tests")]
214        with_tests: bool,
215        /// 生成请求拦截器
216        #[arg(long = "with-interceptors")]
217        with_interceptors: bool,
218        /// 懒加载路由(默认 true)
219        #[arg(long = "lazy-load", default_value_t = true)]
220        lazy_load: bool,
221        /// 强制覆盖非空输出目录
222        #[arg(long)]
223        force: bool,
224    },
225
226    /// 生成 OpenAPI 3.0 文档(对齐 PHP `make:openapi`)
227    ///
228    /// 从路由定义自动生成 OpenAPI 3.0 JSON 文档。
229    #[command(name = "openapi")]
230    Openapi {
231        /// 输出文件路径(默认 `openapi.json`)
232        #[arg(short = 'o', long, default_value = "openapi.json")]
233        output: String,
234        /// API 标题
235        #[arg(long, default_value = "SZ-Rust API")]
236        title: String,
237        /// API 版本
238        #[arg(long, default_value = "1.0.0")]
239        version: String,
240        /// 强制覆盖已存在文件
241        #[arg(long)]
242        force: bool,
243    },
244}
245
246/// 执行 make 子命令
247pub 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
326/// 生成 Model 文件
327///
328/// 对齐 PHP `make:model`:读取 `model.stub`,替换占位符,写入 `app/model/{name}.rs`。
329fn 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
349/// 生成 Controller 文件
350///
351/// 对齐 PHP `make:controller`:根据 `--api` / `--plain` 选择不同 stub。
352fn 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
381/// 生成迁移文件
382///
383/// 对齐 Phinx 风格:生成 `{timestamp}_{name}_up.sql` 和 `{timestamp}_{name}_down.sql`。
384fn 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%}", &timestamp),
402            ("{%table_name%}", &table_name),
403        ],
404    );
405    let down_content = render_template(
406        stubs::MIGRATION_DOWN_STUB,
407        &[
408            ("{%name%}", name),
409            ("{%timestamp%}", &timestamp),
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
424/// 生成填充文件
425///
426/// 对齐 PHP `make:seeder`:在 `seeds/` 目录下生成 `<name>.sql` 文件骨架。
427fn 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%}", &timestamp)],
440    );
441
442    write_file(&file_path, &content)?;
443    println!("Seeder created: {}", file_path.display());
444    Ok(())
445}
446
447/// 生成 Guard 文件(sz-rust 自研)
448fn 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
461/// 生成验证器文件
462///
463/// 对齐 PHP `make:validate`:在 `app/validate/` 目录下生成 `<Name>.rs` 验证器骨架,
464/// 包含 `Validate` 结构体初始化与常见规则示例。
465fn 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
483/// 生成事件类文件
484///
485/// 对齐 PHP `make:event`:读取 `event.stub`,替换占位符,写入 `app/event/{name}.rs`。
486fn 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    // 事件名默认使用类名(对齐 PHP `make:event` 默认行为)
492    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
507/// 生成监听器文件
508///
509/// 对齐 PHP `make:listener`:读取 `listener.stub`,替换占位符,写入 `app/listener/{name}.rs`。
510/// 若未指定 `--event`,事件名默认使用监听器类名。
511fn 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    // 事件名:优先使用 --event 参数,否则默认使用类名
517    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
532/// 生成自定义命令文件
533///
534/// 对齐 PHP `make:command`:读取 `command.stub`,替换占位符,写入 `app/command/{name}.rs`。
535/// 命令名默认为类名的 snake_case 形式(如 `SyncData` → `sync_data`)。
536fn 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    // 命令名:snake_case(类名)(对齐 PHP `make:command` 默认行为)
542    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
557/// 生成服务类文件
558///
559/// 对齐 PHP `make:service`:读取 `service.stub`,替换占位符,写入 `app/service/{name}.rs`。
560fn 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
578/// 生成中间件文件(sz-rust 自研)
579///
580/// 在 `app/middleware/` 目录下生成 `<Name>.rs` 中间件骨架,
581/// 实现 `SzMiddleware` trait,可注册到中间件链。
582fn 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
600/// 生成脚手架(Model + Controller + Migration)
601fn 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
610// ============================================================================
611// 辅助函数(对齐 PHP Make 基类方法)
612// ============================================================================
613
614/// 解析目标(类名 + 模块路径 + 文件路径)
615///
616/// 对齐 PHP `Make::getClassName()` + `getPathName()`:
617///
618/// - `User` → (`User`, `model`, `app/model/User.rs`)
619/// - `admin/User` → (`User`, `controller::admin`, `app/controller/admin/User.rs`)
620/// - `admin@User` → (`User`, `admin::model`, `app/admin/model/User.rs`)
621fn resolve_target(name: &str, layer: &str) -> (String, String, PathBuf) {
622    // 处理 @ 分隔应用名(对齐 PHP getClassName)
623    let (app, class_part) = if let Some(idx) = name.find('@') {
624        (&name[..idx], &name[idx + 1..])
625    } else {
626        ("", name)
627    };
628
629    // 处理 / 分隔子目录(对齐 PHP / → \)
630    let segments: Vec<&str> = class_part.split('/').collect();
631    let class_name = segments.last().unwrap_or(&"").to_string();
632
633    // 构建模块路径
634    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    // 构建文件路径(对齐 PHP getPathName:app\ → app/ + / 替换)
653    let mut path = PathBuf::from("app");
654    if !app.is_empty() {
655        path.push(app);
656    }
657    // 添加层目录
658    path.push(layer);
659    // 添加子目录
660    for seg in &parent_segments {
661        path.push(seg);
662    }
663    // 添加文件名
664    path.push(format!("{}.rs", class_name));
665
666    (class_name, module_path, path)
667}
668
669/// 检查文件是否已存在
670///
671/// 对齐 PHP `Make::execute()` 中 `already exists!` 提示。
672fn 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
679/// 写入文件(自动创建父目录)
680///
681/// 对齐 PHP `mkdir` + `file_put_contents`。
682fn 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
690/// 类名转 snake_case 表名
691///
692/// `User` → `user`,`OrderItem` → `order_item`
693fn 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
704/// 迁移名转表名
705///
706/// `create_users` → `users`,`add_index_to_orders` → `orders`
707fn name_to_table(name: &str) -> String {
708    // 简单提取:create_xxx → xxx
709    if let Some(rest) = name.strip_prefix("create_") {
710        return rest.to_string();
711    }
712    if let Some(rest) = name.strip_prefix("add_") {
713        // add_index_to_orders → orders
714        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
722/// `make:plugin` 命令主流程(P1-T3)
723///
724/// 状态机:Validate → LoadTemplates → BuildContext → Render → WriteFiles
725/// 后续 Batch D 将追加 CargoCheck → Rollback 分支。
726pub 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/// 执行前端代码生成(P4-T1)
915///
916/// 将 CLI 参数转换为 `GenerationConfig`,调用 `CodegenService::generate`,
917/// 输出结构化报告到 stdout。
918#[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
990// ============================================================================
991// T033: make:openapi — OpenAPI 3.0 文档生成
992// ============================================================================
993
994/// 生成 OpenAPI 3.0 JSON 文档
995async 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/// OpenAPI 3.0 规范
1016#[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
1031/// 生成 OpenAPI 规范(从路由定义)
1032fn 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
1062// ============================================================================
1063// T034: --force 标志 + 生成后校验
1064// ============================================================================
1065
1066/// 检查文件是否存在,--force 时允许覆盖
1067fn 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
1074/// 生成后校验:cargo fmt + cargo check + cargo clippy
1075fn 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    /// RAII 守卫:在作用域结束时恢复原始工作目录。
1117    ///
1118    /// 即使测试 panic 也能保证恢复,避免污染后续测试。
1119    /// 配合 `super::super::test_support::acquire_global_lock()` 使用,
1120    /// 确保与 optimize 模块测试的 set_current_dir 互斥。
1121    struct CwdGuard {
1122        original: Option<PathBuf>,
1123        _lock: std::sync::MutexGuard<'static, ()>,
1124    }
1125
1126    impl CwdGuard {
1127        /// 切换到 `new_dir` 并返回守卫。守卫 drop 时恢复原目录。
1128        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        // 使用临时文件确保文件确实存在
1203        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); // up + down
1226
1227        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        // 渲染后不应残留占位符
1272        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        // 第一次创建应成功
1281        execute_make_seeder("001_dup_seed", path).unwrap();
1282        // 第二次创建同名文件应失败
1283        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        // 目录不存在时应自动创建
1294        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        // 通过顶层 execute 入口调用 make validate(验证命令分发正常)
1304        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        // 验证器 struct 与 Validate 引用
1314        assert!(content.contains("pub struct OrderValidate;"));
1315        assert!(content.contains("use sz_rust_core::validate::Validate"));
1316        assert!(content.contains("app::validate"));
1317        // 渲染后不应残留占位符
1318        assert!(!content.contains("{%"));
1319    }
1320
1321    #[test]
1322    fn test_validate_stub_contains_required_elements() {
1323        // 验证 stub 模板包含必要元素:struct 定义、Validate 引用、占位符
1324        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        // 渲染后不应残留占位符
1347        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        // 第一次创建应成功
1356        execute_make_validate("User").unwrap();
1357        // 第二次创建同名文件应失败
1358        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        // 嵌套路径:admin/User → app/validate/admin/User.rs
1368        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    // ---------- make event/listener/command/service 测试 ----------
1379
1380    #[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        // 渲染后不应残留占位符
1396        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        // 未指定 --event,事件名默认使用类名
1429        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        // 默认事件名 = 类名
1440        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        // 指定 --event UserLogin
1450        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        // 命令名应为 snake_case:sync_data
1487        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    // ---------- make controller/guard/middleware/scaffold 测试 ----------
1558
1559    #[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        // scaffold 应生成 model + controller + migration
1673        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        // migration 目录应有文件
1678        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    // ---------- make:plugin 测试 ----------
1849
1850    #[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        // 生成后 cargo check 因无 Cargo.toml 失败,回滚
1873        let result = execute_make_plugin(args).await;
1874        assert!(result.is_err(), "无 Cargo.toml 应失败");
1875        let err = format!("{}", result.unwrap_err());
1876        // 失败文案跨平台不同:Windows 直报 Cargo.toml 缺失,Linux 走
1877        // CompileFailed 包装为 "Compilation failed: [...]"——断言失败族而非文案
1878        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}