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::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::{
extract_module_path, extract_type_name, is_option_type, is_qualified_path, is_seaorm_model,
is_seaorm_relation_type,
};
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},
};
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();
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 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)
}
};
if let Some(info) = relation_info {
relation_fields.push(info);
}
let vis = &field.vis;
let source_field_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) = 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::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,
));
}
}
}
}
if let Some(ref add_fields) = input.add {
for (field_name, field_ty) in add_fields {
let field_ident = syn::Ident::new(field_name, proc_macro2::Span::call_site());
field_tokens.push(quote! {
pub #field_ident: #field_ty
});
}
}
let derive_clone = if input.multipart {
false
} else {
input.derive_clone
};
let clone_derive = if derive_clone {
quote! { Clone, }
} else {
quote! {}
};
let (schema_derive, schema_name_attr) = if input.ignore_schema {
(quote! {}, quote! {})
} else if let Some(ref name) = input.schema_name {
(
quote! { vespera::Schema },
quote! { #[schema(name = #name)] },
)
} else {
(quote! { vespera::Schema }, quote! {})
};
let has_relation_fields = field_mappings.iter().any(|(_, _, _, is_rel)| *is_rel);
let source_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 = 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)*
#(#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;