Skip to content

Commit 4119bb9

Browse files
feat: auto-wire mod declarations in main.rs after import/add resource (rapina-rs#365)
Co-authored-by: Antonio Souza <arfs.antonio@gmail.com>
1 parent 29fa5e3 commit 4119bb9

File tree

3 files changed

+200
-54
lines changed

3 files changed

+200
-54
lines changed

rapina-cli/src/commands/add.rs

Lines changed: 8 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::path::Path;
2+
13
use colored::Colorize;
24

35
use super::codegen::{self, FieldInfo};
@@ -92,59 +94,11 @@ fn validate_resource_name(name: &str) -> Result<(), String> {
9294
Ok(())
9395
}
9496

95-
fn print_next_steps(singular: &str, plural: &str, pascal: &str) {
97+
fn print_next_steps(pascal: &str) {
9698
println!();
9799
println!(" {}:", "Next steps".bright_yellow());
98100
println!();
99-
println!(
100-
" 1. Add the module declaration to {}:",
101-
"src/main.rs".cyan()
102-
);
103-
println!();
104-
println!(" mod {};", plural);
105-
println!(" mod entity;");
106-
println!(" mod migrations;");
107-
println!();
108-
println!(" 2. Register the routes in your {}:", "Router".cyan());
109-
println!();
110-
println!(
111-
" use {plural}::handlers::{{list_{plural}, get_{singular}, create_{singular}, update_{singular}, delete_{singular}}};",
112-
plural = plural,
113-
singular = singular,
114-
);
115-
println!();
116-
println!(" let router = Router::new()");
117-
println!(
118-
" .get(\"/{plural}\", list_{plural})",
119-
plural = plural
120-
);
121-
println!(
122-
" .get(\"/{plural}/:id\", get_{singular})",
123-
plural = plural,
124-
singular = singular,
125-
);
126-
println!(
127-
" .post(\"/{plural}\", create_{singular})",
128-
plural = plural,
129-
singular = singular,
130-
);
131-
println!(
132-
" .put(\"/{plural}/:id\", update_{singular})",
133-
plural = plural,
134-
singular = singular,
135-
);
136-
println!(
137-
" .delete(\"/{plural}/:id\", delete_{singular});",
138-
plural = plural,
139-
singular = singular,
140-
);
141-
println!();
142-
println!(
143-
" 3. Enable the database feature in {}:",
144-
"Cargo.toml".cyan()
145-
);
146-
println!();
147-
println!(" rapina = {{ version = \"...\", features = [\"postgres\"] }}");
101+
println!(" 1. Run {} to verify", "cargo build".cyan());
148102
println!();
149103
println!(
150104
" Resource {} created successfully!",
@@ -184,7 +138,10 @@ pub fn resource(name: &str, field_args: &[String]) -> Result<(), String> {
184138
codegen::update_entity_file(pascal, &fields, None, None, false)?;
185139
codegen::create_migration_file(plural, pascal_plural, &fields, pk_type)?;
186140

187-
print_next_steps(singular, plural, pascal);
141+
if let Err(e) = codegen::wire_main_rs(&[plural.as_str()], Path::new(".")) {
142+
eprintln!(" {} Could not auto-wire main.rs: {}", "!".yellow(), e);
143+
}
144+
print_next_steps(pascal);
188145

189146
Ok(())
190147
}

rapina-cli/src/commands/codegen.rs

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -762,6 +762,190 @@ fn create_feature_module_in(
762762
Ok(())
763763
}
764764

765+
/// Inserts `mod <name>;` declarations into `src/main.rs` for any modules not
766+
/// already declared. Silently returns Ok if main.rs does not exist.
767+
pub(crate) fn wire_main_rs(modules: &[&str], project_root: &Path) -> Result<(), String> {
768+
let main_path = project_root.join("src").join("main.rs");
769+
if !main_path.exists() {
770+
return Ok(());
771+
}
772+
773+
let content =
774+
fs::read_to_string(&main_path).map_err(|e| format!("Failed to read main.rs: {e}"))?;
775+
776+
// Filter out modules already declared.
777+
let new_modules: Vec<&str> = modules
778+
.iter()
779+
.copied()
780+
.filter(|m| {
781+
!content.lines().any(|l| {
782+
l.trim_start().starts_with("mod ")
783+
&& l.trim_end().ends_with(';')
784+
&& l.contains(&format!("mod {m};"))
785+
})
786+
})
787+
.collect();
788+
789+
if new_modules.is_empty() {
790+
return Ok(());
791+
}
792+
793+
let lines: Vec<&str> = content.lines().collect();
794+
795+
// Find the index of the last `mod ...;` line.
796+
let last_mod_idx = lines
797+
.iter()
798+
.rposition(|l| l.trim_start().starts_with("mod ") && l.trim_end().ends_with(';'));
799+
800+
let insertion_line = match last_mod_idx {
801+
Some(idx) => idx + 1,
802+
None => {
803+
// No existing mod declarations — insert before `fn main` or `#[tokio::main]`.
804+
lines
805+
.iter()
806+
.position(|l| l.contains("fn main") || l.contains("#[tokio::main]"))
807+
.unwrap_or(lines.len())
808+
}
809+
};
810+
811+
let mut result = lines[..insertion_line].join("\n");
812+
if !result.ends_with('\n') {
813+
result.push('\n');
814+
}
815+
for m in &new_modules {
816+
result.push_str(&format!("mod {m};\n"));
817+
}
818+
if insertion_line < lines.len() {
819+
result.push_str(&lines[insertion_line..].join("\n"));
820+
if content.ends_with('\n') {
821+
result.push('\n');
822+
}
823+
}
824+
825+
fs::write(&main_path, &result).map_err(|e| format!("Failed to write main.rs: {e}"))?;
826+
827+
for m in &new_modules {
828+
println!(
829+
" {} Wired {} in {}",
830+
"✓".green(),
831+
format!("mod {m};").cyan(),
832+
"src/main.rs".cyan()
833+
);
834+
}
835+
836+
Ok(())
837+
}
838+
839+
#[cfg(test)]
840+
mod wire_tests {
841+
use super::*;
842+
use tempfile::TempDir;
843+
844+
fn write_main(dir: &TempDir, content: &str) -> std::path::PathBuf {
845+
let src = dir.path().join("src");
846+
std::fs::create_dir_all(&src).unwrap();
847+
let path = src.join("main.rs");
848+
std::fs::write(&path, content).unwrap();
849+
dir.path().to_path_buf()
850+
}
851+
852+
#[test]
853+
fn inserts_after_last_mod() {
854+
let dir = TempDir::new().unwrap();
855+
let root = write_main(
856+
&dir,
857+
"\
858+
use rapina::prelude::*;
859+
860+
mod entity;
861+
mod migrations;
862+
863+
#[tokio::main]
864+
async fn main() {}
865+
",
866+
);
867+
wire_main_rs(&["todos"], &root).unwrap();
868+
let content = std::fs::read_to_string(root.join("src/main.rs")).unwrap();
869+
assert!(content.contains("mod todos;"));
870+
let mod_pos = content.find("mod todos;").unwrap();
871+
let main_pos = content.find("#[tokio::main]").unwrap();
872+
assert!(mod_pos < main_pos);
873+
}
874+
875+
#[test]
876+
fn skips_duplicate() {
877+
let dir = TempDir::new().unwrap();
878+
let root = write_main(
879+
&dir,
880+
"\
881+
mod entity;
882+
mod todos;
883+
884+
fn main() {}
885+
",
886+
);
887+
wire_main_rs(&["todos"], &root).unwrap();
888+
let content = std::fs::read_to_string(root.join("src/main.rs")).unwrap();
889+
assert_eq!(content.matches("mod todos;").count(), 1);
890+
}
891+
892+
#[test]
893+
fn inserts_multiple_modules() {
894+
let dir = TempDir::new().unwrap();
895+
let root = write_main(
896+
&dir,
897+
"\
898+
mod entity;
899+
mod migrations;
900+
901+
fn main() {}
902+
",
903+
);
904+
wire_main_rs(&["users", "posts"], &root).unwrap();
905+
let content = std::fs::read_to_string(root.join("src/main.rs")).unwrap();
906+
assert!(content.contains("mod users;"));
907+
assert!(content.contains("mod posts;"));
908+
}
909+
910+
#[test]
911+
fn no_main_rs_is_silent() {
912+
let dir = TempDir::new().unwrap();
913+
let result = wire_main_rs(&["todos"], dir.path());
914+
assert!(result.is_ok());
915+
}
916+
917+
#[test]
918+
fn no_existing_mods_inserts_before_fn_main() {
919+
let dir = TempDir::new().unwrap();
920+
let root = write_main(
921+
&dir,
922+
"\
923+
use rapina::prelude::*;
924+
925+
fn main() {}
926+
",
927+
);
928+
wire_main_rs(&["todos"], &root).unwrap();
929+
let content = std::fs::read_to_string(root.join("src/main.rs")).unwrap();
930+
assert!(content.contains("mod todos;"));
931+
let mod_pos = content.find("mod todos;").unwrap();
932+
let main_pos = content.find("fn main()").unwrap();
933+
assert!(mod_pos < main_pos);
934+
}
935+
936+
#[test]
937+
fn no_double_blank_line() {
938+
let dir = TempDir::new().unwrap();
939+
let root = write_main(&dir, "mod entity;\nmod migrations;\n\nfn main() {}\n");
940+
wire_main_rs(&["todos"], &root).unwrap();
941+
let content = std::fs::read_to_string(root.join("src/main.rs")).unwrap();
942+
assert!(
943+
!content.contains("\n\n\n"),
944+
"triple newline found (double blank line)"
945+
);
946+
}
947+
}
948+
765949
#[cfg(test)]
766950
mod tests {
767951
use super::*;

rapina-cli/src/commands/import.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::collections::HashMap;
2+
use std::path::Path;
23

34
use colored::Colorize;
45

@@ -675,6 +676,12 @@ pub fn database(
675676
imported.push((table.name.clone(), pascal));
676677
}
677678

679+
// Auto-wire mod declarations into src/main.rs
680+
let plural_names: Vec<&str> = imported.iter().map(|(name, _)| name.as_str()).collect();
681+
if let Err(e) = codegen::wire_main_rs(&plural_names, Path::new(".")) {
682+
eprintln!(" {} Could not auto-wire main.rs: {}", "!".yellow(), e);
683+
}
684+
678685
// Summary
679686
println!();
680687
println!(
@@ -691,9 +698,7 @@ pub fn database(
691698
println!(" {}:", "Next steps".bright_yellow());
692699
println!();
693700
println!(" 1. Review generated files in {}", "src/".cyan());
694-
println!(" 2. Add module declarations to {}", "src/main.rs".cyan());
695-
println!(" 3. Register routes in your Router");
696-
println!(" 4. Run {} to verify", "cargo build".cyan());
701+
println!(" 2. Run {} to verify", "cargo build".cyan());
697702
println!();
698703

699704
Ok(())

0 commit comments

Comments
 (0)