1- use std:: collections:: HashSet ;
1+ use std:: collections:: { HashMap , HashSet } ;
22
33use crate :: orm:: OrmExporter ;
44use vespertide_config:: SeaOrmConfig ;
@@ -7,6 +7,37 @@ use vespertide_core::{
77 TableDef ,
88} ;
99
10+ /// Build an absolute `crate::` module path for the target table.
11+ ///
12+ /// `crate_prefix` is derived from the export directory (e.g., `"src/models"` → `"crate::models"`).
13+ /// `to_module` is the module path segments of the target table (e.g., `["admin", "admin"]`).
14+ ///
15+ /// Returns a path like `crate::models::admin::admin`.
16+ fn absolute_module_path ( crate_prefix : & str , to_module : & [ String ] ) -> String {
17+ let mut path = crate_prefix. to_string ( ) ;
18+ for seg in to_module {
19+ path. push_str ( "::" ) ;
20+ path. push_str ( seg) ;
21+ }
22+ path
23+ }
24+
25+ /// Look up the module path for a table name from the module_paths map.
26+ /// Uses `crate::` absolute paths when crate_prefix and module_paths are available.
27+ /// Falls back to `super::{table_name}` when no mapping exists.
28+ fn resolve_entity_module_path (
29+ target_table : & str ,
30+ module_paths : & HashMap < String , Vec < String > > ,
31+ crate_prefix : & str ,
32+ ) -> String {
33+ if !crate_prefix. is_empty ( )
34+ && let Some ( to) = module_paths. get ( target_table)
35+ {
36+ return absolute_module_path ( crate_prefix, to) ;
37+ }
38+ format ! ( "super::{target_table}" )
39+ }
40+
1041pub struct SeaOrmExporter ;
1142
1243/// SeaORM exporter with configuration support.
@@ -55,6 +86,25 @@ impl<'a> SeaOrmExporterWithConfig<'a> {
5586 self . prefix ,
5687 ) )
5788 }
89+
90+ /// Render entity with schema context and module path mappings for correct
91+ /// cross-directory relation paths (e.g., `super::super::admin::admin::Entity`).
92+ pub fn render_entity_with_schema_and_paths (
93+ & self ,
94+ table : & TableDef ,
95+ schema : & [ TableDef ] ,
96+ module_paths : & HashMap < String , Vec < String > > ,
97+ crate_prefix : & str ,
98+ ) -> Result < String , String > {
99+ Ok ( render_entity_with_config_and_paths (
100+ table,
101+ schema,
102+ self . config ,
103+ self . prefix ,
104+ module_paths,
105+ crate_prefix,
106+ ) )
107+ }
58108}
59109
60110/// Render a single table into SeaORM entity code.
@@ -76,10 +126,24 @@ pub fn render_entity_with_config(
76126 schema : & [ TableDef ] ,
77127 config : & SeaOrmConfig ,
78128 prefix : & str ,
129+ ) -> String {
130+ render_entity_with_config_and_paths ( table, schema, config, prefix, & HashMap :: new ( ) , "" )
131+ }
132+
133+ /// Render a single table into SeaORM entity code with schema context, configuration,
134+ /// and module path mappings for correct cross-directory relation paths.
135+ pub fn render_entity_with_config_and_paths (
136+ table : & TableDef ,
137+ schema : & [ TableDef ] ,
138+ config : & SeaOrmConfig ,
139+ prefix : & str ,
140+ module_paths : & HashMap < String , Vec < String > > ,
141+ crate_prefix : & str ,
79142) -> String {
80143 let primary_keys = primary_key_columns ( table) ;
81144 let composite_pk = primary_keys. len ( ) > 1 ;
82- let relation_fields = relation_field_defs_with_schema ( table, schema) ;
145+ let relation_fields =
146+ relation_field_defs_with_schema ( table, schema, module_paths, crate_prefix) ;
83147
84148 // Build sets of columns with single-column unique constraints and indexes
85149 let unique_columns = single_column_unique_set ( & table. constraints ) ;
@@ -439,7 +503,12 @@ fn resolve_fk_target<'a>(
439503 ( ref_table, ref_columns. to_vec ( ) )
440504}
441505
442- fn relation_field_defs_with_schema ( table : & TableDef , schema : & [ TableDef ] ) -> Vec < String > {
506+ fn relation_field_defs_with_schema (
507+ table : & TableDef ,
508+ schema : & [ TableDef ] ,
509+ module_paths : & HashMap < String , Vec < String > > ,
510+ crate_prefix : & str ,
511+ ) -> Vec < String > {
443512 let mut out = Vec :: new ( ) ;
444513 let mut used = HashSet :: new ( ) ;
445514
@@ -550,8 +619,10 @@ fn relation_field_defs_with_schema(table: &TableDef, schema: &[TableDef]) -> Vec
550619 } ;
551620
552621 out. push ( attr) ;
622+ let entity_path =
623+ resolve_entity_module_path ( resolved_table, module_paths, crate_prefix) ;
553624 out. push ( format ! (
554- " pub {field_name}: HasOne<super::{resolved_table }::Entity>,"
625+ " pub {field_name}: HasOne<{entity_path }::Entity>,"
555626 ) ) ;
556627 }
557628 }
@@ -563,6 +634,8 @@ fn relation_field_defs_with_schema(table: &TableDef, schema: &[TableDef]) -> Vec
563634 & mut used,
564635 & entity_count,
565636 & mut used_relation_enums,
637+ module_paths,
638+ crate_prefix,
566639 ) ;
567640 out. extend ( reverse_relations) ;
568641
@@ -748,6 +821,8 @@ fn reverse_relation_field_defs(
748821 used : & mut HashSet < String > ,
749822 entity_count : & std:: collections:: HashMap < String , usize > ,
750823 used_relation_enums : & mut HashSet < String > ,
824+ module_paths : & HashMap < String , Vec < String > > ,
825+ crate_prefix : & str ,
751826) -> Vec < String > {
752827 // First pass: collect all reverse relations
753828 let mut relations: Vec < ReverseRelation > = Vec :: new ( ) ;
@@ -902,9 +977,10 @@ fn reverse_relation_field_defs(
902977 } ;
903978
904979 out. push ( attr) ;
980+ let entity_path =
981+ resolve_entity_module_path ( & rel. target_entity , module_paths, crate_prefix) ;
905982 out. push ( format ! (
906- " pub {field_name}: {rust_type}<super::{}::Entity>," ,
907- rel. target_entity
983+ " pub {field_name}: {rust_type}<{entity_path}::Entity>,"
908984 ) ) ;
909985 }
910986
@@ -1242,6 +1318,55 @@ fn to_snake_case(s: &str) -> String {
12421318 result
12431319}
12441320
1321+ #[ cfg( test) ]
1322+ mod module_path_tests {
1323+ use super :: * ;
1324+
1325+ #[ test]
1326+ fn absolute_module_path_builds_correct_path ( ) {
1327+ let result = absolute_module_path ( "crate::models" , & [ "admin" . into ( ) , "admin" . into ( ) ] ) ;
1328+ assert_eq ! ( result, "crate::models::admin::admin" ) ;
1329+ }
1330+
1331+ #[ test]
1332+ fn absolute_module_path_single_segment ( ) {
1333+ let result = absolute_module_path ( "crate::models" , & [ "user" . into ( ) ] ) ;
1334+ assert_eq ! ( result, "crate::models::user" ) ;
1335+ }
1336+
1337+ #[ test]
1338+ fn absolute_module_path_deep_nesting ( ) {
1339+ let result = absolute_module_path (
1340+ "crate::db::entities" ,
1341+ & [ "company" . into ( ) , "division" . into ( ) , "department" . into ( ) ] ,
1342+ ) ;
1343+ assert_eq ! ( result, "crate::db::entities::company::division::department" ) ;
1344+ }
1345+
1346+ #[ test]
1347+ fn resolve_entity_module_path_with_crate_prefix ( ) {
1348+ let mut module_paths = HashMap :: new ( ) ;
1349+ module_paths. insert ( "admin" . into ( ) , vec ! [ "admin" . into( ) , "admin" . into( ) ] ) ;
1350+ let result = resolve_entity_module_path ( "admin" , & module_paths, "crate::models" ) ;
1351+ assert_eq ! ( result, "crate::models::admin::admin" ) ;
1352+ }
1353+
1354+ #[ test]
1355+ fn resolve_entity_module_path_fallback_when_no_mapping ( ) {
1356+ let module_paths = HashMap :: new ( ) ;
1357+ let result = resolve_entity_module_path ( "user" , & module_paths, "crate::models" ) ;
1358+ assert_eq ! ( result, "super::user" ) ;
1359+ }
1360+
1361+ #[ test]
1362+ fn resolve_entity_module_path_fallback_when_empty_prefix ( ) {
1363+ let mut module_paths = HashMap :: new ( ) ;
1364+ module_paths. insert ( "admin" . into ( ) , vec ! [ "admin" . into( ) , "admin" . into( ) ] ) ;
1365+ let result = resolve_entity_module_path ( "admin" , & module_paths, "" ) ;
1366+ assert_eq ! ( result, "super::admin" ) ;
1367+ }
1368+ }
1369+
12451370#[ cfg( test) ]
12461371mod helper_tests {
12471372 use super :: * ;
0 commit comments