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::axum::response::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::axum::{{
84    extract::Request,
85    middleware::Next,
86    response::Response,
87}};
88
89pub async fn {fn_name}(
90    req: Request,
91    next: Next,
92) -> Response {{
93    // Lakukan sesuatu sebelum request sampai ke controller
94    
95    let response = next.run(req).await;
96    
97    // Lakukan sesuatu setelah request selesai diproses
98    
99    response
100}}
101"#, label = name.to_uppercase(), file_name = file_name, fn_name = fn_name);
102
103    fs::write(&file_path, template).expect("Gagal membuat file middleware");
104    println!("{} {}", "✅ Middleware dibuat:".green(), file_path.cyan());
105
106    update_middleware_mod_rs(&snake_name);
107}
108
109pub fn update_middleware_mod_rs(mod_name: &str) {
110    let mod_path = "src/app/http/middleware/mod.rs";
111    let mut content = String::new();
112    if let Ok(mut file) = fs::File::open(mod_path) {
113        file.read_to_string(&mut content).ok();
114    }
115
116    let line = format!("pub mod {};", mod_name);
117    if content.contains(&line) {
118        return;
119    }
120
121    let mut file = OpenOptions::new()
122        .append(true)
123        .open(mod_path)
124        .expect("Gagal membuka middleware/mod.rs");
125
126    writeln!(file, "{}", line).ok();
127    println!("{} {}", "📝".blue(), "middleware/mod.rs diperbarui.".dimmed());
128}
129
130pub fn make_model(name: &str) {
131    let snake_name = to_snake_case(name);
132    let table_name = format!("{}s", snake_name);
133    let file_path = format!("src/app/models/{}.rs", snake_name);
134
135    if std::path::Path::new(&file_path).exists() {
136        println!("{} {} {}", "⚠️  Model".yellow(), file_path.cyan(), "sudah ada.".yellow());
137        return;
138    }
139
140    let template = format!(
141r#"use rustbasic_core::sea_orm::entity::prelude::*;
142use serde::{{Deserialize, Serialize}};
143
144#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
145#[sea_orm(table_name = "{}")]
146pub struct Model {{
147    #[sea_orm(primary_key)]
148    pub id: i32,
149    pub created_at: Option<DateTime>,
150    pub updated_at: Option<DateTime>,
151}}
152
153#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
154pub enum Relation {{}}
155
156impl ActiveModelBehavior for ActiveModel {{}}
157"#, table_name);
158
159    fs::write(&file_path, template).expect("Gagal membuat file model");
160    println!("{} {}", "✅ Model dibuat:".green(), file_path.cyan());
161
162    update_mod_rs(&to_pascal_case(name), &snake_name);
163}
164
165pub fn update_mod_rs(class_name: &str, snake_name: &str) {
166    let mod_path = "src/app/models/mod.rs";
167    let mut content = String::new();
168    if let Ok(mut file) = fs::File::open(mod_path) {
169        file.read_to_string(&mut content).ok();
170    }
171
172    let mod_line = format!("pub mod {};", snake_name);
173    if content.contains(&mod_line) {
174        return;
175    }
176
177    let mut file = OpenOptions::new()
178        .append(true)
179        .open(mod_path)
180        .expect("Gagal membuka models/mod.rs");
181
182    writeln!(file, "{}", mod_line).ok();
183    writeln!(file, "pub use {}::Entity as {};", snake_name, class_name).ok();
184    
185    println!("{} {}", "📝".blue(), "models/mod.rs diperbarui.".dimmed());
186}
187
188pub fn make_rust_migration(name: &str) {
189    let snake_name = to_snake_case(name);
190    let timestamp = Local::now().format("%Y%m%d_%H%M%S").to_string();
191    let mod_name = format!("m{}_{}", timestamp, snake_name);
192    let file_path = format!("database/migrations/{}.rs", mod_name);
193
194    if std::path::Path::new(&file_path).exists() {
195        println!("{} {} {}", "⚠️  Migration".yellow(), file_path.cyan(), "sudah ada.".yellow());
196        return;
197    }
198
199    let pascal_name = to_pascal_case(name);
200    let table_iden = if pascal_name.ends_with('s') { pascal_name } else { format!("{}s", pascal_name) };
201
202    let template = format!(
203r#"use sea_orm_migration::prelude::*;
204use async_trait::async_trait;
205
206#[derive(Iden)]
207enum {table_iden} {{
208    Table,
209    Id,
210    CreatedAt,
211    UpdatedAt,
212}}
213
214#[derive(Iden)]
215pub struct Migration;
216
217impl MigrationName for Migration {{
218    fn name(&self) -> &str {{
219        "{mod_name}"
220    }}
221}}
222
223#[async_trait]
224impl MigrationTrait for Migration {{
225    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {{
226        manager
227            .create_table(
228                Table::create()
229                    .table({table_iden}::Table)
230                    .if_not_exists()
231                    .col(
232                        ColumnDef::new({table_iden}::Id)
233                            .integer()
234                            .not_null()
235                            .auto_increment()
236                            .primary_key(),
237                    )
238                    .col(
239                        ColumnDef::new({table_iden}::CreatedAt)
240                            .date_time()
241                            .default(Expr::current_timestamp()),
242                    )
243                    .col(
244                        ColumnDef::new({table_iden}::UpdatedAt)
245                            .date_time()
246                            .default(Expr::current_timestamp()),
247                    )
248                    .to_owned(),
249            )
250            .await
251    }}
252
253    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {{
254        manager
255            .drop_table(Table::drop().table({table_iden}::Table).to_owned())
256            .await
257    }}
258}}
259"#, table_iden = table_iden, mod_name = mod_name);
260
261    fs::write(&file_path, template).expect("Gagal membuat file migration");
262    println!("{} {}", "✅ Migration Rust dibuat:".green(), file_path.cyan());
263
264    update_migration_mod_rs(&mod_name);
265}
266
267pub fn make_rust_migration_add(column: &str, table: &str) {
268    let col_snake = to_snake_case(column);
269    let table_snake = to_snake_case(table);
270    let name = format!("add_{}_to_{}", col_snake, table_snake);
271    let timestamp = Local::now().format("%Y%m%d_%H%M%S").to_string();
272    let mod_name = format!("m{}_{}", timestamp, name);
273    let file_path = format!("database/migrations/{}.rs", mod_name);
274
275    let col_pascal = to_pascal_case(column);
276    let table_pascal = to_pascal_case(table);
277
278    let template = format!(
279r#"use sea_orm_migration::prelude::*;
280use async_trait::async_trait;
281
282#[derive(Iden)]
283enum {table_pascal} {{
284    Table,
285    {col_pascal},
286}}
287
288#[derive(Iden)]
289pub struct Migration;
290
291impl MigrationName for Migration {{
292    fn name(&self) -> &str {{
293        "{mod_name}"
294    }}
295}}
296
297#[async_trait]
298impl MigrationTrait for Migration {{
299    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {{
300        manager
301            .alter_table(
302                Table::alter()
303                    .table({table_pascal}::Table)
304                    .add_column(ColumnDef::new({table_pascal}::{col_pascal}).string())
305                    .to_owned(),
306            )
307            .await
308    }}
309
310    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {{
311        manager
312            .alter_table(
313                Table::alter()
314                    .table({table_pascal}::Table)
315                    .drop_column({table_pascal}::{col_pascal})
316                    .to_owned(),
317            )
318            .await
319    }}
320}}
321"#, table_pascal = table_pascal, col_pascal = col_pascal, mod_name = mod_name);
322
323    fs::write(&file_path, template).expect("Gagal membuat file migration");
324    println!("{} {}", "✅ Migration Add dibuat:".green(), file_path.cyan());
325
326    update_migration_mod_rs(&mod_name);
327}
328
329pub fn update_migration_mod_rs(mod_name: &str) {
330    let mod_path = "database/migrations/mod.rs";
331    let mut content = String::new();
332    if let Ok(mut file) = fs::File::open(mod_path) {
333        file.read_to_string(&mut content).ok();
334    }
335
336    // Tambahkan mod declaration
337    if !content.contains(&format!("pub mod {};", mod_name)) {
338        if !content.ends_with('\n') {
339            content.push('\n');
340        }
341        content.push_str(&format!("pub mod {};\n", mod_name));
342    }
343
344    // Tambahkan ke list migrations
345    let search_pattern = "fn migrations() -> Vec<Box<dyn MigrationTrait>> {";
346    if let Some(_pos) = content.find(search_pattern) {
347        let insert_pos = content.find("        ]").unwrap_or(content.len());
348        content.insert_str(insert_pos, &format!("            Box::new({}::Migration),\n", mod_name));
349    }
350
351    fs::write(mod_path, content).expect("Gagal memperbarui database/migrations/mod.rs");
352    println!("{} {}", "📝".blue(), "database/migrations/mod.rs diperbarui.".dimmed());
353}
354
355pub fn make_seeder(name: &str) {
356    let pascal_name = to_pascal_case(name).replace("Seeder", "");
357    let snake_name = to_snake_case(&pascal_name);
358    let class_name = format!("{}Seeder", pascal_name);
359    let file_name = format!("{}_seeder.rs", snake_name);
360    let file_path = format!("database/seeders/{}", file_name);
361
362    if std::path::Path::new(&file_path).exists() {
363        println!("{} {} {}", "⚠️  Seeder".yellow(), file_path.cyan(), "sudah ada.".yellow());
364        return;
365    }
366
367    let template = format!(
368r#"#[allow(unused_imports)]
369use rustbasic_core::sea_orm::{{DatabaseConnection, Set, ActiveModelTrait}};
370use rustbasic_core::colored::Colorize;
371use rustbasic_core::seeder::SeederTrait;
372// use crate::app::models::{snake_name}; // Sesuaikan dengan model Anda
373
374pub struct {class_name};
375
376#[async_trait::async_trait]
377impl SeederTrait for {class_name} {{
378    async fn run(&self, _db: &DatabaseConnection) -> Result<(), rustbasic_core::sea_orm::DbErr> {{
379        println!("   {{}} Sedang memproses {class_name}...", "⏳".blue());
380        
381        // Contoh:
382        /*
383        let _ = {snake_name}::ActiveModel {{
384            name: Set("Example Data".to_owned()),
385            ..Default::default()
386        }}.insert(_db).await?;
387        */
388
389        Ok(())
390    }}
391}}
392"#, class_name = class_name, snake_name = snake_name);
393
394    fs::write(&file_path, template).expect("Gagal membuat file seeder");
395    println!("{} {}", "✅ Seeder dibuat:".green(), file_path.cyan());
396
397    update_seeder_mod_rs(&class_name, &file_name.replace(".rs", ""));
398}
399
400pub fn update_seeder_mod_rs(class_name: &str, mod_name: &str) {
401    // 1. Update database/seeders/mod.rs (mod declaration)
402    let db_mod_path = "database/seeders/mod.rs";
403    let mut db_content = fs::read_to_string(db_mod_path).expect("Gagal membaca seeders/mod.rs");
404    let mod_line = format!("pub mod {};", mod_name);
405    if !db_content.contains(&mod_line) {
406        db_content.push_str(&format!("{}\n", mod_line));
407        fs::write(db_mod_path, db_content).ok();
408    }
409
410    // 2. Update src/app/seeder.rs (registration)
411    let config_path = "src/app/seeder.rs";
412    let mut config_content = fs::read_to_string(config_path).expect("Gagal membaca src/app/seeder.rs");
413    let search_pattern = "let seeders: Vec<Box<dyn SeederTrait>> = vec![";
414    if let Some(pos) = config_content.find(search_pattern) {
415        let insert_pos = pos + search_pattern.len();
416        config_content.insert_str(insert_pos, &format!("\n        Box::new(seeders::{}::{}),", mod_name, class_name));
417        fs::write(config_path, config_content).ok();
418    }
419    
420    println!("{} {}", "📝".blue(), "Pengaturan seeder diperbarui.".dimmed());
421}