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}}
148"#, table_name = table_name);
149
150 fs::write(&file_path, template).expect("Gagal membuat file model");
151 println!("{} {}", "โ
Model dibuat:".green(), file_path.cyan());
152
153 update_mod_rs(&to_pascal_case(name), &snake_name);
154}
155
156pub fn update_mod_rs(_class_name: &str, snake_name: &str) {
157 let mod_path = "src/app/models/mod.rs";
158 let mut content = String::new();
159 if let Ok(mut file) = fs::File::open(mod_path) {
160 file.read_to_string(&mut content).ok();
161 }
162
163 let mod_line = format!("pub mod {};", snake_name);
164 if content.contains(&mod_line) {
165 return;
166 }
167
168 let mut file = OpenOptions::new()
169 .append(true)
170 .open(mod_path)
171 .expect("Gagal membuka models/mod.rs");
172
173 writeln!(file, "{}", mod_line).ok();
174 println!("{} {}", "๐".blue(), "models/mod.rs diperbarui.".dimmed());
177}
178
179pub fn make_rust_migration(name: &str) {
180 let snake_name = to_snake_case(name);
181 let timestamp = Local::now().format("%Y%m%d_%H%M%S").to_string();
182 let mod_name = format!("m{}_{}", timestamp, snake_name);
183 let file_path = format!("database/migrations/{}.rs", mod_name);
184
185 if std::path::Path::new(&file_path).exists() {
186 println!("{} {} {}", "โ ๏ธ Migration".yellow(), file_path.cyan(), "sudah ada.".yellow());
187 return;
188 }
189
190 let table_name = if snake_name.ends_with('s') { snake_name.clone() } else { format!("{}s", snake_name) };
191
192 let template = format!(
193r#"use rustbasic_core::{{Schema, SchemaManager, MigrationTrait, DbErr}};
194use rustbasic_core::async_trait;
195
196pub struct Migration;
197
198#[async_trait]
199impl MigrationTrait for Migration {{
200 fn name(&self) -> &str {{
201 "{mod_name}"
202 }}
203
204 async fn up<'a>(&self, manager: &'a SchemaManager<'a>) -> Result<(), DbErr> {{
205 Schema::create(manager, "{table_name}", |table| {{
206 table.id();
207 // table.string("title").not_null();
208 table.timestamps();
209 }}).await
210 }}
211
212 async fn down<'a>(&self, manager: &'a SchemaManager<'a>) -> Result<(), DbErr> {{
213 Schema::drop(manager, "{table_name}").await
214 }}
215}}
216"#, table_name = table_name, mod_name = mod_name);
217
218 fs::write(&file_path, template).expect("Gagal membuat file migration");
219 println!("{} {}", "โ
Migration Rust dibuat:".green(), file_path.cyan());
220
221 update_migration_mod_rs(&mod_name);
222}
223
224pub fn make_rust_migration_add(column: &str, table: &str) {
225 let col_snake = to_snake_case(column);
226 let table_snake = to_snake_case(table);
227 let name = format!("add_{}_to_{}", col_snake, table_snake);
228 let timestamp = Local::now().format("%Y%m%d_%H%M%S").to_string();
229 let mod_name = format!("m{}_{}", timestamp, name);
230 let file_path = format!("database/migrations/{}.rs", mod_name);
231
232 let template = format!(
233r#"use rustbasic_core::{{Schema, SchemaManager, MigrationTrait, DbErr}};
234use rustbasic_core::async_trait;
235
236pub struct Migration;
237
238#[async_trait]
239impl MigrationTrait for Migration {{
240 fn name(&self) -> &str {{
241 "{mod_name}"
242 }}
243
244 async fn up<'a>(&self, manager: &'a SchemaManager<'a>) -> Result<(), DbErr> {{
245 Schema::table(manager, "{table_snake}", |table| {{
246 table.string("{col_snake}").nullable();
247 }}).await
248 }}
249
250 async fn down<'a>(&self, manager: &'a SchemaManager<'a>) -> Result<(), DbErr> {{
251 Schema::table(manager, "{table_snake}", |table| {{
252 table.drop_column("{col_snake}");
253 }}).await
254 }}
255}}
256"#, table_snake = table_snake, col_snake = col_snake, mod_name = mod_name);
257
258 fs::write(&file_path, template).expect("Gagal membuat file migration");
259 println!("{} {}", "โ
Migration Add dibuat:".green(), file_path.cyan());
260
261 update_migration_mod_rs(&mod_name);
262}
263
264pub fn update_migration_mod_rs(mod_name: &str) {
265 let mod_path = "database/migrations/mod.rs";
266 let mut content = String::new();
267 if let Ok(mut file) = fs::File::open(mod_path) {
268 file.read_to_string(&mut content).ok();
269 }
270
271 if !content.contains(&format!("pub mod {};", mod_name)) {
273 if !content.ends_with('\n') {
274 content.push('\n');
275 }
276 content.push_str(&format!("pub mod {};\n", mod_name));
277 }
278
279 let search_patterns = [
281 "fn migrations() -> Vec<Box<dyn MigrationTrait>> {",
282 "fn migrations()->Vec<Box<dyn MigrationTrait>>{",
283 ];
284 for pattern in &search_patterns {
285 if let Some(_pos) = content.find(pattern) {
286 let insert_pos = content.find(" ]").unwrap_or(content.len());
287 content.insert_str(insert_pos, &format!(" Box::new({}::Migration),\n", mod_name));
288 break;
289 }
290 }
291
292 fs::write(mod_path, content).expect("Gagal memperbarui database/migrations/mod.rs");
293 println!("{} {}", "๐".blue(), "database/migrations/mod.rs diperbarui.".dimmed());
294}
295
296pub fn make_seeder(name: &str) {
297 let pascal_name = to_pascal_case(name).replace("Seeder", "");
298 let snake_name = to_snake_case(&pascal_name);
299 let class_name = format!("{}Seeder", pascal_name);
300 let file_name = format!("{}_seeder.rs", snake_name);
301 let file_path = format!("database/seeders/{}", file_name);
302
303 if std::path::Path::new(&file_path).exists() {
304 println!("{} {} {}", "โ ๏ธ Seeder".yellow(), file_path.cyan(), "sudah ada.".yellow());
305 return;
306 }
307
308 let template = format!(
309r#"use rustbasic_core::seeder;
310use rustbasic_core::colored::Colorize;
311// use crate::app::models::{pascal_name}; // Sesuaikan dengan nama model/struct Entity Anda
312
313seeder! {{
314 {class_name},
315 run(_db) {{
316 println!(" {{}} Sedang memproses {class_name}...", "โณ".blue());
317
318 // Contoh Penggunaan:
319 /*
320 {pascal_name}::create(_db, rustbasic_core::serde_json::json!({{
321 "name": "Example Data",
322 }})).await?;
323 */
324
325 Ok(())
326 }}
327}}
328"#, class_name = class_name, pascal_name = pascal_name);
329
330 fs::write(&file_path, template).expect("Gagal membuat file seeder");
331 println!("{} {}", "โ
Seeder dibuat:".green(), file_path.cyan());
332
333 update_seeder_mod_rs(&class_name, &file_name.replace(".rs", ""));
334}
335
336pub fn update_seeder_mod_rs(class_name: &str, mod_name: &str) {
337 let db_mod_path = "database/seeders/mod.rs";
339 let mut db_content = fs::read_to_string(db_mod_path).expect("Gagal membaca seeders/mod.rs");
340 let mod_line = format!("pub mod {};", mod_name);
341 if !db_content.contains(&mod_line) {
342 db_content.push_str(&format!("{}\n", mod_line));
343 fs::write(db_mod_path, db_content).ok();
344 }
345
346 let config_path = "src/app/seeder.rs";
348 let mut config_content = fs::read_to_string(config_path).expect("Gagal membaca src/app/seeder.rs");
349 let search_pattern = "let seeders: Vec<Box<dyn SeederTrait>> = vec![";
350 if let Some(pos) = config_content.find(search_pattern) {
351 let insert_pos = pos + search_pattern.len();
352 config_content.insert_str(insert_pos, &format!("\n Box::new(seeders::{}::{}),", mod_name, class_name));
353 fs::write(config_path, config_content).ok();
354 }
355
356 println!("{} {}", "๐".blue(), "Pengaturan seeder diperbarui.".dimmed());
357}
358
359pub fn make_test(name: &str, is_unit: bool) {
360 let pascal_name = to_pascal_case(name).replace("Test", "");
361 let snake_name = to_snake_case(&pascal_name);
362 let prefix = if is_unit { "unit" } else { "feature" };
363 let file_name = format!("{}_{}_test.rs", prefix, snake_name);
364 let file_path = format!("tests/{}", file_name);
365
366 if !std::path::Path::new("tests").exists() {
367 fs::create_dir_all("tests").expect("Gagal membuat folder tests");
368 }
369
370 if std::path::Path::new(&file_path).exists() {
371 println!("{} {} {}", "โ ๏ธ Test".yellow(), file_path.cyan(), "sudah ada.".yellow());
372 return;
373 }
374
375 let template = if is_unit {
376 format!(
377r#"/* ---------------------------------------------------------
378 * ๐งช UNIT TEST: {pascal_name} (tests/{file_name})
379 * --------------------------------------------------------- */
380
381#[test]
382fn test_{snake_name}_logic() {{
383 // Contoh asersi sederhana
384 let expected = 42;
385 let actual = 40 + 2;
386
387 assert_eq!(expected, actual, "Fungsi kalkulasi tidak sesuai");
388}}
389"#, pascal_name = pascal_name, file_name = file_name, snake_name = snake_name)
390 } else {
391 format!(
392r#"/* ---------------------------------------------------------
393 * ๐งช FEATURE TEST: {pascal_name} (tests/{file_name})
394 * --------------------------------------------------------- */
395
396use rustbasic_core::testing::TestClient;
397use rustbasic_core::Config;
398
399#[tokio::test]
400async fn test_{snake_name}_page() {{
401 // 1. Muat konfigurasi
402 let cfg = Config::load();
403
404 // 2. Bangun router aplikasi
405 let router = rustbasic::routes::build_router();
406
407 // 3. Setup TestClient in-memory
408 let client = TestClient::new(cfg, router).await;
409
410 // 4. Kirim request ke endpoint (misalnya '/')
411 let response = client.get("/").await;
412
413 // 5. Asersi response status & konten
414 response.assert_status(200);
415}}
416"#, pascal_name = pascal_name, file_name = file_name, snake_name = snake_name)
417 };
418
419 fs::write(&file_path, template).expect("Gagal membuat file test");
420 println!("{} {}", "โ
Test dibuat:".green(), file_path.cyan());
421}
422
423pub fn make_observer(name: &str, model_name: Option<&str>) {
424 let pascal_name = to_pascal_case(name).replace("Observer", "");
425 let snake_name = to_snake_case(&pascal_name);
426 let class_name = format!("{}Observer", pascal_name);
427 let file_name = format!("{}_observer.rs", snake_name);
428
429 let dir_path = "src/app/observers";
431 fs::create_dir_all(dir_path).expect("Gagal membuat folder observers");
432
433 let file_path = format!("{}/{}", dir_path, file_name);
434
435 if std::path::Path::new(&file_path).exists() {
436 println!("{} {} {}", "โ ๏ธ Observer".yellow(), file_path.cyan(), "sudah ada.".yellow());
437 return;
438 }
439
440 let model_pascal = model_name.map(to_pascal_case).unwrap_or_else(|| pascal_name.clone());
441 let mut model_snake = to_snake_case(&model_pascal);
442 if !std::path::Path::new(&format!("src/app/models/{}.rs", model_snake)).exists()
443 && std::path::Path::new(&format!("src/app/models/{}s.rs", model_snake)).exists() {
444 model_snake = format!("{}s", model_snake);
445 }
446
447 let template = format!(
448r#"/* ---------------------------------------------------------
449 * ๐ LABEL: {class_name} (observers/{file_name})
450 * --------------------------------------------------------- */
451
452use crate::app::models::{model_snake}::Model as {model_pascal};
453use rustbasic_core::serde_json::Value;
454
455pub trait {class_name} {{
456 fn creating(data: &mut Value);
457 fn created(model: &{model_pascal});
458 fn updating(data: &mut Value);
459 fn updated(model: &{model_pascal});
460 fn deleting(id: i32);
461 fn deleted(id: i32);
462}}
463
464pub struct {class_name}Impl;
465
466impl {class_name} for {class_name}Impl {{
467 fn creating(_data: &mut Value) {{
468 // Lakukan sesuatu sebelum data disimpan ke database (Before Create)
469 }}
470
471 fn created(_model: &{model_pascal}) {{
472 // Lakukan sesuatu setelah data berhasil disimpan ke database (After Create)
473 }}
474
475 fn updating(_data: &mut Value) {{
476 // Lakukan sesuatu sebelum data diupdate di database (Before Update)
477 }}
478
479 fn updated(_model: &{model_pascal}) {{
480 // Lakukan sesuatu setelah data berhasil diupdate di database (After Update)
481 }}
482
483 fn deleting(_id: i32) {{
484 // Lakukan sesuatu sebelum data dihapus dari database (Before Delete)
485 }}
486
487 fn deleted(_id: i32) {{
488 // Lakukan sesuatu setelah data berhasil dihapus dari database (After Delete)
489 }}
490}}
491"#, class_name = class_name, file_name = file_name, model_pascal = model_pascal, model_snake = model_snake);
492
493 fs::write(&file_path, template).expect("Gagal membuat file observer");
494 println!("{} {}", "โ
Observer dibuat:".green(), file_path.cyan());
495
496 update_observer_mod_rs(&snake_name);
497}
498
499pub fn update_observer_mod_rs(mod_name: &str) {
500 let mod_path = "src/app/observers/mod.rs";
501 let mut content = String::new();
502 if let Ok(mut file) = fs::File::open(mod_path) {
503 file.read_to_string(&mut content).ok();
504 }
505
506 let line = format!("pub mod {}_observer;", mod_name);
507 if !content.contains(&line) {
508 let mut file = OpenOptions::new()
509 .create(true)
510 .append(true)
511 .open(mod_path)
512 .expect("Gagal membuka observers/mod.rs");
513 writeln!(file, "{}", line).ok();
514 println!("{} {}", "๐".blue(), "observers/mod.rs diperbarui.".dimmed());
515 }
516
517 let app_mod_path = "src/app/mod.rs";
519 if let Ok(content) = fs::read_to_string(app_mod_path)
520 && !content.contains("pub mod observers;") {
521 let mut file = OpenOptions::new()
522 .append(true)
523 .open(app_mod_path)
524 .expect("Gagal membuka app/mod.rs");
525 writeln!(file, "pub mod observers;").ok();
526 println!("{} {}", "๐".blue(), "app/mod.rs diperbarui.".dimmed());
527 }
528}
529
530pub fn make_service(name: &str) {
531 let pascal_name = to_pascal_case(name).replace("Service", "");
532 let snake_name = to_snake_case(&pascal_name);
533 let class_name = format!("{}Service", pascal_name);
534 let file_name = format!("{}_service.rs", snake_name);
535
536 let dir_path = "src/app/services";
538 fs::create_dir_all(dir_path).expect("Gagal membuat folder services");
539
540 let file_path = format!("{}/{}", dir_path, file_name);
541
542 if std::path::Path::new(&file_path).exists() {
543 println!("{} {} {}", "โ ๏ธ Service".yellow(), file_path.cyan(), "sudah ada.".yellow());
544 return;
545 }
546
547 let template = format!(
548r#"/* ---------------------------------------------------------
549 * ๐ LABEL: {class_name} (services/{file_name})
550 * --------------------------------------------------------- */
551
552use rustbasic_core::sql::AnyPool;
553
554pub struct {class_name} {{
555 _db: AnyPool,
556}}
557
558impl {class_name} {{
559 pub fn new(db: AnyPool) -> Self {{
560 Self {{ _db: db }}
561 }}
562
563 // Tambahkan fungsi logika bisnis Anda di sini
564}}
565"#, class_name = class_name, file_name = file_name);
566
567 fs::write(&file_path, template).expect("Gagal membuat file service");
568 println!("{} {}", "โ
Service dibuat:".green(), file_path.cyan());
569
570 update_service_mod_rs(&snake_name);
571}
572
573pub fn update_service_mod_rs(mod_name: &str) {
574 let mod_path = "src/app/services/mod.rs";
575 let mut content = String::new();
576 if let Ok(mut file) = fs::File::open(mod_path) {
577 file.read_to_string(&mut content).ok();
578 }
579
580 let line = format!("pub mod {}_service;", mod_name);
581 if !content.contains(&line) {
582 let mut file = OpenOptions::new()
583 .create(true)
584 .append(true)
585 .open(mod_path)
586 .expect("Gagal membuka services/mod.rs");
587 writeln!(file, "{}", line).ok();
588 println!("{} {}", "๐".blue(), "services/mod.rs diperbarui.".dimmed());
589 }
590
591 let app_mod_path = "src/app/mod.rs";
593 if let Ok(content) = fs::read_to_string(app_mod_path)
594 && !content.contains("pub mod services;") {
595 let mut file = OpenOptions::new()
596 .append(true)
597 .open(app_mod_path)
598 .expect("Gagal membuka app/mod.rs");
599 writeln!(file, "pub mod services;").ok();
600 println!("{} {}", "๐".blue(), "app/mod.rs diperbarui.".dimmed());
601 }
602}
603
604