use std::path::{Path, PathBuf};
use clap::Subcommand;
use crate::error::CliError;
use crate::stubs::{self, render_template};
#[derive(Subcommand, Debug)]
pub enum MakeCommand {
#[command(name = "model")]
Model {
name: String,
},
#[command(name = "controller")]
Controller {
name: String,
#[arg(long)]
api: bool,
#[arg(long)]
plain: bool,
},
#[command(name = "migration")]
Migration {
name: String,
#[arg(short = 'p', long, default_value = "migrations")]
path: String,
},
#[command(name = "seeder")]
Seeder {
name: String,
#[arg(short = 'p', long, default_value = "seeds")]
path: String,
},
#[command(name = "guard")]
Guard {
name: String,
},
#[command(name = "validate")]
Validate {
name: String,
},
#[command(name = "event")]
Event {
name: String,
},
#[command(name = "listener")]
Listener {
name: String,
#[arg(long)]
event: Option<String>,
},
#[command(name = "command")]
Command {
name: String,
},
#[command(name = "service")]
Service {
name: String,
},
#[command(name = "middleware")]
Middleware {
name: String,
},
#[command(name = "scaffold")]
Scaffold {
name: String,
},
#[command(name = "plugin")]
Plugin {
#[arg(long)]
template: String,
#[arg(long)]
name: String,
#[arg(long)]
table: Option<String>,
#[arg(long)]
fields: Option<String>,
#[arg(long)]
force: bool,
#[arg(long)]
output: Option<String>,
#[arg(long)]
master: Option<String>,
#[arg(long)]
slave: Option<String>,
#[arg(long)]
master_fields: Option<String>,
#[arg(long)]
slave_fields: Option<String>,
#[arg(long)]
foreign_key: Option<String>,
},
#[command(name = "frontend")]
Frontend {
#[arg(long = "model")]
models: Vec<String>,
#[arg(long = "model-dir", default_value = "src/model/")]
model_dir: String,
#[arg(long, default_value = "vue")]
framework: String,
#[arg(long = "ui", default_value = "element_plus")]
ui: String,
#[arg(long, default_value = "./frontend/")]
output: String,
#[arg(long = "template-dir")]
template_dir: Option<String>,
#[arg(long = "override", default_value = "skip")]
override_strategy: String,
#[arg(long = "with-tests")]
with_tests: bool,
#[arg(long = "with-interceptors")]
with_interceptors: bool,
#[arg(long = "lazy-load", default_value_t = true)]
lazy_load: bool,
#[arg(long)]
force: bool,
},
#[command(name = "openapi")]
Openapi {
#[arg(short = 'o', long, default_value = "openapi.json")]
output: String,
#[arg(long, default_value = "SZ-Rust API")]
title: String,
#[arg(long, default_value = "1.0.0")]
version: String,
#[arg(long)]
force: bool,
},
}
pub async fn execute(cmd: &MakeCommand) -> Result<(), CliError> {
match cmd {
MakeCommand::Model { name } => execute_make_model(name),
MakeCommand::Controller { name, api, plain } => execute_make_controller(name, *api, *plain),
MakeCommand::Migration { name, path } => execute_make_migration(name, path),
MakeCommand::Seeder { name, path } => execute_make_seeder(name, path),
MakeCommand::Guard { name } => execute_make_guard(name),
MakeCommand::Validate { name } => execute_make_validate(name),
MakeCommand::Event { name } => execute_make_event(name),
MakeCommand::Listener { name, event } => execute_make_listener(name, event.as_deref()),
MakeCommand::Command { name } => execute_make_command(name),
MakeCommand::Service { name } => execute_make_service(name),
MakeCommand::Middleware { name } => execute_make_middleware(name),
MakeCommand::Scaffold { name } => execute_make_scaffold(name),
MakeCommand::Plugin {
template,
name,
table,
fields,
force,
output,
master,
slave,
master_fields,
slave_fields,
foreign_key,
} => {
execute_make_plugin(crate::context_builder::PluginCommandArgs {
template: template.clone(),
name: name.clone(),
table: table.clone(),
fields: fields.clone(),
force: *force,
output: output.clone(),
master: master.clone(),
slave: slave.clone(),
master_fields: master_fields.clone(),
slave_fields: slave_fields.clone(),
foreign_key: foreign_key.clone(),
})
.await
}
MakeCommand::Frontend {
models,
model_dir,
framework,
ui,
output,
template_dir,
override_strategy,
with_tests,
with_interceptors,
lazy_load,
force,
} => {
execute_make_frontend(
models,
model_dir,
framework,
ui,
output,
template_dir.as_deref(),
override_strategy,
*with_tests,
*with_interceptors,
*lazy_load,
*force,
)
.await
}
MakeCommand::Openapi {
output,
title,
version,
force,
} => execute_make_openapi(output, title, version, *force).await,
}
}
fn execute_make_model(name: &str) -> Result<(), CliError> {
let (class_name, module_path, file_path) = resolve_target(name, "model");
check_file_exists(&file_path)?;
let namespace = format!("app::{}", module_path);
let table_name = class_to_snake(&class_name);
let content = render_template(
stubs::MODEL_STUB,
&[
("{%className%}", &class_name),
("{%namespace%}", &namespace),
("{%table_name%}", &table_name),
],
);
write_file(&file_path, &content)?;
println!("Model created: {}", file_path.display());
Ok(())
}
fn execute_make_controller(name: &str, api: bool, plain: bool) -> Result<(), CliError> {
let (class_name, module_path, file_path) = resolve_target(name, "controller");
check_file_exists(&file_path)?;
let namespace = format!("app::{}", module_path);
let route = class_to_snake(&class_name);
let template = if plain {
stubs::CONTROLLER_PLAIN_STUB
} else if api {
stubs::CONTROLLER_API_STUB
} else {
stubs::CONTROLLER_STUB
};
let content = render_template(
template,
&[
("{%className%}", &class_name),
("{%namespace%}", &namespace),
("{%route%}", &route),
],
);
write_file(&file_path, &content)?;
println!("Controller created: {}", file_path.display());
Ok(())
}
fn execute_make_migration(name: &str, path: &str) -> Result<(), CliError> {
let dir = Path::new(path);
std::fs::create_dir_all(dir)?;
let timestamp = chrono::Utc::now().format("%Y%m%d%H%M%S").to_string();
let table_name = name_to_table(name);
let up_file = dir.join(format!("{}_{}_up.sql", timestamp, name));
let down_file = dir.join(format!("{}_{}_down.sql", timestamp, name));
check_file_exists(&up_file)?;
check_file_exists(&down_file)?;
let up_content = render_template(
stubs::MIGRATION_UP_STUB,
&[
("{%name%}", name),
("{%timestamp%}", ×tamp),
("{%table_name%}", &table_name),
],
);
let down_content = render_template(
stubs::MIGRATION_DOWN_STUB,
&[
("{%name%}", name),
("{%timestamp%}", ×tamp),
("{%table_name%}", &table_name),
],
);
write_file(&up_file, &up_content)?;
write_file(&down_file, &down_content)?;
println!(
"Migration created: {} & {}",
up_file.display(),
down_file.display()
);
Ok(())
}
fn execute_make_seeder(name: &str, path: &str) -> Result<(), CliError> {
let dir = Path::new(path);
std::fs::create_dir_all(dir)?;
let file_path = dir.join(format!("{}.sql", name));
check_file_exists(&file_path)?;
let timestamp = chrono::Utc::now()
.format("%Y-%m-%d %H:%M:%S UTC")
.to_string();
let content = render_template(
stubs::SEED_STUB,
&[("{%name%}", name), ("{%timestamp%}", ×tamp)],
);
write_file(&file_path, &content)?;
println!("Seeder created: {}", file_path.display());
Ok(())
}
fn execute_make_guard(name: &str) -> Result<(), CliError> {
let (class_name, _module_path, file_path) = resolve_target(name, "guard");
check_file_exists(&file_path)?;
let content = format!(
"//! 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"
);
write_file(&file_path, &content)?;
println!("Guard created: {}", file_path.display());
Ok(())
}
fn execute_make_validate(name: &str) -> Result<(), CliError> {
let (class_name, module_path, file_path) = resolve_target(name, "validate");
check_file_exists(&file_path)?;
let namespace = format!("app::{}", module_path);
let content = render_template(
stubs::VALIDATE_STUB,
&[
("{%className%}", &class_name),
("{%namespace%}", &namespace),
],
);
write_file(&file_path, &content)?;
println!("Validator created: {}", file_path.display());
Ok(())
}
fn execute_make_event(name: &str) -> Result<(), CliError> {
let (class_name, module_path, file_path) = resolve_target(name, "event");
check_file_exists(&file_path)?;
let namespace = format!("app::{}", module_path);
let event_name = class_name.clone();
let content = render_template(
stubs::EVENT_STUB,
&[
("{%className%}", &class_name),
("{%namespace%}", &namespace),
("{%event_name%}", &event_name),
],
);
write_file(&file_path, &content)?;
println!("Event created: {}", file_path.display());
Ok(())
}
fn execute_make_listener(name: &str, event: Option<&str>) -> Result<(), CliError> {
let (class_name, module_path, file_path) = resolve_target(name, "listener");
check_file_exists(&file_path)?;
let namespace = format!("app::{}", module_path);
let event_name = event.unwrap_or(&class_name).to_string();
let content = render_template(
stubs::LISTENER_STUB,
&[
("{%className%}", &class_name),
("{%namespace%}", &namespace),
("{%event_name%}", &event_name),
],
);
write_file(&file_path, &content)?;
println!("Listener created: {}", file_path.display());
Ok(())
}
fn execute_make_command(name: &str) -> Result<(), CliError> {
let (class_name, module_path, file_path) = resolve_target(name, "command");
check_file_exists(&file_path)?;
let namespace = format!("app::{}", module_path);
let command_name = class_to_snake(&class_name);
let content = render_template(
stubs::COMMAND_STUB,
&[
("{%className%}", &class_name),
("{%namespace%}", &namespace),
("{%command_name%}", &command_name),
],
);
write_file(&file_path, &content)?;
println!("Command created: {}", file_path.display());
Ok(())
}
fn execute_make_service(name: &str) -> Result<(), CliError> {
let (class_name, module_path, file_path) = resolve_target(name, "service");
check_file_exists(&file_path)?;
let namespace = format!("app::{}", module_path);
let content = render_template(
stubs::SERVICE_STUB,
&[
("{%className%}", &class_name),
("{%namespace%}", &namespace),
],
);
write_file(&file_path, &content)?;
println!("Service created: {}", file_path.display());
Ok(())
}
fn execute_make_middleware(name: &str) -> Result<(), CliError> {
let (class_name, module_path, file_path) = resolve_target(name, "middleware");
check_file_exists(&file_path)?;
let namespace = format!("app::{}", module_path);
let content = render_template(
stubs::MIDDLEWARE_STUB,
&[
("{%className%}", &class_name),
("{%namespace%}", &namespace),
],
);
write_file(&file_path, &content)?;
println!("Middleware created: {}", file_path.display());
Ok(())
}
fn execute_make_scaffold(name: &str) -> Result<(), CliError> {
println!("Scaffolding for: {}", name);
execute_make_model(name)?;
execute_make_controller(name, false, false)?;
execute_make_migration(&class_to_snake(name), "migrations")?;
println!("Scaffold complete.");
Ok(())
}
fn resolve_target(name: &str, layer: &str) -> (String, String, PathBuf) {
let (app, class_part) = if let Some(idx) = name.find('@') {
(&name[..idx], &name[idx + 1..])
} else {
("", name)
};
let segments: Vec<&str> = class_part.split('/').collect();
let class_name = segments.last().unwrap_or(&"").to_string();
let parent_segments: Vec<&str> = if segments.len() > 1 {
segments[..segments.len() - 1].to_vec()
} else {
Vec::new()
};
let module_path = if app.is_empty() {
if parent_segments.is_empty() {
layer.to_string()
} else {
format!("{}::{}", layer, parent_segments.join("::"))
}
} else if parent_segments.is_empty() {
format!("{}::{}", app, layer)
} else {
format!("{}::{}::{}", app, layer, parent_segments.join("::"))
};
let mut path = PathBuf::from("app");
if !app.is_empty() {
path.push(app);
}
path.push(layer);
for seg in &parent_segments {
path.push(seg);
}
path.push(format!("{}.rs", class_name));
(class_name, module_path, path)
}
fn check_file_exists(path: &Path) -> Result<(), CliError> {
if path.exists() {
return Err(CliError::FileExists(path.display().to_string()));
}
Ok(())
}
fn write_file(path: &Path, content: &str) -> Result<(), CliError> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(path, content)?;
Ok(())
}
fn class_to_snake(s: &str) -> String {
let mut result = String::new();
for (i, ch) in s.chars().enumerate() {
if ch.is_uppercase() && i > 0 {
result.push('_');
}
result.push(ch.to_lowercase().next().unwrap_or(ch));
}
result
}
fn name_to_table(name: &str) -> String {
if let Some(rest) = name.strip_prefix("create_") {
return rest.to_string();
}
if let Some(rest) = name.strip_prefix("add_") {
if let Some(to_pos) = rest.find("_to_") {
return rest[to_pos + 4..].to_string();
}
return rest.to_string();
}
name.to_string()
}
pub async fn execute_make_plugin(
args: crate::context_builder::PluginCommandArgs,
) -> Result<(), CliError> {
use crate::context_builder::TemplateContextBuilder;
use crate::template_engine::TemplateEngine;
use crate::validator::InputValidator;
InputValidator::validate_plugin_name(&args.name)?;
if let Some(ref table) = args.table {
InputValidator::validate_table_name(table)?;
}
if let Some(ref fields) = args.fields {
InputValidator::validate_fields(fields)?;
}
if let Some(ref master) = args.master {
InputValidator::validate_table_name(master)?;
}
if let Some(ref slave) = args.slave {
InputValidator::validate_table_name(slave)?;
}
let template_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("templates");
let engine = TemplateEngine::init(&template_dir).await?;
engine.validate_template_type(&args.template)?;
let is_master_slave = args.template == "master-slave";
let ctx = if is_master_slave {
TemplateContextBuilder::new(args.clone()).build_master_slave()?
} else {
TemplateContextBuilder::new(args.clone()).build()?
};
let output_dir = args
.output
.as_ref()
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("plugins").join(&args.name));
if output_dir.exists() && !args.force {
return Err(CliError::DirExists(output_dir));
}
let template_files: &[(&str, &str)] = match args.template.as_str() {
"master-slave" => &[
(
"plugin-master-slave/master_model.rs.tera",
"src/master_model.rs",
),
(
"plugin-master-slave/slave_model.rs.tera",
"src/slave_model.rs",
),
(
"plugin-master-slave/master_controller.rs.tera",
"src/master_controller.rs",
),
(
"plugin-master-slave/slave_controller.rs.tera",
"src/slave_controller.rs",
),
(
"plugin-master-slave/cascade_service.rs.tera",
"src/cascade_service.rs",
),
(
"plugin-master-slave/datasource_config.rs.tera",
"src/datasource_config.rs",
),
(
"plugin-master-slave/migration.sql.tera",
"migrations/master_slave.sql",
),
("plugin-master-slave/manifest.json.tera", "manifest.json"),
],
"workflow" => &[
("plugin-workflow/model.rs.tera", "src/model.rs"),
("plugin-workflow/controller.rs.tera", "src/controller.rs"),
("plugin-workflow/routes.rs.tera", "src/routes.rs"),
("plugin-workflow/migration.sql.tera", "migrations/table.sql"),
("plugin-workflow/manifest.json.tera", "manifest.json"),
("plugin-workflow/tests.rs.tera", "tests/workflow_test.rs"),
],
"report" => &[
("plugin-report/model.rs.tera", "src/model.rs"),
("plugin-report/controller.rs.tera", "src/controller.rs"),
("plugin-report/routes.rs.tera", "src/routes.rs"),
("plugin-report/migration.sql.tera", "migrations/table.sql"),
("plugin-report/manifest.json.tera", "manifest.json"),
("plugin-report/tests.rs.tera", "tests/report_test.rs"),
],
_ => &[
("plugin-crud/model.rs.tera", "src/model.rs"),
("plugin-crud/controller.rs.tera", "src/controller.rs"),
("plugin-crud/service.rs.tera", "src/service.rs"),
("plugin-crud/repository.rs.tera", "src/repository.rs"),
("plugin-crud/migration.sql.tera", "migrations/table.sql"),
("plugin-crud/routes.rs.tera", "src/routes.rs"),
("plugin-crud/manifest.json.tera", "manifest.json"),
("plugin-crud/tests.rs.tera", "tests/crud_test.rs"),
],
};
let mut rendered_files: Vec<(PathBuf, String)> = Vec::new();
for (template_name, output_path) in template_files {
let content = engine.render(template_name, &ctx)?;
rendered_files.push((output_dir.join(output_path), content));
}
let safety_files: Vec<(String, String)> = rendered_files
.iter()
.map(|(p, c)| (p.display().to_string(), c.clone()))
.collect();
let violations = crate::safety_validator::SafetyValidator::validate_files(&safety_files);
if !violations.is_empty() {
let report = crate::safety_validator::SafetyValidator::format_report(&violations);
eprintln!("{report}");
return Err(CliError::Generic(format!(
"安全检查失败:{} 个违规项,已阻止生成",
violations.len()
)));
}
if output_dir.exists() && args.force {
tokio::fs::remove_dir_all(&output_dir).await?;
}
for (file_path, content) in &rendered_files {
if let Some(parent) = file_path.parent() {
tokio::fs::create_dir_all(parent).await?;
}
tokio::fs::write(file_path, content).await?;
}
let written_paths: Vec<std::path::PathBuf> =
rendered_files.iter().map(|(p, _)| p.clone()).collect();
let check_result = crate::cargo_checker::CargoChecker::check(&output_dir).await;
match check_result {
Ok(result) if result.success => {
println!(
"Plugin '{}' created successfully at: {}",
args.name,
output_dir.display()
);
println!("Files generated:");
for (file_path, _) in &rendered_files {
println!(" - {}", file_path.display());
}
println!("cargo check: PASSED");
Ok(())
}
Ok(result) => {
eprintln!("cargo check: FAILED");
eprintln!("Compilation errors:");
for err in &result.errors {
eprintln!(" {err}");
}
let failures = crate::cargo_checker::CargoChecker::rollback(&written_paths).await;
if !failures.is_empty() {
eprintln!(
"Warning: {} files could not be removed during rollback",
failures.len()
);
}
Err(CliError::CompileFailed(result.errors))
}
Err(e) => {
eprintln!("cargo check could not be executed: {e}");
eprintln!("Rolling back generated files...");
let failures = crate::cargo_checker::CargoChecker::rollback(&written_paths).await;
if !failures.is_empty() {
eprintln!(
"Warning: {} files could not be removed during rollback",
failures.len()
);
}
Err(e)
}
}
}
#[allow(clippy::too_many_arguments)]
async fn execute_make_frontend(
models: &[String],
model_dir: &str,
framework: &str,
ui: &str,
output: &str,
template_dir: Option<&str>,
override_strategy: &str,
with_tests: bool,
with_interceptors: bool,
lazy_load: bool,
force: bool,
) -> Result<(), CliError> {
use sz_rust_frontend_codegen::{
CodegenService, Framework, GenerationConfig, OverrideStrategy, UiLibrary,
};
let fw = match framework.to_lowercase().as_str() {
"vue" => Framework::Vue,
"react" => Framework::React,
other => {
return Err(CliError::Generic(format!(
"不支持的前端框架: {other}(可选: vue, react)"
)));
}
};
let ui_lib = match ui.to_lowercase().as_str() {
"element_plus" | "element-plus" => UiLibrary::ElementPlus,
"ant_design_vue" | "ant-design-vue" => UiLibrary::AntDesignVue,
other => {
return Err(CliError::Generic(format!(
"不支持的 UI 库: {other}(可选: element_plus, ant_design_vue)"
)));
}
};
let strategy = match override_strategy.to_lowercase().as_str() {
"skip" => OverrideStrategy::Skip,
"overwrite" => OverrideStrategy::Overwrite,
"merge" => OverrideStrategy::Merge,
other => {
return Err(CliError::Generic(format!(
"不支持的覆盖策略: {other}(可选: skip, overwrite, merge)"
)));
}
};
let config = GenerationConfig {
models: models.to_vec(),
model_dir: PathBuf::from(model_dir),
framework: fw,
ui_library: ui_lib,
output_dir: PathBuf::from(output),
template_dir: template_dir.map(PathBuf::from),
override_strategy: strategy,
with_tests,
with_interceptors,
lazy_load,
force,
};
let service = CodegenService::new();
let report = service
.generate(config)
.await
.map_err(|e| CliError::Generic(e.to_string()))?;
println!("{}", report.format_cli());
Ok(())
}
async fn execute_make_openapi(
output: &str,
title: &str,
version: &str,
force: bool,
) -> Result<(), CliError> {
let output_path = PathBuf::from(output);
check_file_exists_with_force(&output_path, force)?;
let spec = generate_openapi_spec(title, version);
let json = serde_json::to_string_pretty(&spec)
.map_err(|e| CliError::Generation(format!("OpenAPI serialize failed: {e}")))?;
write_file(&output_path, &json)?;
println!("OpenAPI spec created: {}", output_path.display());
run_post_generation_check()?;
Ok(())
}
#[derive(Debug, serde::Serialize)]
struct OpenApiSpec {
openapi: String,
info: OpenApiInfo,
paths: serde_json::Value,
components: serde_json::Value,
}
#[derive(Debug, serde::Serialize)]
struct OpenApiInfo {
title: String,
version: String,
description: String,
}
fn generate_openapi_spec(title: &str, version: &str) -> OpenApiSpec {
OpenApiSpec {
openapi: "3.0.3".to_string(),
info: OpenApiInfo {
title: title.to_string(),
version: version.to_string(),
description: "Generated by sz-rust make:openapi".to_string(),
},
paths: serde_json::json!({
"/health": {
"get": {
"summary": "Health check",
"responses": {
"200": {"description": "Service healthy"}
}
}
}
}),
components: serde_json::json!({
"schemas": {},
"securitySchemes": {
"bearerAuth": {
"type": "http",
"scheme": "bearer"
}
}
}),
}
}
fn check_file_exists_with_force(path: &Path, force: bool) -> Result<(), CliError> {
if path.exists() && !force {
return Err(CliError::FileExists(path.display().to_string()));
}
Ok(())
}
fn run_post_generation_check() -> Result<(), CliError> {
let fmt_result = std::process::Command::new("cargo")
.args(["fmt", "--check"])
.output();
if let Ok(output) = fmt_result {
if !output.status.success() {
eprintln!("⚠️ cargo fmt --check failed, run `cargo fmt` to fix");
}
}
let check_result = std::process::Command::new("cargo").args(["check"]).output();
if let Ok(output) = check_result {
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(CliError::Generation(format!(
"cargo check failed after generation:\n{stderr}"
)));
}
}
let clippy_result = std::process::Command::new("cargo")
.args(["clippy", "-D", "warnings"])
.output();
if let Ok(output) = clippy_result {
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
eprintln!("⚠️ cargo clippy found warnings:\n{stderr}");
eprintln!(" Run `cargo clippy --fix` to auto-fix.");
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
struct CwdGuard {
original: Option<PathBuf>,
_lock: std::sync::MutexGuard<'static, ()>,
}
impl CwdGuard {
fn switch(new_dir: &Path) -> std::io::Result<Self> {
let lock = super::super::test_support::acquire_global_lock();
let original = std::env::current_dir().ok();
std::env::set_current_dir(new_dir)?;
Ok(Self {
original,
_lock: lock,
})
}
}
impl Drop for CwdGuard {
fn drop(&mut self) {
if let Some(ref orig) = self.original {
let _ = std::env::set_current_dir(orig);
}
}
}
#[test]
fn test_resolve_target_simple_model() {
let (class, module, path) = resolve_target("User", "model");
assert_eq!(class, "User");
assert_eq!(module, "model");
assert_eq!(path, PathBuf::from("app/model/User.rs"));
}
#[test]
fn test_resolve_target_nested_controller() {
let (class, module, path) = resolve_target("admin/User", "controller");
assert_eq!(class, "User");
assert_eq!(module, "controller::admin");
assert_eq!(path, PathBuf::from("app/controller/admin/User.rs"));
}
#[test]
fn test_resolve_target_with_app() {
let (class, module, _path) = resolve_target("admin@User", "model");
assert_eq!(class, "User");
assert_eq!(module, "admin::model");
}
#[test]
fn test_class_to_snake() {
assert_eq!(class_to_snake("User"), "user");
assert_eq!(class_to_snake("OrderItem"), "order_item");
assert_eq!(class_to_snake("API"), "a_p_i");
}
#[test]
fn test_name_to_table_create() {
assert_eq!(name_to_table("create_users"), "users");
assert_eq!(name_to_table("create_orders"), "orders");
}
#[test]
fn test_name_to_table_add() {
assert_eq!(name_to_table("add_index_to_orders"), "orders");
assert_eq!(name_to_table("add_status"), "status");
}
#[test]
fn test_name_to_table_other() {
assert_eq!(name_to_table("custom_migration"), "custom_migration");
}
#[test]
fn test_check_file_exists_nonexistent() {
let result = check_file_exists(Path::new("/nonexistent/path/file.txt"));
assert!(result.is_ok());
}
#[test]
fn test_check_file_exists_existing() {
let temp = tempfile::NamedTempFile::new().unwrap();
let result = check_file_exists(temp.path());
assert!(matches!(result, Err(CliError::FileExists(_))));
}
#[test]
fn test_write_and_read_file() {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join("test_file.txt");
write_file(&file_path, "test content").unwrap();
assert_eq!(std::fs::read_to_string(&file_path).unwrap(), "test content");
}
#[test]
fn test_execute_make_migration_creates_files() {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().to_str().unwrap();
execute_make_migration("create_test_table", path).unwrap();
let entries: Vec<_> = std::fs::read_dir(path).unwrap().collect();
assert_eq!(entries.len(), 2);
let mut has_up = false;
let mut has_down = false;
for entry in entries {
let name = entry.unwrap().file_name();
let name = name.to_string_lossy();
if name.ends_with("_up.sql") {
has_up = true;
}
if name.ends_with("_down.sql") {
has_down = true;
}
}
assert!(has_up);
assert!(has_down);
}
#[test]
fn test_execute_make_model_in_temp() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
execute_make_model("TestUser").unwrap();
let model_path = temp_dir.path().join("app/model/TestUser.rs");
assert!(model_path.exists());
let content = std::fs::read_to_string(&model_path).unwrap();
assert!(content.contains("TestUser"));
assert!(content.contains("test_user"));
}
#[test]
fn test_execute_make_seeder_creates_file() {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().to_str().unwrap();
execute_make_seeder("001_test_seed", path).unwrap();
let seed_path = Path::new(path).join("001_test_seed.sql");
assert!(seed_path.exists());
let content = std::fs::read_to_string(&seed_path).unwrap();
assert!(content.contains("001_test_seed"));
assert!(content.contains("-- Seed:"));
assert!(!content.contains("{%"));
}
#[test]
fn test_execute_make_seeder_file_already_exists() {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().to_str().unwrap();
execute_make_seeder("001_dup_seed", path).unwrap();
let result = execute_make_seeder("001_dup_seed", path);
assert!(matches!(result, Err(CliError::FileExists(_))));
}
#[test]
fn test_execute_make_seeder_creates_directory() {
let temp_dir = tempfile::tempdir().unwrap();
let nested = temp_dir.path().join("nested").join("seeds");
let path = nested.to_str().unwrap();
execute_make_seeder("001_seed", path).unwrap();
assert!(nested.exists());
}
#[tokio::test]
async fn test_make_validate_creates_file() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
let cmd = MakeCommand::Validate {
name: "Order".to_string(),
};
execute(&cmd).await.unwrap();
let validate_path = temp_dir.path().join("app/validate/Order.rs");
assert!(validate_path.exists());
let content = std::fs::read_to_string(&validate_path).unwrap();
assert!(content.contains("pub struct OrderValidate;"));
assert!(content.contains("use sz_rust_core::validate::Validate"));
assert!(content.contains("app::validate"));
assert!(!content.contains("{%"));
}
#[test]
fn test_validate_stub_contains_required_elements() {
assert!(stubs::VALIDATE_STUB.contains("pub struct {%className%}Validate;"));
assert!(stubs::VALIDATE_STUB.contains("use sz_rust_core::validate::Validate"));
assert!(stubs::VALIDATE_STUB.contains("impl {%className%}Validate"));
assert!(stubs::VALIDATE_STUB.contains("pub fn new() -> Validate"));
assert!(stubs::VALIDATE_STUB.contains("{%className%}"));
assert!(stubs::VALIDATE_STUB.contains("{%namespace%}"));
}
#[test]
fn test_execute_make_validate_creates_file() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
execute_make_validate("User").unwrap();
let validate_path = temp_dir.path().join("app/validate/User.rs");
assert!(validate_path.exists());
let content = std::fs::read_to_string(&validate_path).unwrap();
assert!(content.contains("UserValidate"));
assert!(content.contains("use sz_rust_core::validate::Validate"));
assert!(content.contains("app::validate"));
assert!(!content.contains("{%"));
}
#[test]
fn test_execute_make_validate_file_already_exists() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
execute_make_validate("User").unwrap();
let result = execute_make_validate("User");
assert!(matches!(result, Err(CliError::FileExists(_))));
}
#[test]
fn test_execute_make_validate_nested_path() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
execute_make_validate("admin/User").unwrap();
let validate_path = temp_dir.path().join("app/validate/admin/User.rs");
assert!(validate_path.exists());
let content = std::fs::read_to_string(&validate_path).unwrap();
assert!(content.contains("UserValidate"));
assert!(content.contains("app::validate::admin"));
}
#[test]
fn test_execute_make_event_creates_file() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
execute_make_event("UserLogin").unwrap();
let event_path = temp_dir.path().join("app/event/UserLogin.rs");
assert!(event_path.exists());
let content = std::fs::read_to_string(&event_path).unwrap();
assert!(content.contains("pub struct UserLogin;"));
assert!(content.contains("app::event"));
assert!(content.contains("UserLogin"));
assert!(content.contains("use serde_json::Value"));
assert!(!content.contains("{%"));
}
#[test]
fn test_execute_make_event_file_already_exists() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
execute_make_event("UserLogin").unwrap();
let result = execute_make_event("UserLogin");
assert!(matches!(result, Err(CliError::FileExists(_))));
}
#[test]
fn test_execute_make_event_nested_path() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
execute_make_event("admin/UserLogin").unwrap();
let event_path = temp_dir.path().join("app/event/admin/UserLogin.rs");
assert!(event_path.exists());
let content = std::fs::read_to_string(&event_path).unwrap();
assert!(content.contains("app::event::admin"));
}
#[test]
fn test_execute_make_listener_default_event_name() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
execute_make_listener("SendWelcomeEmail", None).unwrap();
let listener_path = temp_dir.path().join("app/listener/SendWelcomeEmail.rs");
assert!(listener_path.exists());
let content = std::fs::read_to_string(&listener_path).unwrap();
assert!(content.contains("pub struct SendWelcomeEmail;"));
assert!(content.contains("app::listener"));
assert!(content.contains("use sz_rust_core::event::{EventError, Listener}"));
assert!(content.contains("impl Listener for SendWelcomeEmail"));
assert!(content.contains(r#""SendWelcomeEmail""#));
assert!(!content.contains("{%"));
}
#[test]
fn test_execute_make_listener_custom_event_name() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
execute_make_listener("SendWelcomeEmail", Some("UserLogin")).unwrap();
let listener_path = temp_dir.path().join("app/listener/SendWelcomeEmail.rs");
assert!(listener_path.exists());
let content = std::fs::read_to_string(&listener_path).unwrap();
assert!(content.contains(r#""UserLogin""#));
assert!(!content.contains(r#""SendWelcomeEmail""#));
assert!(!content.contains("{%"));
}
#[test]
fn test_execute_make_listener_file_already_exists() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
execute_make_listener("SendWelcomeEmail", None).unwrap();
let result = execute_make_listener("SendWelcomeEmail", None);
assert!(matches!(result, Err(CliError::FileExists(_))));
}
#[test]
fn test_execute_make_command_creates_file() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
execute_make_command("SyncData").unwrap();
let command_path = temp_dir.path().join("app/command/SyncData.rs");
assert!(command_path.exists());
let content = std::fs::read_to_string(&command_path).unwrap();
assert!(content.contains("pub struct SyncData;"));
assert!(content.contains("app::command"));
assert!(content.contains("use sz_rust_cli::console::{Command, CommandSignature}"));
assert!(content.contains("impl Command for SyncData"));
assert!(content.contains(r#""sync_data""#));
assert!(!content.contains("{%"));
}
#[test]
fn test_execute_make_command_file_already_exists() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
execute_make_command("SyncData").unwrap();
let result = execute_make_command("SyncData");
assert!(matches!(result, Err(CliError::FileExists(_))));
}
#[test]
fn test_execute_make_command_nested_path() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
execute_make_command("admin/SyncData").unwrap();
let command_path = temp_dir.path().join("app/command/admin/SyncData.rs");
assert!(command_path.exists());
let content = std::fs::read_to_string(&command_path).unwrap();
assert!(content.contains("app::command::admin"));
}
#[test]
fn test_execute_make_service_creates_file() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
execute_make_service("UserService").unwrap();
let service_path = temp_dir.path().join("app/service/UserService.rs");
assert!(service_path.exists());
let content = std::fs::read_to_string(&service_path).unwrap();
assert!(content.contains("pub struct UserService;"));
assert!(content.contains("app::service"));
assert!(content.contains("impl Default for UserService"));
assert!(content.contains("pub fn new() -> Self"));
assert!(!content.contains("{%"));
}
#[test]
fn test_execute_make_service_file_already_exists() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
execute_make_service("UserService").unwrap();
let result = execute_make_service("UserService");
assert!(matches!(result, Err(CliError::FileExists(_))));
}
#[test]
fn test_execute_make_service_nested_path() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
execute_make_service("admin/UserService").unwrap();
let service_path = temp_dir.path().join("app/service/admin/UserService.rs");
assert!(service_path.exists());
let content = std::fs::read_to_string(&service_path).unwrap();
assert!(content.contains("app::service::admin"));
}
#[test]
fn test_execute_make_controller_creates_file() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
execute_make_controller("User", false, false).unwrap();
let controller_path = temp_dir.path().join("app/controller/User.rs");
assert!(controller_path.exists());
let content = std::fs::read_to_string(&controller_path).unwrap();
assert!(content.contains("User"));
assert!(content.contains("app::controller"));
assert!(!content.contains("{%"));
}
#[test]
fn test_execute_make_controller_api() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
execute_make_controller("User", true, false).unwrap();
let controller_path = temp_dir.path().join("app/controller/User.rs");
assert!(controller_path.exists());
let content = std::fs::read_to_string(&controller_path).unwrap();
assert!(!content.contains("{%"));
}
#[test]
fn test_execute_make_controller_plain() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
execute_make_controller("User", false, true).unwrap();
let controller_path = temp_dir.path().join("app/controller/User.rs");
assert!(controller_path.exists());
let content = std::fs::read_to_string(&controller_path).unwrap();
assert!(!content.contains("{%"));
}
#[test]
fn test_execute_make_controller_file_already_exists() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
execute_make_controller("User", false, false).unwrap();
let result = execute_make_controller("User", false, false);
assert!(matches!(result, Err(CliError::FileExists(_))));
}
#[test]
fn test_execute_make_guard_creates_file() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
execute_make_guard("Admin").unwrap();
let guard_path = temp_dir.path().join("app/guard/Admin.rs");
assert!(guard_path.exists());
let content = std::fs::read_to_string(&guard_path).unwrap();
assert!(content.contains("pub struct Admin;"));
assert!(content.contains("impl Guard for Admin"));
assert!(content.contains("use sz_rust_core::guard::Guard"));
}
#[test]
fn test_execute_make_guard_file_already_exists() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
execute_make_guard("Admin").unwrap();
let result = execute_make_guard("Admin");
assert!(matches!(result, Err(CliError::FileExists(_))));
}
#[test]
fn test_execute_make_middleware_creates_file() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
execute_make_middleware("Cors").unwrap();
let middleware_path = temp_dir.path().join("app/middleware/Cors.rs");
assert!(middleware_path.exists());
let content = std::fs::read_to_string(&middleware_path).unwrap();
assert!(content.contains("Cors"));
assert!(content.contains("app::middleware"));
assert!(!content.contains("{%"));
}
#[test]
fn test_execute_make_middleware_file_already_exists() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
execute_make_middleware("Cors").unwrap();
let result = execute_make_middleware("Cors");
assert!(matches!(result, Err(CliError::FileExists(_))));
}
#[test]
fn test_execute_make_scaffold_creates_files() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
execute_make_scaffold("Post").unwrap();
let model_path = temp_dir.path().join("app/model/Post.rs");
let controller_path = temp_dir.path().join("app/controller/Post.rs");
assert!(model_path.exists(), "model should be created");
assert!(controller_path.exists(), "controller should be created");
let migrations_dir = temp_dir.path().join("migrations");
assert!(migrations_dir.exists(), "migrations dir should be created");
let migration_count = std::fs::read_dir(&migrations_dir).unwrap().count();
assert_eq!(
migration_count, 2,
"should create up + down migration files"
);
}
#[tokio::test]
async fn test_execute_dispatch_model() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
let cmd = MakeCommand::Model {
name: "User".to_string(),
};
execute(&cmd).await.unwrap();
let model_path = temp_dir.path().join("app/model/User.rs");
assert!(model_path.exists());
}
#[tokio::test]
async fn test_execute_dispatch_controller() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
let cmd = MakeCommand::Controller {
name: "User".to_string(),
api: false,
plain: false,
};
execute(&cmd).await.unwrap();
let controller_path = temp_dir.path().join("app/controller/User.rs");
assert!(controller_path.exists());
}
#[tokio::test]
async fn test_execute_dispatch_migration() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
let migrations_path = temp_dir.path().join("migrations");
let cmd = MakeCommand::Migration {
name: "create_users".to_string(),
path: migrations_path.to_string_lossy().to_string(),
};
execute(&cmd).await.unwrap();
let migration_count = std::fs::read_dir(&migrations_path).unwrap().count();
assert_eq!(migration_count, 2);
}
#[tokio::test]
async fn test_execute_dispatch_seeder() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
let seeds_path = temp_dir.path().join("seeds");
let cmd = MakeCommand::Seeder {
name: "001_users".to_string(),
path: seeds_path.to_string_lossy().to_string(),
};
execute(&cmd).await.unwrap();
let seeder_path = seeds_path.join("001_users.sql");
assert!(seeder_path.exists());
}
#[tokio::test]
async fn test_execute_dispatch_guard() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
let cmd = MakeCommand::Guard {
name: "Admin".to_string(),
};
execute(&cmd).await.unwrap();
let guard_path = temp_dir.path().join("app/guard/Admin.rs");
assert!(guard_path.exists());
}
#[tokio::test]
async fn test_execute_dispatch_event() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
let cmd = MakeCommand::Event {
name: "UserLogin".to_string(),
};
execute(&cmd).await.unwrap();
let event_path = temp_dir.path().join("app/event/UserLogin.rs");
assert!(event_path.exists());
}
#[tokio::test]
async fn test_execute_dispatch_listener() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
let cmd = MakeCommand::Listener {
name: "SendEmail".to_string(),
event: None,
};
execute(&cmd).await.unwrap();
let listener_path = temp_dir.path().join("app/listener/SendEmail.rs");
assert!(listener_path.exists());
}
#[tokio::test]
async fn test_execute_dispatch_command() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
let cmd = MakeCommand::Command {
name: "SyncData".to_string(),
};
execute(&cmd).await.unwrap();
let command_path = temp_dir.path().join("app/command/SyncData.rs");
assert!(command_path.exists());
}
#[tokio::test]
async fn test_execute_dispatch_service() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
let cmd = MakeCommand::Service {
name: "UserService".to_string(),
};
execute(&cmd).await.unwrap();
let service_path = temp_dir.path().join("app/service/UserService.rs");
assert!(service_path.exists());
}
#[tokio::test]
async fn test_execute_dispatch_middleware() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
let cmd = MakeCommand::Middleware {
name: "Cors".to_string(),
};
execute(&cmd).await.unwrap();
let middleware_path = temp_dir.path().join("app/middleware/Cors.rs");
assert!(middleware_path.exists());
}
#[tokio::test]
async fn test_execute_dispatch_scaffold() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
let cmd = MakeCommand::Scaffold {
name: "Post".to_string(),
};
execute(&cmd).await.unwrap();
let model_path = temp_dir.path().join("app/model/Post.rs");
assert!(model_path.exists());
}
#[tokio::test]
async fn test_execute_make_plugin_crud() {
let temp_dir = tempfile::tempdir().unwrap();
let args = crate::context_builder::PluginCommandArgs {
template: "plugin-crud".to_string(),
name: "test_plugin".to_string(),
table: Some("test_table".to_string()),
fields: Some("id:i32:pk,name:String".to_string()),
force: false,
output: Some(
temp_dir
.path()
.join("myplugin")
.to_string_lossy()
.to_string(),
),
master: None,
slave: None,
master_fields: None,
slave_fields: None,
foreign_key: None,
};
let result = execute_make_plugin(args).await;
assert!(result.is_err(), "无 Cargo.toml 应失败");
let err = format!("{}", result.unwrap_err());
assert!(
err.contains("Cargo.toml")
|| err.contains("CompileFailed")
|| err.contains("Compilation failed"),
"应含编译失败: {err}"
);
}
#[tokio::test]
async fn test_execute_make_plugin_workflow() {
let temp_dir = tempfile::tempdir().unwrap();
let args = crate::context_builder::PluginCommandArgs {
template: "plugin-workflow".to_string(),
name: "wf_plugin".to_string(),
table: None,
fields: Some("id:i32:pk,title:String".to_string()),
force: false,
output: Some(
temp_dir
.path()
.join("wfplugin")
.to_string_lossy()
.to_string(),
),
master: None,
slave: None,
master_fields: None,
slave_fields: None,
foreign_key: None,
};
let result = execute_make_plugin(args).await;
assert!(result.is_err(), "无 Cargo.toml 应失败");
}
#[tokio::test]
async fn test_execute_make_plugin_report() {
let temp_dir = tempfile::tempdir().unwrap();
let args = crate::context_builder::PluginCommandArgs {
template: "plugin-report".to_string(),
name: "rpt_plugin".to_string(),
table: None,
fields: Some("id:i32:pk,data:String".to_string()),
force: false,
output: Some(
temp_dir
.path()
.join("rptplugin")
.to_string_lossy()
.to_string(),
),
master: None,
slave: None,
master_fields: None,
slave_fields: None,
foreign_key: None,
};
let result = execute_make_plugin(args).await;
assert!(result.is_err(), "无 Cargo.toml 应失败");
}
#[tokio::test]
async fn test_execute_make_plugin_dir_exists_no_force() {
let temp_dir = tempfile::tempdir().unwrap();
let output_dir = temp_dir.path().join("existing_plugin");
std::fs::create_dir_all(&output_dir).unwrap();
let args = crate::context_builder::PluginCommandArgs {
template: "plugin-crud".to_string(),
name: "existing".to_string(),
table: None,
fields: Some("id:i32:pk".to_string()),
force: false,
output: Some(output_dir.to_string_lossy().to_string()),
master: None,
slave: None,
master_fields: None,
slave_fields: None,
foreign_key: None,
};
let result = execute_make_plugin(args).await;
assert!(matches!(result, Err(CliError::DirExists(_))));
}
#[tokio::test]
async fn test_execute_make_plugin_force_overwrite() {
let temp_dir = tempfile::tempdir().unwrap();
let output_dir = temp_dir.path().join("force_plugin");
std::fs::create_dir_all(&output_dir).unwrap();
let args = crate::context_builder::PluginCommandArgs {
template: "plugin-crud".to_string(),
name: "forced".to_string(),
table: None,
fields: Some("id:i32:pk".to_string()),
force: true,
output: Some(output_dir.to_string_lossy().to_string()),
master: None,
slave: None,
master_fields: None,
slave_fields: None,
foreign_key: None,
};
let result = execute_make_plugin(args).await;
assert!(result.is_err(), "无 Cargo.toml 应失败");
}
#[tokio::test]
async fn test_execute_make_plugin_invalid_name() {
let temp_dir = tempfile::tempdir().unwrap();
let args = crate::context_builder::PluginCommandArgs {
template: "plugin-crud".to_string(),
name: "InvalidName".to_string(),
table: None,
fields: None,
force: false,
output: Some(temp_dir.path().join("bad").to_string_lossy().to_string()),
master: None,
slave: None,
master_fields: None,
slave_fields: None,
foreign_key: None,
};
let result = execute_make_plugin(args).await;
assert!(result.is_err(), "大写插件名应失败");
}
#[tokio::test]
async fn test_execute_make_plugin_invalid_template() {
let temp_dir = tempfile::tempdir().unwrap();
let args = crate::context_builder::PluginCommandArgs {
template: "nonexistent".to_string(),
name: "test_plug".to_string(),
table: None,
fields: None,
force: false,
output: Some(temp_dir.path().join("bad").to_string_lossy().to_string()),
master: None,
slave: None,
master_fields: None,
slave_fields: None,
foreign_key: None,
};
let result = execute_make_plugin(args).await;
assert!(result.is_err(), "不存在的模板应失败");
}
#[tokio::test]
async fn test_execute_dispatch_plugin() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
let cmd = MakeCommand::Plugin {
template: "plugin-crud".to_string(),
name: "dispatched".to_string(),
table: None,
fields: Some("id:i32:pk".to_string()),
force: false,
output: Some(temp_dir.path().join("disp").to_string_lossy().to_string()),
master: None,
slave: None,
master_fields: None,
slave_fields: None,
foreign_key: None,
};
let result = execute(&cmd).await;
assert!(result.is_err(), "无 Cargo.toml 应失败");
}
#[tokio::test]
async fn test_execute_make_frontend_invalid_framework() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
let result = execute_make_frontend(
&["User".to_string()],
"src/model/",
"invalid_framework",
"element_plus",
"./frontend/",
None,
"skip",
false,
false,
true,
false,
)
.await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("不支持的前端框架"));
}
#[tokio::test]
async fn test_execute_make_frontend_invalid_ui() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
let result = execute_make_frontend(
&["User".to_string()],
"src/model/",
"vue",
"invalid_ui",
"./frontend/",
None,
"skip",
false,
false,
true,
false,
)
.await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("不支持的 UI 库"));
}
#[tokio::test]
async fn test_execute_make_frontend_invalid_override_strategy() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
let result = execute_make_frontend(
&["User".to_string()],
"src/model/",
"vue",
"element_plus",
"./frontend/",
None,
"invalid_strategy",
false,
false,
true,
false,
)
.await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("不支持的覆盖策略"));
}
#[tokio::test]
async fn test_execute_make_frontend_vue_element_plus() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
let model_dir = temp_dir.path().join("src/model");
std::fs::create_dir_all(&model_dir).unwrap();
std::fs::write(
model_dir.join("User.rs"),
"#[derive(Model)]\npub struct User { pub id: i32, pub name: String }",
)
.unwrap();
let output = temp_dir
.path()
.join("frontend")
.to_string_lossy()
.to_string();
let result = execute_make_frontend(
&["User".to_string()],
&model_dir.to_string_lossy(),
"vue",
"element_plus",
&output,
None,
"skip",
false,
false,
true,
false,
)
.await;
assert!(result.is_ok(), "vue+element_plus 应成功: {:?}", result);
}
#[tokio::test]
async fn test_execute_make_frontend_react_ant_design() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
let model_dir = temp_dir.path().join("src/model");
std::fs::create_dir_all(&model_dir).unwrap();
std::fs::write(
model_dir.join("Product.rs"),
"#[derive(Model)]\npub struct Product { pub id: i32, pub name: String }",
)
.unwrap();
let output = temp_dir
.path()
.join("frontend")
.to_string_lossy()
.to_string();
let result = execute_make_frontend(
&["Product".to_string()],
&model_dir.to_string_lossy(),
"react",
"ant_design_vue",
&output,
None,
"overwrite",
true,
true,
false,
true,
)
.await;
assert!(result.is_ok(), "react+ant_design_vue 应成功: {:?}", result);
}
#[tokio::test]
async fn test_execute_make_frontend_element_plus_hyphen() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
let model_dir = temp_dir.path().join("src/model");
std::fs::create_dir_all(&model_dir).unwrap();
std::fs::write(
model_dir.join("Order.rs"),
"#[derive(Model)]\npub struct Order { pub id: i32 }",
)
.unwrap();
let output = temp_dir
.path()
.join("frontend")
.to_string_lossy()
.to_string();
let result = execute_make_frontend(
&["Order".to_string()],
&model_dir.to_string_lossy(),
"vue",
"element-plus",
&output,
None,
"merge",
false,
false,
true,
false,
)
.await;
assert!(result.is_ok(), "element-plus (hyphen) 应成功: {:?}", result);
}
#[tokio::test]
async fn test_execute_make_frontend_ant_design_hyphen() {
let temp_dir = tempfile::tempdir().unwrap();
let _guard = CwdGuard::switch(temp_dir.path()).unwrap();
let model_dir = temp_dir.path().join("src/model");
std::fs::create_dir_all(&model_dir).unwrap();
std::fs::write(
model_dir.join("Item.rs"),
"#[derive(Model)]\npub struct Item { pub id: i32 }",
)
.unwrap();
let output = temp_dir
.path()
.join("frontend")
.to_string_lossy()
.to_string();
let result = execute_make_frontend(
&["Item".to_string()],
&model_dir.to_string_lossy(),
"vue",
"ant-design-vue",
&output,
None,
"skip",
false,
false,
true,
false,
)
.await;
assert!(
result.is_ok(),
"ant-design-vue (hyphen) 应成功: {:?}",
result
);
}
#[test]
fn test_openapi_spec_generation() {
let spec = generate_openapi_spec("Test API", "2.0.0");
assert_eq!(spec.openapi, "3.0.3");
assert_eq!(spec.info.title, "Test API");
assert_eq!(spec.info.version, "2.0.0");
}
#[test]
fn test_openapi_spec_default() {
let spec = generate_openapi_spec("SZ-Rust API", "1.0.0");
assert_eq!(spec.info.title, "SZ-Rust API");
assert_eq!(spec.info.version, "1.0.0");
}
#[test]
fn test_openapi_spec_serialize() {
let spec = generate_openapi_spec("Test", "1.0");
let json = serde_json::to_string(&spec).unwrap();
assert!(json.contains("\"openapi\":\"3.0.3\""));
assert!(json.contains("\"title\":\"Test\""));
assert!(json.contains("bearerAuth"));
}
#[test]
fn test_check_file_exists_with_force() {
let temp = tempfile::NamedTempFile::new().unwrap();
let path = temp.path();
let result = check_file_exists_with_force(path, false);
assert!(result.is_err(), "should error without force");
let result = check_file_exists_with_force(path, true);
assert!(result.is_ok(), "should pass with force");
}
#[test]
fn test_check_file_exists_with_force_nonexistent() {
let path = std::path::Path::new("nonexistent_file_12345.rs");
let result = check_file_exists_with_force(path, false);
assert!(result.is_ok(), "nonexistent file should pass");
}
}