use anyhow::{Context, Result};
use clap::ValueEnum;
use console::{style, Emoji};
use std::fs;
use std::path::Path;
const PROJECT_NAME_PLACEHOLDER: &str = "{{PROJECT_NAME}}";
const PROJECT_NAME_SNAKE_PLACEHOLDER: &str = "{{PROJECT_NAME_SNAKE}}";
const RESOURCE_NAME_PLACEHOLDER: &str = "{{RESOURCE_NAME}}";
const RESOURCE_NAME_PASCAL_PLACEHOLDER: &str = "{{RESOURCE_NAME_PASCAL}}";
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum GenKind {
Api,
Model,
Service,
Repository,
Controller,
Middleware,
Migration,
}
pub fn gen(kind: GenKind, name: &str, list: bool) -> Result<()> {
if list {
return list_gen_types();
}
println!();
println!(
"{} {} {}",
Emoji("🔧", ""),
style("Generating").bold(),
style(format!("{:?}", kind).to_lowercase()).cyan()
);
println!();
match kind {
GenKind::Api => gen_api(name)?,
GenKind::Model => gen_model(name)?,
GenKind::Service => gen_service(name)?,
GenKind::Repository => gen_repository(name)?,
GenKind::Controller => gen_controller(name)?,
GenKind::Middleware => gen_middleware(name)?,
GenKind::Migration => gen_migration(name)?,
}
println!();
println!(
"{} {} generated successfully!",
Emoji("✨", ""),
style(name).cyan().bold()
);
println!();
Ok(())
}
fn list_gen_types() -> Result<()> {
println!();
println!("{} {}", Emoji("📋", ""), style("Available Code Generation Types").bold());
println!("{}", style("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━").dim());
println!();
let types = vec![
("api", "Full CRUD stack", "model + repository + logic + service + controller + routes"),
("model", "Entity/Model", "Generate an entity struct with derive macros"),
("service", "Service layer", "Business logic service with traits"),
("repository", "Repository", "Data access layer with sqlx"),
("controller", "Controller + Routes", "HTTP handler and route definitions"),
("middleware", "Middleware", "Custom middleware (auth, ratelimit, etc.)"),
("migration", "SQL Migration", "Database migration file"),
];
for (name, title, desc) in types {
println!(" {} {}", style(name).cyan().bold(), style(title).white());
println!(" {}", style(desc).dim());
println!();
}
println!(" {} Usage: keg gen <type> <name>", style("ℹ").dim());
println!(" {} Example: keg gen api article", style("ℹ").dim());
Ok(())
}
fn gen_api(name: &str) -> Result<()> {
let pascal = to_pascal_case(name);
let snake = to_snake_case(name);
let replacements = &[
(PROJECT_NAME_PLACEHOLDER, "TODO_project_name"),
(PROJECT_NAME_SNAKE_PLACEHOLDER, "TODO_project_snake"),
(RESOURCE_NAME_PLACEHOLDER, &snake),
(RESOURCE_NAME_PASCAL_PLACEHOLDER, &pascal),
];
let entity_dir = Path::new("internal/model/entity");
fs::create_dir_all(entity_dir).context("Failed to create internal/model/entity/")?;
write_file(
&entity_dir.join(format!("{}.rs", snake)),
&render_template(include_str!("../templates/internal_model_entity_RESOURCE_rs.txt"), replacements),
)?;
println!(" {} internal/model/entity/{}.rs", style("✓").green(), snake);
let repo_dir = Path::new("internal/repository");
fs::create_dir_all(repo_dir).context("Failed to create internal/repository/")?;
write_file(
&repo_dir.join(format!("{}_repo.rs", snake)),
&render_template(include_str!("../templates/internal_repository_RESOURCE_rs.txt"), replacements),
)?;
println!(" {} internal/repository/{}_repo.rs", style("✓").green(), snake);
let logic_dir = Path::new("internal/logic");
fs::create_dir_all(logic_dir).context("Failed to create internal/logic/")?;
write_file(
&logic_dir.join(format!("{}_logic.rs", snake)),
&render_template(include_str!("../templates/internal_logic_RESOURCE_rs.txt"), replacements),
)?;
println!(" {} internal/logic/{}_logic.rs", style("✓").green(), snake);
let service_dir = Path::new("internal/service");
fs::create_dir_all(service_dir).context("Failed to create internal/service/")?;
write_file(
&service_dir.join(format!("{}_service.rs", snake)),
&render_template(include_str!("../templates/internal_service_RESOURCE_rs.txt"), replacements),
)?;
println!(" {} internal/service/{}_service.rs", style("✓").green(), snake);
let controller_dir = Path::new("src/controller/api");
fs::create_dir_all(controller_dir).context("Failed to create src/controller/api/")?;
write_file(
&controller_dir.join(format!("{}.rs", snake)),
&render_template(include_str!("../templates/src_controller_api_RESOURCE_rs.txt"), replacements),
)?;
println!(" {} src/controller/api/{}.rs", style("✓").green(), snake);
let routes_dir = Path::new("src/routes/api/v1");
fs::create_dir_all(routes_dir).context("Failed to create src/routes/api/v1/")?;
write_file(
&routes_dir.join(format!("{}.rs", snake)),
&render_template(include_str!("../templates/src_routes_api_v1_RESOURCE_rs.txt"), replacements),
)?;
println!(" {} src/routes/api/v1/{}.rs", style("✓").green(), snake);
update_mod_file(Path::new("internal/model/entity/mod.rs"), &format!("pub mod {};", snake));
update_mod_file(Path::new("internal/repository/mod.rs"), &format!("pub mod {};", snake));
update_mod_file(Path::new("internal/logic/mod.rs"), &format!("pub mod {};", snake));
update_mod_file(Path::new("internal/service/mod.rs"), &format!("pub mod {};", snake));
update_mod_file(Path::new("src/controller/api/mod.rs"), &format!("pub mod {};", snake));
update_routes_v1_mod(&snake);
Ok(())
}
fn gen_model(name: &str) -> Result<()> {
let pascal = to_pascal_case(name);
let snake = to_snake_case(name);
let entity_dir = Path::new("internal/model/entity");
fs::create_dir_all(entity_dir).context("Failed to create internal/model/entity/")?;
write_file(
&entity_dir.join(format!("{}.rs", snake)),
&render_template(include_str!("../templates/internal_model_entity_RESOURCE_rs.txt"), &[
(RESOURCE_NAME_PLACEHOLDER, &snake),
(RESOURCE_NAME_PASCAL_PLACEHOLDER, &pascal),
]),
)?;
println!(" {} internal/model/entity/{}.rs", style("✓").green(), snake);
update_mod_file(Path::new("internal/model/entity/mod.rs"), &format!("pub mod {};", snake));
Ok(())
}
fn gen_service(name: &str) -> Result<()> {
let pascal = to_pascal_case(name);
let snake = to_snake_case(name);
let service_dir = Path::new("internal/service");
fs::create_dir_all(service_dir).context("Failed to create internal/service/")?;
write_file(
&service_dir.join(format!("{}_service.rs", snake)),
&render_template(include_str!("../templates/internal_service_RESOURCE_rs.txt"), &[
(PROJECT_NAME_PLACEHOLDER, "TODO_project_name"),
(PROJECT_NAME_SNAKE_PLACEHOLDER, "TODO_project_snake"),
(RESOURCE_NAME_PLACEHOLDER, &snake),
(RESOURCE_NAME_PASCAL_PLACEHOLDER, &pascal),
]),
)?;
println!(" {} internal/service/{}_service.rs", style("✓").green(), snake);
update_mod_file(Path::new("internal/service/mod.rs"), &format!("pub mod {};", snake));
Ok(())
}
fn gen_repository(name: &str) -> Result<()> {
let pascal = to_pascal_case(name);
let snake = to_snake_case(name);
let repo_dir = Path::new("internal/repository");
fs::create_dir_all(repo_dir).context("Failed to create internal/repository/")?;
write_file(
&repo_dir.join(format!("{}_repo.rs", snake)),
&render_template(include_str!("../templates/internal_repository_RESOURCE_rs.txt"), &[
(PROJECT_NAME_PLACEHOLDER, "TODO_project_name"),
(PROJECT_NAME_SNAKE_PLACEHOLDER, "TODO_project_snake"),
(RESOURCE_NAME_PLACEHOLDER, &snake),
(RESOURCE_NAME_PASCAL_PLACEHOLDER, &pascal),
]),
)?;
println!(" {} internal/repository/{}_repo.rs", style("✓").green(), snake);
update_mod_file(Path::new("internal/repository/mod.rs"), &format!("pub mod {};", snake));
Ok(())
}
fn gen_controller(name: &str) -> Result<()> {
let pascal = to_pascal_case(name);
let snake = to_snake_case(name);
let controller_dir = Path::new("src/controller/api");
fs::create_dir_all(controller_dir).context("Failed to create src/controller/api/")?;
write_file(
&controller_dir.join(format!("{}.rs", snake)),
&render_template(include_str!("../templates/src_controller_api_RESOURCE_rs.txt"), &[
(PROJECT_NAME_PLACEHOLDER, "TODO_project_name"),
(PROJECT_NAME_SNAKE_PLACEHOLDER, "TODO_project_snake"),
(RESOURCE_NAME_PLACEHOLDER, &snake),
(RESOURCE_NAME_PASCAL_PLACEHOLDER, &pascal),
]),
)?;
println!(" {} src/controller/api/{}.rs", style("✓").green(), snake);
update_mod_file(Path::new("src/controller/api/mod.rs"), &format!("pub mod {};", snake));
let routes_dir = Path::new("src/routes/api/v1");
fs::create_dir_all(routes_dir).context("Failed to create src/routes/api/v1/")?;
write_file(
&routes_dir.join(format!("{}.rs", snake)),
&render_template(include_str!("../templates/src_routes_api_v1_RESOURCE_rs.txt"), &[
(RESOURCE_NAME_PLACEHOLDER, &snake),
]),
)?;
println!(" {} src/routes/api/v1/{}.rs", style("✓").green(), snake);
update_routes_v1_mod(&snake);
Ok(())
}
fn gen_middleware(name: &str) -> Result<()> {
let pascal = to_pascal_case(name);
let snake = to_snake_case(name);
let middleware_dir = Path::new("src/middleware");
fs::create_dir_all(middleware_dir).context("Failed to create src/middleware/")?;
write_file(
&middleware_dir.join(format!("{}.rs", snake)),
&render_template(include_str!("../templates/src_middleware_auth_rs.txt"), &[
(RESOURCE_NAME_PLACEHOLDER, &snake),
(RESOURCE_NAME_PASCAL_PLACEHOLDER, &pascal),
]).replace("JwtAuth", &pascal).replace("JwtAuthMiddleware", &format!("{}Middleware", pascal)).as_str(),
)?;
println!(" {} src/middleware/{}.rs", style("✓").green(), snake);
update_mod_file(Path::new("src/middleware/mod.rs"), &format!("pub mod {};", snake));
Ok(())
}
fn gen_migration(name: &str) -> Result<()> {
let snake = to_snake_case(name);
let timestamp = chrono::Utc::now().format("%Y%m%d%H%M%S").to_string();
let migrations_dir = Path::new("migrations");
fs::create_dir_all(migrations_dir).context("Failed to create migrations/")?;
let mut content = include_str!("../templates/migrations_001_init_sql.txt").to_string();
content = content.replace("{{RESOURCE_NAME}}", &snake);
let file_name = format!("{}_{}.sql", timestamp, snake);
write_file(&migrations_dir.join(&file_name), &content)?;
println!(" {} migrations/{}", style("✓").green(), file_name);
Ok(())
}
fn render_template(content: &str, replacements: &[(&str, &str)]) -> String {
let mut result = content.to_string();
for (placeholder, value) in replacements {
result = result.replace(placeholder, value);
}
result
}
fn write_file(path: &Path, content: &str) -> Result<()> {
fs::write(path, content).context(format!("Failed to write file: {:?}", path))
}
fn update_mod_file(path: &Path, new_line: &str) {
let content = if path.exists() {
fs::read_to_string(path).unwrap_or_default()
} else {
String::new()
};
if !content.contains(new_line.trim()) {
let new_content = if content.trim().is_empty() {
new_line.to_string()
} else {
format!("{}\n{}", content.trim(), new_line)
};
let _ = fs::write(path, new_content);
}
}
fn update_routes_v1_mod(name: &str) {
let path = Path::new("src/routes/api/v1/mod.rs");
let content = if path.exists() {
fs::read_to_string(path).unwrap_or_default()
} else {
r#"//! API v1 routes
pub mod __placeholder__;
"#.to_string()
};
if !content.contains(&format!("pub mod {};", name)) {
let new_content = content.replace("pub mod __placeholder__;", &format!("pub mod {};\npub mod __placeholder__;", name));
let _ = fs::write(path, new_content);
}
}
fn to_snake_case(s: &str) -> String {
let mut result = String::new();
for (i, c) in s.chars().enumerate() {
if c.is_uppercase() && i > 0 {
result.push('_');
}
result.push(c.to_ascii_lowercase());
}
result.replace('-', "_").replace(' ', "_")
}
fn to_pascal_case(s: &str) -> String {
let mut result = String::new();
let mut capitalize_next = true;
for c in s.chars() {
if c == '-' || c == '_' || c == ' ' {
capitalize_next = true;
} else if capitalize_next {
result.extend(c.to_uppercase());
capitalize_next = false;
} else {
result.push(c);
}
}
result
}