1use anyhow::{Context, Result};
13use clap::ValueEnum;
14use console::{style, Emoji};
15use std::fs;
16use std::path::Path;
17
18const PROJECT_NAME_PLACEHOLDER: &str = "{{PROJECT_NAME}}";
20const PROJECT_NAME_SNAKE_PLACEHOLDER: &str = "{{PROJECT_NAME_SNAKE}}";
21const RESOURCE_NAME_PLACEHOLDER: &str = "{{RESOURCE_NAME}}";
22const RESOURCE_NAME_PASCAL_PLACEHOLDER: &str = "{{RESOURCE_NAME_PASCAL}}";
23
24#[derive(Debug, Clone, Copy, ValueEnum)]
26pub enum GenKind {
27 Api,
29 Model,
31 Service,
33 Repository,
35 Controller,
37 Middleware,
39 Migration,
41}
42
43pub fn gen(kind: GenKind, name: &str, list: bool) -> Result<()> {
45 if list {
46 return list_gen_types();
47 }
48
49 println!();
50 println!(
51 "{} {} {}",
52 Emoji("🔧", ""),
53 style("Generating").bold(),
54 style(format!("{:?}", kind).to_lowercase()).cyan()
55 );
56 println!();
57
58 match kind {
59 GenKind::Api => gen_api(name)?,
60 GenKind::Model => gen_model(name)?,
61 GenKind::Service => gen_service(name)?,
62 GenKind::Repository => gen_repository(name)?,
63 GenKind::Controller => gen_controller(name)?,
64 GenKind::Middleware => gen_middleware(name)?,
65 GenKind::Migration => gen_migration(name)?,
66 }
67
68 println!();
69 println!(
70 "{} {} generated successfully!",
71 Emoji("✨", ""),
72 style(name).cyan().bold()
73 );
74 println!();
75
76 Ok(())
77}
78
79fn list_gen_types() -> Result<()> {
81 println!();
82 println!("{} {}", Emoji("📋", ""), style("Available Code Generation Types").bold());
83 println!("{}", style("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━").dim());
84 println!();
85
86 let types = vec![
87 ("api", "Full CRUD stack", "model + repository + logic + service + controller + routes"),
88 ("model", "Entity/Model", "Generate an entity struct with derive macros"),
89 ("service", "Service layer", "Business logic service with traits"),
90 ("repository", "Repository", "Data access layer with sqlx"),
91 ("controller", "Controller + Routes", "HTTP handler and route definitions"),
92 ("middleware", "Middleware", "Custom middleware (auth, ratelimit, etc.)"),
93 ("migration", "SQL Migration", "Database migration file"),
94 ];
95
96 for (name, title, desc) in types {
97 println!(" {} {}", style(name).cyan().bold(), style(title).white());
98 println!(" {}", style(desc).dim());
99 println!();
100 }
101
102 println!(" {} Usage: keg gen <type> <name>", style("ℹ").dim());
103 println!(" {} Example: keg gen api article", style("ℹ").dim());
104
105 Ok(())
106}
107
108fn gen_api(name: &str) -> Result<()> {
111 let pascal = to_pascal_case(name);
112 let snake = to_snake_case(name);
113
114 let replacements = &[
115 (PROJECT_NAME_PLACEHOLDER, "TODO_project_name"),
116 (PROJECT_NAME_SNAKE_PLACEHOLDER, "TODO_project_snake"),
117 (RESOURCE_NAME_PLACEHOLDER, &snake),
118 (RESOURCE_NAME_PASCAL_PLACEHOLDER, &pascal),
119 ];
120
121 let entity_dir = Path::new("internal/model/entity");
123 fs::create_dir_all(entity_dir).context("Failed to create internal/model/entity/")?;
124 write_file(
125 &entity_dir.join(format!("{}.rs", snake)),
126 &render_template(include_str!("../templates/internal_model_entity_RESOURCE_rs.txt"), replacements),
127 )?;
128 println!(" {} internal/model/entity/{}.rs", style("✓").green(), snake);
129
130 let repo_dir = Path::new("internal/repository");
132 fs::create_dir_all(repo_dir).context("Failed to create internal/repository/")?;
133 write_file(
134 &repo_dir.join(format!("{}_repo.rs", snake)),
135 &render_template(include_str!("../templates/internal_repository_RESOURCE_rs.txt"), replacements),
136 )?;
137 println!(" {} internal/repository/{}_repo.rs", style("✓").green(), snake);
138
139 let logic_dir = Path::new("internal/logic");
141 fs::create_dir_all(logic_dir).context("Failed to create internal/logic/")?;
142 write_file(
143 &logic_dir.join(format!("{}_logic.rs", snake)),
144 &render_template(include_str!("../templates/internal_logic_RESOURCE_rs.txt"), replacements),
145 )?;
146 println!(" {} internal/logic/{}_logic.rs", style("✓").green(), snake);
147
148 let service_dir = Path::new("internal/service");
150 fs::create_dir_all(service_dir).context("Failed to create internal/service/")?;
151 write_file(
152 &service_dir.join(format!("{}_service.rs", snake)),
153 &render_template(include_str!("../templates/internal_service_RESOURCE_rs.txt"), replacements),
154 )?;
155 println!(" {} internal/service/{}_service.rs", style("✓").green(), snake);
156
157 let controller_dir = Path::new("src/controller/api");
159 fs::create_dir_all(controller_dir).context("Failed to create src/controller/api/")?;
160 write_file(
161 &controller_dir.join(format!("{}.rs", snake)),
162 &render_template(include_str!("../templates/src_controller_api_RESOURCE_rs.txt"), replacements),
163 )?;
164 println!(" {} src/controller/api/{}.rs", style("✓").green(), snake);
165
166 let routes_dir = Path::new("src/routes/api/v1");
168 fs::create_dir_all(routes_dir).context("Failed to create src/routes/api/v1/")?;
169 write_file(
170 &routes_dir.join(format!("{}.rs", snake)),
171 &render_template(include_str!("../templates/src_routes_api_v1_RESOURCE_rs.txt"), replacements),
172 )?;
173 println!(" {} src/routes/api/v1/{}.rs", style("✓").green(), snake);
174
175 update_mod_file(Path::new("internal/model/entity/mod.rs"), &format!("pub mod {};", snake));
177 update_mod_file(Path::new("internal/repository/mod.rs"), &format!("pub mod {};", snake));
178 update_mod_file(Path::new("internal/logic/mod.rs"), &format!("pub mod {};", snake));
179 update_mod_file(Path::new("internal/service/mod.rs"), &format!("pub mod {};", snake));
180 update_mod_file(Path::new("src/controller/api/mod.rs"), &format!("pub mod {};", snake));
181
182 update_routes_v1_mod(&snake);
184
185 Ok(())
186}
187
188fn gen_model(name: &str) -> Result<()> {
191 let pascal = to_pascal_case(name);
192 let snake = to_snake_case(name);
193
194 let entity_dir = Path::new("internal/model/entity");
195 fs::create_dir_all(entity_dir).context("Failed to create internal/model/entity/")?;
196 write_file(
197 &entity_dir.join(format!("{}.rs", snake)),
198 &render_template(include_str!("../templates/internal_model_entity_RESOURCE_rs.txt"), &[
199 (RESOURCE_NAME_PLACEHOLDER, &snake),
200 (RESOURCE_NAME_PASCAL_PLACEHOLDER, &pascal),
201 ]),
202 )?;
203 println!(" {} internal/model/entity/{}.rs", style("✓").green(), snake);
204
205 update_mod_file(Path::new("internal/model/entity/mod.rs"), &format!("pub mod {};", snake));
206
207 Ok(())
208}
209
210fn gen_service(name: &str) -> Result<()> {
213 let pascal = to_pascal_case(name);
214 let snake = to_snake_case(name);
215
216 let service_dir = Path::new("internal/service");
217 fs::create_dir_all(service_dir).context("Failed to create internal/service/")?;
218 write_file(
219 &service_dir.join(format!("{}_service.rs", snake)),
220 &render_template(include_str!("../templates/internal_service_RESOURCE_rs.txt"), &[
221 (PROJECT_NAME_PLACEHOLDER, "TODO_project_name"),
222 (PROJECT_NAME_SNAKE_PLACEHOLDER, "TODO_project_snake"),
223 (RESOURCE_NAME_PLACEHOLDER, &snake),
224 (RESOURCE_NAME_PASCAL_PLACEHOLDER, &pascal),
225 ]),
226 )?;
227 println!(" {} internal/service/{}_service.rs", style("✓").green(), snake);
228
229 update_mod_file(Path::new("internal/service/mod.rs"), &format!("pub mod {};", snake));
230
231 Ok(())
232}
233
234fn gen_repository(name: &str) -> Result<()> {
237 let pascal = to_pascal_case(name);
238 let snake = to_snake_case(name);
239
240 let repo_dir = Path::new("internal/repository");
241 fs::create_dir_all(repo_dir).context("Failed to create internal/repository/")?;
242 write_file(
243 &repo_dir.join(format!("{}_repo.rs", snake)),
244 &render_template(include_str!("../templates/internal_repository_RESOURCE_rs.txt"), &[
245 (PROJECT_NAME_PLACEHOLDER, "TODO_project_name"),
246 (PROJECT_NAME_SNAKE_PLACEHOLDER, "TODO_project_snake"),
247 (RESOURCE_NAME_PLACEHOLDER, &snake),
248 (RESOURCE_NAME_PASCAL_PLACEHOLDER, &pascal),
249 ]),
250 )?;
251 println!(" {} internal/repository/{}_repo.rs", style("✓").green(), snake);
252
253 update_mod_file(Path::new("internal/repository/mod.rs"), &format!("pub mod {};", snake));
254
255 Ok(())
256}
257
258fn gen_controller(name: &str) -> Result<()> {
261 let pascal = to_pascal_case(name);
262 let snake = to_snake_case(name);
263
264 let controller_dir = Path::new("src/controller/api");
265 fs::create_dir_all(controller_dir).context("Failed to create src/controller/api/")?;
266 write_file(
267 &controller_dir.join(format!("{}.rs", snake)),
268 &render_template(include_str!("../templates/src_controller_api_RESOURCE_rs.txt"), &[
269 (PROJECT_NAME_PLACEHOLDER, "TODO_project_name"),
270 (PROJECT_NAME_SNAKE_PLACEHOLDER, "TODO_project_snake"),
271 (RESOURCE_NAME_PLACEHOLDER, &snake),
272 (RESOURCE_NAME_PASCAL_PLACEHOLDER, &pascal),
273 ]),
274 )?;
275 println!(" {} src/controller/api/{}.rs", style("✓").green(), snake);
276
277 update_mod_file(Path::new("src/controller/api/mod.rs"), &format!("pub mod {};", snake));
278
279 let routes_dir = Path::new("src/routes/api/v1");
281 fs::create_dir_all(routes_dir).context("Failed to create src/routes/api/v1/")?;
282 write_file(
283 &routes_dir.join(format!("{}.rs", snake)),
284 &render_template(include_str!("../templates/src_routes_api_v1_RESOURCE_rs.txt"), &[
285 (RESOURCE_NAME_PLACEHOLDER, &snake),
286 ]),
287 )?;
288 println!(" {} src/routes/api/v1/{}.rs", style("✓").green(), snake);
289
290 update_routes_v1_mod(&snake);
291
292 Ok(())
293}
294
295fn gen_middleware(name: &str) -> Result<()> {
298 let pascal = to_pascal_case(name);
299 let snake = to_snake_case(name);
300
301 let middleware_dir = Path::new("src/middleware");
302 fs::create_dir_all(middleware_dir).context("Failed to create src/middleware/")?;
303 write_file(
304 &middleware_dir.join(format!("{}.rs", snake)),
305 &render_template(include_str!("../templates/src_middleware_auth_rs.txt"), &[
306 (RESOURCE_NAME_PLACEHOLDER, &snake),
307 (RESOURCE_NAME_PASCAL_PLACEHOLDER, &pascal),
308 ]).replace("JwtAuth", &pascal).replace("JwtAuthMiddleware", &format!("{}Middleware", pascal)).as_str(),
309 )?;
310 println!(" {} src/middleware/{}.rs", style("✓").green(), snake);
311
312 update_mod_file(Path::new("src/middleware/mod.rs"), &format!("pub mod {};", snake));
313
314 Ok(())
315}
316
317fn gen_migration(name: &str) -> Result<()> {
320 let snake = to_snake_case(name);
321 let timestamp = chrono::Utc::now().format("%Y%m%d%H%M%S").to_string();
322
323 let migrations_dir = Path::new("migrations");
324 fs::create_dir_all(migrations_dir).context("Failed to create migrations/")?;
325
326 let mut content = include_str!("../templates/migrations_001_init_sql.txt").to_string();
327 content = content.replace("{{RESOURCE_NAME}}", &snake);
328
329 let file_name = format!("{}_{}.sql", timestamp, snake);
330 write_file(&migrations_dir.join(&file_name), &content)?;
331 println!(" {} migrations/{}", style("✓").green(), file_name);
332
333 Ok(())
334}
335
336fn render_template(content: &str, replacements: &[(&str, &str)]) -> String {
339 let mut result = content.to_string();
340 for (placeholder, value) in replacements {
341 result = result.replace(placeholder, value);
342 }
343 result
344}
345
346fn write_file(path: &Path, content: &str) -> Result<()> {
347 fs::write(path, content).context(format!("Failed to write file: {:?}", path))
348}
349
350fn update_mod_file(path: &Path, new_line: &str) {
351 let content = if path.exists() {
352 fs::read_to_string(path).unwrap_or_default()
353 } else {
354 String::new()
355 };
356
357 if !content.contains(new_line.trim()) {
358 let new_content = if content.trim().is_empty() {
359 new_line.to_string()
360 } else {
361 format!("{}\n{}", content.trim(), new_line)
362 };
363 let _ = fs::write(path, new_content);
364 }
365}
366
367fn update_routes_v1_mod(name: &str) {
368 let path = Path::new("src/routes/api/v1/mod.rs");
369 let content = if path.exists() {
370 fs::read_to_string(path).unwrap_or_default()
371 } else {
372 r#"//! API v1 routes
373pub mod __placeholder__;
374"#.to_string()
375 };
376
377 if !content.contains(&format!("pub mod {};", name)) {
378 let new_content = content.replace("pub mod __placeholder__;", &format!("pub mod {};\npub mod __placeholder__;", name));
379 let _ = fs::write(path, new_content);
380 }
381}
382
383fn to_snake_case(s: &str) -> String {
384 let mut result = String::new();
385 for (i, c) in s.chars().enumerate() {
386 if c.is_uppercase() && i > 0 {
387 result.push('_');
388 }
389 result.push(c.to_ascii_lowercase());
390 }
391 result.replace('-', "_").replace(' ', "_")
392}
393
394fn to_pascal_case(s: &str) -> String {
395 let mut result = String::new();
396 let mut capitalize_next = true;
397 for c in s.chars() {
398 if c == '-' || c == '_' || c == ' ' {
399 capitalize_next = true;
400 } else if capitalize_next {
401 result.extend(c.to_uppercase());
402 capitalize_next = false;
403 } else {
404 result.push(c);
405 }
406 }
407 result
408}