Skip to content

Commit 18f0179

Browse files
committed
Replace type issue
1 parent 51bcabb commit 18f0179

4 files changed

Lines changed: 161 additions & 12 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"changes":{"crates/vespera_core/Cargo.toml":"Patch","crates/vespera/Cargo.toml":"Patch","crates/vespera_macro/Cargo.toml":"Patch"},"note":"Support serde flatten, tagged, untagged","date":"2026-02-05T12:30:36.574038700Z"}

crates/vespera_macro/src/schema_macro/inline_types.rs

Lines changed: 152 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use quote::quote;
99
use super::{
1010
circular::detect_circular_fields,
1111
file_lookup::find_model_from_schema_path,
12-
seaorm::RelationFieldInfo,
12+
seaorm::{RelationFieldInfo, convert_type_with_chrono},
1313
type_utils::{capitalize_first, is_seaorm_relation_type},
1414
};
1515
use crate::parser::{extract_rename_all, extract_skip};
@@ -123,10 +123,12 @@ pub fn generate_inline_relation_type_from_def(
123123
.cloned()
124124
.collect();
125125

126-
let field_ty = &field.ty;
126+
// Convert SeaORM datetime types to chrono equivalents
127+
// This prevents users from needing to import sea_orm::prelude::DateTimeWithTimeZone
128+
let converted_ty = convert_type_with_chrono(&field.ty, source_module_path);
127129
fields.push(InlineField {
128130
name: field_ident.clone(),
129-
ty: quote!(#field_ty),
131+
ty: converted_ty,
130132
attrs: kept_attrs,
131133
});
132134
}
@@ -150,6 +152,7 @@ pub fn generate_inline_relation_type_from_def(
150152
pub fn generate_inline_relation_type_no_relations(
151153
parent_type_name: &syn::Ident,
152154
rel_info: &RelationFieldInfo,
155+
source_module_path: &[String],
153156
schema_name_override: Option<&str>,
154157
) -> Option<InlineRelationType> {
155158
// Find the target model definition
@@ -160,6 +163,7 @@ pub fn generate_inline_relation_type_no_relations(
160163
generate_inline_relation_type_no_relations_from_def(
161164
parent_type_name,
162165
rel_info,
166+
source_module_path,
163167
schema_name_override,
164168
model_def,
165169
)
@@ -169,6 +173,7 @@ pub fn generate_inline_relation_type_no_relations(
169173
pub fn generate_inline_relation_type_no_relations_from_def(
170174
parent_type_name: &syn::Ident,
171175
rel_info: &RelationFieldInfo,
176+
source_module_path: &[String],
172177
schema_name_override: Option<&str>,
173178
model_def: &str,
174179
) -> Option<InlineRelationType> {
@@ -214,10 +219,12 @@ pub fn generate_inline_relation_type_no_relations_from_def(
214219
.cloned()
215220
.collect();
216221

217-
let field_ty = &field.ty;
222+
// Convert SeaORM datetime types to chrono equivalents
223+
// This prevents users from needing to import sea_orm::prelude::DateTimeWithTimeZone
224+
let converted_ty = convert_type_with_chrono(&field.ty, source_module_path);
218225
fields.push(InlineField {
219226
name: field_ident.clone(),
220-
ty: quote!(#field_ty),
227+
ty: converted_ty,
221228
attrs: kept_attrs,
222229
});
223230
}
@@ -584,6 +591,7 @@ mod tests {
584591
let result = generate_inline_relation_type_no_relations_from_def(
585592
&parent_type_name,
586593
&rel_info,
594+
&[],
587595
None,
588596
model_def,
589597
);
@@ -626,6 +634,7 @@ mod tests {
626634
let result = generate_inline_relation_type_no_relations_from_def(
627635
&parent_type_name,
628636
&rel_info,
637+
&[],
629638
None,
630639
model_def,
631640
);
@@ -786,6 +795,7 @@ mod tests {
786795
let result = generate_inline_relation_type_no_relations_from_def(
787796
&parent_type_name,
788797
&rel_info,
798+
&[],
789799
Some("UserSchema"),
790800
model_def,
791801
);
@@ -906,7 +916,17 @@ pub struct Model {
906916
inline_type_info: None,
907917
};
908918

909-
let result = generate_inline_relation_type_no_relations(&parent_type_name, &rel_info, None);
919+
let source_module_path = vec![
920+
"crate".to_string(),
921+
"models".to_string(),
922+
"user".to_string(),
923+
];
924+
let result = generate_inline_relation_type_no_relations(
925+
&parent_type_name,
926+
&rel_info,
927+
&source_module_path,
928+
None,
929+
);
910930

911931
// Restore original CARGO_MANIFEST_DIR
912932
// SAFETY: This is a test that runs single-threaded
@@ -1001,7 +1021,8 @@ pub struct Model {
10011021
inline_type_info: None,
10021022
};
10031023

1004-
let result = generate_inline_relation_type_no_relations(&parent_type_name, &rel_info, None);
1024+
let result =
1025+
generate_inline_relation_type_no_relations(&parent_type_name, &rel_info, &[], None);
10051026

10061027
// Restore original CARGO_MANIFEST_DIR
10071028
// SAFETY: This is a test that runs single-threaded
@@ -1016,4 +1037,128 @@ pub struct Model {
10161037
// Should return None when file not found
10171038
assert!(result.is_none());
10181039
}
1040+
1041+
#[test]
1042+
fn test_generate_inline_relation_type_converts_datetime_types() {
1043+
// Test that DateTimeWithTimeZone is converted to vespera::chrono::DateTime<FixedOffset>
1044+
let parent_type_name = syn::Ident::new("MemoSchema", proc_macro2::Span::call_site());
1045+
let rel_info = RelationFieldInfo {
1046+
field_name: syn::Ident::new("user", proc_macro2::Span::call_site()),
1047+
relation_type: "BelongsTo".to_string(),
1048+
schema_path: quote!(super::user::Schema),
1049+
is_optional: false,
1050+
inline_type_info: None,
1051+
};
1052+
let source_module_path = vec![
1053+
"crate".to_string(),
1054+
"models".to_string(),
1055+
"memo".to_string(),
1056+
];
1057+
1058+
// Model with DateTimeWithTimeZone field AND circular reference
1059+
let model_def = r#"pub struct Model {
1060+
pub id: i32,
1061+
pub name: String,
1062+
pub created_at: DateTimeWithTimeZone,
1063+
pub memo: BelongsTo<memo::Entity>
1064+
}"#;
1065+
1066+
let result = generate_inline_relation_type_from_def(
1067+
&parent_type_name,
1068+
&rel_info,
1069+
&source_module_path,
1070+
None,
1071+
model_def,
1072+
);
1073+
assert!(result.is_some());
1074+
1075+
let inline_type = result.unwrap();
1076+
assert_eq!(inline_type.type_name.to_string(), "MemoSchema_User");
1077+
1078+
// Find created_at field and check its type was converted
1079+
let created_at_field = inline_type
1080+
.fields
1081+
.iter()
1082+
.find(|f| f.name == "created_at")
1083+
.expect("created_at field should exist");
1084+
1085+
let ty_str = created_at_field.ty.to_string();
1086+
// Should be converted to vespera::chrono::DateTime<FixedOffset>
1087+
assert!(
1088+
ty_str.contains("vespera :: chrono :: DateTime"),
1089+
"DateTimeWithTimeZone should be converted to vespera::chrono::DateTime, got: {}",
1090+
ty_str
1091+
);
1092+
assert!(
1093+
ty_str.contains("FixedOffset"),
1094+
"Should contain FixedOffset, got: {}",
1095+
ty_str
1096+
);
1097+
}
1098+
1099+
#[test]
1100+
fn test_generate_inline_relation_type_no_relations_converts_datetime_types() {
1101+
// Test that DateTimeWithTimeZone is converted in no_relations variant too
1102+
let parent_type_name = syn::Ident::new("UserSchema", proc_macro2::Span::call_site());
1103+
let rel_info = RelationFieldInfo {
1104+
field_name: syn::Ident::new("memos", proc_macro2::Span::call_site()),
1105+
relation_type: "HasMany".to_string(),
1106+
schema_path: quote!(super::memo::Schema),
1107+
is_optional: false,
1108+
inline_type_info: None,
1109+
};
1110+
1111+
// Model with DateTimeWithTimeZone field
1112+
let model_def = r#"pub struct Model {
1113+
pub id: i32,
1114+
pub title: String,
1115+
pub created_at: DateTimeWithTimeZone,
1116+
pub updated_at: Option<DateTimeWithTimeZone>,
1117+
pub user: BelongsTo<user::Entity>
1118+
}"#;
1119+
1120+
let result = generate_inline_relation_type_no_relations_from_def(
1121+
&parent_type_name,
1122+
&rel_info,
1123+
&[],
1124+
None,
1125+
model_def,
1126+
);
1127+
assert!(result.is_some());
1128+
1129+
let inline_type = result.unwrap();
1130+
1131+
// Find created_at field and check its type was converted
1132+
let created_at_field = inline_type
1133+
.fields
1134+
.iter()
1135+
.find(|f| f.name == "created_at")
1136+
.expect("created_at field should exist");
1137+
1138+
let ty_str = created_at_field.ty.to_string();
1139+
assert!(
1140+
ty_str.contains("vespera :: chrono :: DateTime"),
1141+
"DateTimeWithTimeZone should be converted, got: {}",
1142+
ty_str
1143+
);
1144+
1145+
// Also check Option<DateTimeWithTimeZone>
1146+
let updated_at_field = inline_type
1147+
.fields
1148+
.iter()
1149+
.find(|f| f.name == "updated_at")
1150+
.expect("updated_at field should exist");
1151+
1152+
let updated_ty_str = updated_at_field.ty.to_string();
1153+
assert!(
1154+
updated_ty_str.contains("Option"),
1155+
"Should be Option type, got: {}",
1156+
updated_ty_str
1157+
);
1158+
assert!(
1159+
updated_ty_str.contains("vespera :: chrono :: DateTime"),
1160+
"Option<DateTimeWithTimeZone> should be converted, got: {}",
1161+
updated_ty_str
1162+
);
1163+
}
10191164
}

crates/vespera_macro/src/schema_macro/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ pub fn generate_schema_type_code(
279279
if let Some(inline_type) = generate_inline_relation_type_no_relations(
280280
new_type_name,
281281
&rel_info,
282+
&source_module_path,
282283
input.schema_name.as_deref(),
283284
) {
284285
let inline_type_def = generate_inline_type_definition(&inline_type);

examples/axum-example/src/routes/memos.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
use std::sync::Arc;
1111

1212
// Import types used by the source model that we want to include in generated structs
13-
use sea_orm::entity::prelude::DateTimeWithTimeZone;
1413
use vespera::{
1514
axum::{
1615
Json,
@@ -71,14 +70,16 @@ pub async fn update_memo(Json(req): Json<UpdateMemoRequest>) -> Json<UpdateMemoR
7170
pub async fn get_memo(Path(id): Path<i32>) -> Json<MemoResponse> {
7271
// In real app, this would be a DB query returning Model
7372
// schema_type! generates From<Model> for MemoResponse, so .into() works
73+
// Create a default datetime using vespera's chrono re-export
74+
let now: vespera::chrono::DateTime<vespera::chrono::FixedOffset> = vespera::chrono::Utc::now().fixed_offset();
7475
let model = crate::models::memo::Model {
7576
id,
7677
user_id: 1, // Example user ID
7778
title: "Test Memo".to_string(),
7879
content: "This is test content".to_string(),
7980
status: crate::models::memo::MemoStatus::Published,
80-
created_at: DateTimeWithTimeZone::default(),
81-
updated_at: DateTimeWithTimeZone::default(),
81+
created_at: now,
82+
updated_at: now,
8283
};
8384
Json(model.into())
8485
}
@@ -90,14 +91,15 @@ pub async fn get_memo_rel(
9091
) -> Json<MemoResponseRel> {
9192
// In real app, this would be a DB query returning Model
9293
// schema_type! generates From<Model> for MemoResponse, so .into() works
94+
let now: vespera::chrono::DateTime<vespera::chrono::FixedOffset> = vespera::chrono::Utc::now().fixed_offset();
9395
let model = crate::models::memo::Model {
9496
id,
9597
user_id: 1, // Example user ID
9698
title: "Test Memo".to_string(),
9799
content: "This is test content".to_string(),
98100
status: crate::models::memo::MemoStatus::Published,
99-
created_at: DateTimeWithTimeZone::default(),
100-
updated_at: DateTimeWithTimeZone::default(),
101+
created_at: now,
102+
updated_at: now,
101103
};
102104
Json(
103105
MemoResponseRel::from_model(model, app_state.db.as_ref())

0 commit comments

Comments
 (0)