@@ -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;\n mod migrations;\n \n fn 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) ]
766950mod tests {
767951 use super :: * ;
0 commit comments