use proc_macro2::TokenStream;
use quote::quote;
use std::collections::HashSet;
use std::path::Path;
use syn::punctuated::Punctuated;
use syn::{Ident, LitStr, Token, Type, bracketed, parenthesized, parse::Parse, parse::ParseStream};
use crate::metadata::StructMetadata;
use crate::parser::{
extract_default, extract_field_rename, extract_rename_all, extract_skip,
extract_skip_serializing_if, parse_type_to_schema_ref, rename_field, strip_raw_prefix,
};
use vespera_core::schema::{Schema, SchemaRef, SchemaType};
pub struct SchemaInput {
pub ty: Type,
pub omit: Option<Vec<String>>,
pub pick: Option<Vec<String>>,
}
impl Parse for SchemaInput {
fn parse(input: ParseStream) -> syn::Result<Self> {
let ty: Type = input.parse()?;
let mut omit = None;
let mut pick = None;
while input.peek(Token![,]) {
input.parse::<Token![,]>()?;
if input.is_empty() {
break;
}
let ident: Ident = input.parse()?;
let ident_str = ident.to_string();
match ident_str.as_str() {
"omit" => {
input.parse::<Token![=]>()?;
let content;
let _ = bracketed!(content in input);
let fields: Punctuated<LitStr, Token![,]> =
content.parse_terminated(|input| input.parse::<LitStr>(), Token![,])?;
omit = Some(fields.into_iter().map(|s| s.value()).collect());
}
"pick" => {
input.parse::<Token![=]>()?;
let content;
let _ = bracketed!(content in input);
let fields: Punctuated<LitStr, Token![,]> =
content.parse_terminated(|input| input.parse::<LitStr>(), Token![,])?;
pick = Some(fields.into_iter().map(|s| s.value()).collect());
}
_ => {
return Err(syn::Error::new(
ident.span(),
format!(
"unknown parameter: `{}`. Expected `omit` or `pick`",
ident_str
),
));
}
}
}
if omit.is_some() && pick.is_some() {
return Err(syn::Error::new(
input.span(),
"cannot use both `omit` and `pick` in the same schema! invocation",
));
}
Ok(SchemaInput { ty, omit, pick })
}
}
pub fn generate_schema_code(
input: &SchemaInput,
schema_storage: &[StructMetadata],
) -> Result<TokenStream, syn::Error> {
let type_name = extract_type_name(&input.ty)?;
let struct_def = schema_storage
.iter()
.find(|s| s.name == type_name)
.ok_or_else(|| {
syn::Error::new_spanned(
&input.ty,
format!(
"type `{}` not found. Make sure it has #[derive(Schema)] before this macro invocation",
type_name
),
)
})?;
let parsed_struct: syn::ItemStruct = syn::parse_str(&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.clone().unwrap_or_default().into_iter().collect();
let pick_set: HashSet<String> = input.pick.clone().unwrap_or_default().into_iter().collect();
let schema_tokens =
generate_filtered_schema(&parsed_struct, &omit_set, &pick_set, schema_storage)?;
Ok(schema_tokens)
}
fn extract_type_name(ty: &Type) -> Result<String, syn::Error> {
match ty {
Type::Path(type_path) => {
let segment = type_path.path.segments.last().ok_or_else(|| {
syn::Error::new_spanned(ty, "expected a type path with at least one segment")
})?;
Ok(segment.ident.to_string())
}
_ => Err(syn::Error::new_spanned(
ty,
"expected a type path (e.g., `User` or `crate::User`)",
)),
}
}
fn is_qualified_path(ty: &Type) -> bool {
match ty {
Type::Path(type_path) => type_path.path.segments.len() > 1,
_ => false,
}
}
fn is_seaorm_relation_type(ty: &Type) -> bool {
match ty {
Type::Path(type_path) => {
if let Some(segment) = type_path.path.segments.last() {
let ident = segment.ident.to_string();
matches!(ident.as_str(), "HasOne" | "HasMany" | "BelongsTo")
} else {
false
}
}
_ => false,
}
}
fn is_seaorm_model(struct_item: &syn::ItemStruct) -> bool {
for attr in &struct_item.attrs {
let path = attr.path();
if path.is_ident("sea_orm") {
return true;
}
let segments: Vec<_> = path.segments.iter().map(|s| s.ident.to_string()).collect();
if segments.first().is_some_and(|s| s == "sea_orm") {
return true;
}
}
false
}
fn is_primitive_or_known_type(name: &str) -> bool {
matches!(
name,
"bool"
| "char"
| "str"
| "i8"
| "i16"
| "i32"
| "i64"
| "i128"
| "isize"
| "u8"
| "u16"
| "u32"
| "u64"
| "u128"
| "usize"
| "f32"
| "f64"
| "String"
| "Vec"
| "Option"
| "Result"
| "Box"
| "Rc"
| "Arc"
| "HashMap"
| "HashSet"
| "BTreeMap"
| "BTreeSet"
| "DateTime"
| "NaiveDateTime"
| "NaiveDate"
| "NaiveTime"
| "Utc"
| "Local"
| "FixedOffset"
| "DateTimeWithTimeZone"
| "DateTimeUtc"
| "DateTimeLocal"
| "Uuid"
| "Value"
)
}
fn resolve_type_to_absolute_path(ty: &Type, source_module_path: &[String]) -> TokenStream {
let type_path = match ty {
Type::Path(tp) => tp,
_ => return quote! { #ty },
};
if type_path.path.segments.len() > 1 {
return quote! { #ty };
}
let segment = match type_path.path.segments.first() {
Some(s) => s,
None => return quote! { #ty },
};
let ident_str = segment.ident.to_string();
if is_primitive_or_known_type(&ident_str) {
return quote! { #ty };
}
if source_module_path.is_empty() {
return quote! { #ty };
}
let path_idents: Vec<syn::Ident> = source_module_path
.iter()
.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 }
}
fn convert_seaorm_type_to_chrono(ty: &Type, source_module_path: &[String]) -> TokenStream {
let type_path = match ty {
Type::Path(tp) => tp,
_ => return quote! { #ty },
};
let segment = match type_path.path.segments.last() {
Some(s) => s,
None => return quote! { #ty },
};
let ident_str = segment.ident.to_string();
match ident_str.as_str() {
"DateTimeWithTimeZone" => {
quote! { vespera::chrono::DateTime<vespera::chrono::FixedOffset> }
}
"DateTimeUtc" => quote! { vespera::chrono::DateTime<vespera::chrono::Utc> },
"DateTimeLocal" => quote! { vespera::chrono::DateTime<vespera::chrono::Local> },
_ => resolve_type_to_absolute_path(ty, source_module_path),
}
}
fn convert_type_with_chrono(ty: &Type, source_module_path: &[String]) -> TokenStream {
if let Type::Path(type_path) = ty
&& let Some(segment) = type_path.path.segments.first()
&& segment.ident == "Option"
{
if let syn::PathArguments::AngleBracketed(args) = &segment.arguments
&& let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first()
{
let converted_inner = convert_seaorm_type_to_chrono(inner_ty, source_module_path);
return quote! { Option<#converted_inner> };
}
}
if let Type::Path(type_path) = ty
&& let Some(segment) = type_path.path.segments.first()
&& segment.ident == "Vec"
{
if let syn::PathArguments::AngleBracketed(args) = &segment.arguments
&& let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first()
{
let converted_inner = convert_seaorm_type_to_chrono(inner_ty, source_module_path);
return quote! { Vec<#converted_inner> };
}
}
convert_seaorm_type_to_chrono(ty, source_module_path)
}
#[derive(Clone)]
struct RelationFieldInfo {
field_name: syn::Ident,
relation_type: String,
schema_path: TokenStream,
is_optional: bool,
inline_type_info: Option<(syn::Ident, Vec<String>)>,
}
fn extract_belongs_to_from_field(attrs: &[syn::Attribute]) -> Option<String> {
for attr in attrs {
if attr.path().is_ident("sea_orm") {
let mut from_field = None;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("from")
&& let Ok(value) = meta.value()
&& let Ok(lit) = value.parse::<syn::LitStr>()
{
from_field = Some(lit.value());
}
Ok(())
});
if from_field.is_some() {
return from_field;
}
}
}
None
}
fn is_field_optional_in_struct(struct_item: &syn::ItemStruct, field_name: &str) -> bool {
if let syn::Fields::Named(fields_named) = &struct_item.fields {
for field in &fields_named.named {
if let Some(ident) = &field.ident
&& ident == field_name
{
return is_option_type(&field.ty);
}
}
}
false
}
fn convert_relation_type_to_schema_with_info(
ty: &Type,
field_attrs: &[syn::Attribute],
parsed_struct: &syn::ItemStruct,
source_module_path: &[String],
field_name: syn::Ident,
) -> Option<(TokenStream, RelationFieldInfo)> {
let type_path = match ty {
Type::Path(tp) => tp,
_ => return None,
};
let segment = type_path.path.segments.last()?;
let ident_str = segment.ident.to_string();
let args = match &segment.arguments {
syn::PathArguments::AngleBracketed(args) => args,
_ => return None,
};
let inner_ty = match args.args.first()? {
syn::GenericArgument::Type(ty) => ty,
_ => return None,
};
let inner_path = match inner_ty {
Type::Path(tp) => tp,
_ => return None,
};
let segments: Vec<String> = inner_path
.path
.segments
.iter()
.map(|s| s.ident.to_string())
.collect();
let absolute_segments: Vec<String> = if !segments.is_empty() && segments[0] == "super" {
let super_count = segments.iter().take_while(|s| *s == "super").count();
let parent_path_len = source_module_path.len().saturating_sub(super_count);
let mut abs = source_module_path[..parent_path_len].to_vec();
for seg in segments.iter().skip(super_count) {
if seg == "Entity" {
abs.push("Schema".to_string());
} else {
abs.push(seg.clone());
}
}
abs
} else if !segments.is_empty() && segments[0] == "crate" {
segments
.iter()
.map(|s| {
if s == "Entity" {
"Schema".to_string()
} else {
s.clone()
}
})
.collect()
} else {
let parent_path_len = source_module_path.len().saturating_sub(1);
let mut abs = source_module_path[..parent_path_len].to_vec();
for seg in &segments {
if seg == "Entity" {
abs.push("Schema".to_string());
} else {
abs.push(seg.clone());
}
}
abs
};
let path_idents: Vec<syn::Ident> = absolute_segments
.iter()
.map(|s| syn::Ident::new(s, proc_macro2::Span::call_site()))
.collect();
let schema_path = quote! { #(#path_idents)::* };
match ident_str.as_str() {
"HasOne" => {
let fk_field = extract_belongs_to_from_field(field_attrs);
let is_optional = fk_field
.as_ref()
.map(|f| is_field_optional_in_struct(parsed_struct, f))
.unwrap_or(true);
let converted = if is_optional {
quote! { Option<Box<#schema_path>> }
} else {
quote! { Box<#schema_path> }
};
let info = RelationFieldInfo {
field_name,
relation_type: "HasOne".to_string(),
schema_path: schema_path.clone(),
is_optional,
inline_type_info: None, };
Some((converted, info))
}
"HasMany" => {
let converted = quote! { Vec<#schema_path> };
let info = RelationFieldInfo {
field_name,
relation_type: "HasMany".to_string(),
schema_path: schema_path.clone(),
is_optional: false,
inline_type_info: None, };
Some((converted, info))
}
"BelongsTo" => {
let fk_field = extract_belongs_to_from_field(field_attrs);
let is_optional = fk_field
.as_ref()
.map(|f| is_field_optional_in_struct(parsed_struct, f))
.unwrap_or(true);
let converted = if is_optional {
quote! { Option<Box<#schema_path>> }
} else {
quote! { Box<#schema_path> }
};
let info = RelationFieldInfo {
field_name,
relation_type: "BelongsTo".to_string(),
schema_path: schema_path.clone(),
is_optional,
inline_type_info: None, };
Some((converted, info))
}
_ => None,
}
}
#[allow(dead_code)]
fn convert_relation_type_to_schema(
ty: &Type,
field_attrs: &[syn::Attribute],
parsed_struct: &syn::ItemStruct,
source_module_path: &[String],
) -> Option<TokenStream> {
let type_path = match ty {
Type::Path(tp) => tp,
_ => return None,
};
let segment = type_path.path.segments.last()?;
let ident_str = segment.ident.to_string();
let args = match &segment.arguments {
syn::PathArguments::AngleBracketed(args) => args,
_ => return None,
};
let inner_ty = match args.args.first()? {
syn::GenericArgument::Type(ty) => ty,
_ => return None,
};
let inner_path = match inner_ty {
Type::Path(tp) => tp,
_ => return None,
};
let segments: Vec<String> = inner_path
.path
.segments
.iter()
.map(|s| s.ident.to_string())
.collect();
let absolute_segments: Vec<String> = if !segments.is_empty() && segments[0] == "super" {
let super_count = segments.iter().take_while(|s| *s == "super").count();
let parent_path_len = source_module_path.len().saturating_sub(super_count);
let mut abs = source_module_path[..parent_path_len].to_vec();
for seg in segments.iter().skip(super_count) {
if seg == "Entity" {
abs.push("Schema".to_string());
} else {
abs.push(seg.clone());
}
}
abs
} else if !segments.is_empty() && segments[0] == "crate" {
segments
.iter()
.map(|s| {
if s == "Entity" {
"Schema".to_string()
} else {
s.clone()
}
})
.collect()
} else {
let parent_path_len = source_module_path.len().saturating_sub(1);
let mut abs = source_module_path[..parent_path_len].to_vec();
for seg in &segments {
if seg == "Entity" {
abs.push("Schema".to_string());
} else {
abs.push(seg.clone());
}
}
abs
};
let path_idents: Vec<syn::Ident> = absolute_segments
.iter()
.map(|s| syn::Ident::new(s, proc_macro2::Span::call_site()))
.collect();
let schema_path = quote! { #(#path_idents)::* };
match ident_str.as_str() {
"HasOne" => {
Some(quote! { Option<Box<#schema_path>> })
}
"HasMany" => {
Some(quote! { Vec<#schema_path> })
}
"BelongsTo" => {
if let Some(from_field) = extract_belongs_to_from_field(field_attrs) {
if is_field_optional_in_struct(parsed_struct, &from_field) {
Some(quote! { Option<Box<#schema_path>> })
} else {
Some(quote! { Box<#schema_path> })
}
} else {
Some(quote! { Option<Box<#schema_path>> })
}
}
_ => None,
}
}
fn generate_filtered_schema(
struct_item: &syn::ItemStruct,
omit_set: &HashSet<String>,
pick_set: &HashSet<String>,
schema_storage: &[StructMetadata],
) -> Result<TokenStream, syn::Error> {
let rename_all = extract_rename_all(&struct_item.attrs);
let known_schemas: std::collections::HashMap<String, String> = schema_storage
.iter()
.map(|s| (s.name.clone(), s.definition.clone()))
.collect();
let struct_definitions = known_schemas.clone();
let mut property_tokens = Vec::new();
let mut required_fields = Vec::new();
if let syn::Fields::Named(fields_named) = &struct_item.fields {
for field in &fields_named.named {
if extract_skip(&field.attrs) {
continue;
}
let rust_field_name = field
.ident
.as_ref()
.map(|i| strip_raw_prefix(&i.to_string()).to_string())
.unwrap_or_else(|| "unknown".to_string());
let field_name = if let Some(renamed) = extract_field_rename(&field.attrs) {
renamed
} else {
rename_field(&rust_field_name, rename_all.as_deref())
};
if !omit_set.is_empty()
&& (omit_set.contains(&rust_field_name) || omit_set.contains(&field_name))
{
continue;
}
if !pick_set.is_empty()
&& !pick_set.contains(&rust_field_name)
&& !pick_set.contains(&field_name)
{
continue;
}
let field_type = &field.ty;
let schema_ref =
parse_type_to_schema_ref(field_type, &known_schemas, &struct_definitions);
let schema_ref_tokens = schema_ref_to_tokens(&schema_ref);
property_tokens.push(quote! {
properties.insert(#field_name.to_string(), #schema_ref_tokens);
});
let has_default = extract_default(&field.attrs).is_some();
let has_skip_serializing_if = extract_skip_serializing_if(&field.attrs);
let is_optional = is_option_type(field_type);
if !is_optional && !has_default && !has_skip_serializing_if {
required_fields.push(field_name.clone());
}
}
}
let required_tokens = if required_fields.is_empty() {
quote! { None }
} else {
let required_strs: Vec<&str> = required_fields.iter().map(|s| s.as_str()).collect();
quote! { Some(vec![#(#required_strs.to_string()),*]) }
};
Ok(quote! {
{
let mut properties = std::collections::BTreeMap::new();
#(#property_tokens)*
vespera::schema::Schema {
schema_type: Some(vespera::schema::SchemaType::Object),
properties: if properties.is_empty() { None } else { Some(properties) },
required: #required_tokens,
..vespera::schema::Schema::new(vespera::schema::SchemaType::Object)
}
}
})
}
fn is_option_type(ty: &Type) -> bool {
match ty {
Type::Path(type_path) => type_path
.path
.segments
.first()
.map(|s| s.ident == "Option")
.unwrap_or(false),
_ => false,
}
}
fn schema_ref_to_tokens(schema_ref: &SchemaRef) -> TokenStream {
match schema_ref {
SchemaRef::Ref(reference) => {
let ref_path = &reference.ref_path;
quote! {
vespera::schema::SchemaRef::Ref(vespera::schema::Reference::new(#ref_path.to_string()))
}
}
SchemaRef::Inline(schema) => {
let schema_tokens = schema_to_tokens(schema);
quote! {
vespera::schema::SchemaRef::Inline(Box::new(#schema_tokens))
}
}
}
}
fn schema_to_tokens(schema: &Schema) -> TokenStream {
let schema_type_tokens = match &schema.schema_type {
Some(SchemaType::String) => quote! { Some(vespera::schema::SchemaType::String) },
Some(SchemaType::Number) => quote! { Some(vespera::schema::SchemaType::Number) },
Some(SchemaType::Integer) => quote! { Some(vespera::schema::SchemaType::Integer) },
Some(SchemaType::Boolean) => quote! { Some(vespera::schema::SchemaType::Boolean) },
Some(SchemaType::Array) => quote! { Some(vespera::schema::SchemaType::Array) },
Some(SchemaType::Object) => quote! { Some(vespera::schema::SchemaType::Object) },
Some(SchemaType::Null) => quote! { Some(vespera::schema::SchemaType::Null) },
None => quote! { None },
};
let format_tokens = match &schema.format {
Some(f) => quote! { Some(#f.to_string()) },
None => quote! { None },
};
let nullable_tokens = match schema.nullable {
Some(true) => quote! { Some(true) },
Some(false) => quote! { Some(false) },
None => quote! { None },
};
let ref_path_tokens = match &schema.ref_path {
Some(rp) => quote! { Some(#rp.to_string()) },
None => quote! { None },
};
let items_tokens = match &schema.items {
Some(items) => {
let inner = schema_ref_to_tokens(items);
quote! { Some(Box::new(#inner)) }
}
None => quote! { None },
};
let properties_tokens = match &schema.properties {
Some(props) => {
let entries: Vec<_> = props
.iter()
.map(|(k, v)| {
let v_tokens = schema_ref_to_tokens(v);
quote! { (#k.to_string(), #v_tokens) }
})
.collect();
quote! {
Some({
let mut map = std::collections::BTreeMap::new();
#(map.insert(#entries.0, #entries.1);)*
map
})
}
}
None => quote! { None },
};
let required_tokens = match &schema.required {
Some(req) => {
let req_strs: Vec<_> = req.iter().map(|s| s.as_str()).collect();
quote! { Some(vec![#(#req_strs.to_string()),*]) }
}
None => quote! { None },
};
quote! {
vespera::schema::Schema {
ref_path: #ref_path_tokens,
schema_type: #schema_type_tokens,
format: #format_tokens,
nullable: #nullable_tokens,
items: #items_tokens,
properties: #properties_tokens,
required: #required_tokens,
..vespera::schema::Schema::new(vespera::schema::SchemaType::Object)
}
}
}
fn find_struct_from_path(
ty: &Type,
schema_name_hint: Option<&str>,
) -> Option<(StructMetadata, Vec<String>)> {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok()?;
let src_dir = Path::new(&manifest_dir).join("src");
let type_path = match ty {
Type::Path(tp) => tp,
_ => return None,
};
let segments: Vec<String> = type_path
.path
.segments
.iter()
.map(|s| s.ident.to_string())
.collect();
if segments.is_empty() {
return None;
}
let struct_name = segments.last()?.clone();
let module_segments: Vec<&str> = segments[..segments.len() - 1]
.iter()
.filter(|s| *s != "crate" && *s != "self" && *s != "super")
.map(|s| s.as_str())
.collect();
if module_segments.is_empty() {
return find_struct_by_name_in_all_files(&src_dir, &struct_name, schema_name_hint);
}
let type_module_path: Vec<String> = segments[..segments.len() - 1].to_vec();
let file_paths = vec![
src_dir.join(format!("{}.rs", module_segments.join("/"))),
src_dir.join(format!("{}/mod.rs", module_segments.join("/"))),
];
for file_path in file_paths {
if !file_path.exists() {
continue;
}
let content = std::fs::read_to_string(&file_path).ok()?;
let file_ast = syn::parse_file(&content).ok()?;
for item in &file_ast.items {
match item {
syn::Item::Struct(struct_item) if struct_item.ident == struct_name => {
return Some((
StructMetadata::new_model(
struct_name.clone(),
quote::quote!(#struct_item).to_string(),
),
type_module_path,
));
}
_ => continue,
}
}
}
None
}
fn find_struct_by_name_in_all_files(
src_dir: &Path,
struct_name: &str,
schema_name_hint: Option<&str>,
) -> Option<(StructMetadata, Vec<String>)> {
let mut rs_files = Vec::new();
collect_rs_files_recursive(src_dir, &mut rs_files);
let mut found_structs: Vec<(std::path::PathBuf, StructMetadata)> = Vec::new();
for file_path in rs_files {
let content = match std::fs::read_to_string(&file_path) {
Ok(c) => c,
Err(_) => continue,
};
let file_ast = match syn::parse_file(&content) {
Ok(ast) => ast,
Err(_) => continue,
};
for item in &file_ast.items {
if let syn::Item::Struct(struct_item) = item
&& struct_item.ident == struct_name
{
found_structs.push((
file_path.clone(),
StructMetadata::new_model(
struct_name.to_string(),
quote::quote!(#struct_item).to_string(),
),
));
}
}
}
match found_structs.len() {
0 => None,
1 => {
let (path, metadata) = found_structs.remove(0);
let module_path = file_path_to_module_path(&path, src_dir);
Some((metadata, module_path))
}
_ => {
if let Some(hint) = schema_name_hint {
let hint_lower = hint.to_lowercase();
let prefix = hint_lower
.strip_suffix("schema")
.or_else(|| hint_lower.strip_suffix("response"))
.or_else(|| hint_lower.strip_suffix("request"))
.unwrap_or(&hint_lower);
let matching: Vec<_> = found_structs
.into_iter()
.filter(|(path, _)| {
path.file_stem()
.and_then(|s| s.to_str())
.is_some_and(|name| name.to_lowercase().contains(prefix))
})
.collect();
if matching.len() == 1 {
let (path, metadata) = matching.into_iter().next().unwrap();
let module_path = file_path_to_module_path(&path, src_dir);
return Some((metadata, module_path));
}
}
None
}
}
}
fn collect_rs_files_recursive(dir: &Path, files: &mut Vec<std::path::PathBuf>) {
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return,
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect_rs_files_recursive(&path, files);
} else if path.extension().is_some_and(|ext| ext == "rs") {
files.push(path);
}
}
}
fn file_path_to_module_path(file_path: &Path, src_dir: &Path) -> Vec<String> {
let relative = match file_path.strip_prefix(src_dir) {
Ok(r) => r,
Err(_) => return vec!["crate".to_string()],
};
let mut segments = vec!["crate".to_string()];
for component in relative.components() {
if let std::path::Component::Normal(os_str) = component
&& let Some(s) = os_str.to_str()
{
if let Some(name) = s.strip_suffix(".rs") {
if name != "mod" && name != "lib" {
segments.push(name.to_string());
}
} else {
segments.push(s.to_string());
}
}
}
segments
}
fn find_struct_from_schema_path(path_str: &str) -> Option<StructMetadata> {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok()?;
let src_dir = Path::new(&manifest_dir).join("src");
let segments: Vec<&str> = path_str.split("::").filter(|s| !s.is_empty()).collect();
if segments.is_empty() {
return None;
}
let struct_name = segments.last()?.to_string();
let module_segments: Vec<&str> = segments[..segments.len() - 1]
.iter()
.filter(|s| **s != "crate" && **s != "self" && **s != "super")
.copied()
.collect();
if module_segments.is_empty() {
return None;
}
let file_paths = vec![
src_dir.join(format!("{}.rs", module_segments.join("/"))),
src_dir.join(format!("{}/mod.rs", module_segments.join("/"))),
];
for file_path in file_paths {
if !file_path.exists() {
continue;
}
let content = std::fs::read_to_string(&file_path).ok()?;
let file_ast = syn::parse_file(&content).ok()?;
for item in &file_ast.items {
match item {
syn::Item::Struct(struct_item) if struct_item.ident == struct_name => {
return Some(StructMetadata::new_model(
struct_name.clone(),
quote::quote!(#struct_item).to_string(),
));
}
_ => continue,
}
}
}
None
}
fn find_model_from_schema_path(schema_path_str: &str) -> Option<StructMetadata> {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok()?;
let src_dir = Path::new(&manifest_dir).join("src");
let segments: Vec<&str> = schema_path_str
.split("::")
.map(|s| s.trim())
.filter(|s| !s.is_empty() && *s != "Schema")
.collect();
if segments.is_empty() {
return None;
}
let module_segments: Vec<&str> = segments
.iter()
.filter(|s| **s != "crate" && **s != "self" && **s != "super")
.copied()
.collect();
if module_segments.is_empty() {
return None;
}
let file_paths = vec![
src_dir.join(format!("{}.rs", module_segments.join("/"))),
src_dir.join(format!("{}/mod.rs", module_segments.join("/"))),
];
for file_path in file_paths {
if !file_path.exists() {
continue;
}
let content = std::fs::read_to_string(&file_path).ok()?;
let file_ast = syn::parse_file(&content).ok()?;
for item in &file_ast.items {
if let syn::Item::Struct(struct_item) = item
&& struct_item.ident == "Model"
{
return Some(StructMetadata::new_model(
"Model".to_string(),
quote::quote!(#struct_item).to_string(),
));
}
}
}
None
}
struct InlineRelationType {
type_name: syn::Ident,
fields: Vec<InlineField>,
rename_all: String,
}
struct InlineField {
name: syn::Ident,
ty: TokenStream,
attrs: Vec<syn::Attribute>,
}
fn generate_inline_relation_type(
parent_type_name: &syn::Ident,
rel_info: &RelationFieldInfo,
source_module_path: &[String],
schema_name_override: Option<&str>,
) -> Option<InlineRelationType> {
let schema_path_str = rel_info.schema_path.to_string();
let model_metadata = find_model_from_schema_path(&schema_path_str)?;
let model_def = &model_metadata.definition;
let parsed_model: syn::ItemStruct = syn::parse_str(model_def).ok()?;
let circular_fields = detect_circular_fields("", source_module_path, model_def);
if circular_fields.is_empty() {
return None;
}
let rename_all =
extract_rename_all(&parsed_model.attrs).unwrap_or_else(|| "camelCase".to_string());
let parent_name = match schema_name_override {
Some(name) => name.to_string(),
None => parent_type_name.to_string(),
};
let field_name_pascal = capitalize_first(&rel_info.field_name.to_string());
let inline_type_name = syn::Ident::new(
&format!("{}_{}", parent_name, field_name_pascal),
proc_macro2::Span::call_site(),
);
let mut fields = Vec::new();
if let syn::Fields::Named(fields_named) = &parsed_model.fields {
for field in &fields_named.named {
let field_ident = field.ident.as_ref()?;
let field_name_str = field_ident.to_string();
if circular_fields.contains(&field_name_str) {
continue;
}
if is_seaorm_relation_type(&field.ty) {
continue;
}
if extract_skip(&field.attrs) {
continue;
}
let serde_attrs: Vec<syn::Attribute> = field
.attrs
.iter()
.filter(|attr| attr.path().is_ident("serde"))
.cloned()
.collect();
let field_ty = &field.ty;
fields.push(InlineField {
name: field_ident.clone(),
ty: quote::quote!(#field_ty),
attrs: serde_attrs,
});
}
}
Some(InlineRelationType {
type_name: inline_type_name,
fields,
rename_all,
})
}
fn generate_inline_relation_type_no_relations(
parent_type_name: &syn::Ident,
rel_info: &RelationFieldInfo,
schema_name_override: Option<&str>,
) -> Option<InlineRelationType> {
let schema_path_str = rel_info.schema_path.to_string();
let model_metadata = find_model_from_schema_path(&schema_path_str)?;
let model_def = &model_metadata.definition;
let parsed_model: syn::ItemStruct = syn::parse_str(model_def).ok()?;
let rename_all =
extract_rename_all(&parsed_model.attrs).unwrap_or_else(|| "camelCase".to_string());
let parent_name = match schema_name_override {
Some(name) => name.to_string(),
None => parent_type_name.to_string(),
};
let field_name_pascal = capitalize_first(&rel_info.field_name.to_string());
let inline_type_name = syn::Ident::new(
&format!("{}_{}", parent_name, field_name_pascal),
proc_macro2::Span::call_site(),
);
let mut fields = Vec::new();
if let syn::Fields::Named(fields_named) = &parsed_model.fields {
for field in &fields_named.named {
let field_ident = field.ident.as_ref()?;
if is_seaorm_relation_type(&field.ty) {
continue;
}
if extract_skip(&field.attrs) {
continue;
}
let serde_attrs: Vec<syn::Attribute> = field
.attrs
.iter()
.filter(|attr| attr.path().is_ident("serde"))
.cloned()
.collect();
let field_ty = &field.ty;
fields.push(InlineField {
name: field_ident.clone(),
ty: quote::quote!(#field_ty),
attrs: serde_attrs,
});
}
}
Some(InlineRelationType {
type_name: inline_type_name,
fields,
rename_all,
})
}
fn generate_inline_type_definition(inline_type: &InlineRelationType) -> TokenStream {
let type_name = &inline_type.type_name;
let rename_all = &inline_type.rename_all;
let field_tokens: Vec<TokenStream> = inline_type
.fields
.iter()
.map(|f| {
let name = &f.name;
let ty = &f.ty;
let attrs = &f.attrs;
quote! {
#(#attrs)*
pub #name: #ty
}
})
.collect();
quote! {
#[derive(Clone, serde::Serialize, serde::Deserialize, vespera::Schema)]
#[serde(rename_all = #rename_all)]
pub struct #type_name {
#(#field_tokens),*
}
}
}
pub struct SchemaTypeInput {
pub new_type: Ident,
pub source_type: Type,
pub omit: Option<Vec<String>>,
pub pick: Option<Vec<String>>,
pub rename: Option<Vec<(String, String)>>,
pub add: Option<Vec<(String, Type)>>,
pub derive_clone: bool,
pub partial: Option<PartialMode>,
pub ignore_schema: bool,
pub schema_name: Option<String>,
pub rename_all: Option<String>,
}
#[derive(Clone, Debug)]
pub enum PartialMode {
All,
Fields(Vec<String>),
}
struct AddField {
name: String,
ty: Type,
}
impl Parse for AddField {
fn parse(input: ParseStream) -> syn::Result<Self> {
let content;
parenthesized!(content in input);
let name: LitStr = content.parse()?;
content.parse::<Token![:]>()?;
let ty: Type = content.parse()?;
Ok(AddField {
name: name.value(),
ty,
})
}
}
struct RenamePair {
from: String,
to: String,
}
impl Parse for RenamePair {
fn parse(input: ParseStream) -> syn::Result<Self> {
let content;
parenthesized!(content in input);
let from: LitStr = content.parse()?;
content.parse::<Token![,]>()?;
let to: LitStr = content.parse()?;
Ok(RenamePair {
from: from.value(),
to: to.value(),
})
}
}
impl Parse for SchemaTypeInput {
fn parse(input: ParseStream) -> syn::Result<Self> {
let new_type: Ident = input.parse()?;
let from_ident: Ident = input.parse()?;
if from_ident != "from" {
return Err(syn::Error::new(
from_ident.span(),
format!("expected `from`, found `{}`", from_ident),
));
}
let source_type: Type = input.parse()?;
let mut omit = None;
let mut pick = None;
let mut rename = None;
let mut add = None;
let mut derive_clone = true;
let mut partial = None;
let mut ignore_schema = false;
let mut schema_name = None;
let mut rename_all = None;
while input.peek(Token![,]) {
input.parse::<Token![,]>()?;
if input.is_empty() {
break;
}
let ident: Ident = input.parse()?;
let ident_str = ident.to_string();
match ident_str.as_str() {
"omit" => {
input.parse::<Token![=]>()?;
let content;
let _ = bracketed!(content in input);
let fields: Punctuated<LitStr, Token![,]> =
content.parse_terminated(|input| input.parse::<LitStr>(), Token![,])?;
omit = Some(fields.into_iter().map(|s| s.value()).collect());
}
"pick" => {
input.parse::<Token![=]>()?;
let content;
let _ = bracketed!(content in input);
let fields: Punctuated<LitStr, Token![,]> =
content.parse_terminated(|input| input.parse::<LitStr>(), Token![,])?;
pick = Some(fields.into_iter().map(|s| s.value()).collect());
}
"rename" => {
input.parse::<Token![=]>()?;
let content;
let _ = bracketed!(content in input);
let pairs: Punctuated<RenamePair, Token![,]> =
content.parse_terminated(RenamePair::parse, Token![,])?;
rename = Some(pairs.into_iter().map(|p| (p.from, p.to)).collect());
}
"add" => {
input.parse::<Token![=]>()?;
let content;
let _ = bracketed!(content in input);
let fields: Punctuated<AddField, Token![,]> =
content.parse_terminated(AddField::parse, Token![,])?;
add = Some(fields.into_iter().map(|f| (f.name, f.ty)).collect());
}
"clone" => {
input.parse::<Token![=]>()?;
let value: syn::LitBool = input.parse()?;
derive_clone = value.value();
}
"partial" => {
if input.peek(Token![=]) {
input.parse::<Token![=]>()?;
let content;
let _ = bracketed!(content in input);
let fields: Punctuated<LitStr, Token![,]> =
content.parse_terminated(|input| input.parse::<LitStr>(), Token![,])?;
partial = Some(PartialMode::Fields(
fields.into_iter().map(|s| s.value()).collect(),
));
} else {
partial = Some(PartialMode::All);
}
}
"ignore" => {
ignore_schema = true;
}
"name" => {
input.parse::<Token![=]>()?;
let name_lit: LitStr = input.parse()?;
schema_name = Some(name_lit.value());
}
"rename_all" => {
input.parse::<Token![=]>()?;
let rename_all_lit: LitStr = input.parse()?;
rename_all = Some(rename_all_lit.value());
}
_ => {
return Err(syn::Error::new(
ident.span(),
format!(
"unknown parameter: `{}`. Expected `omit`, `pick`, `rename`, `add`, `clone`, `partial`, `ignore`, `name`, or `rename_all`",
ident_str
),
));
}
}
}
if omit.is_some() && pick.is_some() {
return Err(syn::Error::new(
input.span(),
"cannot use both `omit` and `pick` in the same schema_type! invocation",
));
}
Ok(SchemaTypeInput {
new_type,
source_type,
omit,
pick,
rename,
add,
derive_clone,
partial,
ignore_schema,
schema_name,
rename_all,
})
}
}
fn extract_module_path(ty: &Type) -> Vec<String> {
match ty {
Type::Path(type_path) => {
let segments: Vec<String> = type_path
.path
.segments
.iter()
.map(|s| s.ident.to_string())
.collect();
if segments.len() > 1 {
segments[..segments.len() - 1].to_vec()
} else {
vec![]
}
}
_ => vec![],
}
}
fn detect_circular_fields(
_source_schema_name: &str,
source_module_path: &[String],
related_schema_def: &str,
) -> Vec<String> {
let mut circular_fields = Vec::new();
let Ok(parsed) = syn::parse_str::<syn::ItemStruct>(related_schema_def) else {
return circular_fields;
};
let source_module = source_module_path.last().map(|s| s.as_str()).unwrap_or("");
if let syn::Fields::Named(fields_named) = &parsed.fields {
for field in &fields_named.named {
let Some(field_ident) = &field.ident else {
continue;
};
let field_name = field_ident.to_string();
let field_ty = &field.ty;
let ty_str = quote::quote!(#field_ty).to_string();
let ty_str_normalized = ty_str.replace(' ', "");
if ty_str_normalized.contains("HasMany<") {
continue;
}
let is_circular = (ty_str_normalized.contains("HasOne<")
|| ty_str_normalized.contains("BelongsTo<")
|| ty_str_normalized.contains("Box<"))
&& (ty_str_normalized.contains(&format!("{}::Schema", source_module))
|| ty_str_normalized.contains(&format!("{}::Entity", source_module))
|| ty_str_normalized
.contains(&format!("{}Schema", capitalize_first(source_module))));
if is_circular {
circular_fields.push(field_name);
}
}
}
circular_fields
}
fn has_fk_relations(model_def: &str) -> bool {
let Ok(parsed) = syn::parse_str::<syn::ItemStruct>(model_def) else {
return false;
};
if let syn::Fields::Named(fields_named) = &parsed.fields {
for field in &fields_named.named {
let field_ty = &field.ty;
let ty_str = quote::quote!(#field_ty).to_string().replace(' ', "");
if ty_str.contains("HasOne<") || ty_str.contains("BelongsTo<") {
return true;
}
}
}
false
}
fn capitalize_first(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
}
}
fn generate_inline_struct_construction(
schema_path: &TokenStream,
related_schema_def: &str,
circular_fields: &[String],
var_name: &str,
) -> TokenStream {
let Ok(parsed) = syn::parse_str::<syn::ItemStruct>(related_schema_def) else {
let var_ident = syn::Ident::new(var_name, proc_macro2::Span::call_site());
return quote! { <#schema_path as From<_>>::from(#var_ident) };
};
let var_ident = syn::Ident::new(var_name, proc_macro2::Span::call_site());
let fields_named = match &parsed.fields {
syn::Fields::Named(f) => f,
_ => {
return quote! { <#schema_path as From<_>>::from(#var_ident) };
}
};
let field_assignments: Vec<TokenStream> = fields_named
.named
.iter()
.filter_map(|field| {
let field_ident = field.ident.as_ref()?;
let field_name = field_ident.to_string();
if extract_skip(&field.attrs) {
return None;
}
if circular_fields.contains(&field_name) || is_seaorm_relation_type(&field.ty) {
Some(generate_default_for_relation_field(
&field.ty,
field_ident,
&field.attrs,
fields_named,
))
} else {
Some(quote! { #field_ident: #var_ident.#field_ident })
}
})
.collect();
quote! {
#schema_path {
#(#field_assignments),*
}
}
}
fn generate_inline_type_construction(
inline_type_name: &syn::Ident,
included_fields: &[String],
related_model_def: &str,
var_name: &str,
) -> TokenStream {
let Ok(parsed) = syn::parse_str::<syn::ItemStruct>(related_model_def) else {
return quote! { Default::default() };
};
let var_ident = syn::Ident::new(var_name, proc_macro2::Span::call_site());
let fields_named = match &parsed.fields {
syn::Fields::Named(f) => f,
_ => {
return quote! { Default::default() };
}
};
let field_assignments: Vec<TokenStream> = fields_named
.named
.iter()
.filter_map(|field| {
let field_ident = field.ident.as_ref()?;
let field_name = field_ident.to_string();
if extract_skip(&field.attrs) {
return None;
}
if is_seaorm_relation_type(&field.ty) {
return None;
}
if included_fields.contains(&field_name) {
Some(quote! { #field_ident: #var_ident.#field_ident })
} else {
None
}
})
.collect();
quote! {
#inline_type_name {
#(#field_assignments),*
}
}
}
fn is_circular_relation_required(related_model_def: &str, circular_field_name: &str) -> bool {
let Ok(parsed) = syn::parse_str::<syn::ItemStruct>(related_model_def) else {
return false;
};
if let syn::Fields::Named(fields_named) = &parsed.fields {
for field in &fields_named.named {
let Some(field_ident) = &field.ident else {
continue;
};
if *field_ident != circular_field_name {
continue;
}
let ty_str = quote::quote!(#field.ty).to_string().replace(' ', "");
if ty_str.contains("HasOne<") || ty_str.contains("BelongsTo<") {
let fk_field = extract_belongs_to_from_field(&field.attrs);
if let Some(fk) = fk_field {
for f in &fields_named.named {
if f.ident.as_ref().map(|i| i.to_string()) == Some(fk.clone()) {
return !is_option_type(&f.ty);
}
}
}
}
}
}
false
}
fn generate_default_for_relation_field(
ty: &Type,
field_ident: &syn::Ident,
field_attrs: &[syn::Attribute],
all_fields: &syn::FieldsNamed,
) -> TokenStream {
let ty_str = quote::quote!(#ty).to_string().replace(' ', "");
if ty_str.contains("HasMany<") {
quote! { #field_ident: vec![] }
} else if ty_str.contains("HasOne<") || ty_str.contains("BelongsTo<") {
let fk_field = extract_belongs_to_from_field(field_attrs);
let is_optional = fk_field
.as_ref()
.map(|fk| {
all_fields.named.iter().any(|f| {
f.ident.as_ref().map(|i| i.to_string()) == Some(fk.clone())
&& is_option_type(&f.ty)
})
})
.unwrap_or(true);
if is_optional {
quote! { #field_ident: None }
} else {
quote! { #field_ident: Box::new(__parent_stub__.clone()) }
}
} else {
quote! { #field_ident: Default::default() }
}
}
fn generate_from_model_with_relations(
new_type_name: &syn::Ident,
source_type: &Type,
field_mappings: &[(syn::Ident, syn::Ident, bool, bool)],
relation_fields: &[RelationFieldInfo],
source_module_path: &[String],
_schema_storage: &[StructMetadata],
) -> TokenStream {
let relation_loads: Vec<TokenStream> = relation_fields
.iter()
.map(|rel| {
let field_name = &rel.field_name;
let entity_path =
build_entity_path_from_schema_path(&rel.schema_path, source_module_path);
match rel.relation_type.as_str() {
"HasOne" | "BelongsTo" => {
quote! {
let #field_name = model.find_related(#entity_path).one(db).await?;
}
}
"HasMany" => {
quote! {
let #field_name = model.find_related(#entity_path).all(db).await?;
}
}
_ => quote! {},
}
})
.collect();
let needs_parent_stub = relation_fields.iter().any(|rel| {
if rel.relation_type != "HasMany" {
return false;
}
if rel.inline_type_info.is_some() {
return false;
}
let schema_path_str = rel.schema_path.to_string().replace(' ', "");
let model_path_str = schema_path_str.replace("::Schema", "::Model");
let related_model = find_struct_from_schema_path(&model_path_str);
if let Some(ref model) = related_model {
let circular_fields = detect_circular_fields(
new_type_name.to_string().as_str(),
source_module_path,
&model.definition,
);
circular_fields
.iter()
.any(|cf| is_circular_relation_required(&model.definition, cf))
} else {
false
}
});
let parent_stub_fields: Vec<TokenStream> = if needs_parent_stub {
field_mappings
.iter()
.map(|(new_ident, source_ident, _wrapped, is_relation)| {
if *is_relation {
if let Some(rel) = relation_fields
.iter()
.find(|r| &r.field_name == source_ident)
{
match rel.relation_type.as_str() {
"HasMany" => quote! { #new_ident: vec![] },
_ if rel.is_optional => quote! { #new_ident: None },
_ => quote! { #new_ident: None },
}
} else {
quote! { #new_ident: Default::default() }
}
} else {
quote! { #new_ident: model.#source_ident.clone() }
}
})
.collect()
} else {
vec![]
};
let field_assignments: Vec<TokenStream> = field_mappings
.iter()
.map(|(new_ident, source_ident, wrapped, is_relation)| {
if *is_relation {
if let Some(rel) = relation_fields.iter().find(|r| &r.field_name == source_ident) {
let schema_path = &rel.schema_path;
let schema_path_str = schema_path.to_string().replace(' ', "");
let model_path_str = schema_path_str.replace("::Schema", "::Model");
let related_model_from_file = find_struct_from_schema_path(&model_path_str);
let related_def_str = related_model_from_file
.as_ref()
.map(|s| s.definition.as_str())
.unwrap_or("");
let circular_fields = detect_circular_fields(
new_type_name.to_string().as_str(),
source_module_path,
related_def_str,
);
let has_circular = !circular_fields.is_empty();
if let Some((ref inline_type_name, ref included_fields)) = rel.inline_type_info {
let inline_construct = generate_inline_type_construction(
inline_type_name,
included_fields,
related_def_str,
"r",
);
match rel.relation_type.as_str() {
"HasOne" | "BelongsTo" => {
if rel.is_optional {
quote! {
#new_ident: #source_ident.map(|r| Box::new(#inline_construct))
}
} else {
quote! {
#new_ident: Box::new({
let r = #source_ident.ok_or_else(|| sea_orm::DbErr::RecordNotFound(
format!("Required relation '{}' not found", stringify!(#source_ident))
))?;
#inline_construct
})
}
}
}
"HasMany" => {
quote! {
#new_ident: #source_ident.into_iter().map(|r| #inline_construct).collect()
}
}
_ => quote! { #new_ident: Default::default() },
}
} else {
match rel.relation_type.as_str() {
"HasOne" | "BelongsTo" => {
if has_circular {
let inline_construct = generate_inline_struct_construction(
schema_path,
related_def_str,
&circular_fields,
"r",
);
if rel.is_optional {
quote! {
#new_ident: #source_ident.map(|r| Box::new(#inline_construct))
}
} else {
quote! {
#new_ident: Box::new({
let r = #source_ident.ok_or_else(|| sea_orm::DbErr::RecordNotFound(
format!("Required relation '{}' not found", stringify!(#source_ident))
))?;
#inline_construct
})
}
}
} else {
let target_has_fk = has_fk_relations(related_def_str);
if target_has_fk {
if rel.is_optional {
quote! {
#new_ident: match #source_ident {
Some(r) => Some(Box::new(#schema_path::from_model(r, db).await?)),
None => None,
}
}
} else {
quote! {
#new_ident: Box::new(#schema_path::from_model(
#source_ident.ok_or_else(|| sea_orm::DbErr::RecordNotFound(
format!("Required relation '{}' not found", stringify!(#source_ident))
))?,
db,
).await?)
}
}
} else {
if rel.is_optional {
quote! {
#new_ident: #source_ident.map(|r| Box::new(<#schema_path as From<_>>::from(r)))
}
} else {
quote! {
#new_ident: Box::new(<#schema_path as From<_>>::from(
#source_ident.ok_or_else(|| sea_orm::DbErr::RecordNotFound(
format!("Required relation '{}' not found", stringify!(#source_ident))
))?
))
}
}
}
}
}
"HasMany" => {
if has_circular {
let inline_construct = generate_inline_struct_construction(
schema_path,
related_def_str,
&circular_fields,
"r",
);
quote! {
#new_ident: #source_ident.into_iter().map(|r| #inline_construct).collect()
}
} else {
let target_has_fk = has_fk_relations(related_def_str);
if target_has_fk {
let inline_construct = generate_inline_struct_construction(
schema_path,
related_def_str,
&[], "r",
);
quote! {
#new_ident: #source_ident.into_iter().map(|r| #inline_construct).collect()
}
} else {
quote! {
#new_ident: #source_ident.into_iter().map(|r| <#schema_path as From<_>>::from(r)).collect()
}
}
}
}
_ => quote! { #new_ident: Default::default() },
}
}
} else {
quote! { #new_ident: Default::default() }
}
} else if *wrapped {
quote! { #new_ident: Some(model.#source_ident) }
} else {
quote! { #new_ident: model.#source_ident }
}
})
.collect();
let parent_stub_def = if needs_parent_stub {
quote! {
#[allow(unused_variables)]
let __parent_stub__ = Self {
#(#parent_stub_fields),*
};
}
} else {
quote! {}
};
quote! {
impl #new_type_name {
pub async fn from_model(
model: #source_type,
db: &sea_orm::DatabaseConnection,
) -> Result<Self, sea_orm::DbErr> {
use sea_orm::ModelTrait;
#(#relation_loads)*
#parent_stub_def
Ok(Self {
#(#field_assignments),*
})
}
}
}
}
fn build_entity_path_from_schema_path(
schema_path: &TokenStream,
_source_module_path: &[String],
) -> TokenStream {
let path_str = schema_path.to_string();
let segments: Vec<&str> = path_str.split("::").map(|s| s.trim()).collect();
let entity_segments: Vec<String> = segments
.iter()
.map(|s| {
if *s == "Schema" {
"Entity".to_string()
} else {
s.to_string()
}
})
.collect();
let path_idents: Vec<syn::Ident> = entity_segments
.iter()
.map(|s| syn::Ident::new(s, proc_macro2::Span::call_site()))
.collect();
quote! { #(#path_idents)::* }
}
pub fn generate_schema_type_code(
input: &SchemaTypeInput,
schema_storage: &[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, module_path)) =
find_struct_from_path(&input.source_type, schema_name_hint)
{
struct_def_owned = found;
if source_module_path.is_empty() {
source_module_path = module_path;
}
&struct_def_owned
} else if let Some(found) = schema_storage.iter().find(|s| s.name == source_type_name) {
found
} else {
return Err(syn::Error::new_spanned(
&input.source_type,
format!(
"type `{}` 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",
source_type_name
),
));
}
} else {
if let Some(found) = schema_storage.iter().find(|s| s.name == 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 `{}` 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)",
source_type_name
),
));
}
};
let parsed_struct: syn::ItemStruct = syn::parse_str(&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: HashSet<String> =
if let syn::Fields::Named(fields_named) = &parsed_struct.fields {
fields_named
.named
.iter()
.filter_map(|f| f.ident.as_ref())
.map(|i| strip_raw_prefix(&i.to_string()).to_string())
.collect()
} else {
HashSet::new()
};
if let Some(ref pick_fields) = input.pick {
for field in pick_fields {
if !source_field_names.contains(field) {
return Err(syn::Error::new_spanned(
&input.source_type,
format!(
"field `{}` does not exist in type `{}`. Available fields: {:?}",
field,
source_type_name,
source_field_names.iter().collect::<Vec<_>>()
),
));
}
}
}
if let Some(ref omit_fields) = input.omit {
for field in omit_fields {
if !source_field_names.contains(field) {
return Err(syn::Error::new_spanned(
&input.source_type,
format!(
"field `{}` does not exist in type `{}`. Available fields: {:?}",
field,
source_type_name,
source_field_names.iter().collect::<Vec<_>>()
),
));
}
}
}
if let Some(ref rename_pairs) = input.rename {
for (from_field, _) in rename_pairs {
if !source_field_names.contains(from_field) {
return Err(syn::Error::new_spanned(
&input.source_type,
format!(
"field `{}` does not exist in type `{}`. Available fields: {:?}",
from_field,
source_type_name,
source_field_names.iter().collect::<Vec<_>>()
),
));
}
}
}
if let Some(PartialMode::Fields(ref partial_fields)) = input.partial {
for field in partial_fields {
if !source_field_names.contains(field) {
return Err(syn::Error::new_spanned(
&input.source_type,
format!(
"partial field `{}` does not exist in type `{}`. Available fields: {:?}",
field,
source_type_name,
source_field_names.iter().collect::<Vec<_>>()
),
));
}
}
}
let omit_set: HashSet<String> = input.omit.clone().unwrap_or_default().into_iter().collect();
let pick_set: HashSet<String> = input.pick.clone().unwrap_or_default().into_iter().collect();
let partial_all = matches!(input.partial, Some(PartialMode::All));
let partial_set: HashSet<String> = match &input.partial {
Some(PartialMode::Fields(fields)) => fields.iter().cloned().collect(),
_ => HashSet::new(),
};
let rename_map: std::collections::HashMap<String, String> = input
.rename
.clone()
.unwrap_or_default()
.into_iter()
.collect();
let serde_attrs_without_rename_all: Vec<_> = parsed_struct
.attrs
.iter()
.filter(|attr| {
if !attr.path().is_ident("serde") {
return false;
}
let mut has_rename_all = false;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("rename_all") {
has_rename_all = true;
}
Ok(())
});
!has_rename_all
})
.collect();
let effective_rename_all = if let Some(ref ra) = input.rename_all {
ra.clone()
} else {
extract_rename_all(&parsed_struct.attrs).unwrap_or_else(|| "camelCase".to_string())
};
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();
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(|i| strip_raw_prefix(&i.to_string()).to_string())
.unwrap_or_else(|| "unknown".to_string());
if !omit_set.is_empty() && omit_set.contains(&rust_field_name) {
continue;
}
if !pick_set.is_empty() && !pick_set.contains(&rust_field_name) {
continue;
}
let is_relation = is_seaorm_relation_type(&field.ty);
let original_ty = &field.ty;
let should_wrap_option = (partial_all || partial_set.contains(&rust_field_name))
&& !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,
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 serde_field_attrs: Vec<_> = field
.attrs
.iter()
.filter(|attr| attr.path().is_ident("serde"))
.collect();
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: Vec<_> = serde_field_attrs
.iter()
.filter(|attr| {
let mut has_rename = false;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("rename") {
has_rename = true;
}
Ok(())
});
!has_rename
})
.collect();
let json_name =
extract_field_rename(&field.attrs).unwrap_or_else(|| rust_field_name.clone());
field_tokens.push(quote! {
#(#filtered_attrs)*
#[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! {
#(#serde_field_attrs)*
#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 clone_derive = if input.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 = 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! {}
};
let generated_tokens = quote! {
#(#inline_type_definitions)*
#[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 = if let Some(ref custom_name) = input.schema_name {
let struct_def = quote! {
#[serde(rename_all = #effective_rename_all)]
#(#serde_attrs_without_rename_all)*
pub struct #new_type_name {
#(#field_tokens),*
}
};
Some(StructMetadata::new(
custom_name.clone(),
struct_def.to_string(),
))
} else {
None
};
Ok((generated_tokens, metadata))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_schema_input_simple() {
let tokens = quote::quote!(User);
let input: SchemaInput = syn::parse2(tokens).unwrap();
assert!(input.omit.is_none());
assert!(input.pick.is_none());
}
#[test]
fn test_parse_schema_input_with_omit() {
let tokens = quote::quote!(User, omit = ["password", "secret"]);
let input: SchemaInput = syn::parse2(tokens).unwrap();
let omit = input.omit.unwrap();
assert_eq!(omit, vec!["password", "secret"]);
}
#[test]
fn test_parse_schema_input_with_pick() {
let tokens = quote::quote!(User, pick = ["id", "name"]);
let input: SchemaInput = syn::parse2(tokens).unwrap();
let pick = input.pick.unwrap();
assert_eq!(pick, vec!["id", "name"]);
}
#[test]
fn test_parse_schema_input_omit_and_pick_error() {
let tokens = quote::quote!(User, omit = ["a"], pick = ["b"]);
let result: syn::Result<SchemaInput> = syn::parse2(tokens);
assert!(result.is_err());
}
#[test]
fn test_parse_schema_type_input_simple() {
let tokens = quote::quote!(CreateUser from User);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert_eq!(input.new_type.to_string(), "CreateUser");
assert!(input.omit.is_none());
assert!(input.pick.is_none());
assert!(input.rename.is_none());
assert!(input.derive_clone);
}
#[test]
fn test_parse_schema_type_input_with_pick() {
let tokens = quote::quote!(CreateUser from User, pick = ["name", "email"]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert_eq!(input.new_type.to_string(), "CreateUser");
let pick = input.pick.unwrap();
assert_eq!(pick, vec!["name", "email"]);
}
#[test]
fn test_parse_schema_type_input_with_rename() {
let tokens =
quote::quote!(UserDTO from User, rename = [("id", "user_id"), ("name", "full_name")]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert_eq!(input.new_type.to_string(), "UserDTO");
let rename = input.rename.unwrap();
assert_eq!(rename.len(), 2);
assert_eq!(rename[0], ("id".to_string(), "user_id".to_string()));
assert_eq!(rename[1], ("name".to_string(), "full_name".to_string()));
}
#[test]
fn test_parse_schema_type_input_with_single_rename() {
let tokens = quote::quote!(UserDTO from User, rename = [("id", "user_id")]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let rename = input.rename.unwrap();
assert_eq!(rename.len(), 1);
assert_eq!(rename[0], ("id".to_string(), "user_id".to_string()));
}
#[test]
fn test_parse_schema_type_input_with_pick_and_rename() {
let tokens =
quote::quote!(UserDTO from User, pick = ["id", "name"], rename = [("id", "user_id")]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert_eq!(input.pick.unwrap(), vec!["id", "name"]);
assert_eq!(
input.rename.unwrap(),
vec![("id".to_string(), "user_id".to_string())]
);
}
#[test]
fn test_parse_schema_type_input_with_omit_and_rename() {
let tokens =
quote::quote!(UserPublic from User, omit = ["password"], rename = [("id", "user_id")]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert_eq!(input.omit.unwrap(), vec!["password"]);
assert_eq!(
input.rename.unwrap(),
vec![("id".to_string(), "user_id".to_string())]
);
}
#[test]
fn test_parse_schema_type_input_with_clone_false() {
let tokens = quote::quote!(NonCloneUser from User, clone = false);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert!(!input.derive_clone);
}
#[test]
fn test_parse_schema_type_input_unknown_param_error() {
let tokens = quote::quote!(UserDTO from User, unknown = ["a"]);
let result: syn::Result<SchemaTypeInput> = syn::parse2(tokens);
assert!(result.is_err());
match result {
Err(e) => assert!(e.to_string().contains("unknown parameter")),
Ok(_) => panic!("Expected error"),
}
}
#[test]
fn test_parse_schema_type_input_with_add_single() {
let tokens = quote::quote!(UserWithTimestamp from User, add = [("created_at": String)]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert_eq!(input.new_type.to_string(), "UserWithTimestamp");
let add = input.add.unwrap();
assert_eq!(add.len(), 1);
assert_eq!(add[0].0, "created_at");
}
#[test]
fn test_parse_schema_type_input_with_add_multiple() {
let tokens = quote::quote!(UserWithMeta from User, add = [("created_at": String), ("updated_at": Option<String>)]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let add = input.add.unwrap();
assert_eq!(add.len(), 2);
assert_eq!(add[0].0, "created_at");
assert_eq!(add[1].0, "updated_at");
}
#[test]
fn test_parse_schema_type_input_with_pick_and_add() {
let tokens = quote::quote!(CreateUserWithMeta from User, pick = ["name", "email"], add = [("request_id": String)]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert_eq!(input.pick.unwrap(), vec!["name", "email"]);
let add = input.add.unwrap();
assert_eq!(add.len(), 1);
assert_eq!(add[0].0, "request_id");
}
#[test]
fn test_parse_schema_type_input_with_omit_and_add() {
let tokens = quote::quote!(UserPublicWithMeta from User, omit = ["password"], add = [("display_name": String)]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert_eq!(input.omit.unwrap(), vec!["password"]);
let add = input.add.unwrap();
assert_eq!(add.len(), 1);
assert_eq!(add[0].0, "display_name");
}
#[test]
fn test_parse_schema_type_input_with_add_complex_type() {
let tokens = quote::quote!(UserWithVec from User, add = [("tags": Vec<String>)]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let add = input.add.unwrap();
assert_eq!(add.len(), 1);
assert_eq!(add[0].0, "tags");
}
#[test]
fn test_parse_schema_type_input_with_partial_all() {
let tokens = quote::quote!(UpdateUser from User, partial);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert!(matches!(input.partial, Some(PartialMode::All)));
}
#[test]
fn test_parse_schema_type_input_with_partial_fields() {
let tokens = quote::quote!(UpdateUser from User, partial = ["name", "email"]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
match input.partial {
Some(PartialMode::Fields(fields)) => {
assert_eq!(fields, vec!["name", "email"]);
}
_ => panic!("Expected PartialMode::Fields"),
}
}
#[test]
fn test_parse_schema_type_input_with_pick_and_partial() {
let tokens = quote::quote!(UpdateUser from User, pick = ["name", "email"], partial);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert_eq!(input.pick.unwrap(), vec!["name", "email"]);
assert!(matches!(input.partial, Some(PartialMode::All)));
}
#[test]
fn test_parse_schema_type_input_with_pick_and_partial_fields() {
let tokens =
quote::quote!(UpdateUser from User, pick = ["name", "email"], partial = ["name"]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert_eq!(input.pick.unwrap(), vec!["name", "email"]);
match input.partial {
Some(PartialMode::Fields(fields)) => {
assert_eq!(fields, vec!["name"]);
}
_ => panic!("Expected PartialMode::Fields"),
}
}
#[test]
fn test_generate_schema_type_code_with_partial_all() {
let storage = vec![create_test_struct_metadata(
"User",
"pub struct User { pub id: i32, pub name: String, pub bio: Option<String> }",
)];
let tokens = quote::quote!(UpdateUser from User, partial);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(output.contains("Option < i32 >"));
assert!(output.contains("Option < String >"));
}
#[test]
fn test_generate_schema_type_code_with_partial_fields() {
let storage = vec![create_test_struct_metadata(
"User",
"pub struct User { pub id: i32, pub name: String, pub email: String }",
)];
let tokens = quote::quote!(UpdateUser from User, partial = ["name"]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(output.contains("UpdateUser"));
}
#[test]
fn test_generate_schema_type_code_partial_nonexistent_field() {
let storage = vec![create_test_struct_metadata(
"User",
"pub struct User { pub id: i32, pub name: String }",
)];
let tokens = quote::quote!(UpdateUser from User, partial = ["nonexistent"]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("does not exist"));
assert!(err.contains("nonexistent"));
}
#[test]
fn test_generate_schema_type_code_partial_from_impl_wraps_some() {
let storage = vec![create_test_struct_metadata(
"User",
"pub struct User { pub id: i32, pub name: String }",
)];
let tokens = quote::quote!(UpdateUser from User, partial);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(output.contains("Some (source . id)"));
assert!(output.contains("Some (source . name)"));
}
fn create_test_struct_metadata(
name: &str,
definition: &str,
) -> crate::metadata::StructMetadata {
crate::metadata::StructMetadata::new(name.to_string(), definition.to_string())
}
#[test]
fn test_generate_schema_code_simple_struct() {
let storage = vec![create_test_struct_metadata(
"User",
"pub struct User { pub id: i32, pub name: String }",
)];
let tokens = quote::quote!(User);
let input: SchemaInput = syn::parse2(tokens).unwrap();
let result = generate_schema_code(&input, &storage);
assert!(result.is_ok());
let output = result.unwrap().to_string();
assert!(output.contains("properties"));
assert!(output.contains("Schema"));
}
#[test]
fn test_generate_schema_code_with_omit() {
let storage = vec![create_test_struct_metadata(
"User",
"pub struct User { pub id: i32, pub name: String, pub password: String }",
)];
let tokens = quote::quote!(User, omit = ["password"]);
let input: SchemaInput = syn::parse2(tokens).unwrap();
let result = generate_schema_code(&input, &storage);
assert!(result.is_ok());
let output = result.unwrap().to_string();
assert!(output.contains("properties"));
}
#[test]
fn test_generate_schema_code_with_pick() {
let storage = vec![create_test_struct_metadata(
"User",
"pub struct User { pub id: i32, pub name: String, pub email: String }",
)];
let tokens = quote::quote!(User, pick = ["id", "name"]);
let input: SchemaInput = syn::parse2(tokens).unwrap();
let result = generate_schema_code(&input, &storage);
assert!(result.is_ok());
let output = result.unwrap().to_string();
assert!(output.contains("properties"));
}
#[test]
fn test_generate_schema_code_type_not_found() {
let storage: Vec<crate::metadata::StructMetadata> = vec![];
let tokens = quote::quote!(NonExistent);
let input: SchemaInput = syn::parse2(tokens).unwrap();
let result = generate_schema_code(&input, &storage);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("not found"));
}
#[test]
fn test_generate_schema_code_malformed_definition() {
let storage = vec![create_test_struct_metadata(
"BadStruct",
"this is not valid rust code {{{",
)];
let tokens = quote::quote!(BadStruct);
let input: SchemaInput = syn::parse2(tokens).unwrap();
let result = generate_schema_code(&input, &storage);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("failed to parse"));
}
#[test]
fn test_schema_ref_to_tokens_ref_variant() {
use vespera_core::schema::{Reference, SchemaRef};
let schema_ref = SchemaRef::Ref(Reference::new("#/components/schemas/User".to_string()));
let tokens = schema_ref_to_tokens(&schema_ref);
let output = tokens.to_string();
assert!(output.contains("SchemaRef :: Ref"));
assert!(output.contains("Reference :: new"));
}
#[test]
fn test_schema_ref_to_tokens_inline_variant() {
use vespera_core::schema::{Schema, SchemaRef, SchemaType};
let schema = Schema::new(SchemaType::String);
let schema_ref = SchemaRef::Inline(Box::new(schema));
let tokens = schema_ref_to_tokens(&schema_ref);
let output = tokens.to_string();
assert!(output.contains("SchemaRef :: Inline"));
assert!(output.contains("Box :: new"));
}
#[test]
fn test_schema_to_tokens_string_type() {
use vespera_core::schema::{Schema, SchemaType};
let schema = Schema::new(SchemaType::String);
let tokens = schema_to_tokens(&schema);
let output = tokens.to_string();
assert!(output.contains("SchemaType :: String"));
}
#[test]
fn test_schema_to_tokens_integer_type() {
use vespera_core::schema::{Schema, SchemaType};
let schema = Schema::new(SchemaType::Integer);
let tokens = schema_to_tokens(&schema);
let output = tokens.to_string();
assert!(output.contains("SchemaType :: Integer"));
}
#[test]
fn test_schema_to_tokens_number_type() {
use vespera_core::schema::{Schema, SchemaType};
let schema = Schema::new(SchemaType::Number);
let tokens = schema_to_tokens(&schema);
let output = tokens.to_string();
assert!(output.contains("SchemaType :: Number"));
}
#[test]
fn test_schema_to_tokens_boolean_type() {
use vespera_core::schema::{Schema, SchemaType};
let schema = Schema::new(SchemaType::Boolean);
let tokens = schema_to_tokens(&schema);
let output = tokens.to_string();
assert!(output.contains("SchemaType :: Boolean"));
}
#[test]
fn test_schema_to_tokens_array_type() {
use vespera_core::schema::{Schema, SchemaType};
let schema = Schema::new(SchemaType::Array);
let tokens = schema_to_tokens(&schema);
let output = tokens.to_string();
assert!(output.contains("SchemaType :: Array"));
}
#[test]
fn test_schema_to_tokens_object_type() {
use vespera_core::schema::{Schema, SchemaType};
let schema = Schema::new(SchemaType::Object);
let tokens = schema_to_tokens(&schema);
let output = tokens.to_string();
assert!(output.contains("SchemaType :: Object"));
}
#[test]
fn test_schema_to_tokens_null_type() {
use vespera_core::schema::{Schema, SchemaType};
let schema = Schema::new(SchemaType::Null);
let tokens = schema_to_tokens(&schema);
let output = tokens.to_string();
assert!(output.contains("SchemaType :: Null"));
}
#[test]
fn test_schema_to_tokens_with_format() {
use vespera_core::schema::{Schema, SchemaType};
let mut schema = Schema::new(SchemaType::String);
schema.format = Some("date-time".to_string());
let tokens = schema_to_tokens(&schema);
let output = tokens.to_string();
assert!(output.contains("date-time"));
}
#[test]
fn test_schema_to_tokens_with_nullable() {
use vespera_core::schema::{Schema, SchemaType};
let mut schema = Schema::new(SchemaType::String);
schema.nullable = Some(true);
let tokens = schema_to_tokens(&schema);
let output = tokens.to_string();
assert!(output.contains("Some (true)"));
}
#[test]
fn test_schema_to_tokens_with_ref_path() {
use vespera_core::schema::{Schema, SchemaType};
let mut schema = Schema::new(SchemaType::Object);
schema.ref_path = Some("#/components/schemas/User".to_string());
let tokens = schema_to_tokens(&schema);
let output = tokens.to_string();
assert!(output.contains("#/components/schemas/User"));
}
#[test]
fn test_schema_to_tokens_with_items() {
use vespera_core::schema::{Schema, SchemaRef, SchemaType};
let mut schema = Schema::new(SchemaType::Array);
let item_schema = Schema::new(SchemaType::String);
schema.items = Some(Box::new(SchemaRef::Inline(Box::new(item_schema))));
let tokens = schema_to_tokens(&schema);
let output = tokens.to_string();
assert!(output.contains("items"));
assert!(output.contains("Some (Box :: new"));
}
#[test]
fn test_schema_to_tokens_with_properties() {
use std::collections::BTreeMap;
use vespera_core::schema::{Schema, SchemaRef, SchemaType};
let mut schema = Schema::new(SchemaType::Object);
let mut props = BTreeMap::new();
props.insert(
"name".to_string(),
SchemaRef::Inline(Box::new(Schema::new(SchemaType::String))),
);
schema.properties = Some(props);
let tokens = schema_to_tokens(&schema);
let output = tokens.to_string();
assert!(output.contains("properties"));
assert!(output.contains("name"));
}
#[test]
fn test_schema_to_tokens_with_required() {
use vespera_core::schema::{Schema, SchemaType};
let mut schema = Schema::new(SchemaType::Object);
schema.required = Some(vec!["id".to_string(), "name".to_string()]);
let tokens = schema_to_tokens(&schema);
let output = tokens.to_string();
assert!(output.contains("required"));
assert!(output.contains("id"));
assert!(output.contains("name"));
}
#[test]
fn test_generate_schema_type_code_pick_nonexistent_field() {
let storage = vec![create_test_struct_metadata(
"User",
"pub struct User { pub id: i32, pub name: String }",
)];
let tokens = quote::quote!(NewUser from User, pick = ["nonexistent"]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("does not exist"));
assert!(err.contains("nonexistent"));
}
#[test]
fn test_generate_schema_type_code_omit_nonexistent_field() {
let storage = vec![create_test_struct_metadata(
"User",
"pub struct User { pub id: i32, pub name: String }",
)];
let tokens = quote::quote!(NewUser from User, omit = ["nonexistent"]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("does not exist"));
assert!(err.contains("nonexistent"));
}
#[test]
fn test_generate_schema_type_code_rename_nonexistent_field() {
let storage = vec![create_test_struct_metadata(
"User",
"pub struct User { pub id: i32, pub name: String }",
)];
let tokens = quote::quote!(NewUser from User, rename = [("nonexistent", "new_name")]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("does not exist"));
assert!(err.contains("nonexistent"));
}
#[test]
fn test_generate_schema_type_code_type_not_found() {
let storage: Vec<crate::metadata::StructMetadata> = vec![];
let tokens = quote::quote!(NewUser from NonExistent);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("not found"));
}
#[test]
fn test_generate_schema_type_code_success() {
let storage = vec![create_test_struct_metadata(
"User",
"pub struct User { pub id: i32, pub name: String }",
)];
let tokens = quote::quote!(CreateUser from User, pick = ["name"]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(output.contains("CreateUser"));
assert!(output.contains("name"));
}
#[test]
fn test_generate_schema_type_code_with_omit() {
let storage = vec![create_test_struct_metadata(
"User",
"pub struct User { pub id: i32, pub name: String, pub password: String }",
)];
let tokens = quote::quote!(SafeUser from User, omit = ["password"]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(output.contains("SafeUser"));
assert!(!output.contains("password"));
}
#[test]
fn test_generate_schema_type_code_with_add() {
let storage = vec![create_test_struct_metadata(
"User",
"pub struct User { pub id: i32, pub name: String }",
)];
let tokens = quote::quote!(UserWithExtra from User, add = [("extra": String)]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(output.contains("UserWithExtra"));
assert!(output.contains("extra"));
}
#[test]
fn test_generate_schema_type_code_generates_from_impl() {
let storage = vec![create_test_struct_metadata(
"User",
"pub struct User { pub id: i32, pub name: String }",
)];
let tokens = quote::quote!(UserResponse from User, pick = ["id", "name"]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(output.contains("impl From"));
assert!(output.contains("for UserResponse"));
}
#[test]
fn test_generate_schema_type_code_no_from_impl_with_add() {
let storage = vec![create_test_struct_metadata(
"User",
"pub struct User { pub id: i32, pub name: String }",
)];
let tokens = quote::quote!(UserWithExtra from User, add = [("extra": String)]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(!output.contains("impl From"));
}
#[test]
fn test_is_option_type_true() {
let ty: syn::Type = syn::parse_str("Option<String>").unwrap();
assert!(is_option_type(&ty));
}
#[test]
fn test_is_option_type_false() {
let ty: syn::Type = syn::parse_str("String").unwrap();
assert!(!is_option_type(&ty));
}
#[test]
fn test_is_option_type_vec_false() {
let ty: syn::Type = syn::parse_str("Vec<String>").unwrap();
assert!(!is_option_type(&ty));
}
#[test]
fn test_extract_type_name_simple() {
let ty: syn::Type = syn::parse_str("User").unwrap();
let name = extract_type_name(&ty).unwrap();
assert_eq!(name, "User");
}
#[test]
fn test_extract_type_name_with_path() {
let ty: syn::Type = syn::parse_str("crate::models::User").unwrap();
let name = extract_type_name(&ty).unwrap();
assert_eq!(name, "User");
}
#[test]
fn test_extract_type_name_non_path_error() {
let ty: syn::Type = syn::parse_str("&str").unwrap();
let result = extract_type_name(&ty);
assert!(result.is_err());
}
#[test]
fn test_parse_schema_type_input_with_rename_all() {
let tokens = quote::quote!(NewType from User, rename_all = "snake_case");
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert_eq!(input.rename_all.as_deref(), Some("snake_case"));
}
#[test]
fn test_parse_schema_type_input_rename_all_with_other_params() {
let tokens =
quote::quote!(NewType from User, pick = ["id", "name"], rename_all = "snake_case");
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert_eq!(input.pick.unwrap(), vec!["id", "name"]);
assert_eq!(input.rename_all.as_deref(), Some("snake_case"));
}
}