use crate::utils::error::Result;
use std::fs;
use std::path::Path;
pub struct BusinessLogicSeparator;
impl BusinessLogicSeparator {
pub fn generate_cli_wrapper(verb: &str, noun: &str, module_path: Option<&str>) -> String {
let module = module_path.unwrap_or("domain");
let struct_name = Self::to_pascal_case(&format!("{}-{}", verb, noun));
let function_name = Self::to_snake_case(&format!("{}_{}", verb, noun));
format!(
r"//! CLI wrapper for {} {}
//!
//! This is a thin synchronous wrapper that delegates to async business logic.
use crate::utils::error::Result;
use clap::Args;
use crate::{}::{};
/// CLI arguments for {} {}
#[derive(Debug, Args)]
pub struct {}Args {{
// FUTURE: Add CLI arguments here
}}
/// {} {} command (sync wrapper)
#[{}]
pub fn {}(args: {}Args) -> Result<()> {{
// Delegate to async business logic
tokio::runtime::Runtime::new()?.block_on(async {{
{}(args).await
}})
}}
",
verb,
noun,
module,
function_name,
verb,
noun,
struct_name,
verb,
noun,
verb,
function_name,
struct_name,
function_name
)
}
pub fn generate_domain_skeleton(verb: &str, noun: &str) -> String {
let function_name = Self::to_snake_case(&format!("{}_{}", verb, noun));
let struct_name = Self::to_pascal_case(&format!("{}-{}", verb, noun));
let template = format!(
r"//! Business logic for {} {}
//!
//! This module contains the async business logic implementation.
//! Modify this file freely - it will never be regenerated.
use crate::utils::error::Result;
/// Arguments for {} {} operation
#[derive(Debug)]
pub struct {}Args {{
// FUTURE: Add business logic arguments here
}}
/// {} {} business logic (async)
pub async fn {}(args: {}Args) -> Result<()> {{
// Default implementation - replace with your logic
// FUTURE: Implement actual business logic here
Ok(())
}}
#[cfg(test)]
mod tests {{
use super::*;
#[tokio::test]
async fn test_{}() {{
let args = {}Args {{}};
let result = {}(args).await;
assert!(result.is_ok());
}}
}}
",
verb,
noun,
verb,
noun,
struct_name,
verb,
noun,
function_name,
struct_name,
function_name,
struct_name,
function_name
);
template
.replace(
&format!("/// {} {} business logic (async)", verb, noun),
&format!("/// {} {} business logic (async)\n///\n/// {{% frozen id=\"business_logic\" %}}\n/// FUTURE: Implement business logic here\n/// {{% endfrozen %}}", verb, noun)
)
.replace(
"// Default implementation - replace with your logic",
"{{% frozen id=\"implementation\" %}}\n // Default implementation - replace with your logic"
)
.replace(
&format!("println!(\"Executing {} {} with args: {{{{:?}}}}\", args);\n Ok(())", verb, noun),
&format!("println!(\"Executing {} {} with args: {{{{:?}}}}\", args);\n Ok(())\n {{% endfrozen %}}", verb, noun)
)
}
pub fn business_logic_exists(path: &Path) -> bool {
path.exists()
}
pub fn generate_separated_files(
cli_path: &Path, domain_path: &Path, verb: &str, noun: &str, force_domain: bool,
) -> Result<()> {
let cli_content = Self::generate_cli_wrapper(verb, noun, None);
if let Some(parent) = cli_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(cli_path, cli_content)?;
if force_domain || !Self::business_logic_exists(domain_path) {
let domain_content = Self::generate_domain_skeleton(verb, noun);
if let Some(parent) = domain_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(domain_path, domain_content)?;
}
Ok(())
}
fn to_pascal_case(s: &str) -> String {
s.split(|c: char| !c.is_alphanumeric())
.filter(|s| !s.is_empty())
.map(|word| {
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().chain(chars).collect(),
}
})
.collect()
}
fn to_snake_case(s: &str) -> String {
s.split(|c: char| !c.is_alphanumeric())
.filter(|s| !s.is_empty())
.map(|s| s.to_lowercase())
.collect::<Vec<_>>()
.join("_")
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_generate_cli_wrapper() {
let cli_code = BusinessLogicSeparator::generate_cli_wrapper("create", "project", None);
assert!(cli_code.contains("CreateProjectArgs"));
assert!(cli_code.contains("#[create]"));
assert!(cli_code.contains("pub fn create_project"));
assert!(cli_code.contains("use crate::domain::create_project"));
}
#[test]
fn test_generate_domain_skeleton() {
let domain_code = BusinessLogicSeparator::generate_domain_skeleton("delete", "user");
assert!(domain_code.contains("DeleteUserArgs"));
assert!(domain_code.contains("pub async fn delete_user"));
assert!(domain_code.contains("{% frozen id=\"business_logic\" %}"));
assert!(domain_code.contains("#[tokio::test]"));
}
#[test]
fn test_business_logic_exists() {
let temp_dir = TempDir::new().unwrap();
let existing_file = temp_dir.path().join("existing.rs");
let nonexistent_file = temp_dir.path().join("nonexistent.rs");
fs::write(&existing_file, "content").unwrap();
assert!(BusinessLogicSeparator::business_logic_exists(
&existing_file
));
assert!(!BusinessLogicSeparator::business_logic_exists(
&nonexistent_file
));
}
#[test]
fn test_generate_separated_files() {
let temp_dir = TempDir::new().unwrap();
let cli_path = temp_dir.path().join("cli/list_task.rs");
let domain_path = temp_dir.path().join("domain/list_task.rs");
let result = BusinessLogicSeparator::generate_separated_files(
&cli_path,
&domain_path,
"list",
"task",
false,
);
assert!(result.is_ok());
assert!(cli_path.exists());
assert!(domain_path.exists());
let cli_content = fs::read_to_string(&cli_path).unwrap();
assert!(cli_content.contains("ListTaskArgs"));
let domain_content = fs::read_to_string(&domain_path).unwrap();
assert!(domain_content.contains("pub async fn list_task"));
}
#[test]
fn test_no_overwrite_existing_domain() {
let temp_dir = TempDir::new().unwrap();
let cli_path = temp_dir.path().join("cli/update_file.rs");
let domain_path = temp_dir.path().join("domain/update_file.rs");
fs::create_dir_all(domain_path.parent().unwrap()).unwrap();
fs::write(&domain_path, "// My custom implementation").unwrap();
let result = BusinessLogicSeparator::generate_separated_files(
&cli_path,
&domain_path,
"update",
"file",
false, );
assert!(result.is_ok());
let domain_content = fs::read_to_string(&domain_path).unwrap();
assert!(domain_content.contains("// My custom implementation"));
assert!(!domain_content.contains("pub async fn update_file"));
}
#[test]
fn test_force_overwrite_domain() {
let temp_dir = TempDir::new().unwrap();
let cli_path = temp_dir.path().join("cli/update_file.rs");
let domain_path = temp_dir.path().join("domain/update_file.rs");
fs::create_dir_all(domain_path.parent().unwrap()).unwrap();
fs::write(&domain_path, "// Old implementation").unwrap();
let result = BusinessLogicSeparator::generate_separated_files(
&cli_path,
&domain_path,
"update",
"file",
true, );
assert!(result.is_ok());
let domain_content = fs::read_to_string(&domain_path).unwrap();
assert!(!domain_content.contains("// Old implementation"));
assert!(domain_content.contains("pub async fn update_file"));
}
#[test]
fn test_to_pascal_case() {
assert_eq!(
BusinessLogicSeparator::to_pascal_case("create-project"),
"CreateProject"
);
assert_eq!(
BusinessLogicSeparator::to_pascal_case("delete_user"),
"DeleteUser"
);
assert_eq!(
BusinessLogicSeparator::to_pascal_case("list-all-tasks"),
"ListAllTasks"
);
}
#[test]
fn test_to_snake_case() {
assert_eq!(
BusinessLogicSeparator::to_snake_case("CreateProject"),
"createproject"
);
assert_eq!(
BusinessLogicSeparator::to_snake_case("delete-user"),
"delete_user"
);
assert_eq!(
BusinessLogicSeparator::to_snake_case("list-all-tasks"),
"list_all_tasks"
);
}
}