mod circular;
mod codegen;
pub mod file_cache;
mod file_lookup;
mod from_model;
mod inline_types;
mod input;
mod seaorm;
mod transformation;
pub mod type_utils;
mod validation;
pub use file_cache::print_profile_summary;
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use codegen::generate_filtered_schema;
use file_lookup::find_struct_from_path;
use from_model::generate_from_model_with_relations;
use inline_types::{
generate_inline_relation_type, generate_inline_relation_type_no_relations,
generate_inline_type_definition,
};
pub use input::{PartialMode, SchemaInput, SchemaTypeInput};
use proc_macro2::TokenStream;
use quote::quote;
use seaorm::{
RelationFieldInfo, convert_relation_type_to_schema_with_info, convert_type_with_chrono,
extract_sea_orm_default_value, has_sea_orm_primary_key, is_sql_function_default,
};
use transformation::{
build_omit_set, build_partial_config, build_pick_set, build_rename_map, determine_rename_all,
extract_doc_attrs, extract_field_serde_attrs, extract_form_data_attrs,
extract_serde_attrs_without_rename_all, filter_out_serde_rename, should_skip_field,
should_wrap_in_option,
};
use type_utils::{
capitalize_first, extract_module_path, extract_type_name, is_option_type, is_qualified_path,
is_seaorm_model, is_seaorm_relation_type, snake_to_pascal_case,
};
use validation::{
extract_source_field_names, validate_omit_fields, validate_partial_fields,
validate_pick_fields, validate_rename_fields,
};
use crate::{
metadata::StructMetadata,
parser::{extract_default, extract_field_rename, strip_raw_prefix_owned},
};
#[cfg(test)]
struct __VesperaSameFileLookupFixture {
value: i32,
}
fn derive_response_base_name(name: &str) -> String {
for suffix in ["Response", "Request", "Schema"] {
if let Some(stripped) = name.strip_suffix(suffix)
&& !stripped.is_empty()
{
return stripped.to_string();
}
}
name.to_string()
}
fn find_same_file_struct_metadata<'a>(
struct_name: &str,
schema_storage: &'a HashMap<String, StructMetadata>,
) -> Option<Cow<'a, StructMetadata>> {
if let Some(metadata) = schema_storage.get(struct_name) {
return Some(Cow::Borrowed(metadata));
}
let file_path = proc_macro2::Span::call_site().local_file();
#[cfg(test)]
let file_path = file_path.or_else(|| {
Some(
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("src")
.join("schema_macro")
.join("mod.rs"),
)
});
let file_path = file_path?;
let definition = file_cache::get_struct_definition(&file_path, struct_name)?;
Some(Cow::Owned(StructMetadata::new(
struct_name.to_string(),
definition,
)))
}
fn related_model_type_from_schema_path(schema_path: &TokenStream) -> Option<syn::Type> {
let schema_path_str = schema_path.to_string().replace("Schema", "Model");
syn::parse_str(&schema_path_str).ok()
}
fn schema_component_name_from_path(schema_path: &TokenStream) -> String {
let path_str = schema_path.to_string();
let segments: Vec<&str> = path_str.split("::").map(str::trim).collect();
if segments.last().is_some_and(|s| *s == "Schema") && segments.len() > 1 {
format!("{}Schema", capitalize_first(segments[segments.len() - 2]))
} else {
segments
.last()
.map_or_else(|| "Schema".to_string(), |s| (*s).to_string())
}
}
fn has_derive(struct_item: &syn::ItemStruct, derive_name: &str) -> bool {
struct_item.attrs.iter().any(|attr| {
if !attr.path().is_ident("derive") {
return false;
}
let mut found = false;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident(derive_name) {
found = true;
}
Ok(())
});
found
})
}
fn build_named_struct_field_assignments(
struct_item: &syn::ItemStruct,
source_expr: &TokenStream,
) -> syn::Result<Vec<TokenStream>> {
let syn::Fields::Named(fields_named) = &struct_item.fields else {
return Err(syn::Error::new_spanned(
struct_item,
"same-file relation override DTO must be a named-field struct",
));
};
let assignments = fields_named
.named
.iter()
.filter_map(|field| {
field.ident.as_ref().map(|ident| {
quote! { #ident: #source_expr . #ident.clone() }
})
})
.collect();
Ok(assignments)
}
fn build_proxy_fields(struct_item: &syn::ItemStruct) -> syn::Result<Vec<TokenStream>> {
let syn::Fields::Named(fields_named) = &struct_item.fields else {
return Err(syn::Error::new_spanned(
struct_item,
"same-file relation override DTO must be a named-field struct",
));
};
let fields = fields_named
.named
.iter()
.filter_map(|field| {
field.ident.as_ref().map(|ident| {
let ty = &field.ty;
let attrs: Vec<_> = field
.attrs
.iter()
.filter(|attr| attr.path().is_ident("serde") || attr.path().is_ident("doc"))
.collect();
quote! {
#(#attrs)*
#ident: #ty
}
})
})
.collect();
Ok(fields)
}
fn build_proxy_to_dto_assignments(struct_item: &syn::ItemStruct) -> syn::Result<Vec<TokenStream>> {
let syn::Fields::Named(fields_named) = &struct_item.fields else {
return Err(syn::Error::new_spanned(
struct_item,
"same-file relation override DTO must be a named-field struct",
));
};
let assignments = fields_named
.named
.iter()
.filter_map(|field| {
field
.ident
.as_ref()
.map(|ident| quote! { #ident: proxy.#ident })
})
.collect();
Ok(assignments)
}
fn build_clone_assignments(struct_item: &syn::ItemStruct) -> syn::Result<Vec<TokenStream>> {
let syn::Fields::Named(fields_named) = &struct_item.fields else {
return Err(syn::Error::new_spanned(
struct_item,
"same-file relation override DTO must be a named-field struct",
));
};
let assignments = fields_named
.named
.iter()
.filter_map(|field| {
field.ident.as_ref().map(|ident| {
quote! { #ident: self.#ident.clone() }
})
})
.collect();
Ok(assignments)
}
fn maybe_generate_same_file_relation_override(
new_type_name: &syn::Ident,
field_name: &str,
rel_info: &RelationFieldInfo,
schema_storage: &HashMap<String, StructMetadata>,
) -> syn::Result<Option<(TokenStream, TokenStream)>> {
let response_base = derive_response_base_name(&new_type_name.to_string());
let dto_name = format!("{}In{}", snake_to_pascal_case(field_name), response_base);
let Some(dto_meta) = find_same_file_struct_metadata(&dto_name, schema_storage) else {
return Ok(None);
};
let dto_struct: syn::ItemStruct = file_cache::parse_struct_cached(&dto_meta.definition)
.map_err(|e| syn::Error::new(proc_macro2::Span::call_site(), e.to_string()))?;
let dto_ident = syn::Ident::new(&dto_name, proc_macro2::Span::call_site());
let wrapper_ident = syn::Ident::new(
&format!(
"__Vespera{}{}Relation",
new_type_name,
snake_to_pascal_case(field_name)
),
proc_macro2::Span::call_site(),
);
let proxy_ident = syn::Ident::new(
&format!(
"__Vespera{}{}Proxy",
new_type_name,
snake_to_pascal_case(field_name)
),
proc_macro2::Span::call_site(),
);
let schema_ref_name = schema_component_name_from_path(&rel_info.schema_path);
let dto_serde_attrs: Vec<_> = dto_struct
.attrs
.iter()
.filter(|attr| attr.path().is_ident("serde"))
.collect();
let dto_doc_attrs: Vec<_> = dto_struct
.attrs
.iter()
.filter(|attr| attr.path().is_ident("doc"))
.collect();
let proxy_fields = build_proxy_fields(&dto_struct)?;
let proxy_to_dto = build_proxy_to_dto_assignments(&dto_struct)?;
let clone_assignments = build_clone_assignments(&dto_struct)?;
let Some(model_ty) = related_model_type_from_schema_path(&rel_info.schema_path) else {
return Ok(None);
};
let source_expr = quote! { source };
let from_model_assignments = build_named_struct_field_assignments(&dto_struct, &source_expr)?;
let clone_impl = if has_derive(&dto_struct, "Clone") {
quote! {}
} else {
quote! {
impl Clone for #dto_ident {
fn clone(&self) -> Self {
Self {
#(#clone_assignments),*
}
}
}
}
};
let deserialize_impl = if has_derive(&dto_struct, "Deserialize") {
quote! {}
} else {
quote! {
#[derive(serde::Deserialize)]
#(#dto_serde_attrs)*
struct #proxy_ident {
#(#proxy_fields),*
}
impl<'de> serde::Deserialize<'de> for #dto_ident {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let proxy = #proxy_ident::deserialize(deserializer)?;
Ok(Self {
#(#proxy_to_dto),*
})
}
}
}
};
let helpers = quote! {
#clone_impl
#deserialize_impl
impl From<#model_ty> for #dto_ident {
fn from(source: #model_ty) -> Self {
Self {
#(#from_model_assignments),*
}
}
}
#(#dto_doc_attrs)*
#[derive(serde::Serialize, serde::Deserialize, Clone, vespera::Schema)]
#[serde(transparent)]
#[schema(ref = #schema_ref_name, nullable)]
struct #wrapper_ident(pub Option<#dto_ident>);
impl From<Option<#model_ty>> for #wrapper_ident {
fn from(source: Option<#model_ty>) -> Self {
Self(source.map(Into::into))
}
}
};
Ok(Some((quote! { #wrapper_ident }, helpers)))
}
pub fn generate_schema_code(
input: &SchemaInput,
schema_storage: &HashMap<String, StructMetadata>,
) -> Result<TokenStream, syn::Error> {
let type_name = extract_type_name(&input.ty)?;
let struct_def = schema_storage.get(&type_name).ok_or_else(|| syn::Error::new_spanned(&input.ty, format!("type `{type_name}` not found. Make sure it has #[derive(Schema)] before this macro invocation")))?;
let parsed_struct: syn::ItemStruct = file_cache::parse_struct_cached(&struct_def.definition)
.map_err(|e| {
syn::Error::new_spanned(
&input.ty,
format!("failed to parse struct definition for `{type_name}`: {e}"),
)
})?;
let omit_set: HashSet<String> = input.omit.iter().flatten().cloned().collect();
let pick_set: HashSet<String> = input.pick.iter().flatten().cloned().collect();
let schema_tokens =
generate_filtered_schema(&parsed_struct, &omit_set, &pick_set, schema_storage);
Ok(schema_tokens)
}
#[allow(clippy::too_many_lines)]
pub fn generate_schema_type_code(
input: &SchemaTypeInput,
schema_storage: &HashMap<String, StructMetadata>,
) -> Result<(TokenStream, Option<StructMetadata>), syn::Error> {
let source_type_name = extract_type_name(&input.source_type)?;
let mut source_module_path = extract_module_path(&input.source_type);
let struct_def_owned: StructMetadata;
let schema_name_hint = input.schema_name.as_deref();
let struct_def = if is_qualified_path(&input.source_type) {
if let Some(found) = schema_storage.get(&source_type_name) {
found
} else if let Some((found, module_path)) =
find_struct_from_path(&input.source_type, schema_name_hint)
{
struct_def_owned = found;
source_module_path = module_path;
&struct_def_owned
} else {
return Err(syn::Error::new_spanned(
&input.source_type,
format!(
"type `{source_type_name}` not found. Either:\n\
1. Use #[derive(Schema)] in the same file\n\
2. Use full module path like `crate::models::memo::Model` to reference a struct from another file"
),
));
}
} else {
if let Some(found) = schema_storage.get(&source_type_name) {
found
} else if let Some((found, module_path)) =
find_struct_from_path(&input.source_type, schema_name_hint)
{
struct_def_owned = found;
source_module_path = module_path;
&struct_def_owned
} else {
return Err(syn::Error::new_spanned(
&input.source_type,
format!(
"type `{source_type_name}` not found. Either:\n\
1. Use #[derive(Schema)] in the same file\n\
2. Use full module path like `crate::models::memo::Model` to reference a struct from another file\n\
3. If using `name = \"XxxSchema\"`, ensure the file name matches (e.g., xxx.rs)"
),
));
}
};
let parsed_struct: syn::ItemStruct = file_cache::parse_struct_cached(&struct_def.definition)
.map_err(|e| {
syn::Error::new_spanned(
&input.source_type,
format!("failed to parse struct definition for `{source_type_name}`: {e}"),
)
})?;
let source_field_names = extract_source_field_names(&parsed_struct);
validate_pick_fields(
input.pick.as_ref(),
&source_field_names,
&input.source_type,
&source_type_name,
)?;
validate_omit_fields(
input.omit.as_ref(),
&source_field_names,
&input.source_type,
&source_type_name,
)?;
validate_rename_fields(
input.rename.as_ref(),
&source_field_names,
&input.source_type,
&source_type_name,
)?;
let partial_fields_to_validate = match &input.partial {
Some(PartialMode::Fields(fields)) => Some(fields),
_ => None,
};
validate_partial_fields(
partial_fields_to_validate,
&source_field_names,
&input.source_type,
&source_type_name,
)?;
let omit_set = build_omit_set(input.omit.as_ref());
let pick_set = build_pick_set(input.pick.as_ref());
let (partial_all, partial_set) = build_partial_config(&input.partial);
let rename_map = build_rename_map(input.rename.as_ref());
let serde_attrs_without_rename_all =
extract_serde_attrs_without_rename_all(&parsed_struct.attrs);
let struct_doc_attrs = extract_doc_attrs(&parsed_struct.attrs);
let effective_rename_all =
determine_rename_all(input.rename_all.as_ref(), &parsed_struct.attrs);
let is_source_seaorm_model = is_seaorm_model(&parsed_struct);
let new_type_name = &input.new_type;
let mut field_tokens = Vec::new();
let mut field_mappings: Vec<(syn::Ident, syn::Ident, bool, bool)> = Vec::new();
let mut relation_fields: Vec<RelationFieldInfo> = Vec::new();
let mut inline_type_definitions: Vec<TokenStream> = Vec::new();
let mut default_functions: Vec<TokenStream> = Vec::new();
let mut relation_override_helpers: Vec<TokenStream> = Vec::new();
if let syn::Fields::Named(fields_named) = &parsed_struct.fields {
for field in &fields_named.named {
let rust_field_name = field.ident.as_ref().map_or_else(
|| "unknown".to_string(),
|i| strip_raw_prefix_owned(i.to_string()),
);
if should_skip_field(&rust_field_name, &omit_set, &pick_set) {
continue;
}
if input.omit_default
&& (extract_sea_orm_default_value(&field.attrs).is_some()
|| has_sea_orm_primary_key(&field.attrs))
{
continue;
}
let is_relation = is_seaorm_relation_type(&field.ty);
if input.multipart && is_relation {
continue;
}
let original_ty = &field.ty;
let should_wrap_option = should_wrap_in_option(
&rust_field_name,
partial_all,
&partial_set,
is_option_type(original_ty),
is_relation,
);
let (field_ty, relation_info): (Box<dyn quote::ToTokens>, Option<RelationFieldInfo>) =
if is_relation {
if let Some((converted, mut rel_info)) =
convert_relation_type_to_schema_with_info(
original_ty,
&field.attrs,
&parsed_struct,
&source_module_path,
field.ident.clone().unwrap(),
)
{
if rel_info.relation_type == "HasMany" {
if !pick_set.contains(&rust_field_name) {
continue;
}
if let Some(inline_type) = generate_inline_relation_type_no_relations(
new_type_name,
&rel_info,
&source_module_path,
input.schema_name.as_deref(),
) {
let inline_type_def = generate_inline_type_definition(&inline_type);
inline_type_definitions.push(inline_type_def);
let inline_type_name = &inline_type.type_name;
let included_fields: Vec<String> = inline_type
.fields
.iter()
.map(|f| f.name.to_string())
.collect();
rel_info.inline_type_info =
Some((inline_type.type_name.clone(), included_fields));
let inline_field_ty = quote! { Vec<#inline_type_name> };
(Box::new(inline_field_ty), Some(rel_info))
} else {
continue;
}
} else {
if input.add.is_some()
&& let Some((override_field_ty, helper_tokens)) =
maybe_generate_same_file_relation_override(
new_type_name,
&rust_field_name,
&rel_info,
schema_storage,
)?
{
relation_override_helpers.push(helper_tokens);
(Box::new(override_field_ty), Some(rel_info))
} else
if let Some(inline_type) = generate_inline_relation_type(
new_type_name,
&rel_info,
&source_module_path,
input.schema_name.as_deref(),
) {
let inline_type_def = generate_inline_type_definition(&inline_type);
inline_type_definitions.push(inline_type_def);
let inline_type_name = &inline_type.type_name;
let circular_fields: Vec<String> = inline_type
.fields
.iter()
.map(|f| f.name.to_string())
.collect();
rel_info.inline_type_info =
Some((inline_type.type_name.clone(), circular_fields));
let inline_field_ty = if rel_info.is_optional {
quote! { Option<Box<#inline_type_name>> }
} else {
quote! { Box<#inline_type_name> }
};
(Box::new(inline_field_ty), Some(rel_info))
} else {
(Box::new(converted), Some(rel_info))
}
}
} else {
continue;
}
} else {
let converted_ty = convert_type_with_chrono(original_ty, &source_module_path);
if should_wrap_option {
(Box::new(quote! { Option<#converted_ty> }), None)
} else {
(Box::new(converted_ty), None)
}
};
relation_fields.extend(relation_info);
let vis: &syn::Visibility = &field.vis;
let source_field_ident: syn::Ident = field.ident.clone().unwrap();
let doc_attrs = extract_doc_attrs(&field.attrs);
if input.multipart {
let form_data_attrs = extract_form_data_attrs(&field.attrs);
if let Some(new_name) = rename_map.get(&rust_field_name) {
let new_field_ident =
syn::Ident::new(new_name, field.ident.as_ref().unwrap().span());
field_tokens.push(quote! {
#(#doc_attrs)*
#(#form_data_attrs)*
#vis #new_field_ident: #field_ty
});
field_mappings.push((
new_field_ident,
source_field_ident,
should_wrap_option,
is_relation,
));
} else {
let field_ident = field.ident.clone().unwrap();
field_tokens.push(quote! {
#(#doc_attrs)*
#(#form_data_attrs)*
#vis #field_ident: #field_ty
});
field_mappings.push((
field_ident.clone(),
field_ident,
should_wrap_option,
is_relation,
));
}
} else {
let serde_field_attrs = extract_field_serde_attrs(&field.attrs);
let (serde_default_attr, schema_default_attr): (
proc_macro2::TokenStream,
proc_macro2::TokenStream,
) = generate_sea_orm_default_attrs(
&field.attrs,
new_type_name,
&rust_field_name,
original_ty,
&field_ty,
should_wrap_option || is_option_type(original_ty),
&mut default_functions,
);
if let Some(new_name) = rename_map.get(&rust_field_name) {
let new_field_ident: syn::Ident =
syn::Ident::new(new_name, field.ident.as_ref().unwrap().span());
let filtered_attrs = filter_out_serde_rename(&serde_field_attrs);
let json_name = extract_field_rename(&field.attrs)
.unwrap_or_else(|| rust_field_name.clone());
field_tokens.push(quote! {
#(#doc_attrs)*
#(#filtered_attrs)*
#serde_default_attr
#schema_default_attr
#[serde(rename = #json_name)]
#vis #new_field_ident: #field_ty
});
field_mappings.push((
new_field_ident,
source_field_ident,
should_wrap_option,
is_relation,
));
} else {
let field_ident = field.ident.clone().unwrap();
field_tokens.push(quote! {
#(#doc_attrs)*
#(#serde_field_attrs)*
#serde_default_attr
#schema_default_attr
#vis #field_ident: #field_ty
});
field_mappings.push((
field_ident.clone(),
field_ident,
should_wrap_option,
is_relation,
));
}
}
}
}
for (field_name, field_ty) in input.add.iter().flatten() {
let field_ident: syn::Ident = syn::Ident::new(field_name, proc_macro2::Span::call_site());
field_tokens.push(quote! {
pub #field_ident: #field_ty
});
}
let derive_clone: bool = if input.multipart {
false
} else {
input.derive_clone
};
let clone_derive: proc_macro2::TokenStream = if derive_clone {
quote! { Clone, }
} else {
quote! {}
};
let schema_derive: proc_macro2::TokenStream;
let schema_name_attr: proc_macro2::TokenStream;
if input.ignore_schema {
schema_derive = quote! {};
schema_name_attr = quote! {};
} else if let Some(ref name) = input.schema_name {
schema_derive = quote! { vespera::Schema };
schema_name_attr = quote! { #[schema(name = #name)] };
} else {
schema_derive = quote! { vespera::Schema };
schema_name_attr = quote! {};
}
let has_relation_fields = field_mappings.iter().any(|(_, _, _, is_rel)| *is_rel);
let source_type: &syn::Type = &input.source_type;
let (from_impl, from_model_impl) = if input.multipart {
(quote! {}, quote! {})
} else {
let from_impl = if input.add.is_none() && !has_relation_fields {
let field_assignments: Vec<_> = field_mappings
.iter()
.map(|(new_ident, source_ident, wrapped, _is_relation)| {
if *wrapped {
quote! { #new_ident: Some(source.#source_ident) }
} else {
quote! { #new_ident: source.#source_ident }
}
})
.collect();
quote! {
impl From<#source_type> for #new_type_name {
fn from(source: #source_type) -> Self {
Self {
#(#field_assignments),*
}
}
}
}
} else {
quote! {}
};
let from_model_impl =
if is_source_seaorm_model && input.add.is_none() && has_relation_fields {
generate_from_model_with_relations(
new_type_name,
source_type,
&field_mappings,
&relation_fields,
&source_module_path,
schema_storage,
)
} else {
quote! {}
};
(from_impl, from_model_impl)
};
let generated_tokens: proc_macro2::TokenStream = if input.multipart {
quote! {
#(#inline_type_definitions)*
#(#struct_doc_attrs)*
#[derive(vespera::Multipart, #clone_derive #schema_derive)]
#schema_name_attr
#[serde(rename_all = #effective_rename_all)]
pub struct #new_type_name {
#(#field_tokens),*
}
}
} else {
quote! {
#(#inline_type_definitions)*
#(#relation_override_helpers)*
#(#default_functions)*
#(#struct_doc_attrs)*
#[derive(serde::Serialize, serde::Deserialize, #clone_derive #schema_derive)]
#schema_name_attr
#[serde(rename_all = #effective_rename_all)]
#(#serde_attrs_without_rename_all)*
pub struct #new_type_name {
#(#field_tokens),*
}
#from_impl
#from_model_impl
}
};
let metadata = input.schema_name.as_ref().map(|custom_name| {
let struct_def = quote! {
#[serde(rename_all = #effective_rename_all)]
#(#serde_attrs_without_rename_all)*
pub struct #new_type_name {
#(#field_tokens),*
}
};
StructMetadata::new(custom_name.clone(), struct_def.to_string())
});
Ok((generated_tokens, metadata))
}
fn generate_sea_orm_default_attrs(
original_attrs: &[syn::Attribute],
struct_name: &syn::Ident,
field_name: &str,
original_ty: &syn::Type,
field_ty: &dyn quote::ToTokens,
is_optional_or_partial: bool,
default_functions: &mut Vec<TokenStream>,
) -> (TokenStream, TokenStream) {
if is_optional_or_partial {
return (quote! {}, quote! {});
}
let default_value = extract_sea_orm_default_value(original_attrs);
let has_pk = has_sea_orm_primary_key(original_attrs);
if default_value.is_none() && !has_pk {
return (quote! {}, quote! {});
}
let has_existing_serde_default = extract_default(original_attrs).is_some();
match &default_value {
Some(value) if !is_sql_function_default(value) => {
let schema_default_attr = quote! { #[schema(default = #value)] };
if has_existing_serde_default {
return (quote! {}, schema_default_attr);
}
if !is_parseable_type(original_ty) {
return (quote! {}, schema_default_attr);
}
let fn_name = format!("default_{struct_name}_{field_name}");
let fn_ident = syn::Ident::new(&fn_name, proc_macro2::Span::call_site());
default_functions.push(quote! {
#[allow(non_snake_case)]
fn #fn_ident() -> #field_ty {
#value.parse().unwrap()
}
});
let serde_default_attr = quote! { #[serde(default = #fn_name)] };
(serde_default_attr, schema_default_attr)
}
_ => {
let Some((default_expr, schema_default_str)) =
sql_function_default_for_type(original_ty)
else {
return (quote! {}, quote! {});
};
let schema_default_attr = quote! { #[schema(default = #schema_default_str)] };
if has_existing_serde_default {
return (quote! {}, schema_default_attr);
}
let fn_name = format!("default_{struct_name}_{field_name}");
let fn_ident = syn::Ident::new(&fn_name, proc_macro2::Span::call_site());
default_functions.push(quote! {
#[allow(non_snake_case)]
fn #fn_ident() -> #field_ty {
#default_expr
}
});
let serde_default_attr = quote! { #[serde(default = #fn_name)] };
(serde_default_attr, schema_default_attr)
}
}
}
fn sql_function_default_for_type(original_ty: &syn::Type) -> Option<(TokenStream, String)> {
let syn::Type::Path(type_path) = original_ty else {
return None;
};
let segment = type_path.path.segments.last()?;
let type_name = segment.ident.to_string();
match type_name.as_str() {
"DateTimeWithTimeZone" | "DateTimeUtc" | "DateTime" => {
let expr = quote! {
vespera::chrono::DateTime::<vespera::chrono::Utc>::UNIX_EPOCH.fixed_offset()
};
Some((expr, "1970-01-01T00:00:00+00:00".to_string()))
}
"NaiveDateTime" => {
let expr = quote! {
vespera::chrono::NaiveDateTime::UNIX_EPOCH
};
Some((expr, "1970-01-01T00:00:00".to_string()))
}
"NaiveDate" => {
let expr = quote! {
vespera::chrono::NaiveDate::default()
};
Some((expr, "1970-01-01".to_string()))
}
"NaiveTime" | "Time" => {
let expr = quote! {
vespera::chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap()
};
Some((expr, "00:00:00".to_string()))
}
"Uuid" => Some((
quote! { Default::default() },
"00000000-0000-0000-0000-000000000000".to_string(),
)),
"i8" | "i16" | "i32" | "i64" | "i128" | "isize" | "u8" | "u16" | "u32" | "u64" | "u128"
| "usize" | "f32" | "f64" | "Decimal" => {
Some((quote! { Default::default() }, "0".to_string()))
}
"bool" => Some((quote! { Default::default() }, "false".to_string())),
"String" => Some((quote! { Default::default() }, String::new())),
_ => None,
}
}
fn is_parseable_type(ty: &syn::Type) -> bool {
let syn::Type::Path(type_path) = ty else {
return false;
};
let Some(segment) = type_path.path.segments.last() else {
return false;
};
type_utils::PRIMITIVE_TYPE_NAMES.contains(&segment.ident.to_string().as_str())
}
#[cfg(test)]
mod tests;