Skip to content

Commit 614fc9c

Browse files
committed
Fix default issue
1 parent 34d40ea commit 614fc9c

8 files changed

Lines changed: 453 additions & 164 deletions

File tree

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

Lines changed: 32 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,8 @@ use vespera_core::schema::{Schema, SchemaRef, SchemaType};
1010

1111
use super::{
1212
serde_attrs::{
13-
extract_default, extract_doc_comment, extract_field_rename, extract_flatten,
14-
extract_rename_all, extract_skip, extract_skip_serializing_if, rename_field,
15-
strip_raw_prefix,
13+
extract_doc_comment, extract_field_rename, extract_flatten, extract_rename_all,
14+
extract_skip, rename_field, strip_raw_prefix,
1615
},
1716
type_schema::parse_type_to_schema_ref,
1817
};
@@ -104,36 +103,21 @@ pub fn parse_struct_to_schema(
104103
}
105104
}
106105

107-
// Check for default attribute
108-
let has_default = extract_default(&field.attrs).is_some();
109-
110-
// Check for skip_serializing_if attribute
111-
let has_skip_serializing_if = extract_skip_serializing_if(&field.attrs);
106+
// Required is determined solely by nullability (Option<T>).
107+
// Fields with #[serde(default)] still have defaults applied in
108+
// openapi_generator, but that does NOT affect required status.
109+
let is_optional = matches!(
110+
field_type,
111+
Type::Path(type_path)
112+
if type_path
113+
.path
114+
.segments
115+
.first()
116+
.is_some_and(|s| s.ident == "Option")
117+
);
112118

113-
// If default or skip_serializing_if is present, mark field as optional (not required)
114-
// and set default value if it's a simple default (not a function)
115-
if has_default || has_skip_serializing_if {
116-
// For default = "function_name", we'll handle it in openapi_generator
117-
// For now, just mark as optional
118-
if let SchemaRef::Inline(ref mut _schema) = schema_ref {
119-
// Default will be set later in openapi_generator if it's a function
120-
// For simple default, we could set it here, but serde handles it
121-
}
122-
} else {
123-
// Check if field is Option<T>
124-
let is_optional = matches!(
125-
field_type,
126-
Type::Path(type_path)
127-
if type_path
128-
.path
129-
.segments
130-
.first()
131-
.is_some_and(|s| s.ident == "Option")
132-
);
133-
134-
if !is_optional {
135-
required.push(field_name.clone());
136-
}
119+
if !is_optional {
120+
required.push(field_name.clone());
137121
}
138122

139123
properties.insert(field_name, schema_ref);
@@ -212,20 +196,16 @@ mod tests {
212196
let props = schema.properties.as_ref().unwrap();
213197
assert!(props.contains_key("id"));
214198
assert!(props.contains_key("name"));
215-
assert!(
216-
schema
217-
.required
218-
.as_ref()
219-
.unwrap()
220-
.contains(&"id".to_string())
221-
);
222-
assert!(
223-
!schema
224-
.required
225-
.as_ref()
226-
.unwrap()
227-
.contains(&"name".to_string())
228-
);
199+
assert!(schema
200+
.required
201+
.as_ref()
202+
.unwrap()
203+
.contains(&"id".to_string()));
204+
assert!(!schema
205+
.required
206+
.as_ref()
207+
.unwrap()
208+
.contains(&"name".to_string()));
229209
}
230210

231211
#[test]
@@ -283,14 +263,15 @@ mod tests {
283263
}
284264

285265
// Test struct with default and skip_serializing_if
266+
// Required is determined solely by nullability (Option<T>), not by defaults.
286267
#[test]
287268
fn test_parse_struct_to_schema_with_default_fields() {
288269
let struct_item: syn::ItemStruct = syn::parse_str(
289270
r#"
290271
struct Config {
291272
required_field: i32,
292273
#[serde(default)]
293-
optional_with_default: String,
274+
with_default: String,
294275
#[serde(skip_serializing_if = "Option::is_none")]
295276
maybe_skip: Option<i32>,
296277
}
@@ -300,14 +281,14 @@ mod tests {
300281
let schema = parse_struct_to_schema(&struct_item, &HashSet::new(), &HashMap::new());
301282
let props = schema.properties.as_ref().unwrap();
302283
assert!(props.contains_key("required_field"));
303-
assert!(props.contains_key("optional_with_default"));
284+
assert!(props.contains_key("with_default"));
304285
assert!(props.contains_key("maybe_skip"));
305286

306287
let required = schema.required.as_ref().unwrap();
307288
assert!(required.contains(&"required_field".to_string()));
308-
// Fields with default should NOT be required
309-
assert!(!required.contains(&"optional_with_default".to_string()));
310-
// Fields with skip_serializing_if should NOT be required
289+
// Non-nullable fields are always required, even with #[serde(default)]
290+
assert!(required.contains(&"with_default".to_string()));
291+
// Option<T> fields are not required (nullable)
311292
assert!(!required.contains(&"maybe_skip".to_string()));
312293
}
313294

crates/vespera_macro/src/schema_macro/mod.rs

Lines changed: 126 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ use proc_macro2::TokenStream;
2929
use quote::quote;
3030
use seaorm::{
3131
RelationFieldInfo, convert_relation_type_to_schema_with_info, convert_type_with_chrono,
32-
extract_sea_orm_default_value, is_sql_function_default,
32+
extract_sea_orm_default_value, has_sea_orm_primary_key, is_sql_function_default,
3333
};
3434
use transformation::{
3535
build_omit_set, build_partial_config, build_pick_set, build_rename_map, determine_rename_all,
@@ -408,8 +408,8 @@ pub fn generate_schema_type_code(
408408
// that may have ORM-specific attributes we don't want in the generated struct
409409
let serde_field_attrs = extract_field_serde_attrs(&field.attrs);
410410

411-
// Generate serde default + schema(default) from sea_orm(default_value)
412-
// Only for non-partial, non-Option fields with literal (non-SQL-function) defaults
411+
// Generate serde default + schema(default) from sea_orm(default_value) or primary_key
412+
// Handles literal defaults, SQL function defaults, and implicit auto-increment
413413
let (serde_default_attr, schema_default_attr) = generate_sea_orm_default_attrs(
414414
&field.attrs,
415415
new_type_name,
@@ -622,20 +622,26 @@ pub fn generate_schema_type_code(
622622
}
623623

624624
/// Generate `#[serde(default = "...")]` and `#[schema(default = "...")]` attributes
625-
/// from `#[sea_orm(default_value = ...)]` on source fields.
625+
/// from `#[sea_orm(default_value = ...)]` or `#[sea_orm(primary_key)]` on source fields.
626626
///
627627
/// Returns `(serde_default_attr, schema_default_attr)` as `TokenStream`s.
628628
/// - `serde_default_attr`: `#[serde(default = "default_structname_field")]` for deserialization
629629
/// - `schema_default_attr`: `#[schema(default = "value")]` for OpenAPI default value
630630
///
631631
/// Also generates a companion default function and appends it to `default_functions`.
632632
///
633+
/// Handles three categories of defaults:
634+
/// 1. **Literal defaults** (`default_value = "42"`, `"draft"`, `0.7`):
635+
/// Generates parse-based default function + schema default.
636+
/// 2. **SQL function defaults** (`default_value = "NOW()"`, `"gen_random_uuid()"`):
637+
/// Generates type-specific default function + schema default with type's zero value.
638+
/// 3. **Primary key** (implicit auto-increment):
639+
/// Treated as having an implicit default — generates type-specific default.
640+
///
633641
/// Skips serde default generation when:
634-
/// - The field type doesn't implement `FromStr` (enums, custom types)
635-
/// - The field already has `#[serde(default)]`
636642
/// - The field is wrapped in `Option` (partial mode or already optional)
637-
///
638-
/// Always generates `#[schema(default)]` for OpenAPI when a literal default exists.
643+
/// - The field already has `#[serde(default)]`
644+
/// - For literal defaults: the field type doesn't implement `FromStr`
639645
fn generate_sea_orm_default_attrs(
640646
original_attrs: &[syn::Attribute],
641647
struct_name: &syn::Ident,
@@ -650,46 +656,129 @@ fn generate_sea_orm_default_attrs(
650656
return (quote! {}, quote! {});
651657
}
652658

653-
// Check for sea_orm(default_value)
654-
let Some(default_value) = extract_sea_orm_default_value(original_attrs) else {
655-
return (quote! {}, quote! {});
656-
};
659+
// Check for sea_orm(default_value) and sea_orm(primary_key)
660+
let default_value = extract_sea_orm_default_value(original_attrs);
661+
let has_pk = has_sea_orm_primary_key(original_attrs);
657662

658-
// SQL functions like NOW(), CURRENT_TIMESTAMP(), gen_random_uuid()
659-
// can't be expressed as concrete JSON defaults — skip entirely.
660-
if is_sql_function_default(&default_value) {
663+
// No default source found
664+
if default_value.is_none() && !has_pk {
661665
return (quote! {}, quote! {});
662666
}
663667

664-
// Generate #[schema(default = "value")] for OpenAPI (always, regardless of type support)
665-
let schema_default_attr = quote! { #[schema(default = #default_value)] };
666-
667-
// Check if field already has serde(default)
668668
let has_existing_serde_default = extract_default(original_attrs).is_some();
669-
if has_existing_serde_default {
670-
return (quote! {}, schema_default_attr);
671-
}
672669

673-
// Only generate serde default function for types known to implement FromStr
674-
if !is_parseable_type(original_ty) {
675-
return (quote! {}, schema_default_attr);
676-
}
670+
match &default_value {
671+
// Literal default (e.g., "42", "draft", "0.7")
672+
Some(value) if !is_sql_function_default(value) => {
673+
let schema_default_attr = quote! { #[schema(default = #value)] };
674+
675+
if has_existing_serde_default {
676+
return (quote! {}, schema_default_attr);
677+
}
677678

678-
// Generate default function with struct-specific name to avoid collisions:
679-
// fn default_{StructName}_{field_name}() -> Type { "value".parse().unwrap() }
680-
let fn_name = format!("default_{struct_name}_{field_name}");
681-
let fn_ident = syn::Ident::new(&fn_name, proc_macro2::Span::call_site());
679+
if !is_parseable_type(original_ty) {
680+
return (quote! {}, schema_default_attr);
681+
}
682+
683+
let fn_name = format!("default_{struct_name}_{field_name}");
684+
let fn_ident = syn::Ident::new(&fn_name, proc_macro2::Span::call_site());
685+
686+
default_functions.push(quote! {
687+
#[allow(non_snake_case)]
688+
fn #fn_ident() -> #field_ty {
689+
#value.parse().unwrap()
690+
}
691+
});
682692

683-
default_functions.push(quote! {
684-
#[allow(non_snake_case)]
685-
fn #fn_ident() -> #field_ty {
686-
#default_value.parse().unwrap()
693+
let serde_default_attr = quote! { #[serde(default = #fn_name)] };
694+
(serde_default_attr, schema_default_attr)
687695
}
688-
});
696+
// SQL function default (NOW(), gen_random_uuid(), etc.) or primary_key auto-increment
697+
_ => {
698+
let Some((default_expr, schema_default_str)) =
699+
sql_function_default_for_type(original_ty)
700+
else {
701+
return (quote! {}, quote! {});
702+
};
703+
704+
let schema_default_attr = quote! { #[schema(default = #schema_default_str)] };
705+
706+
if has_existing_serde_default {
707+
return (quote! {}, schema_default_attr);
708+
}
709+
710+
let fn_name = format!("default_{struct_name}_{field_name}");
711+
let fn_ident = syn::Ident::new(&fn_name, proc_macro2::Span::call_site());
689712

690-
let serde_default_attr = quote! { #[serde(default = #fn_name)] };
713+
default_functions.push(quote! {
714+
#[allow(non_snake_case)]
715+
fn #fn_ident() -> #field_ty {
716+
#default_expr
717+
}
718+
});
719+
720+
let serde_default_attr = quote! { #[serde(default = #fn_name)] };
721+
(serde_default_attr, schema_default_attr)
722+
}
723+
}
724+
}
691725

692-
(serde_default_attr, schema_default_attr)
726+
/// Return a type-appropriate (Rust default expression, OpenAPI default string) pair
727+
/// for fields with SQL function defaults or implicit auto-increment.
728+
///
729+
/// The Rust expression is used in the generated `#[serde(default = "fn")]` function body.
730+
/// The OpenAPI string is used in `#[schema(default = "value")]`.
731+
fn sql_function_default_for_type(original_ty: &syn::Type) -> Option<(TokenStream, String)> {
732+
let syn::Type::Path(type_path) = original_ty else {
733+
return None;
734+
};
735+
let segment = type_path.path.segments.last()?;
736+
let type_name = segment.ident.to_string();
737+
738+
match type_name.as_str() {
739+
"DateTimeWithTimeZone" | "DateTimeUtc" => {
740+
let expr = quote! {
741+
vespera::chrono::DateTime::<vespera::chrono::Utc>::UNIX_EPOCH.fixed_offset()
742+
};
743+
Some((expr, "1970-01-01T00:00:00+00:00".to_string()))
744+
}
745+
"DateTime" => {
746+
// Could be chrono::DateTime<Tz> — use UTC epoch
747+
let expr = quote! {
748+
vespera::chrono::DateTime::<vespera::chrono::Utc>::UNIX_EPOCH.fixed_offset()
749+
};
750+
Some((expr, "1970-01-01T00:00:00+00:00".to_string()))
751+
}
752+
"NaiveDateTime" => {
753+
let expr = quote! {
754+
vespera::chrono::NaiveDateTime::UNIX_EPOCH
755+
};
756+
Some((expr, "1970-01-01T00:00:00".to_string()))
757+
}
758+
"NaiveDate" => {
759+
let expr = quote! {
760+
vespera::chrono::NaiveDate::default()
761+
};
762+
Some((expr, "1970-01-01".to_string()))
763+
}
764+
"NaiveTime" | "Time" => {
765+
let expr = quote! {
766+
vespera::chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap()
767+
};
768+
Some((expr, "00:00:00".to_string()))
769+
}
770+
"Uuid" => Some((
771+
quote! { Default::default() },
772+
"00000000-0000-0000-0000-000000000000".to_string(),
773+
)),
774+
"i8" | "i16" | "i32" | "i64" | "i128" | "isize" | "u8" | "u16" | "u32" | "u64"
775+
| "u128" | "usize" | "f32" | "f64" | "Decimal" => {
776+
Some((quote! { Default::default() }, "0".to_string()))
777+
}
778+
"bool" => Some((quote! { Default::default() }, "false".to_string())),
779+
"String" => Some((quote! { Default::default() }, String::new())),
780+
_ => None,
781+
}
693782
}
694783

695784
/// Check if a type is known to implement `FromStr` and can use `.parse().unwrap()`.

0 commit comments

Comments
 (0)