Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changepacks/changepack_log_PR7xcm5PHokMNm_YBIdNP.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"changes":{"crates/vespera_core/Cargo.toml":"Patch","crates/vespera_macro/Cargo.toml":"Patch","crates/vespera/Cargo.toml":"Patch"},"note":"Fix enum issue","date":"2026-02-05T17:24:37.031358300Z"}
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 27 additions & 3 deletions crates/vespera_macro/src/schema_macro/inline_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ use super::{
circular::detect_circular_fields,
file_lookup::find_model_from_schema_path,
seaorm::{RelationFieldInfo, convert_type_with_chrono},
type_utils::{is_seaorm_relation_type, snake_to_pascal_case},
type_utils::{
extract_module_path_from_schema_path, is_seaorm_relation_type, snake_to_pascal_case,
},
};
use crate::parser::{extract_rename_all, extract_skip};

Expand Down Expand Up @@ -69,6 +71,16 @@ pub fn generate_inline_relation_type_from_def(
// Parse the model struct
let parsed_model: syn::ItemStruct = syn::parse_str(model_def).ok()?;

// IMPORTANT: Use the TARGET model's module path for type resolution, not the parent's.
// This ensures enum types like `AuthProvider` are resolved to `crate::models::user::AuthProvider`
// instead of incorrectly using the parent module path.
let target_module_path = extract_module_path_from_schema_path(&rel_info.schema_path);
let effective_module_path = if target_module_path.is_empty() {
source_module_path
} else {
&target_module_path
};

// Detect circular fields
let circular_fields = detect_circular_fields("", source_module_path, model_def);

Expand Down Expand Up @@ -125,7 +137,8 @@ pub fn generate_inline_relation_type_from_def(

// Convert SeaORM datetime types to chrono equivalents
// This prevents users from needing to import sea_orm::prelude::DateTimeWithTimeZone
let converted_ty = convert_type_with_chrono(&field.ty, source_module_path);
// Use the target model's module path to correctly resolve enum types
let converted_ty = convert_type_with_chrono(&field.ty, effective_module_path);
fields.push(InlineField {
name: field_ident.clone(),
ty: converted_ty,
Expand Down Expand Up @@ -180,6 +193,16 @@ pub fn generate_inline_relation_type_no_relations_from_def(
// Parse the model struct
let parsed_model: syn::ItemStruct = syn::parse_str(model_def).ok()?;

// IMPORTANT: Use the TARGET model's module path for type resolution, not the parent's.
// This ensures enum types like `StoryStatus` are resolved to `crate::models::story::StoryStatus`
// instead of incorrectly using the parent module path.
let target_module_path = extract_module_path_from_schema_path(&rel_info.schema_path);
let effective_module_path = if target_module_path.is_empty() {
source_module_path
} else {
&target_module_path
};

// Get rename_all from model (or default to camelCase)
let rename_all =
extract_rename_all(&parsed_model.attrs).unwrap_or_else(|| "camelCase".to_string());
Expand Down Expand Up @@ -221,7 +244,8 @@ pub fn generate_inline_relation_type_no_relations_from_def(

// Convert SeaORM datetime types to chrono equivalents
// This prevents users from needing to import sea_orm::prelude::DateTimeWithTimeZone
let converted_ty = convert_type_with_chrono(&field.ty, source_module_path);
// Use the target model's module path to correctly resolve enum types
let converted_ty = convert_type_with_chrono(&field.ty, effective_module_path);
fields.push(InlineField {
name: field_ident.clone(),
ty: converted_ty,
Expand Down
66 changes: 66 additions & 0 deletions crates/vespera_macro/src/schema_macro/type_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ pub fn is_primitive_or_known_type(name: &str) -> bool {
| "DateTimeWithTimeZone"
| "DateTimeUtc"
| "DateTimeLocal"
| "Date" // SeaORM re-export of chrono::NaiveDate
| "Time" // SeaORM re-export of chrono::NaiveTime
// UUID
| "Uuid"
// Serde JSON
Expand Down Expand Up @@ -175,6 +177,30 @@ pub fn resolve_type_to_absolute_path(ty: &Type, source_module_path: &[String]) -
quote! { #(#path_idents)::* :: #type_ident #args }
}

/// Extract module path from a schema path TokenStream.
///
/// The schema_path is something like `crate::models::user::Schema`.
/// This returns `["crate", "models", "user"]` (excluding the final type name).
pub fn extract_module_path_from_schema_path(schema_path: &proc_macro2::TokenStream) -> Vec<String> {
let path_str = schema_path.to_string();
// Parse segments: "crate :: models :: user :: Schema" -> ["crate", "models", "user", "Schema"]
let segments: Vec<&str> = path_str
.split("::")
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.collect();

// Return all but the last segment (which is "Schema" or "Entity")
if segments.len() > 1 {
segments[..segments.len() - 1]
.iter()
.map(|s| s.to_string())
.collect()
} else {
vec![]
}
}

/// Extract the module path from a type (excluding the type name itself).
/// e.g., `crate::models::memo::Model` -> ["crate", "models", "memo"]
pub fn extract_module_path(ty: &Type) -> Vec<String> {
Expand Down Expand Up @@ -679,4 +705,44 @@ mod tests {
let ty: syn::Type = syn::parse_str("Vec<DateTime<Utc>>").unwrap();
assert!(is_primitive_like(&ty));
}

// Tests for extract_module_path_from_schema_path

#[rstest]
#[case("crate :: models :: user :: Schema", vec!["crate", "models", "user"])]
#[case("crate :: models :: nested :: deep :: Model", vec!["crate", "models", "nested", "deep"])]
#[case("super :: user :: Entity", vec!["super", "user"])]
#[case("super :: Model", vec!["super"])]
#[case("Schema", vec![])]
#[case("Model", vec![])]
fn test_extract_module_path_from_schema_path(
#[case] path_str: &str,
#[case] expected: Vec<&str>,
) {
let tokens: proc_macro2::TokenStream = path_str.parse().unwrap();
let result = extract_module_path_from_schema_path(&tokens);
let expected: Vec<String> = expected.into_iter().map(|s| s.to_string()).collect();
assert_eq!(result, expected);
}

#[test]
fn test_extract_module_path_from_schema_path_empty() {
let tokens = proc_macro2::TokenStream::new();
let result = extract_module_path_from_schema_path(&tokens);
assert!(result.is_empty());
}

#[test]
fn test_extract_module_path_from_schema_path_with_generics() {
// Even with generics, should extract module path correctly
let tokens: proc_macro2::TokenStream =
"crate :: models :: user :: Schema < T >".parse().unwrap();
let result = extract_module_path_from_schema_path(&tokens);
// Note: The current implementation splits by "::" which may include generics in last segment
// This test documents current behavior
assert!(!result.is_empty());
assert_eq!(result[0], "crate");
assert_eq!(result[1], "models");
assert_eq!(result[2], "user");
}
}