use std::path::Path;
use syn::Type;
use crate::metadata::StructMetadata;
use std::path::PathBuf;
#[inline]
fn candidate_file_paths(src_dir: &Path, module_segments: &[&str]) -> [PathBuf; 2] {
let joined = module_segments.join("/");
[
src_dir.join(format!("{joined}.rs")),
src_dir.join(format!("{joined}/mod.rs")),
]
}
pub fn find_struct_from_path(
ty: &Type,
schema_name_hint: Option<&str>,
) -> Option<(StructMetadata, Vec<String>)> {
let manifest_dir = super::file_cache::get_manifest_dir()?;
let src_dir = Path::new(&manifest_dir).join("src");
let Type::Path(type_path) = ty else {
return None;
};
let segments: Vec<String> = type_path
.path
.segments
.iter()
.map(|s| s.ident.to_string())
.collect();
if segments.is_empty() {
return None;
}
let struct_name = segments.last()?.clone();
let module_segments: Vec<&str> = segments[..segments.len() - 1]
.iter()
.filter(|s| *s != "crate" && *s != "self" && *s != "super")
.map(std::string::String::as_str)
.collect();
if module_segments.is_empty() {
return find_struct_by_name_in_all_files(&src_dir, &struct_name, schema_name_hint);
}
let type_module_path: Vec<String> = segments[..segments.len() - 1].to_vec();
let file_paths = candidate_file_paths(&src_dir, &module_segments);
for file_path in file_paths {
if !file_path.exists() {
continue;
}
if let Some(definition) = super::file_cache::get_struct_definition(&file_path, &struct_name)
{
return Some((
StructMetadata::new_model(struct_name, definition),
type_module_path,
));
}
}
None
}
#[allow(clippy::too_many_lines)]
pub fn find_struct_by_name_in_all_files(
src_dir: &Path,
struct_name: &str,
schema_name_hint: Option<&str>,
) -> Option<(StructMetadata, Vec<String>)> {
let mut rs_files = super::file_cache::get_struct_candidates(src_dir, struct_name);
let prefix_normalized = schema_name_hint.map(derive_hint_prefix);
if let Some(prefix_normalized) = &prefix_normalized {
let (candidates, rest): (Vec<_>, Vec<_>) = rs_files.into_iter().partition(|path| {
path.file_stem()
.and_then(|s| s.to_str())
.is_some_and(|name| {
let norm = normalize_name(name);
norm == *prefix_normalized || norm.contains(prefix_normalized.as_str())
})
});
let mut found_in_candidates: Vec<(std::path::PathBuf, StructMetadata)> = Vec::new();
for file_path in &candidates {
if let Some(definition) =
super::file_cache::get_struct_definition(file_path, struct_name)
{
found_in_candidates.push((
file_path.clone(),
StructMetadata::new_model(struct_name.to_string(), definition),
));
}
}
if found_in_candidates.len() == 1 {
let (path, metadata) = found_in_candidates.remove(0);
let module_path = file_path_to_module_path(&path, src_dir);
return Some((metadata, module_path));
}
if found_in_candidates.len() > 1 {
let exact_match: Vec<_> = found_in_candidates
.iter()
.filter(|(path, _)| {
path.file_stem()
.and_then(|s| s.to_str())
.is_some_and(|name| normalize_name(name) == *prefix_normalized)
})
.collect();
if exact_match.len() == 1 {
let (path, metadata) = exact_match[0];
let module_path = file_path_to_module_path(path, src_dir);
return Some((metadata.clone(), module_path));
}
return None;
}
rs_files = rest;
}
let mut found_structs: Vec<(std::path::PathBuf, StructMetadata)> = Vec::new();
for file_path in rs_files {
if let Some(definition) = super::file_cache::get_struct_definition(&file_path, struct_name)
{
found_structs.push((
file_path.clone(),
StructMetadata::new_model(struct_name.to_string(), definition),
));
}
}
match found_structs.len() {
1 => {
let (path, metadata) = found_structs.remove(0);
let module_path = file_path_to_module_path(&path, src_dir);
Some((metadata, module_path))
}
_ => None,
}
}
fn derive_hint_prefix(hint: &str) -> String {
let hint_lower = hint.to_lowercase();
let prefix = hint_lower
.strip_suffix("schema")
.or_else(|| hint_lower.strip_suffix("response"))
.or_else(|| hint_lower.strip_suffix("request"))
.unwrap_or(&hint_lower);
normalize_name(prefix)
}
#[inline]
fn normalize_name(s: &str) -> String {
s.chars()
.filter(|&c| c != '_')
.map(|c| c.to_ascii_lowercase())
.collect()
}
pub fn collect_rs_files_recursive(dir: &Path, files: &mut Vec<std::path::PathBuf>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect_rs_files_recursive(&path, files);
} else if path.extension().is_some_and(|ext| ext == "rs") {
files.push(path);
}
}
}
pub fn file_path_to_module_path(file_path: &Path, src_dir: &Path) -> Vec<String> {
let Ok(relative) = file_path.strip_prefix(src_dir) else {
return vec!["crate".to_string()];
};
let mut segments = vec!["crate".to_string()];
for component in relative.components() {
if let std::path::Component::Normal(os_str) = component
&& let Some(s) = os_str.to_str()
{
if let Some(name) = s.strip_suffix(".rs") {
if name != "mod" && name != "lib" {
segments.push(name.to_string());
}
} else {
segments.push(s.to_string());
}
}
}
segments
}
pub fn find_struct_from_schema_path(path_str: &str) -> Option<StructMetadata> {
let manifest_dir = super::file_cache::get_manifest_dir()?;
let src_dir = Path::new(&manifest_dir).join("src");
let segments: Vec<&str> = path_str.split("::").filter(|s| !s.is_empty()).collect();
if segments.is_empty() {
return None;
}
let struct_name = segments.last()?.to_string();
let module_segments: Vec<&str> = segments[..segments.len() - 1]
.iter()
.filter(|s| **s != "crate" && **s != "self" && **s != "super")
.copied()
.collect();
if module_segments.is_empty() {
return None;
}
let file_paths = candidate_file_paths(&src_dir, &module_segments);
for file_path in file_paths {
if !file_path.exists() {
continue;
}
if let Some(definition) = super::file_cache::get_struct_definition(&file_path, &struct_name)
{
return Some(StructMetadata::new_model(struct_name, definition));
}
}
None
}
#[allow(clippy::too_many_lines)]
pub fn find_fk_column_from_target_entity(
target_schema_path: &str,
via_rel: &str,
) -> Option<String> {
use crate::schema_macro::seaorm::{extract_belongs_to_from_field, extract_relation_enum};
let manifest_dir = super::file_cache::get_manifest_dir()?;
let src_dir = Path::new(&manifest_dir).join("src");
let segments: Vec<&str> = target_schema_path
.split("::")
.map(str::trim)
.filter(|s| !s.is_empty() && *s != "Schema" && *s != "Entity")
.collect();
let module_segments: Vec<&str> = segments
.iter()
.filter(|s| **s != "crate" && **s != "self" && **s != "super")
.copied()
.collect();
if module_segments.is_empty() {
return None;
}
let file_paths = candidate_file_paths(&src_dir, &module_segments);
for file_path in file_paths {
if !file_path.exists() {
continue;
}
let Some(model_def) = super::file_cache::get_struct_definition(&file_path, "Model") else {
continue;
};
let Ok(model) = super::file_cache::parse_struct_cached(&model_def) else {
continue;
};
if let syn::Fields::Named(fields_named) = &model.fields {
for field in &fields_named.named {
let field_relation_enum = extract_relation_enum(&field.attrs);
if field_relation_enum.as_deref() == Some(via_rel) {
return extract_belongs_to_from_field(&field.attrs);
}
}
}
}
None
}
#[allow(clippy::too_many_lines)]
pub fn find_model_from_schema_path(schema_path_str: &str) -> Option<StructMetadata> {
let manifest_dir = super::file_cache::get_manifest_dir()?;
let src_dir = Path::new(&manifest_dir).join("src");
let segments: Vec<&str> = schema_path_str
.split("::")
.map(str::trim)
.filter(|s| !s.is_empty() && *s != "Schema")
.collect();
if segments.is_empty() {
return None;
}
let module_segments: Vec<&str> = segments
.iter()
.filter(|s| **s != "crate" && **s != "self" && **s != "super")
.copied()
.collect();
if module_segments.is_empty() {
return None;
}
let file_paths = candidate_file_paths(&src_dir, &module_segments);
for file_path in file_paths {
if !file_path.exists() {
continue;
}
if let Some(definition) = super::file_cache::get_struct_definition(&file_path, "Model") {
return Some(StructMetadata::new_model("Model".to_string(), definition));
}
}
None
}
#[cfg(test)]
mod tests {
use std::path::Path;
use serial_test::serial;
use tempfile::TempDir;
use super::*;
#[test]
fn test_file_path_to_module_path_simple() {
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path();
let file_path = src_dir.join("models").join("user.rs");
let result = file_path_to_module_path(&file_path, src_dir);
assert_eq!(result, vec!["crate", "models", "user"]);
}
#[test]
fn test_file_path_to_module_path_mod_rs() {
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path();
let file_path = src_dir.join("models").join("mod.rs");
let result = file_path_to_module_path(&file_path, src_dir);
assert_eq!(result, vec!["crate", "models"]);
}
#[test]
fn test_file_path_to_module_path_lib_rs() {
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path();
let file_path = src_dir.join("lib.rs");
let result = file_path_to_module_path(&file_path, src_dir);
assert_eq!(result, vec!["crate"]);
}
#[test]
fn test_file_path_to_module_path_not_under_src() {
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
let file_path = temp_dir.path().join("other").join("file.rs");
let result = file_path_to_module_path(&file_path, &src_dir);
assert_eq!(result, vec!["crate"]);
}
#[test]
fn test_collect_rs_files_recursive_empty_dir() {
let temp_dir = TempDir::new().unwrap();
let mut files = Vec::new();
collect_rs_files_recursive(temp_dir.path(), &mut files);
assert!(files.is_empty());
}
#[test]
fn test_collect_rs_files_recursive_nonexistent_dir() {
let mut files = Vec::new();
collect_rs_files_recursive(Path::new("/nonexistent/path"), &mut files);
assert!(files.is_empty());
}
#[test]
fn test_collect_rs_files_recursive_with_files() {
let temp_dir = TempDir::new().unwrap();
std::fs::write(temp_dir.path().join("main.rs"), "fn main() {}").unwrap();
std::fs::create_dir(temp_dir.path().join("models")).unwrap();
std::fs::write(
temp_dir.path().join("models").join("user.rs"),
"struct User;",
)
.unwrap();
std::fs::write(temp_dir.path().join("other.txt"), "not a rust file").unwrap();
let mut files = Vec::new();
collect_rs_files_recursive(temp_dir.path(), &mut files);
assert_eq!(files.len(), 2);
assert!(files.iter().all(|f| f.extension().unwrap() == "rs"));
}
#[test]
fn test_find_struct_from_path_non_path_type() {
use syn::Type;
let ty: Type = syn::parse_str("&str").unwrap();
let original = std::env::var("CARGO_MANIFEST_DIR").ok();
let temp_dir = TempDir::new().unwrap();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let result = find_struct_from_path(&ty, None);
unsafe {
if let Some(dir) = original {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
assert!(result.is_none(), "Non-path type should return None");
}
#[test]
fn test_find_struct_from_path_empty_segments() {
use syn::{Path, TypePath};
let empty_path = Path {
leading_colon: None,
segments: syn::punctuated::Punctuated::new(),
};
let ty = Type::Path(TypePath {
qself: None,
path: empty_path,
});
let original = std::env::var("CARGO_MANIFEST_DIR").ok();
let temp_dir = TempDir::new().unwrap();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let result = find_struct_from_path(&ty, None);
unsafe {
if let Some(dir) = original {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
assert!(result.is_none(), "Empty segments should return None");
}
#[test]
#[serial]
fn test_find_struct_from_path_file_with_non_matching_items() {
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
let models_dir = src_dir.join("models");
std::fs::create_dir_all(&models_dir).unwrap();
let content = r"
pub enum SomeEnum { A, B }
pub fn some_function() {}
pub const SOME_CONST: i32 = 42;
pub trait SomeTrait {}
pub struct NotTarget { pub x: i32 }
pub struct Target { pub id: i32 }
";
std::fs::write(models_dir.join("mixed.rs"), content).unwrap();
let original = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let ty: Type = syn::parse_str("crate::models::mixed::Target").unwrap();
let result = find_struct_from_path(&ty, None);
unsafe {
if let Some(dir) = original {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
assert!(result.is_some(), "Should find Target struct");
let (metadata, _) = result.unwrap();
assert!(metadata.definition.contains("Target"));
}
#[test]
#[serial]
fn test_find_struct_by_name_unreadable_file() {
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path();
std::fs::write(
src_dir.join("valid.rs"),
"pub struct Target { pub id: i32 }",
)
.unwrap();
let broken = src_dir.join("broken.rs");
let nonexistent = src_dir.join("nonexistent");
#[cfg(unix)]
let _ = std::os::unix::fs::symlink(&nonexistent, &broken);
#[cfg(windows)]
let _ = std::os::windows::fs::symlink_file(&nonexistent, &broken);
let result = find_struct_by_name_in_all_files(src_dir, "Target", None);
assert!(
result.is_some(),
"Should find Target, skipping broken symlink"
);
}
#[test]
#[serial]
fn test_find_struct_by_name_unparseable_file() {
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path();
std::fs::write(src_dir.join("broken.rs"), "this is not valid rust {{{{").unwrap();
std::fs::write(
src_dir.join("valid.rs"),
"pub struct Target { pub id: i32 }",
)
.unwrap();
let result = find_struct_by_name_in_all_files(src_dir, "Target", None);
assert!(
result.is_some(),
"Should find Target in valid file, skipping broken"
);
}
#[test]
#[serial]
fn test_find_struct_disambiguation_with_hint() {
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path();
std::fs::create_dir(src_dir.join("models")).unwrap();
std::fs::write(
src_dir.join("models").join("user.rs"),
"pub struct Model { pub id: i32, pub name: String }",
)
.unwrap();
std::fs::write(
src_dir.join("models").join("memo.rs"),
"pub struct Model { pub id: i32, pub title: String }",
)
.unwrap();
let result_no_hint = find_struct_by_name_in_all_files(src_dir, "Model", None);
assert!(
result_no_hint.is_none(),
"Without hint, multiple Models should be ambiguous"
);
let result_with_hint =
find_struct_by_name_in_all_files(src_dir, "Model", Some("UserSchema"));
assert!(
result_with_hint.is_some(),
"With UserSchema hint, should find user.rs"
);
let (metadata, module_path) = result_with_hint.unwrap();
assert!(
metadata.definition.contains("name"),
"Should be user Model with name field"
);
assert!(
module_path.contains(&"user".to_string()),
"Module path should contain 'user'"
);
let result_memo = find_struct_by_name_in_all_files(src_dir, "Model", Some("MemoSchema"));
assert!(
result_memo.is_some(),
"With MemoSchema hint, should find memo.rs"
);
let (metadata_memo, _) = result_memo.unwrap();
assert!(
metadata_memo.definition.contains("title"),
"Should be memo Model with title field"
);
}
#[test]
#[serial]
fn test_find_struct_disambiguation_with_response_suffix() {
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path();
std::fs::create_dir(src_dir.join("models")).unwrap();
std::fs::write(
src_dir.join("models").join("user.rs"),
"pub struct Data { pub id: i32 }",
)
.unwrap();
std::fs::write(
src_dir.join("models").join("item.rs"),
"pub struct Data { pub name: String }",
)
.unwrap();
let result = find_struct_by_name_in_all_files(src_dir, "Data", Some("UserResponse"));
assert!(
result.is_some(),
"With UserResponse hint, should find user.rs"
);
}
#[test]
#[serial]
fn test_find_struct_disambiguation_with_request_suffix() {
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path();
std::fs::create_dir(src_dir.join("models")).unwrap();
std::fs::write(
src_dir.join("models").join("user.rs"),
"pub struct Input { pub id: i32 }",
)
.unwrap();
std::fs::write(
src_dir.join("models").join("item.rs"),
"pub struct Input { pub name: String }",
)
.unwrap();
let result = find_struct_by_name_in_all_files(src_dir, "Input", Some("UserRequest"));
assert!(
result.is_some(),
"With UserRequest hint, should find user.rs"
);
}
#[test]
#[serial]
fn test_find_struct_disambiguation_still_ambiguous() {
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path();
std::fs::create_dir(src_dir.join("models")).unwrap();
std::fs::write(
src_dir.join("models").join("user_admin.rs"),
"pub struct Model { pub id: i32 }",
)
.unwrap();
std::fs::write(
src_dir.join("models").join("user_regular.rs"),
"pub struct Model { pub name: String }",
)
.unwrap();
let result = find_struct_by_name_in_all_files(src_dir, "Model", Some("UserSchema"));
assert!(
result.is_none(),
"Multiple files matching hint should still be ambiguous"
);
}
#[test]
#[serial]
fn test_find_struct_disambiguation_snake_case_filename() {
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path();
std::fs::create_dir(src_dir.join("models")).unwrap();
std::fs::write(
src_dir.join("models").join("admin_user.rs"),
"pub struct Model { pub id: i32, pub role: String }",
)
.unwrap();
std::fs::write(
src_dir.join("models").join("regular_user.rs"),
"pub struct Model { pub id: i32, pub name: String }",
)
.unwrap();
let result = find_struct_by_name_in_all_files(src_dir, "Model", Some("AdminUserSchema"));
assert!(
result.is_some(),
"AdminUserSchema hint should match admin_user.rs"
);
let (metadata, module_path) = result.unwrap();
assert!(
metadata.definition.contains("role"),
"Should be admin_user Model with role field"
);
assert!(
module_path.contains(&"admin_user".to_string()),
"Module path should contain 'admin_user'"
);
let result_regular =
find_struct_by_name_in_all_files(src_dir, "Model", Some("RegularUserSchema"));
assert!(
result_regular.is_some(),
"RegularUserSchema hint should match regular_user.rs"
);
let (metadata_regular, _) = result_regular.unwrap();
assert!(
metadata_regular.definition.contains("name"),
"Should be regular_user Model with name field"
);
}
#[test]
fn test_find_struct_from_schema_path_empty_string() {
let original = std::env::var("CARGO_MANIFEST_DIR").ok();
let temp_dir = TempDir::new().unwrap();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let result = find_struct_from_schema_path("");
unsafe {
if let Some(dir) = original {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
assert!(result.is_none(), "Empty path should return None");
}
#[test]
fn test_find_struct_from_schema_path_no_module() {
let original = std::env::var("CARGO_MANIFEST_DIR").ok();
let temp_dir = TempDir::new().unwrap();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let result = find_struct_from_schema_path("crate::Schema");
unsafe {
if let Some(dir) = original {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
assert!(result.is_none(), "Path with no module should return None");
}
#[test]
#[serial]
fn test_find_struct_from_schema_path_with_non_struct_items() {
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
let models_dir = src_dir.join("models");
std::fs::create_dir_all(&models_dir).unwrap();
let content = r"
pub enum NotStruct { A, B }
pub fn not_struct() {}
pub struct Target { pub id: i32 }
pub const NOT_STRUCT: i32 = 1;
";
std::fs::write(models_dir.join("item.rs"), content).unwrap();
let original = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let result = find_struct_from_schema_path("crate::models::item::Target");
unsafe {
if let Some(dir) = original {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
assert!(result.is_some(), "Should find Target struct");
assert!(result.unwrap().definition.contains("Target"));
}
#[test]
fn test_find_model_from_schema_path_empty_after_filter() {
let original = std::env::var("CARGO_MANIFEST_DIR").ok();
let temp_dir = TempDir::new().unwrap();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let result = find_model_from_schema_path("Schema");
unsafe {
if let Some(dir) = original {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
assert!(result.is_none(), "Empty segments should return None");
}
#[test]
fn test_find_model_from_schema_path_no_module() {
let original = std::env::var("CARGO_MANIFEST_DIR").ok();
let temp_dir = TempDir::new().unwrap();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let result = find_model_from_schema_path("crate::Schema");
unsafe {
if let Some(dir) = original {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
assert!(result.is_none(), "No module segments should return None");
}
#[test]
#[serial]
fn test_find_model_from_schema_path_success() {
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
let models_dir = src_dir.join("models");
std::fs::create_dir_all(&models_dir).unwrap();
let content = "pub struct Model { pub id: i32, pub name: String }";
std::fs::write(models_dir.join("user.rs"), content).unwrap();
let original = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let result = find_model_from_schema_path("crate::models::user::Schema");
unsafe {
if let Some(dir) = original {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
assert!(result.is_some(), "Should find Model");
assert!(result.unwrap().definition.contains("Model"));
}
#[test]
#[serial]
fn test_find_struct_disambiguation_fallback_contains() {
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path();
std::fs::create_dir(src_dir.join("models")).unwrap();
std::fs::write(
src_dir.join("models").join("special_item.rs"),
"pub struct Model { pub special_field: i32 }",
)
.unwrap();
std::fs::write(
src_dir.join("models").join("regular.rs"),
"pub struct Model { pub regular_field: String }",
)
.unwrap();
let result = find_struct_by_name_in_all_files(src_dir, "Model", Some("SpecialSchema"));
assert!(
result.is_some(),
"SpecialSchema hint should match special_item.rs via contains fallback"
);
let (metadata, module_path) = result.unwrap();
assert!(
metadata.definition.contains("special_field"),
"Should be special_item Model with special_field"
);
assert!(
module_path.contains(&"special_item".to_string()),
"Module path should contain 'special_item'"
);
}
#[test]
#[serial]
fn test_find_fk_column_from_target_entity_success() {
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
let models_dir = src_dir.join("models");
std::fs::create_dir_all(&models_dir).unwrap();
let notification_model = r#"
pub struct Model {
pub id: i32,
pub message: String,
pub target_user_id: i32,
#[sea_orm(belongs_to = "super::user::Entity", from = "target_user_id", to = "id", relation_enum = "TargetUser")]
pub target_user: BelongsTo<super::user::Entity>,
}
"#;
std::fs::write(models_dir.join("notification.rs"), notification_model).unwrap();
let original = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let result =
find_fk_column_from_target_entity("crate::models::notification::Schema", "TargetUser");
unsafe {
if let Some(dir) = original {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
assert_eq!(
result,
Some("target_user_id".to_string()),
"Should find FK column 'target_user_id'"
);
}
#[test]
#[serial]
fn test_find_fk_column_from_target_entity_mod_rs() {
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
let models_dir = src_dir.join("models").join("notification");
std::fs::create_dir_all(&models_dir).unwrap();
let notification_model = r#"
pub struct Model {
pub id: i32,
pub sender_id: i32,
#[sea_orm(belongs_to = "super::super::user::Entity", from = "sender_id", to = "id", relation_enum = "Sender")]
pub sender: BelongsTo<super::super::user::Entity>,
}
"#;
std::fs::write(models_dir.join("mod.rs"), notification_model).unwrap();
let original = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let result =
find_fk_column_from_target_entity("crate::models::notification::Schema", "Sender");
unsafe {
if let Some(dir) = original {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
assert_eq!(
result,
Some("sender_id".to_string()),
"Should find FK column from mod.rs"
);
}
#[test]
#[serial]
fn test_find_fk_column_from_target_entity_empty_module_segments() {
let temp_dir = TempDir::new().unwrap();
let original = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let result = find_fk_column_from_target_entity("crate::Schema", "SomeRelation");
unsafe {
if let Some(dir) = original {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
assert!(result.is_none(), "Empty module segments should return None");
}
#[test]
#[serial]
fn test_find_fk_column_from_target_entity_file_not_found() {
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
std::fs::create_dir_all(&src_dir).unwrap();
let original = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let result =
find_fk_column_from_target_entity("crate::models::nonexistent::Schema", "SomeRelation");
unsafe {
if let Some(dir) = original {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
assert!(result.is_none(), "Non-existent file should return None");
}
#[test]
#[serial]
fn test_find_fk_column_from_target_entity_unparseable_file() {
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
let models_dir = src_dir.join("models");
std::fs::create_dir_all(&models_dir).unwrap();
std::fs::write(models_dir.join("broken.rs"), "this is not valid rust {{{{").unwrap();
let original = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let result =
find_fk_column_from_target_entity("crate::models::broken::Schema", "SomeRelation");
unsafe {
if let Some(dir) = original {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
assert!(result.is_none(), "Unparseable file should return None");
}
#[test]
#[serial]
fn test_find_fk_column_from_target_entity_no_model_struct() {
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
let models_dir = src_dir.join("models");
std::fs::create_dir_all(&models_dir).unwrap();
let content = r"
pub struct SomethingElse {
pub id: i32,
}
pub enum Status { Active, Inactive }
";
std::fs::write(models_dir.join("nomodel.rs"), content).unwrap();
let original = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let result =
find_fk_column_from_target_entity("crate::models::nomodel::Schema", "SomeRelation");
unsafe {
if let Some(dir) = original {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
assert!(
result.is_none(),
"File without Model struct should return None"
);
}
#[test]
#[serial]
fn test_find_fk_column_from_target_entity_no_matching_relation_enum() {
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
let models_dir = src_dir.join("models");
std::fs::create_dir_all(&models_dir).unwrap();
let model = r#"
pub struct Model {
pub id: i32,
pub user_id: i32,
#[sea_orm(belongs_to = "super::user::Entity", from = "user_id", to = "id", relation_enum = "Author")]
pub user: BelongsTo<super::user::Entity>,
}
"#;
std::fs::write(models_dir.join("comment.rs"), model).unwrap();
let original = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let result =
find_fk_column_from_target_entity("crate::models::comment::Schema", "TargetUser");
unsafe {
if let Some(dir) = original {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
assert!(
result.is_none(),
"Non-matching relation_enum should return None"
);
}
#[test]
#[serial]
fn test_find_fk_column_from_target_entity_tuple_struct() {
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
let models_dir = src_dir.join("models");
std::fs::create_dir_all(&models_dir).unwrap();
let model = "pub struct Model(i32, String);";
std::fs::write(models_dir.join("tuple.rs"), model).unwrap();
let original = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let result =
find_fk_column_from_target_entity("crate::models::tuple::Schema", "SomeRelation");
unsafe {
if let Some(dir) = original {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
assert!(result.is_none(), "Tuple struct Model should return None");
}
#[test]
#[serial]
fn test_find_fk_column_from_target_entity_field_no_from_attr() {
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
let models_dir = src_dir.join("models");
std::fs::create_dir_all(&models_dir).unwrap();
let model = r#"
pub struct Model {
pub id: i32,
pub user_id: i32,
#[sea_orm(belongs_to = "super::user::Entity", to = "id", relation_enum = "TargetUser")]
pub user: BelongsTo<super::user::Entity>,
}
"#;
std::fs::write(models_dir.join("nofrom.rs"), model).unwrap();
let original = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let result =
find_fk_column_from_target_entity("crate::models::nofrom::Schema", "TargetUser");
unsafe {
if let Some(dir) = original {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
assert!(
result.is_none(),
"Field without 'from' attribute should return None"
);
}
#[test]
#[serial]
fn test_find_struct_candidate_unparseable_file() {
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path();
std::fs::write(
src_dir.join("user.rs"),
"pub struct Model {{{{ broken syntax",
)
.unwrap();
std::fs::write(src_dir.join("valid.rs"), "pub struct Model { pub id: i32 }").unwrap();
let result = find_struct_by_name_in_all_files(src_dir, "Model", Some("UserSchema"));
assert!(
result.is_some(),
"Should find Model in valid.rs after skipping unparseable candidate user.rs"
);
}
#[test]
#[serial]
fn test_find_struct_exact_filename_disambiguation() {
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path();
std::fs::write(src_dir.join("user.rs"), "pub struct Model { pub id: i32 }").unwrap();
std::fs::write(
src_dir.join("user_extended.rs"),
"pub struct Model { pub name: String }",
)
.unwrap();
let result = find_struct_by_name_in_all_files(src_dir, "Model", Some("UserSchema"));
assert!(result.is_some(), "Should resolve via exact filename match");
let (metadata, _) = result.unwrap();
assert!(
metadata.definition.contains("id"),
"Should return user.rs Model (with id field)"
);
}
#[test]
#[serial]
fn test_find_struct_no_match_in_candidates_falls_to_rest() {
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path();
std::fs::write(
src_dir.join("user.rs"),
"pub struct Other { pub x: i32 } // Model ref",
)
.unwrap();
std::fs::write(src_dir.join("data.rs"), "pub struct Model { pub id: i32 }").unwrap();
let result = find_struct_by_name_in_all_files(src_dir, "Model", Some("UserSchema"));
assert!(
result.is_some(),
"Should find Model in data.rs after candidates had no match"
);
}
#[test]
#[serial]
fn test_find_struct_full_scan_unparseable_file() {
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path();
std::fs::write(
src_dir.join("user.rs"),
"pub struct Other { pub x: i32 } // Model",
)
.unwrap();
std::fs::write(src_dir.join("broken.rs"), "Model unparseable {{{{{").unwrap();
std::fs::write(src_dir.join("valid.rs"), "pub struct Model { pub id: i32 }").unwrap();
let result = find_struct_by_name_in_all_files(src_dir, "Model", Some("UserSchema"));
assert!(
result.is_some(),
"Should find Model in valid.rs after skipping unparseable broken.rs in rest"
);
}
#[test]
#[serial]
fn test_find_struct_from_path_qualified_module_path() {
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
let models_dir = src_dir.join("models");
std::fs::create_dir_all(&models_dir).unwrap();
std::fs::write(
models_dir.join("user.rs"),
"pub struct Model { pub id: i32, pub name: String }",
)
.unwrap();
let original = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let ty: syn::Type = syn::parse_str("crate::models::user::Model").unwrap();
let result = find_struct_from_path(&ty, None);
unsafe {
if let Some(dir) = original {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
assert!(
result.is_some(),
"Should find Model struct via qualified path"
);
let (metadata, module_path) = result.unwrap();
assert!(
metadata.definition.contains("Model"),
"Definition should contain Model"
);
assert_eq!(
module_path,
vec!["crate", "models", "user"],
"Module path should be inferred from type path"
);
}
#[test]
#[serial]
fn test_find_struct_from_path_mod_rs_variant() {
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
let models_dir = src_dir.join("models").join("user");
std::fs::create_dir_all(&models_dir).unwrap();
std::fs::write(
models_dir.join("mod.rs"),
"pub struct Model { pub id: i32, pub email: String }",
)
.unwrap();
let original = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let ty: syn::Type = syn::parse_str("crate::models::user::Model").unwrap();
let result = find_struct_from_path(&ty, None);
unsafe {
if let Some(dir) = original {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
assert!(result.is_some(), "Should find Model struct via mod.rs path");
let (metadata, _) = result.unwrap();
assert!(
metadata.definition.contains("email"),
"Should find the correct Model with email field"
);
}
#[test]
#[serial]
fn test_find_fk_column_parse_struct_cached_failure() {
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
let models_dir = src_dir.join("models");
std::fs::create_dir_all(&models_dir).unwrap();
let model_file = models_dir.join("item.rs");
std::fs::write(&model_file, "pub struct Model { pub id: i32 }").unwrap();
crate::schema_macro::file_cache::inject_struct_definition_for_test(
&model_file,
"Model",
"not valid rust {{ struct }}",
);
let original = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let result =
find_fk_column_from_target_entity("crate::models::item::Schema", "SomeRelation");
unsafe {
if let Some(dir) = original {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
assert!(
result.is_none(),
"Should return None when struct definition fails to parse"
);
}
}