Skip to content

Commit 6df4696

Browse files
committed
Fix json column type
1 parent e164a12 commit 6df4696

5 files changed

Lines changed: 203 additions & 10 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"changes":{"Cargo.toml":"Patch"},"note":"Fix json column type","date":"2026-04-21T05:05:38.721991Z"}

Cargo.lock

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

crates/vespera_macro/src/schema_macro/seaorm.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -710,6 +710,17 @@ mod tests {
710710
assert_eq!(output.trim(), "vespera :: tempfile :: NamedTempFile");
711711
}
712712

713+
#[test]
714+
fn test_convert_type_with_chrono_json_alias_uses_public_value_path() {
715+
let ty: syn::Type = syn::parse_str("Json").unwrap();
716+
let tokens = convert_type_with_chrono(
717+
&ty,
718+
&["crate".to_string(), "models".to_string(), "json_case".to_string()],
719+
);
720+
let output = tokens.to_string();
721+
assert_eq!(output.trim(), "vespera :: serde_json :: Value");
722+
}
723+
713724
// =========================================================================
714725
// Tests for convert_relation_type_to_schema_with_info
715726
// =========================================================================

crates/vespera_macro/src/schema_macro/tests.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2029,3 +2029,59 @@ pub struct Model {
20292029
let output = tokens.to_string();
20302030
assert!(output.contains("UserSchema"));
20312031
}
2032+
2033+
#[test]
2034+
#[serial]
2035+
fn test_generate_schema_type_code_cross_module_json_alias_uses_public_path() {
2036+
use tempfile::TempDir;
2037+
2038+
let temp_dir = TempDir::new().unwrap();
2039+
let src_dir = temp_dir.path().join("src");
2040+
let models_dir = src_dir.join("models");
2041+
let routes_dir = src_dir.join("routes");
2042+
std::fs::create_dir_all(&models_dir).unwrap();
2043+
std::fs::create_dir_all(&routes_dir).unwrap();
2044+
2045+
let json_case_model = r#"
2046+
use sea_orm::entity::prelude::*;
2047+
2048+
#[sea_orm::model]
2049+
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
2050+
#[sea_orm(table_name = "json_case")]
2051+
pub struct Model {
2052+
#[sea_orm(primary_key)]
2053+
pub id: i32,
2054+
pub payload: Json,
2055+
}
2056+
2057+
impl ActiveModelBehavior for ActiveModel {}
2058+
"#;
2059+
std::fs::write(models_dir.join("json_case.rs"), json_case_model).unwrap();
2060+
std::fs::write(
2061+
routes_dir.join("json_case.rs"),
2062+
"vespera::schema_type!(RouteJsonCaseSchema from crate::models::json_case::Model);",
2063+
)
2064+
.unwrap();
2065+
2066+
let original_manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok();
2067+
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
2068+
2069+
let tokens = quote!(RouteJsonCaseSchema from crate::models::json_case::Model);
2070+
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
2071+
let storage: HashMap<String, StructMetadata> = HashMap::new();
2072+
let result = generate_schema_type_code(&input, &storage);
2073+
2074+
unsafe {
2075+
if let Some(dir) = original_manifest_dir {
2076+
std::env::set_var("CARGO_MANIFEST_DIR", dir);
2077+
} else {
2078+
std::env::remove_var("CARGO_MANIFEST_DIR");
2079+
}
2080+
}
2081+
2082+
assert!(result.is_ok());
2083+
let (tokens, _metadata) = result.unwrap();
2084+
let output = tokens.to_string();
2085+
assert!(output.contains("pub payload : vespera :: serde_json :: Value"));
2086+
assert!(!output.contains("crate :: models :: json_case :: Json"));
2087+
}

crates/vespera_macro/src/schema_macro/type_utils.rs

Lines changed: 130 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
use proc_macro2::TokenStream;
66
use quote::quote;
77
use serde_json;
8-
use syn::Type;
8+
use syn::{GenericArgument, PathArguments, Type};
99

1010
/// Primitive type names shared across the crate.
1111
/// Used by both `is_primitive_type()` (parser) and `is_parseable_type()` (schema_macro).
@@ -154,6 +154,79 @@ pub fn is_primitive_or_known_type(name: &str) -> bool {
154154
)
155155
}
156156

157+
fn resolve_public_type_path(name: &str) -> Option<TokenStream> {
158+
match name {
159+
// SeaORM re-exports `serde_json::Value` as `Json`; emit a stable public path.
160+
"Json" | "Value" => Some(quote! { vespera::serde_json::Value }),
161+
_ => None,
162+
}
163+
}
164+
165+
fn normalize_known_type_in_generic(ty: &Type, source_module_path: &[String]) -> TokenStream {
166+
let Type::Path(type_path) = ty else {
167+
return quote! { #ty };
168+
};
169+
170+
let Some(segment) = type_path.path.segments.last() else {
171+
return quote! { #ty };
172+
};
173+
174+
let ident_str = segment.ident.to_string();
175+
176+
if let Some(public_path) = resolve_public_type_path(&ident_str) {
177+
return quote! { #public_path };
178+
}
179+
180+
if type_path.path.segments.len() > 1 {
181+
let rendered_segments: Vec<_> = type_path
182+
.path
183+
.segments
184+
.iter()
185+
.map(|segment| {
186+
let ident = &segment.ident;
187+
let args = render_path_arguments(&segment.arguments, source_module_path);
188+
quote! { #ident #args }
189+
})
190+
.collect();
191+
192+
if type_path.path.leading_colon.is_some() {
193+
return quote! { :: #(#rendered_segments)::* };
194+
}
195+
196+
return quote! { #(#rendered_segments)::* };
197+
}
198+
199+
if is_primitive_or_known_type(&ident_str) {
200+
let ident = &segment.ident;
201+
let args = render_path_arguments(&segment.arguments, source_module_path);
202+
return quote! { #ident #args };
203+
}
204+
205+
quote! { #ty }
206+
}
207+
208+
fn render_path_arguments(args: &PathArguments, source_module_path: &[String]) -> TokenStream {
209+
match args {
210+
PathArguments::None => quote! {},
211+
PathArguments::AngleBracketed(angle_args) => {
212+
let rendered_args: Vec<_> = angle_args
213+
.args
214+
.iter()
215+
.map(|arg| match arg {
216+
GenericArgument::Type(inner_ty) => {
217+
let resolved = normalize_known_type_in_generic(inner_ty, source_module_path);
218+
quote! { #resolved }
219+
}
220+
_ => quote! { #arg },
221+
})
222+
.collect();
223+
224+
quote! { <#(#rendered_args),*> }
225+
}
226+
PathArguments::Parenthesized(_) => quote! { #args },
227+
}
228+
}
229+
157230
/// Resolve a simple type to an absolute path using the source module path.
158231
///
159232
/// For example, if `source_module_path` is `["crate", "models", "memo"]` and
@@ -166,9 +239,28 @@ pub fn resolve_type_to_absolute_path(ty: &Type, source_module_path: &[String]) -
166239
return quote! { #ty };
167240
};
168241

242+
if type_path.path.segments.is_empty() {
243+
return quote! { #ty };
244+
}
245+
169246
// If path has multiple segments (already qualified like `crate::foo::Bar`), return as-is
170247
if type_path.path.segments.len() > 1 {
171-
return quote! { #ty };
248+
let rendered_segments: Vec<_> = type_path
249+
.path
250+
.segments
251+
.iter()
252+
.map(|segment| {
253+
let ident = &segment.ident;
254+
let args = render_path_arguments(&segment.arguments, source_module_path);
255+
quote! { #ident #args }
256+
})
257+
.collect();
258+
259+
if type_path.path.leading_colon.is_some() {
260+
return quote! { :: #(#rendered_segments)::* };
261+
}
262+
263+
return quote! { #(#rendered_segments)::* };
172264
}
173265

174266
// Get the single segment
@@ -177,15 +269,22 @@ pub fn resolve_type_to_absolute_path(ty: &Type, source_module_path: &[String]) -
177269
};
178270

179271
let ident_str = segment.ident.to_string();
272+
let args = render_path_arguments(&segment.arguments, source_module_path);
273+
274+
if let Some(public_path) = resolve_public_type_path(&ident_str) {
275+
return quote! { #public_path };
276+
}
180277

181278
// If it's a primitive or known type, return as-is
182279
if is_primitive_or_known_type(&ident_str) {
183-
return quote! { #ty };
280+
let type_ident = &segment.ident;
281+
return quote! { #type_ident #args };
184282
}
185283

186284
// If no source module path, return as-is
187285
if source_module_path.is_empty() {
188-
return quote! { #ty };
286+
let type_ident = &segment.ident;
287+
return quote! { #type_ident #args };
189288
}
190289

191290
// Build absolute path: source_module_path + type_name
@@ -194,7 +293,6 @@ pub fn resolve_type_to_absolute_path(ty: &Type, source_module_path: &[String]) -
194293
.map(|s| syn::Ident::new(s, proc_macro2::Span::call_site()))
195294
.collect();
196295
let type_ident = &segment.ident;
197-
let args = &segment.arguments;
198296

199297
quote! { #(#path_idents)::* :: #type_ident #args }
200298
}
@@ -559,6 +657,33 @@ mod tests {
559657
assert_eq!(output.trim(), "Decimal");
560658
}
561659

660+
#[test]
661+
fn test_resolve_type_to_absolute_path_json_alias_uses_public_path() {
662+
let ty: syn::Type = syn::parse_str("Json").unwrap();
663+
let module_path = vec![
664+
"crate".to_string(),
665+
"models".to_string(),
666+
"json_case".to_string(),
667+
];
668+
let tokens = resolve_type_to_absolute_path(&ty, &module_path);
669+
let output = tokens.to_string();
670+
assert_eq!(output.trim(), "vespera :: serde_json :: Value");
671+
}
672+
673+
#[test]
674+
fn test_resolve_type_to_absolute_path_known_container_normalizes_inner_json_alias() {
675+
let ty: syn::Type = syn::parse_str("HashMap<String, Json>").unwrap();
676+
let module_path = vec![
677+
"crate".to_string(),
678+
"models".to_string(),
679+
"json_case".to_string(),
680+
];
681+
let tokens = resolve_type_to_absolute_path(&ty, &module_path);
682+
let output = tokens.to_string();
683+
assert!(output.contains("HashMap < String , vespera :: serde_json :: Value >"));
684+
assert!(!output.contains("crate :: models :: json_case :: Json"));
685+
}
686+
562687
#[test]
563688
fn test_resolve_type_to_absolute_path_custom_type() {
564689
let ty: syn::Type = syn::parse_str("MemoStatus").unwrap();

0 commit comments

Comments
 (0)