Skip to content

Commit 30edaaf

Browse files
authored
Merge pull request #128 from indianaPoly/main
fix: use crate:: absolute paths for SeaORM cross-directory relations
2 parents 5b61c66 + 56d663c commit 30edaaf

4 files changed

Lines changed: 210 additions & 17 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"changes":{"crates/vespertide/Cargo.toml":"Patch","crates/vespertide-naming/Cargo.toml":"Patch","crates/vespertide-core/Cargo.toml":"Patch","crates/vespertide-cli/Cargo.toml":"Patch","crates/vespertide-planner/Cargo.toml":"Patch","crates/vespertide-config/Cargo.toml":"Patch","crates/vespertide-query/Cargo.toml":"Patch","crates/vespertide-macro/Cargo.toml":"Patch","crates/vespertide-loader/Cargo.toml":"Patch","crates/vespertide-exporter/Cargo.toml":"Patch"},"note":"Use create:: paths for SeaORM cross-directory relations","date":"2026-03-31T10:03:40.393673Z"}

Cargo.lock

Lines changed: 10 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/vespertide-cli/src/commands/export.rs

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::collections::HashMap;
12
use std::path::{Path, PathBuf};
23

34
use anyhow::{Context, Result};
@@ -61,6 +62,19 @@ pub async fn cmd_export(orm: OrmArg, export_dir: Option<PathBuf>) -> Result<()>
6162
// Extract all tables for schema context (used for FK chain resolution)
6263
let all_tables: Vec<TableDef> = normalized_models.iter().map(|(t, _)| t.clone()).collect();
6364

65+
// Build module path mappings for SeaORM cross-directory relation resolution.
66+
// Maps table_name -> module path segments (e.g., "admin" -> ["admin", "admin"])
67+
let module_paths: HashMap<String, Vec<String>> = normalized_models
68+
.iter()
69+
.map(|(table, rel_path)| {
70+
let segments = rel_path_to_module_segments(rel_path);
71+
(table.name.clone(), segments)
72+
})
73+
.collect();
74+
75+
// Derive crate:: prefix from export directory (e.g., "src/models" -> "crate::models")
76+
let crate_prefix = export_dir_to_crate_prefix(&target_root);
77+
6478
// Create SeaORM exporter with config if needed
6579
let seaorm_exporter = SeaOrmExporterWithConfig::new(config.seaorm(), config.prefix());
6680

@@ -70,7 +84,12 @@ pub async fn cmd_export(orm: OrmArg, export_dir: Option<PathBuf>) -> Result<()>
7084
.map(|(table, rel_path)| {
7185
let code = match orm_kind {
7286
Orm::SeaOrm => seaorm_exporter
73-
.render_entity_with_schema(table, &all_tables)
87+
.render_entity_with_schema_and_paths(
88+
table,
89+
&all_tables,
90+
&module_paths,
91+
&crate_prefix,
92+
)
7493
.map_err(|e| anyhow::anyhow!(e)),
7594
_ => render_entity_with_schema(orm_kind, table, &all_tables)
7695
.map_err(|e| anyhow::anyhow!(e)),
@@ -117,6 +136,54 @@ pub async fn cmd_export(orm: OrmArg, export_dir: Option<PathBuf>) -> Result<()>
117136
Ok(())
118137
}
119138

139+
/// Derive `crate::` prefix from the export directory path.
140+
///
141+
/// For example: `src/models` → `crate::models`, `src/db/entities` → `crate::db::entities`.
142+
/// If the path doesn't start with `src/`, returns empty string (fallback to `super::` behavior).
143+
fn export_dir_to_crate_prefix(export_dir: &Path) -> String {
144+
let normalized = export_dir.to_string_lossy().replace('\\', "/");
145+
let stripped = normalized.strip_prefix("./").unwrap_or(&normalized);
146+
147+
if let Some(after_src) = stripped.strip_prefix("src/") {
148+
let module_path = after_src.trim_end_matches('/').replace('/', "::");
149+
format!("crate::{module_path}")
150+
} else {
151+
String::new()
152+
}
153+
}
154+
155+
/// Convert a relative model file path to Rust module path segments.
156+
///
157+
/// For example: `admin/admin.json` → `["admin", "admin"]`
158+
/// `estimate/estimate_checker.vespertide.json` → `["estimate", "estimate_checker"]`
159+
fn rel_path_to_module_segments(rel_path: &Path) -> Vec<String> {
160+
let mut segments = Vec::new();
161+
162+
// Add directory components
163+
if let Some(parent) = rel_path.parent() {
164+
for component in parent.components() {
165+
if let std::path::Component::Normal(name) = component
166+
&& let Some(s) = name.to_str()
167+
{
168+
segments.push(sanitize_filename(s).to_string());
169+
}
170+
}
171+
}
172+
173+
// Add file stem (strip extensions and .vespertide suffix)
174+
if let Some(file_name) = rel_path.file_name().and_then(|n| n.to_str()) {
175+
let (stem, _) = if let Some(dot_idx) = file_name.rfind('.') {
176+
file_name.split_at(dot_idx)
177+
} else {
178+
(file_name, "")
179+
};
180+
let stem = stem.strip_suffix(".vespertide").unwrap_or(stem);
181+
segments.push(sanitize_filename(stem).to_string());
182+
}
183+
184+
segments
185+
}
186+
120187
fn resolve_export_dir(export_dir: Option<PathBuf>, config: &VespertideConfig) -> PathBuf {
121188
if let Some(dir) = export_dir {
122189
return dir;

crates/vespertide-exporter/src/seaorm/mod.rs

Lines changed: 131 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::collections::HashSet;
1+
use std::collections::{HashMap, HashSet};
22

33
use crate::orm::OrmExporter;
44
use 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+
1041
pub 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)]
12461371
mod helper_tests {
12471372
use super::*;

0 commit comments

Comments
 (0)