use std::collections::HashMap;
use proc_macro2::TokenStream;
use quote::quote;
use super::{
seaorm::extract_belongs_to_from_field,
type_utils::{
SeaOrmRelationKind, capitalize_first, first_generic_type_arg, is_option_type,
is_seaorm_relation_type, seaorm_relation_inner_type, seaorm_relation_kind,
},
};
use crate::parser::extract_skip;
#[derive(Clone)]
pub struct CircularAnalysis {
pub circular_fields: Vec<String>,
pub has_fk_relations: bool,
pub circular_field_required: HashMap<String, bool>,
}
pub fn analyze_circular_refs(source_module_path: &[String], definition: &str) -> CircularAnalysis {
let Ok(parsed) = super::file_cache::parse_struct_cached(definition) else {
return CircularAnalysis {
circular_fields: Vec::new(),
has_fk_relations: false,
circular_field_required: HashMap::new(),
};
};
let syn::Fields::Named(fields_named) = &parsed.fields else {
return CircularAnalysis {
circular_fields: Vec::new(),
has_fk_relations: false,
circular_field_required: HashMap::new(),
};
};
let source_module = source_module_path
.last()
.map_or("", std::string::String::as_str);
let mut circular_fields = Vec::with_capacity(fields_named.named.len());
let mut has_fk = false;
let mut circular_field_required = HashMap::with_capacity(fields_named.named.len());
let field_by_name: HashMap<String, &syn::Field> = fields_named
.named
.iter()
.filter_map(|f| f.ident.as_ref().map(|id| (id.to_string(), f)))
.collect();
let capitalized_pattern = format!("{}Schema", capitalize_first(source_module));
for field in &fields_named.named {
let field_ident = field.ident.as_ref().expect("named field has ident");
let field_name = field_ident.to_string();
if seaorm_relation_kind(&field.ty).is_some_and(SeaOrmRelationKind::is_fk_backed) {
has_fk = true;
let required = extract_belongs_to_from_field(&field.attrs).is_some_and(|fk| {
field_by_name
.get(&fk)
.is_some_and(|f| !is_option_type(&f.ty))
});
circular_field_required.insert(field_name.clone(), required);
}
if is_circular_relation_type(&field.ty, source_module, &capitalized_pattern) {
circular_fields.push(field_name);
}
}
CircularAnalysis {
circular_fields,
has_fk_relations: has_fk,
circular_field_required,
}
}
fn is_circular_relation_type(
ty: &syn::Type,
source_module: &str,
capitalized_schema: &str,
) -> bool {
match seaorm_relation_kind(ty) {
Some(SeaOrmRelationKind::HasMany) => false,
Some(SeaOrmRelationKind::HasOne | SeaOrmRelationKind::BelongsTo) => {
seaorm_relation_inner_type(ty).is_some_and(|inner| {
type_targets_source_schema(inner, source_module, capitalized_schema)
})
}
None => type_targets_source_schema(ty, source_module, capitalized_schema),
}
}
fn transparent_inner_type<'a>(ty: &'a syn::Type, wrapper: &str) -> Option<&'a syn::Type> {
let syn::Type::Path(type_path) = ty else {
return None;
};
let segment = type_path.path.segments.last()?;
if segment.ident != wrapper {
return None;
}
first_generic_type_arg(segment)
}
fn type_targets_source_schema(
ty: &syn::Type,
source_module: &str,
capitalized_schema: &str,
) -> bool {
if let Some(inner) =
transparent_inner_type(ty, "Option").or_else(|| transparent_inner_type(ty, "Box"))
{
return type_targets_source_schema(inner, source_module, capitalized_schema);
}
let syn::Type::Path(type_path) = ty else {
return false;
};
let segments: Vec<_> = type_path
.path
.segments
.iter()
.map(|segment| segment.ident.to_string())
.collect();
match segments.as_slice() {
[last] => last == capitalized_schema,
[.., module, last] => {
module == source_module && (last == "Schema" || last == "Entity")
|| last == capitalized_schema
}
[] => false,
}
}
pub fn generate_default_for_relation_field(
ty: &syn::Type,
field_ident: &syn::Ident,
field_attrs: &[syn::Attribute],
all_fields: &syn::FieldsNamed,
) -> TokenStream {
if seaorm_relation_kind(ty) == Some(SeaOrmRelationKind::HasMany) {
quote! { #field_ident: vec![] }
} else if seaorm_relation_kind(ty).is_some_and(SeaOrmRelationKind::is_fk_backed) {
let fk_field = extract_belongs_to_from_field(field_attrs);
let is_optional = fk_field.as_ref().is_none_or(|fk| {
all_fields.named.iter().any(|f| {
f.ident.as_ref().map(std::string::ToString::to_string) == Some(fk.clone())
&& is_option_type(&f.ty)
})
});
if is_optional {
quote! { #field_ident: None }
} else {
quote! { #field_ident: Box::new(__parent_stub__.clone()) }
}
} else {
quote! { #field_ident: Default::default() }
}
}
pub fn generate_inline_struct_construction(
schema_path: &TokenStream,
related_schema_def: &str,
circular_fields: &[String],
var_name: &str,
) -> TokenStream {
let Ok(parsed) = super::file_cache::parse_struct_cached(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 syn::Fields::Named(fields_named) = &parsed.fields else {
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),*
}
}
}
pub fn generate_inline_type_construction(
inline_type_name: &syn::Ident,
included_fields: &[String],
related_model_def: &str,
var_name: &str,
) -> TokenStream {
let Ok(parsed) = super::file_cache::parse_struct_cached(related_model_def) else {
return quote! { Default::default() };
};
let var_ident = syn::Ident::new(var_name, proc_macro2::Span::call_site());
let syn::Fields::Named(fields_named) = &parsed.fields else {
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),*
}
}
}
#[cfg(test)]
mod tests;
#[cfg(test)]
mod coverage_tests {
use super::*;
#[test]
fn plain_and_empty_path_types_do_not_require_relation_wrappers() {
let plain: syn::Type = syn::parse_str(std::hint::black_box("MemoSchema")).unwrap();
assert!(is_circular_relation_type(
&plain,
std::hint::black_box("memo"),
std::hint::black_box("MemoSchema")
));
let empty = syn::Type::Path(syn::TypePath {
attrs: Vec::new(),
qself: None,
path: syn::Path {
leading_colon: None,
segments: syn::punctuated::Punctuated::new(),
},
});
assert!(!is_circular_relation_type(
std::hint::black_box(&empty),
"memo",
"MemoSchema"
));
}
}