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_gpQF3tSrOG8PO5FF9lBxf.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"changes":{"Cargo.toml":"Patch"},"note":"Fix json column type","date":"2026-04-21T05:05:38.721991Z"}
10 changes: 5 additions & 5 deletions Cargo.lock

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

3 changes: 1 addition & 2 deletions crates/vespera_jni/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,7 @@ mod jni_impl {
unowned_env
.with_env(|env| -> jni::errors::Result<JObject<'local>> {
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());
};

Expand Down
5 changes: 2 additions & 3 deletions crates/vespera_macro/src/collector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,12 +173,11 @@ pub fn collect_file_fingerprints(folder_path: &Path) -> MacroResult<HashMap<Stri
}
let mtime = std::fs::metadata(&file)
.and_then(|m| m.modified())
.map(|t| {
.map_or(0, |t| {
t.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
})
.unwrap_or(0);
});
fingerprints.insert(file.display().to_string(), mtime);
}
Ok(fingerprints)
Expand Down
5 changes: 1 addition & 4 deletions crates/vespera_macro/src/parser/schema/struct_schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -364,10 +364,7 @@ mod tests {
)
.unwrap();
let schema = parse_struct_to_schema(&struct_item, &HashSet::new(), &HashMap::new());
assert_eq!(
schema.description,
Some("Struct description".to_string())
);
assert_eq!(schema.description, Some("Struct description".to_string()));
let props = schema.properties.unwrap();
if let SchemaRef::Inline(id_schema) = props.get("id").unwrap() {
assert_eq!(id_schema.description, Some("Field description".to_string()));
Expand Down
15 changes: 15 additions & 0 deletions crates/vespera_macro/src/schema_macro/seaorm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,21 @@ mod tests {
assert_eq!(output.trim(), "vespera :: tempfile :: NamedTempFile");
}

#[test]
fn test_convert_type_with_chrono_json_alias_uses_public_value_path() {
let ty: syn::Type = syn::parse_str("Json").unwrap();
let tokens = convert_type_with_chrono(
&ty,
&[
"crate".to_string(),
"models".to_string(),
"json_case".to_string(),
],
);
let output = tokens.to_string();
assert_eq!(output.trim(), "vespera :: serde_json :: Value");
}

// =========================================================================
// Tests for convert_relation_type_to_schema_with_info
// =========================================================================
Expand Down
56 changes: 56 additions & 0 deletions crates/vespera_macro/src/schema_macro/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2029,3 +2029,59 @@ pub struct Model {
let output = tokens.to_string();
assert!(output.contains("UserSchema"));
}

#[test]
#[serial]
fn test_generate_schema_type_code_cross_module_json_alias_uses_public_path() {
use tempfile::TempDir;

let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
let models_dir = src_dir.join("models");
let routes_dir = src_dir.join("routes");
std::fs::create_dir_all(&models_dir).unwrap();
std::fs::create_dir_all(&routes_dir).unwrap();

let json_case_model = r#"
use sea_orm::entity::prelude::*;

#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "json_case")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub payload: Json,
}

impl ActiveModelBehavior for ActiveModel {}
"#;
std::fs::write(models_dir.join("json_case.rs"), json_case_model).unwrap();
std::fs::write(
routes_dir.join("json_case.rs"),
"vespera::schema_type!(RouteJsonCaseSchema from crate::models::json_case::Model);",
)
.unwrap();

let original_manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };

let tokens = quote!(RouteJsonCaseSchema from crate::models::json_case::Model);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let storage: HashMap<String, StructMetadata> = 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"));
}
137 changes: 132 additions & 5 deletions crates/vespera_macro/src/schema_macro/type_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -154,6 +154,81 @@ pub fn is_primitive_or_known_type(name: &str) -> bool {
)
}

fn resolve_public_type_path(name: &str) -> Option<TokenStream> {
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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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 }
}
Expand Down Expand Up @@ -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<String, 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!(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();
Expand Down
15 changes: 6 additions & 9 deletions crates/vespera_macro/src/vespera_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.")))?;
}
Expand Down Expand Up @@ -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| {
Expand Down Expand Up @@ -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(
Expand Down