Skip to content

Commit c4e1056

Browse files
authored
Merge pull request #122 from dev-five-git/fix-json-column-type
Fix json column type
2 parents e164a12 + ef9eeb4 commit c4e1056

9 files changed

Lines changed: 219 additions & 28 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_jni/src/lib.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,7 @@ mod jni_impl {
6464
unowned_env
6565
.with_env(|env| -> jni::errors::Result<JObject<'local>> {
6666
let Ok(input) = request_json.try_to_string(env) else {
67-
let err =
68-
vespera_inprocess::serialize_error("invalid request envelope string");
67+
let err = vespera_inprocess::serialize_error("invalid request envelope string");
6968
return Ok(env.new_string(err)?.into());
7069
};
7170

crates/vespera_macro/src/collector.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,12 +173,11 @@ pub fn collect_file_fingerprints(folder_path: &Path) -> MacroResult<HashMap<Stri
173173
}
174174
let mtime = std::fs::metadata(&file)
175175
.and_then(|m| m.modified())
176-
.map(|t| {
176+
.map_or(0, |t| {
177177
t.duration_since(std::time::UNIX_EPOCH)
178178
.unwrap_or_default()
179179
.as_secs()
180-
})
181-
.unwrap_or(0);
180+
});
182181
fingerprints.insert(file.display().to_string(), mtime);
183182
}
184183
Ok(fingerprints)

crates/vespera_macro/src/parser/schema/struct_schema.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -364,10 +364,7 @@ mod tests {
364364
)
365365
.unwrap();
366366
let schema = parse_struct_to_schema(&struct_item, &HashSet::new(), &HashMap::new());
367-
assert_eq!(
368-
schema.description,
369-
Some("Struct description".to_string())
370-
);
367+
assert_eq!(schema.description, Some("Struct description".to_string()));
371368
let props = schema.properties.unwrap();
372369
if let SchemaRef::Inline(id_schema) = props.get("id").unwrap() {
373370
assert_eq!(id_schema.description, Some("Field description".to_string()));

crates/vespera_macro/src/schema_macro/seaorm.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -710,6 +710,21 @@ 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+
&[
719+
"crate".to_string(),
720+
"models".to_string(),
721+
"json_case".to_string(),
722+
],
723+
);
724+
let output = tokens.to_string();
725+
assert_eq!(output.trim(), "vespera :: serde_json :: Value");
726+
}
727+
713728
// =========================================================================
714729
// Tests for convert_relation_type_to_schema_with_info
715730
// =========================================================================

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: 132 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,81 @@ 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| {
216+
if let GenericArgument::Type(inner_ty) = arg {
217+
let resolved =
218+
normalize_known_type_in_generic(inner_ty, source_module_path);
219+
quote! { #resolved }
220+
} else {
221+
quote! { #arg }
222+
}
223+
})
224+
.collect();
225+
226+
quote! { <#(#rendered_args),*> }
227+
}
228+
PathArguments::Parenthesized(_) => quote! { #args },
229+
}
230+
}
231+
157232
/// Resolve a simple type to an absolute path using the source module path.
158233
///
159234
/// 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]) -
166241
return quote! { #ty };
167242
};
168243

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

174268
// Get the single segment
@@ -177,15 +271,22 @@ pub fn resolve_type_to_absolute_path(ty: &Type, source_module_path: &[String]) -
177271
};
178272

179273
let ident_str = segment.ident.to_string();
274+
let args = render_path_arguments(&segment.arguments, source_module_path);
275+
276+
if let Some(public_path) = resolve_public_type_path(&ident_str) {
277+
return quote! { #public_path };
278+
}
180279

181280
// If it's a primitive or known type, return as-is
182281
if is_primitive_or_known_type(&ident_str) {
183-
return quote! { #ty };
282+
let type_ident = &segment.ident;
283+
return quote! { #type_ident #args };
184284
}
185285

186286
// If no source module path, return as-is
187287
if source_module_path.is_empty() {
188-
return quote! { #ty };
288+
let type_ident = &segment.ident;
289+
return quote! { #type_ident #args };
189290
}
190291

191292
// 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]) -
194295
.map(|s| syn::Ident::new(s, proc_macro2::Span::call_site()))
195296
.collect();
196297
let type_ident = &segment.ident;
197-
let args = &segment.arguments;
198298

199299
quote! { #(#path_idents)::* :: #type_ident #args }
200300
}
@@ -559,6 +659,33 @@ mod tests {
559659
assert_eq!(output.trim(), "Decimal");
560660
}
561661

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

crates/vespera_macro/src/vespera_impl.rs

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,8 @@ pub fn generate_and_write_openapi(
181181
if let Some(parent) = file_path.parent() {
182182
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)))?;
183183
}
184-
let should_write = std::fs::read_to_string(file_path)
185-
.map(|existing| existing != json_pretty)
186-
.unwrap_or(true);
184+
let should_write =
185+
std::fs::read_to_string(file_path).map_or(true, |existing| existing != json_pretty);
187186
if should_write {
188187
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.")))?;
189188
}
@@ -302,9 +301,8 @@ pub fn ensure_openapi_files_from_cache(
302301
};
303302
for openapi_file_name in openapi_file_names {
304303
let file_path = Path::new(openapi_file_name);
305-
let should_write = std::fs::read_to_string(file_path)
306-
.map(|existing| existing != *pretty)
307-
.unwrap_or(true);
304+
let should_write =
305+
std::fs::read_to_string(file_path).map_or(true, |existing| existing != *pretty);
308306
if should_write {
309307
if let Some(parent) = file_path.parent() {
310308
std::fs::create_dir_all(parent).map_err(|e| {
@@ -351,9 +349,8 @@ fn write_spec_for_embedding(
351349
)
352350
})?;
353351
let spec_file = vespera_dir.join("vespera_spec.json");
354-
let should_write = std::fs::read_to_string(&spec_file)
355-
.map(|existing| existing != json)
356-
.unwrap_or(true);
352+
let should_write =
353+
std::fs::read_to_string(&spec_file).map_or(true, |existing| existing != json);
357354
if should_write {
358355
std::fs::write(&spec_file, &json).map_err(|e| {
359356
syn::Error::new(

0 commit comments

Comments
 (0)