use std::path::{Path, PathBuf};
use syn::Type;
use crate::metadata::StructMetadata;
#[derive(Debug, Clone)]
pub enum LookupError {
InvalidTypePath,
MissingManifestDir,
NotFound {
struct_name: String,
searched: Vec<PathBuf>,
},
BareNotFound { struct_name: String },
}
impl LookupError {
pub fn to_syn_error(&self, span: &impl quote::ToTokens) -> syn::Error {
match self {
Self::InvalidTypePath => syn::Error::new_spanned(
span,
"schema_type! source must be a type path like `Model` or `crate::models::user::Model`",
),
Self::MissingManifestDir => syn::Error::new_spanned(
span,
"schema_type! source type not found: CARGO_MANIFEST_DIR is not set",
),
Self::NotFound {
struct_name,
searched,
} => syn::Error::new_spanned(
span,
format!(
"schema_type! struct `{struct_name}` not found. Searched: {}",
render_paths(searched)
),
),
Self::BareNotFound { struct_name } => syn::Error::new_spanned(
span,
format!(
"struct `{struct_name}` not found in this file; use a qualified path like `crate::models::<module>::{struct_name}`"
),
),
}
}
}
fn render_paths(paths: &[PathBuf]) -> String {
if paths.is_empty() {
"<none>".to_string()
} else {
paths
.iter()
.map(|path| path.display().to_string())
.collect::<Vec<_>>()
.join(", ")
}
}
#[inline]
pub(super) 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")),
]
}
#[cfg(test)]
pub fn find_struct_from_path(
ty: &Type,
schema_name_hint: Option<&str>,
) -> Option<(StructMetadata, Vec<String>)> {
find_struct_from_path_detailed(ty, schema_name_hint).ok()
}
pub fn find_struct_from_path_detailed(
ty: &Type,
_schema_name_hint: Option<&str>,
) -> Result<(StructMetadata, Vec<String>), LookupError> {
let manifest_dir = crate::schema_macro::file_cache::get_manifest_dir()
.ok_or(LookupError::MissingManifestDir)?;
let src_dir = Path::new(&manifest_dir).join("src");
let Type::Path(type_path) = ty else {
return Err(LookupError::InvalidTypePath);
};
let segments: Vec<String> = type_path
.path
.segments
.iter()
.map(|s| s.ident.to_string())
.collect();
if segments.is_empty() {
return Err(LookupError::InvalidTypePath);
}
let struct_name = segments.last().ok_or(LookupError::InvalidTypePath)?.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_bare_struct_in_call_site(&src_dir, &struct_name);
}
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 let Some(definition) =
crate::schema_macro::file_cache::get_struct_definition(&file_path, &struct_name)
{
return Ok((
StructMetadata::new_model(struct_name, definition),
type_module_path,
));
}
}
Err(LookupError::NotFound {
struct_name,
searched: candidate_file_paths(&src_dir, &module_segments)
.into_iter()
.collect(),
})
}
fn find_bare_struct_in_call_site(
src_dir: &Path,
struct_name: &str,
) -> Result<(StructMetadata, Vec<String>), LookupError> {
proc_macro2::Span::call_site().local_file().map_or_else(
|| {
Err(LookupError::BareNotFound {
struct_name: struct_name.to_string(),
})
},
|file_path| find_bare_struct_in_file(&file_path, struct_name, src_dir),
)
}
fn find_bare_struct_in_file(
file_path: &Path,
struct_name: &str,
src_dir: &Path,
) -> Result<(StructMetadata, Vec<String>), LookupError> {
let Some(definition) =
crate::schema_macro::file_cache::get_struct_definition(file_path, struct_name)
else {
return Err(LookupError::BareNotFound {
struct_name: struct_name.to_string(),
});
};
Ok((
StructMetadata::new_model(struct_name.to_string(), definition),
file_path_to_module_path(file_path, src_dir),
))
}
fn collect_rs_entry(
entry: &std::fs::DirEntry,
file_type: std::fs::FileType,
files: &mut Vec<PathBuf>,
) {
let path = entry.path();
if file_type.is_dir() {
collect_rs_files_recursive(&path, files);
} else if file_type.is_file() && path.extension().is_some_and(|ext| ext == "rs") {
files.push(path);
}
}
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() {
if let Ok(file_type) = entry.file_type() {
collect_rs_entry(&entry, file_type, files);
}
}
}
pub fn file_path_to_module_path(file_path: &Path, src_dir: &Path) -> Vec<String> {
let relative = file_path
.strip_prefix(src_dir)
.ok()
.map(std::path::Path::to_path_buf)
.or_else(|| relative_path_after_src(file_path))
.unwrap_or_default();
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
}
fn relative_path_after_src(file_path: &Path) -> Option<PathBuf> {
let mut seen_src = false;
let mut relative = PathBuf::new();
for component in file_path.components() {
let std::path::Component::Normal(os_str) = component else {
continue;
};
if seen_src {
relative.push(os_str);
} else if os_str == "src" {
seen_src = true;
}
}
seen_src.then_some(relative)
}
pub fn find_struct_from_schema_path(path_str: &str) -> Option<StructMetadata> {
let manifest_dir = crate::schema_macro::file_cache::get_manifest_dir()?;
let src_dir = Path::new(&manifest_dir).join("src");
let segments: Vec<&str> = path_str
.split("::")
.map(str::trim)
.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 let Some(definition) =
crate::schema_macro::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_model_from_schema_path(schema_path_str: &str) -> Option<StructMetadata> {
let manifest_dir = crate::schema_macro::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 let Some(definition) =
crate::schema_macro::file_cache::get_struct_definition(&file_path, "Model")
{
return Some(StructMetadata::new_model("Model".to_string(), definition));
}
}
None
}
#[cfg(test)]
mod tests;