Skip to main content

rustbasic_cli/
scaffolding.rs

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    // Tidak ada lagi pub use Entity karena model! macro sudah menggenerate type alias
183    
184    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    // Tambahkan mod declaration
280    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    // Tambahkan ke list migrations (pattern baru MigratorTrait)
288    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    // 1. Update database/seeders/mod.rs (mod declaration)
346    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    // 2. Update src/app/seeder.rs (registration)
355    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    // Ensure src/app/observers/ directory exists
438    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    // Register observers module in src/app/mod.rs if not already present
526    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    // Ensure src/app/services/ directory exists
545    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    // Register services module in src/app/mod.rs if not already present
600    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