Skip to main content

rustbasic_cli/
scaffolding.rs

1use std::fs::{self, OpenOptions};
2use std::io::{Read, Write};
3use chrono::Local;
4use 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    // Tidak ada lagi pub use Entity karena model! macro sudah menggenerate type alias
175    
176    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    // Tambahkan mod declaration
272    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    // Tambahkan ke list migrations (pattern baru MigratorTrait)
280    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    // 1. Update database/seeders/mod.rs (mod declaration)
338    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    // 2. Update src/app/seeder.rs (registration)
347    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    // Ensure src/app/observers/ directory exists
430    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    // Register observers module in src/app/mod.rs if not already present
518    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    // Ensure src/app/services/ directory exists
537    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::sqlx::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    // Register services module in src/app/mod.rs if not already present
592    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