diff --git a/.changepacks/changepack_log_gpQF3tSrOG8PO5FF9lBxf.json b/.changepacks/changepack_log_gpQF3tSrOG8PO5FF9lBxf.json new file mode 100644 index 0000000..4c0ab71 --- /dev/null +++ b/.changepacks/changepack_log_gpQF3tSrOG8PO5FF9lBxf.json @@ -0,0 +1 @@ +{"changes":{"Cargo.toml":"Patch"},"note":"Fix json column type","date":"2026-04-21T05:05:38.721991Z"} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index a21e11f..b00af2e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3712,7 +3712,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vespera" -version = "0.1.48" +version = "0.1.50" dependencies = [ "axum", "axum-extra", @@ -3731,7 +3731,7 @@ dependencies = [ [[package]] name = "vespera_core" -version = "0.1.48" +version = "0.1.50" dependencies = [ "rstest", "serde", @@ -3740,7 +3740,7 @@ dependencies = [ [[package]] name = "vespera_inprocess" -version = "0.1.48" +version = "0.1.50" dependencies = [ "axum", "http", @@ -3753,7 +3753,7 @@ dependencies = [ [[package]] name = "vespera_jni" -version = "0.1.48" +version = "0.1.50" dependencies = [ "jni", "tokio", @@ -3762,7 +3762,7 @@ dependencies = [ [[package]] name = "vespera_macro" -version = "0.1.48" +version = "0.1.50" dependencies = [ "insta", "proc-macro2", diff --git a/crates/vespera_jni/src/lib.rs b/crates/vespera_jni/src/lib.rs index 172cbe4..ae7b024 100644 --- a/crates/vespera_jni/src/lib.rs +++ b/crates/vespera_jni/src/lib.rs @@ -64,8 +64,7 @@ mod jni_impl { unowned_env .with_env(|env| -> jni::errors::Result> { let Ok(input) = request_json.try_to_string(env) else { - let err = - vespera_inprocess::serialize_error("invalid request envelope string"); + let err = vespera_inprocess::serialize_error("invalid request envelope string"); return Ok(env.new_string(err)?.into()); }; diff --git a/crates/vespera_macro/src/collector.rs b/crates/vespera_macro/src/collector.rs index 650bc7d..28e5534 100644 --- a/crates/vespera_macro/src/collector.rs +++ b/crates/vespera_macro/src/collector.rs @@ -173,12 +173,11 @@ pub fn collect_file_fingerprints(folder_path: &Path) -> MacroResult = HashMap::new(); + let result = generate_schema_type_code(&input, &storage); + + unsafe { + if let Some(dir) = original_manifest_dir { + std::env::set_var("CARGO_MANIFEST_DIR", dir); + } else { + std::env::remove_var("CARGO_MANIFEST_DIR"); + } + } + + assert!(result.is_ok()); + let (tokens, _metadata) = result.unwrap(); + let output = tokens.to_string(); + assert!(output.contains("pub payload : vespera :: serde_json :: Value")); + assert!(!output.contains("crate :: models :: json_case :: Json")); +} diff --git a/crates/vespera_macro/src/schema_macro/type_utils.rs b/crates/vespera_macro/src/schema_macro/type_utils.rs index 08937ab..903bee6 100644 --- a/crates/vespera_macro/src/schema_macro/type_utils.rs +++ b/crates/vespera_macro/src/schema_macro/type_utils.rs @@ -5,7 +5,7 @@ use proc_macro2::TokenStream; use quote::quote; use serde_json; -use syn::Type; +use syn::{GenericArgument, PathArguments, Type}; /// Primitive type names shared across the crate. /// Used by both `is_primitive_type()` (parser) and `is_parseable_type()` (schema_macro). @@ -154,6 +154,81 @@ pub fn is_primitive_or_known_type(name: &str) -> bool { ) } +fn resolve_public_type_path(name: &str) -> Option { + match name { + // SeaORM re-exports `serde_json::Value` as `Json`; emit a stable public path. + "Json" | "Value" => Some(quote! { vespera::serde_json::Value }), + _ => None, + } +} + +fn normalize_known_type_in_generic(ty: &Type, source_module_path: &[String]) -> TokenStream { + let Type::Path(type_path) = ty else { + return quote! { #ty }; + }; + + let Some(segment) = type_path.path.segments.last() else { + return quote! { #ty }; + }; + + let ident_str = segment.ident.to_string(); + + if let Some(public_path) = resolve_public_type_path(&ident_str) { + return quote! { #public_path }; + } + + if type_path.path.segments.len() > 1 { + let rendered_segments: Vec<_> = type_path + .path + .segments + .iter() + .map(|segment| { + let ident = &segment.ident; + let args = render_path_arguments(&segment.arguments, source_module_path); + quote! { #ident #args } + }) + .collect(); + + if type_path.path.leading_colon.is_some() { + return quote! { :: #(#rendered_segments)::* }; + } + + return quote! { #(#rendered_segments)::* }; + } + + if is_primitive_or_known_type(&ident_str) { + let ident = &segment.ident; + let args = render_path_arguments(&segment.arguments, source_module_path); + return quote! { #ident #args }; + } + + quote! { #ty } +} + +fn render_path_arguments(args: &PathArguments, source_module_path: &[String]) -> TokenStream { + match args { + PathArguments::None => quote! {}, + PathArguments::AngleBracketed(angle_args) => { + let rendered_args: Vec<_> = angle_args + .args + .iter() + .map(|arg| { + if let GenericArgument::Type(inner_ty) = arg { + let resolved = + normalize_known_type_in_generic(inner_ty, source_module_path); + quote! { #resolved } + } else { + quote! { #arg } + } + }) + .collect(); + + quote! { <#(#rendered_args),*> } + } + PathArguments::Parenthesized(_) => quote! { #args }, + } +} + /// Resolve a simple type to an absolute path using the source module path. /// /// For example, if `source_module_path` is `["crate", "models", "memo"]` and @@ -166,9 +241,28 @@ pub fn resolve_type_to_absolute_path(ty: &Type, source_module_path: &[String]) - return quote! { #ty }; }; + if type_path.path.segments.is_empty() { + return quote! { #ty }; + } + // If path has multiple segments (already qualified like `crate::foo::Bar`), return as-is if type_path.path.segments.len() > 1 { - return quote! { #ty }; + let rendered_segments: Vec<_> = type_path + .path + .segments + .iter() + .map(|segment| { + let ident = &segment.ident; + let args = render_path_arguments(&segment.arguments, source_module_path); + quote! { #ident #args } + }) + .collect(); + + if type_path.path.leading_colon.is_some() { + return quote! { :: #(#rendered_segments)::* }; + } + + return quote! { #(#rendered_segments)::* }; } // Get the single segment @@ -177,15 +271,22 @@ pub fn resolve_type_to_absolute_path(ty: &Type, source_module_path: &[String]) - }; let ident_str = segment.ident.to_string(); + let args = render_path_arguments(&segment.arguments, source_module_path); + + if let Some(public_path) = resolve_public_type_path(&ident_str) { + return quote! { #public_path }; + } // If it's a primitive or known type, return as-is if is_primitive_or_known_type(&ident_str) { - return quote! { #ty }; + let type_ident = &segment.ident; + return quote! { #type_ident #args }; } // If no source module path, return as-is if source_module_path.is_empty() { - return quote! { #ty }; + let type_ident = &segment.ident; + return quote! { #type_ident #args }; } // Build absolute path: source_module_path + type_name @@ -194,7 +295,6 @@ pub fn resolve_type_to_absolute_path(ty: &Type, source_module_path: &[String]) - .map(|s| syn::Ident::new(s, proc_macro2::Span::call_site())) .collect(); let type_ident = &segment.ident; - let args = &segment.arguments; quote! { #(#path_idents)::* :: #type_ident #args } } @@ -559,6 +659,33 @@ mod tests { assert_eq!(output.trim(), "Decimal"); } + #[test] + fn test_resolve_type_to_absolute_path_json_alias_uses_public_path() { + let ty: syn::Type = syn::parse_str("Json").unwrap(); + let module_path = vec![ + "crate".to_string(), + "models".to_string(), + "json_case".to_string(), + ]; + let tokens = resolve_type_to_absolute_path(&ty, &module_path); + let output = tokens.to_string(); + assert_eq!(output.trim(), "vespera :: serde_json :: Value"); + } + + #[test] + fn test_resolve_type_to_absolute_path_known_container_normalizes_inner_json_alias() { + let ty: syn::Type = syn::parse_str("HashMap").unwrap(); + let module_path = vec![ + "crate".to_string(), + "models".to_string(), + "json_case".to_string(), + ]; + let tokens = resolve_type_to_absolute_path(&ty, &module_path); + let output = tokens.to_string(); + assert!(output.contains("HashMap < String , vespera :: serde_json :: Value >")); + assert!(!output.contains("crate :: models :: json_case :: Json")); + } + #[test] fn test_resolve_type_to_absolute_path_custom_type() { let ty: syn::Type = syn::parse_str("MemoStatus").unwrap(); diff --git a/crates/vespera_macro/src/vespera_impl.rs b/crates/vespera_macro/src/vespera_impl.rs index 6933ed3..86a19a3 100644 --- a/crates/vespera_macro/src/vespera_impl.rs +++ b/crates/vespera_macro/src/vespera_impl.rs @@ -181,9 +181,8 @@ pub fn generate_and_write_openapi( if let Some(parent) = file_path.parent() { std::fs::create_dir_all(parent).map_err(|e| err_call_site(format!("OpenAPI output: failed to create directory '{}'. Error: {}. Ensure the path is valid and writable.", parent.display(), e)))?; } - let should_write = std::fs::read_to_string(file_path) - .map(|existing| existing != json_pretty) - .unwrap_or(true); + let should_write = + std::fs::read_to_string(file_path).map_or(true, |existing| existing != json_pretty); if should_write { std::fs::write(file_path, &json_pretty).map_err(|e| err_call_site(format!("OpenAPI output: failed to write file '{openapi_file_name}'. Error: {e}. Ensure the file path is writable.")))?; } @@ -302,9 +301,8 @@ pub fn ensure_openapi_files_from_cache( }; for openapi_file_name in openapi_file_names { let file_path = Path::new(openapi_file_name); - let should_write = std::fs::read_to_string(file_path) - .map(|existing| existing != *pretty) - .unwrap_or(true); + let should_write = + std::fs::read_to_string(file_path).map_or(true, |existing| existing != *pretty); if should_write { if let Some(parent) = file_path.parent() { std::fs::create_dir_all(parent).map_err(|e| { @@ -351,9 +349,8 @@ fn write_spec_for_embedding( ) })?; let spec_file = vespera_dir.join("vespera_spec.json"); - let should_write = std::fs::read_to_string(&spec_file) - .map(|existing| existing != json) - .unwrap_or(true); + let should_write = + std::fs::read_to_string(&spec_file).map_or(true, |existing| existing != json); if should_write { std::fs::write(&spec_file, &json).map_err(|e| { syn::Error::new(