1use std::fs::{self, OpenOptions};
2use std::io::{Read, Write};
3use rustbasic_core::chrono::Local;
4use rustbasic_core::colored::*;
5use crate::utils::{to_snake_case, to_pascal_case};
6
7pub fn make_controller(name: &str) {
8 let pascal_name = to_pascal_case(name).replace("Controller", "");
9 let snake_name = to_snake_case(&pascal_name);
10 let class_name = format!("{}Controller", pascal_name);
11 let file_name = format!("{}_controller.rs", snake_name);
12 let file_path = format!("src/app/http/controllers/{}", file_name);
13
14 if std::path::Path::new(&file_path).exists() {
15 println!("{} {} {}", "โ ๏ธ Controller".yellow(), file_path.cyan(), "sudah ada.".yellow());
16 return;
17 }
18
19 let template = format!(
20r#"/* ---------------------------------------------------------
21 * ๐ LABEL: {class_name} ({file_name})
22 * --------------------------------------------------------- */
23
24use crate::app::inertia::inertia;
25use rustbasic_core::requests::Request;
26use rustbasic_core::router::Response;
27use rustbasic_core::serde_json::json;
28
29pub struct {class_name};
30
31impl {class_name} {{
32 pub async fn index(req: Request) -> Response {{
33 inertia(&req, "{pascal_name}", json!({{
34 "title": "{class_name}"
35 }}))
36 }}
37}}
38"#, class_name = class_name, file_name = file_name, pascal_name = pascal_name);
39
40 fs::write(&file_path, template).expect("Gagal membuat file controller");
41 println!("{} {}", "โ
Controller dibuat:".green(), file_path.cyan());
42
43 update_controller_mod_rs(&file_name.replace(".rs", ""));
44}
45
46pub fn update_controller_mod_rs(mod_name: &str) {
47 let mod_path = "src/app/http/controllers/mod.rs";
48 let mut content = String::new();
49 if let Ok(mut file) = fs::File::open(mod_path) {
50 file.read_to_string(&mut content).ok();
51 }
52
53 let line = format!("pub mod {};", mod_name);
54 if content.contains(&line) {
55 return;
56 }
57
58 let mut file = OpenOptions::new()
59 .append(true)
60 .open(mod_path)
61 .expect("Gagal membuka controllers/mod.rs");
62
63 writeln!(file, "{}", line).ok();
64 println!("{} {}", "๐".blue(), "controllers/mod.rs diperbarui.".dimmed());
65}
66
67pub fn make_middleware(name: &str) {
68 let snake_name = to_snake_case(name).replace("_middleware", "");
69 let fn_name = format!("{}_middleware", snake_name);
70 let file_name = format!("{}.rs", snake_name);
71 let file_path = format!("src/app/http/middleware/{}", file_name);
72
73 if std::path::Path::new(&file_path).exists() {
74 println!("{} {} {}", "โ ๏ธ Middleware".yellow(), file_path.cyan(), "sudah ada.".yellow());
75 return;
76 }
77
78 let template = format!(
79r#"/* ---------------------------------------------------------
80 * ๐ LABEL: {label} (middleware/{file_name})
81 * --------------------------------------------------------- */
82
83use rustbasic_core::middleware::Next;
84use rustbasic_core::requests::Request;
85use rustbasic_core::router::Response;
86
87pub async fn {fn_name}(
88 req: Request,
89 next: Next,
90) -> Response {{
91 // Lakukan sesuatu sebelum request sampai ke controller
92
93 let response = next.run(req).await;
94
95 // Lakukan sesuatu setelah request selesai diproses
96
97 response
98}}
99"#, label = name.to_uppercase(), file_name = file_name, fn_name = fn_name);
100
101 fs::write(&file_path, template).expect("Gagal membuat file middleware");
102 println!("{} {}", "โ
Middleware dibuat:".green(), file_path.cyan());
103
104 update_middleware_mod_rs(&snake_name);
105}
106
107pub fn update_middleware_mod_rs(mod_name: &str) {
108 let mod_path = "src/app/http/middleware/mod.rs";
109 let mut content = String::new();
110 if let Ok(mut file) = fs::File::open(mod_path) {
111 file.read_to_string(&mut content).ok();
112 }
113
114 let line = format!("pub mod {};", mod_name);
115 if content.contains(&line) {
116 return;
117 }
118
119 let mut file = OpenOptions::new()
120 .append(true)
121 .open(mod_path)
122 .expect("Gagal membuka middleware/mod.rs");
123
124 writeln!(file, "{}", line).ok();
125 println!("{} {}", "๐".blue(), "middleware/mod.rs diperbarui.".dimmed());
126}
127
128pub fn make_model(name: &str) {
129 let snake_name = to_snake_case(name);
130 let table_name = format!("{}s", snake_name);
131 let file_path = format!("src/app/models/{}.rs", snake_name);
132
133 if std::path::Path::new(&file_path).exists() {
134 println!("{} {} {}", "โ ๏ธ Model".yellow(), file_path.cyan(), "sudah ada.".yellow());
135 return;
136 }
137
138 let template = format!(
139r#"use rustbasic_core::model;
140
141model! {{
142 table: "{table_name}",
143 Model {{
144 pub id: i32,
145 // tambahkan field lainnya di sini
146
147 // Contoh Cast Boolean (0/1 dari DB dikonversi otomatis ke bool di Rust):
148 // #[serde(default, deserialize_with = "rustbasic_core::support::casts::deserialize_bool", serialize_with = "rustbasic_core::support::casts::serialize_bool")]
149 // pub is_active: bool,
150
151 // Contoh Cast JSON (Kolom TEXT berisi JSON di DB dikonversi otomatis ke serde_json::Value di Rust):
152 // #[serde(default, deserialize_with = "rustbasic_core::support::casts::deserialize_option_json", serialize_with = "rustbasic_core::support::casts::serialize_option_json")]
153 // pub metadata: Option<rustbasic_core::serde_json::Value>,
154 }}
155}}
156"#, table_name = table_name);
157
158 fs::write(&file_path, template).expect("Gagal membuat file model");
159 println!("{} {}", "โ
Model dibuat:".green(), file_path.cyan());
160
161 update_mod_rs(&to_pascal_case(name), &snake_name);
162}
163
164pub fn update_mod_rs(_class_name: &str, snake_name: &str) {
165 let mod_path = "src/app/models/mod.rs";
166 let mut content = String::new();
167 if let Ok(mut file) = fs::File::open(mod_path) {
168 file.read_to_string(&mut content).ok();
169 }
170
171 let mod_line = format!("pub mod {};", snake_name);
172 if content.contains(&mod_line) {
173 return;
174 }
175
176 let mut file = OpenOptions::new()
177 .append(true)
178 .open(mod_path)
179 .expect("Gagal membuka models/mod.rs");
180
181 writeln!(file, "{}", mod_line).ok();
182 println!("{} {}", "๐".blue(), "models/mod.rs diperbarui.".dimmed());
185}
186
187pub fn make_rust_migration(name: &str) {
188 let snake_name = to_snake_case(name);
189 let timestamp = Local::now().format("%Y%m%d_%H%M%S").to_string();
190 let mod_name = format!("m{}_{}", timestamp, snake_name);
191 let file_path = format!("database/migrations/{}.rs", mod_name);
192
193 if std::path::Path::new(&file_path).exists() {
194 println!("{} {} {}", "โ ๏ธ Migration".yellow(), file_path.cyan(), "sudah ada.".yellow());
195 return;
196 }
197
198 let table_name = if snake_name.ends_with('s') { snake_name.clone() } else { format!("{}s", snake_name) };
199
200 let template = format!(
201r#"use rustbasic_core::{{Schema, SchemaManager, MigrationTrait, DbErr}};
202use rustbasic_core::async_trait;
203
204pub struct Migration;
205
206#[async_trait]
207impl MigrationTrait for Migration {{
208 fn name(&self) -> &str {{
209 "{mod_name}"
210 }}
211
212 async fn up<'a>(&self, manager: &'a SchemaManager<'a>) -> Result<(), DbErr> {{
213 Schema::create(manager, "{table_name}", |table| {{
214 table.id();
215 // table.string("title").not_null();
216 table.timestamps();
217 }}).await
218 }}
219
220 async fn down<'a>(&self, manager: &'a SchemaManager<'a>) -> Result<(), DbErr> {{
221 Schema::drop(manager, "{table_name}").await
222 }}
223}}
224"#, table_name = table_name, mod_name = mod_name);
225
226 fs::write(&file_path, template).expect("Gagal membuat file migration");
227 println!("{} {}", "โ
Migration Rust dibuat:".green(), file_path.cyan());
228
229 update_migration_mod_rs(&mod_name);
230}
231
232pub fn make_rust_migration_add(column: &str, table: &str) {
233 let col_snake = to_snake_case(column);
234 let table_snake = to_snake_case(table);
235 let name = format!("add_{}_to_{}", col_snake, table_snake);
236 let timestamp = Local::now().format("%Y%m%d_%H%M%S").to_string();
237 let mod_name = format!("m{}_{}", timestamp, name);
238 let file_path = format!("database/migrations/{}.rs", mod_name);
239
240 let template = format!(
241r#"use rustbasic_core::{{Schema, SchemaManager, MigrationTrait, DbErr}};
242use rustbasic_core::async_trait;
243
244pub struct Migration;
245
246#[async_trait]
247impl MigrationTrait for Migration {{
248 fn name(&self) -> &str {{
249 "{mod_name}"
250 }}
251
252 async fn up<'a>(&self, manager: &'a SchemaManager<'a>) -> Result<(), DbErr> {{
253 Schema::table(manager, "{table_snake}", |table| {{
254 table.string("{col_snake}").nullable();
255 }}).await
256 }}
257
258 async fn down<'a>(&self, manager: &'a SchemaManager<'a>) -> Result<(), DbErr> {{
259 Schema::table(manager, "{table_snake}", |table| {{
260 table.drop_column("{col_snake}");
261 }}).await
262 }}
263}}
264"#, table_snake = table_snake, col_snake = col_snake, mod_name = mod_name);
265
266 fs::write(&file_path, template).expect("Gagal membuat file migration");
267 println!("{} {}", "โ
Migration Add dibuat:".green(), file_path.cyan());
268
269 update_migration_mod_rs(&mod_name);
270}
271
272pub fn update_migration_mod_rs(mod_name: &str) {
273 let mod_path = "database/migrations/mod.rs";
274 let mut content = String::new();
275 if let Ok(mut file) = fs::File::open(mod_path) {
276 file.read_to_string(&mut content).ok();
277 }
278
279 if !content.contains(&format!("pub mod {};", mod_name)) {
281 if !content.ends_with('\n') {
282 content.push('\n');
283 }
284 content.push_str(&format!("pub mod {};\n", mod_name));
285 }
286
287 let search_patterns = [
289 "fn migrations() -> Vec<Box<dyn MigrationTrait>> {",
290 "fn migrations()->Vec<Box<dyn MigrationTrait>>{",
291 ];
292 for pattern in &search_patterns {
293 if let Some(_pos) = content.find(pattern) {
294 let insert_pos = content.find(" ]").unwrap_or(content.len());
295 content.insert_str(insert_pos, &format!(" Box::new({}::Migration),\n", mod_name));
296 break;
297 }
298 }
299
300 fs::write(mod_path, content).expect("Gagal memperbarui database/migrations/mod.rs");
301 println!("{} {}", "๐".blue(), "database/migrations/mod.rs diperbarui.".dimmed());
302}
303
304pub fn make_seeder(name: &str) {
305 let pascal_name = to_pascal_case(name).replace("Seeder", "");
306 let snake_name = to_snake_case(&pascal_name);
307 let class_name = format!("{}Seeder", pascal_name);
308 let file_name = format!("{}_seeder.rs", snake_name);
309 let file_path = format!("database/seeders/{}", file_name);
310
311 if std::path::Path::new(&file_path).exists() {
312 println!("{} {} {}", "โ ๏ธ Seeder".yellow(), file_path.cyan(), "sudah ada.".yellow());
313 return;
314 }
315
316 let template = format!(
317r#"use rustbasic_core::seeder;
318use rustbasic_core::colored::Colorize;
319// use crate::app::models::{pascal_name}; // Sesuaikan dengan nama model/struct Entity Anda
320
321seeder! {{
322 {class_name},
323 run(_db) {{
324 println!(" {{}} Sedang memproses {class_name}...", "โณ".blue());
325
326 // Contoh Penggunaan:
327 /*
328 {pascal_name}::create(_db, rustbasic_core::serde_json::json!({{
329 "name": "Example Data",
330 }})).await?;
331 */
332
333 Ok(())
334 }}
335}}
336"#, class_name = class_name, pascal_name = pascal_name);
337
338 fs::write(&file_path, template).expect("Gagal membuat file seeder");
339 println!("{} {}", "โ
Seeder dibuat:".green(), file_path.cyan());
340
341 update_seeder_mod_rs(&class_name, &file_name.replace(".rs", ""));
342}
343
344pub fn update_seeder_mod_rs(class_name: &str, mod_name: &str) {
345 let db_mod_path = "database/seeders/mod.rs";
347 let mut db_content = fs::read_to_string(db_mod_path).expect("Gagal membaca seeders/mod.rs");
348 let mod_line = format!("pub mod {};", mod_name);
349 if !db_content.contains(&mod_line) {
350 db_content.push_str(&format!("{}\n", mod_line));
351 fs::write(db_mod_path, db_content).ok();
352 }
353
354 let config_path = "src/app/seeder.rs";
356 let mut config_content = fs::read_to_string(config_path).expect("Gagal membaca src/app/seeder.rs");
357 let search_pattern = "let seeders: Vec<Box<dyn SeederTrait>> = vec![";
358 if let Some(pos) = config_content.find(search_pattern) {
359 let insert_pos = pos + search_pattern.len();
360 config_content.insert_str(insert_pos, &format!("\n Box::new(seeders::{}::{}),", mod_name, class_name));
361 fs::write(config_path, config_content).ok();
362 }
363
364 println!("{} {}", "๐".blue(), "Pengaturan seeder diperbarui.".dimmed());
365}
366
367pub fn make_test(name: &str, is_unit: bool) {
368 let pascal_name = to_pascal_case(name).replace("Test", "");
369 let snake_name = to_snake_case(&pascal_name);
370 let prefix = if is_unit { "unit" } else { "feature" };
371 let file_name = format!("{}_{}_test.rs", prefix, snake_name);
372 let file_path = format!("tests/{}", file_name);
373
374 if !std::path::Path::new("tests").exists() {
375 fs::create_dir_all("tests").expect("Gagal membuat folder tests");
376 }
377
378 if std::path::Path::new(&file_path).exists() {
379 println!("{} {} {}", "โ ๏ธ Test".yellow(), file_path.cyan(), "sudah ada.".yellow());
380 return;
381 }
382
383 let template = if is_unit {
384 format!(
385r#"/* ---------------------------------------------------------
386 * ๐งช UNIT TEST: {pascal_name} (tests/{file_name})
387 * --------------------------------------------------------- */
388
389#[test]
390fn test_{snake_name}_logic() {{
391 // Contoh asersi sederhana
392 let expected = 42;
393 let actual = 40 + 2;
394
395 assert_eq!(expected, actual, "Fungsi kalkulasi tidak sesuai");
396}}
397"#, pascal_name = pascal_name, file_name = file_name, snake_name = snake_name)
398 } else {
399 format!(
400r#"/* ---------------------------------------------------------
401 * ๐งช FEATURE TEST: {pascal_name} (tests/{file_name})
402 * --------------------------------------------------------- */
403
404use rustbasic_core::testing::TestClient;
405use rustbasic_core::Config;
406
407#[tokio::test]
408async fn test_{snake_name}_page() {{
409 // 1. Muat konfigurasi
410 let cfg = Config::load();
411
412 // 2. Bangun router aplikasi
413 let router = rustbasic::routes::build_router();
414
415 // 3. Setup TestClient in-memory
416 let client = TestClient::new(cfg, router).await;
417
418 // 4. Kirim request ke endpoint (misalnya '/')
419 let response = client.get("/").await;
420
421 // 5. Asersi response status & konten
422 response.assert_status(200);
423}}
424"#, pascal_name = pascal_name, file_name = file_name, snake_name = snake_name)
425 };
426
427 fs::write(&file_path, template).expect("Gagal membuat file test");
428 println!("{} {}", "โ
Test dibuat:".green(), file_path.cyan());
429}
430
431pub fn make_observer(name: &str, model_name: Option<&str>) {
432 let pascal_name = to_pascal_case(name).replace("Observer", "");
433 let snake_name = to_snake_case(&pascal_name);
434 let class_name = format!("{}Observer", pascal_name);
435 let file_name = format!("{}_observer.rs", snake_name);
436
437 let dir_path = "src/app/observers";
439 fs::create_dir_all(dir_path).expect("Gagal membuat folder observers");
440
441 let file_path = format!("{}/{}", dir_path, file_name);
442
443 if std::path::Path::new(&file_path).exists() {
444 println!("{} {} {}", "โ ๏ธ Observer".yellow(), file_path.cyan(), "sudah ada.".yellow());
445 return;
446 }
447
448 let model_pascal = model_name.map(to_pascal_case).unwrap_or_else(|| pascal_name.clone());
449 let mut model_snake = to_snake_case(&model_pascal);
450 if !std::path::Path::new(&format!("src/app/models/{}.rs", model_snake)).exists()
451 && std::path::Path::new(&format!("src/app/models/{}s.rs", model_snake)).exists() {
452 model_snake = format!("{}s", model_snake);
453 }
454
455 let template = format!(
456r#"/* ---------------------------------------------------------
457 * ๐ LABEL: {class_name} (observers/{file_name})
458 * --------------------------------------------------------- */
459
460use crate::app::models::{model_snake}::Model as {model_pascal};
461use rustbasic_core::serde_json::Value;
462
463pub trait {class_name} {{
464 fn creating(data: &mut Value);
465 fn created(model: &{model_pascal});
466 fn updating(data: &mut Value);
467 fn updated(model: &{model_pascal});
468 fn deleting(id: i32);
469 fn deleted(id: i32);
470}}
471
472pub struct {class_name}Impl;
473
474impl {class_name} for {class_name}Impl {{
475 fn creating(_data: &mut Value) {{
476 // Lakukan sesuatu sebelum data disimpan ke database (Before Create)
477 }}
478
479 fn created(_model: &{model_pascal}) {{
480 // Lakukan sesuatu setelah data berhasil disimpan ke database (After Create)
481 }}
482
483 fn updating(_data: &mut Value) {{
484 // Lakukan sesuatu sebelum data diupdate di database (Before Update)
485 }}
486
487 fn updated(_model: &{model_pascal}) {{
488 // Lakukan sesuatu setelah data berhasil diupdate di database (After Update)
489 }}
490
491 fn deleting(_id: i32) {{
492 // Lakukan sesuatu sebelum data dihapus dari database (Before Delete)
493 }}
494
495 fn deleted(_id: i32) {{
496 // Lakukan sesuatu setelah data berhasil dihapus dari database (After Delete)
497 }}
498}}
499"#, class_name = class_name, file_name = file_name, model_pascal = model_pascal, model_snake = model_snake);
500
501 fs::write(&file_path, template).expect("Gagal membuat file observer");
502 println!("{} {}", "โ
Observer dibuat:".green(), file_path.cyan());
503
504 update_observer_mod_rs(&snake_name);
505}
506
507pub fn update_observer_mod_rs(mod_name: &str) {
508 let mod_path = "src/app/observers/mod.rs";
509 let mut content = String::new();
510 if let Ok(mut file) = fs::File::open(mod_path) {
511 file.read_to_string(&mut content).ok();
512 }
513
514 let line = format!("pub mod {}_observer;", mod_name);
515 if !content.contains(&line) {
516 let mut file = OpenOptions::new()
517 .create(true)
518 .append(true)
519 .open(mod_path)
520 .expect("Gagal membuka observers/mod.rs");
521 writeln!(file, "{}", line).ok();
522 println!("{} {}", "๐".blue(), "observers/mod.rs diperbarui.".dimmed());
523 }
524
525 let app_mod_path = "src/app/mod.rs";
527 if let Ok(content) = fs::read_to_string(app_mod_path)
528 && !content.contains("pub mod observers;") {
529 let mut file = OpenOptions::new()
530 .append(true)
531 .open(app_mod_path)
532 .expect("Gagal membuka app/mod.rs");
533 writeln!(file, "pub mod observers;").ok();
534 println!("{} {}", "๐".blue(), "app/mod.rs diperbarui.".dimmed());
535 }
536}
537
538pub fn make_service(name: &str) {
539 let pascal_name = to_pascal_case(name).replace("Service", "");
540 let snake_name = to_snake_case(&pascal_name);
541 let class_name = format!("{}Service", pascal_name);
542 let file_name = format!("{}_service.rs", snake_name);
543
544 let dir_path = "src/app/services";
546 fs::create_dir_all(dir_path).expect("Gagal membuat folder services");
547
548 let file_path = format!("{}/{}", dir_path, file_name);
549
550 if std::path::Path::new(&file_path).exists() {
551 println!("{} {} {}", "โ ๏ธ Service".yellow(), file_path.cyan(), "sudah ada.".yellow());
552 return;
553 }
554
555 let template = format!(
556r#"/* ---------------------------------------------------------
557 * ๐ LABEL: {class_name} (services/{file_name})
558 * --------------------------------------------------------- */
559
560use rustbasic_core::sql::AnyPool;
561
562pub struct {class_name} {{
563 _db: AnyPool,
564}}
565
566impl {class_name} {{
567 pub fn new(db: AnyPool) -> Self {{
568 Self {{ _db: db }}
569 }}
570
571 // Tambahkan fungsi logika bisnis Anda di sini
572}}
573"#, class_name = class_name, file_name = file_name);
574
575 fs::write(&file_path, template).expect("Gagal membuat file service");
576 println!("{} {}", "โ
Service dibuat:".green(), file_path.cyan());
577
578 update_service_mod_rs(&snake_name);
579}
580
581pub fn update_service_mod_rs(mod_name: &str) {
582 let mod_path = "src/app/services/mod.rs";
583 let mut content = String::new();
584 if let Ok(mut file) = fs::File::open(mod_path) {
585 file.read_to_string(&mut content).ok();
586 }
587
588 let line = format!("pub mod {}_service;", mod_name);
589 if !content.contains(&line) {
590 let mut file = OpenOptions::new()
591 .create(true)
592 .append(true)
593 .open(mod_path)
594 .expect("Gagal membuka services/mod.rs");
595 writeln!(file, "{}", line).ok();
596 println!("{} {}", "๐".blue(), "services/mod.rs diperbarui.".dimmed());
597 }
598
599 let app_mod_path = "src/app/mod.rs";
601 if let Ok(content) = fs::read_to_string(app_mod_path)
602 && !content.contains("pub mod services;") {
603 let mut file = OpenOptions::new()
604 .append(true)
605 .open(app_mod_path)
606 .expect("Gagal membuka app/mod.rs");
607 writeln!(file, "pub mod services;").ok();
608 println!("{} {}", "๐".blue(), "app/mod.rs diperbarui.".dimmed());
609 }
610}
611
612