use proc_macro2::TokenStream;
use quote::quote;
use syn::Type;
use super::{
circular::{
detect_circular_fields, generate_inline_struct_construction,
generate_inline_type_construction, has_fk_relations, is_circular_relation_required,
},
file_lookup::{find_fk_column_from_target_entity, find_struct_from_schema_path},
seaorm::RelationFieldInfo,
type_utils::snake_to_pascal_case,
};
use crate::metadata::StructMetadata;
pub 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_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" => {
if let Some(ref relation_enum_name) = rel.relation_enum {
let relation_variant = syn::Ident::new(relation_enum_name, proc_macro2::Span::call_site());
if rel.is_optional {
if let Some(ref fk_col) = rel.fk_column {
let fk_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
quote! {
let #field_name = match &model.#fk_ident {
Some(fk_value) => #entity_path::find_by_id(fk_value.clone()).one(db).await?,
None => None,
};
}
} else {
quote! {
let #field_name = Entity::find_related(Relation::#relation_variant)
.filter(<Entity as sea_orm::EntityTrait>::PrimaryKey::eq(&model))
.one(db)
.await?;
}
}
} else {
if let Some(ref fk_col) = rel.fk_column {
let fk_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
quote! {
let #field_name = #entity_path::find_by_id(model.#fk_ident.clone()).one(db).await?;
}
} else {
quote! {
let #field_name = Entity::find_related(Relation::#relation_variant)
.filter(<Entity as sea_orm::EntityTrait>::PrimaryKey::eq(&model))
.one(db)
.await?;
}
}
}
} else {
quote! {
let #field_name = model.find_related(#entity_path).one(db).await?;
}
}
}
"HasMany" => {
if let Some(ref via_rel_value) = rel.via_rel {
let schema_path_str = rel.schema_path.to_string().replace(' ', "");
if let Some(fk_col_name) = find_fk_column_from_target_entity(&schema_path_str, via_rel_value) {
let fk_col_pascal = snake_to_pascal_case(&fk_col_name);
let fk_col_ident = syn::Ident::new(&fk_col_pascal, proc_macro2::Span::call_site());
let entity_path_str = entity_path.to_string().replace(' ', "");
let column_path_str = entity_path_str.replace(":: Entity", ":: Column");
let column_path_idents: Vec<syn::Ident> = column_path_str.split("::").map(|s| s.trim()).filter(|s| !s.is_empty()).map(|s| syn::Ident::new(s, proc_macro2::Span::call_site())).collect();
quote! {
let #field_name = #(#column_path_idents)::*::#fk_col_ident
.into_column()
.eq(model.id.clone())
.into_condition();
let #field_name = #entity_path::find()
.filter(#field_name)
.all(db)
.await?;
}
} else {
quote! {
let #field_name: Vec<_> = vec![];
}
}
} else if let Some(via_rel_value) = &rel.relation_enum {
let schema_path_str = rel.schema_path.to_string().replace(' ', "");
if let Some(fk_col_name) = find_fk_column_from_target_entity(&schema_path_str, via_rel_value) {
let fk_col_pascal = snake_to_pascal_case(&fk_col_name);
let fk_col_ident = syn::Ident::new(&fk_col_pascal, proc_macro2::Span::call_site());
let entity_path_str = entity_path.to_string().replace(' ', "");
let column_path_str = entity_path_str.replace(":: Entity", ":: Column");
let column_path_idents: Vec<syn::Ident> = column_path_str.split("::").map(|s| s.trim()).filter(|s| !s.is_empty()).map(|s| syn::Ident::new(s, proc_macro2::Span::call_site())).collect();
quote! {
let #field_name = #(#column_path_idents)::*::#fk_col_ident
.into_column()
.eq(model.id.clone())
.into_condition();
let #field_name = #entity_path::find()
.filter(#field_name)
.all(db)
.await?;
}
} else {
quote! {
let #field_name: Vec<_> = vec![];
}
}
} else {
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! {
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),*
})
}
}
}
}
#[cfg(test)]
mod tests {
use serial_test::serial;
use super::*;
#[test]
fn test_build_entity_path_from_schema_path() {
let schema_path = quote! { crate::models::user::Schema };
let result = build_entity_path_from_schema_path(&schema_path, &[]);
let output = result.to_string();
assert!(output.contains("crate"));
assert!(output.contains("models"));
assert!(output.contains("user"));
assert!(output.contains("Entity"));
assert!(!output.contains("Schema"));
}
#[test]
fn test_build_entity_path_simple() {
let schema_path = quote! { user::Schema };
let result = build_entity_path_from_schema_path(&schema_path, &[]);
let output = result.to_string();
assert!(output.contains("user"));
assert!(output.contains("Entity"));
}
#[test]
fn test_build_entity_path_deeply_nested() {
let schema_path = quote! { crate::api::models::entities::user::Schema };
let result = build_entity_path_from_schema_path(&schema_path, &[]);
let output = result.to_string();
assert!(output.contains("api"));
assert!(output.contains("models"));
assert!(output.contains("entities"));
assert!(output.contains("user"));
assert!(output.contains("Entity"));
assert!(!output.contains("Schema"));
}
#[test]
fn test_build_entity_path_single_segment() {
let schema_path = quote! { Schema };
let result = build_entity_path_from_schema_path(&schema_path, &[]);
let output = result.to_string();
assert!(output.contains("Entity"));
}
fn create_test_relation_info(
field_name: &str,
relation_type: &str,
schema_path: TokenStream,
is_optional: bool,
) -> RelationFieldInfo {
RelationFieldInfo {
field_name: syn::Ident::new(field_name, proc_macro2::Span::call_site()),
relation_type: relation_type.to_string(),
schema_path,
is_optional,
inline_type_info: None,
relation_enum: None,
fk_column: None,
via_rel: None,
}
}
#[test]
fn test_generate_from_model_with_required_relation() {
let new_type_name = syn::Ident::new("MemoSchema", proc_macro2::Span::call_site());
let source_type: Type = syn::parse_str("Model").unwrap();
let field_mappings = vec![
(
syn::Ident::new("id", proc_macro2::Span::call_site()),
syn::Ident::new("id", proc_macro2::Span::call_site()),
false,
false,
),
(
syn::Ident::new("user", proc_macro2::Span::call_site()),
syn::Ident::new("user", proc_macro2::Span::call_site()),
false,
true,
),
];
let relation_fields = vec![create_test_relation_info(
"user",
"HasOne",
quote! { user::Schema },
false,
)];
let source_module_path = vec![
"crate".to_string(),
"models".to_string(),
"memo".to_string(),
];
let tokens = generate_from_model_with_relations(
&new_type_name,
&source_type,
&field_mappings,
&relation_fields,
&source_module_path,
&[],
);
let output = tokens.to_string();
assert!(output.contains("impl MemoSchema"));
assert!(output.contains("DbErr :: RecordNotFound"));
}
#[test]
fn test_generate_from_model_with_wrapped_fields() {
let new_type_name = syn::Ident::new("TestSchema", proc_macro2::Span::call_site());
let source_type: Type = syn::parse_str("Model").unwrap();
let field_mappings = vec![(
syn::Ident::new("id", proc_macro2::Span::call_site()),
syn::Ident::new("id", proc_macro2::Span::call_site()),
true, false,
)];
let relation_fields = vec![];
let source_module_path = vec!["crate".to_string()];
let tokens = generate_from_model_with_relations(
&new_type_name,
&source_type,
&field_mappings,
&relation_fields,
&source_module_path,
&[],
);
let output = tokens.to_string();
assert!(output.contains("Some (model . id)"));
}
#[test]
fn test_generate_from_model_with_has_one_optional() {
let new_type_name = syn::Ident::new("MemoSchema", proc_macro2::Span::call_site());
let source_type: Type = syn::parse_str("Model").unwrap();
let field_mappings = vec![
(
syn::Ident::new("id", proc_macro2::Span::call_site()),
syn::Ident::new("id", proc_macro2::Span::call_site()),
false,
false,
),
(
syn::Ident::new("user", proc_macro2::Span::call_site()),
syn::Ident::new("user", proc_macro2::Span::call_site()),
false,
true,
),
];
let relation_fields = vec![create_test_relation_info(
"user",
"HasOne",
quote! { user::Schema },
true,
)];
let source_module_path = vec![
"crate".to_string(),
"models".to_string(),
"memo".to_string(),
];
let tokens = generate_from_model_with_relations(
&new_type_name,
&source_type,
&field_mappings,
&relation_fields,
&source_module_path,
&[],
);
let output = tokens.to_string();
assert!(output.contains("impl MemoSchema"));
assert!(output.contains("pub async fn from_model"));
assert!(output.contains("sea_orm :: DatabaseConnection"));
assert!(output.contains("Result < Self , sea_orm :: DbErr >"));
assert!(output.contains("find_related"));
assert!(output.contains(". one (db)"));
}
#[test]
fn test_generate_from_model_with_has_many() {
let new_type_name = syn::Ident::new("UserSchema", proc_macro2::Span::call_site());
let source_type: Type = syn::parse_str("Model").unwrap();
let field_mappings = vec![
(
syn::Ident::new("id", proc_macro2::Span::call_site()),
syn::Ident::new("id", proc_macro2::Span::call_site()),
false,
false,
),
(
syn::Ident::new("memos", proc_macro2::Span::call_site()),
syn::Ident::new("memos", proc_macro2::Span::call_site()),
false,
true,
),
];
let relation_fields = vec![create_test_relation_info(
"memos",
"HasMany",
quote! { memo::Schema },
false,
)];
let source_module_path = vec![
"crate".to_string(),
"models".to_string(),
"user".to_string(),
];
let tokens = generate_from_model_with_relations(
&new_type_name,
&source_type,
&field_mappings,
&relation_fields,
&source_module_path,
&[],
);
let output = tokens.to_string();
assert!(output.contains("impl UserSchema"));
assert!(output.contains("pub async fn from_model"));
assert!(output.contains(". all (db)"));
}
#[test]
fn test_generate_from_model_with_belongs_to() {
let new_type_name = syn::Ident::new("MemoSchema", proc_macro2::Span::call_site());
let source_type: Type = syn::parse_str("Model").unwrap();
let field_mappings = vec![
(
syn::Ident::new("id", proc_macro2::Span::call_site()),
syn::Ident::new("id", proc_macro2::Span::call_site()),
false,
false,
),
(
syn::Ident::new("user", proc_macro2::Span::call_site()),
syn::Ident::new("user", proc_macro2::Span::call_site()),
false,
true,
),
];
let relation_fields = vec![create_test_relation_info(
"user",
"BelongsTo",
quote! { user::Schema },
true,
)];
let source_module_path = vec![
"crate".to_string(),
"models".to_string(),
"memo".to_string(),
];
let tokens = generate_from_model_with_relations(
&new_type_name,
&source_type,
&field_mappings,
&relation_fields,
&source_module_path,
&[],
);
let output = tokens.to_string();
assert!(output.contains("impl MemoSchema"));
assert!(output.contains("find_related"));
assert!(output.contains(". one (db)"));
}
#[test]
fn test_generate_from_model_no_relations() {
let new_type_name = syn::Ident::new("SimpleSchema", proc_macro2::Span::call_site());
let source_type: Type = syn::parse_str("Model").unwrap();
let field_mappings = vec![
(
syn::Ident::new("id", proc_macro2::Span::call_site()),
syn::Ident::new("id", proc_macro2::Span::call_site()),
false,
false,
),
(
syn::Ident::new("name", proc_macro2::Span::call_site()),
syn::Ident::new("name", proc_macro2::Span::call_site()),
false,
false,
),
];
let relation_fields = vec![];
let source_module_path = vec!["crate".to_string()];
let tokens = generate_from_model_with_relations(
&new_type_name,
&source_type,
&field_mappings,
&relation_fields,
&source_module_path,
&[],
);
let output = tokens.to_string();
assert!(output.contains("impl SimpleSchema"));
assert!(output.contains("id : model . id"));
assert!(output.contains("name : model . name"));
}
#[test]
fn test_generate_from_model_with_inline_type() {
let new_type_name = syn::Ident::new("MemoSchema", proc_macro2::Span::call_site());
let source_type: Type = syn::parse_str("Model").unwrap();
let field_mappings = vec![
(
syn::Ident::new("id", proc_macro2::Span::call_site()),
syn::Ident::new("id", proc_macro2::Span::call_site()),
false,
false,
),
(
syn::Ident::new("user", proc_macro2::Span::call_site()),
syn::Ident::new("user", proc_macro2::Span::call_site()),
false,
true,
),
];
let mut rel_info =
create_test_relation_info("user", "HasOne", quote! { user::Schema }, true);
rel_info.inline_type_info = Some((
syn::Ident::new("MemoSchema_User", proc_macro2::Span::call_site()),
vec!["id".to_string(), "name".to_string()],
));
let relation_fields = vec![rel_info];
let source_module_path = vec![
"crate".to_string(),
"models".to_string(),
"memo".to_string(),
];
let tokens = generate_from_model_with_relations(
&new_type_name,
&source_type,
&field_mappings,
&relation_fields,
&source_module_path,
&[],
);
let output = tokens.to_string();
assert!(output.contains("impl MemoSchema"));
assert!(output.contains("find_related"));
}
#[test]
fn test_generate_from_model_unknown_relation_type() {
let new_type_name = syn::Ident::new("TestSchema", proc_macro2::Span::call_site());
let source_type: Type = syn::parse_str("Model").unwrap();
let field_mappings = vec![
(
syn::Ident::new("id", proc_macro2::Span::call_site()),
syn::Ident::new("id", proc_macro2::Span::call_site()),
false,
false,
),
(
syn::Ident::new("unknown", proc_macro2::Span::call_site()),
syn::Ident::new("unknown", proc_macro2::Span::call_site()),
false,
true,
),
];
let relation_fields = vec![create_test_relation_info(
"unknown",
"UnknownType",
quote! { some::Schema },
true,
)];
let source_module_path = vec!["crate".to_string()];
let tokens = generate_from_model_with_relations(
&new_type_name,
&source_type,
&field_mappings,
&relation_fields,
&source_module_path,
&[],
);
let output = tokens.to_string();
assert!(output.contains("impl TestSchema"));
}
#[test]
fn test_generate_from_model_relation_field_not_in_mappings() {
let new_type_name = syn::Ident::new("TestSchema", proc_macro2::Span::call_site());
let source_type: Type = syn::parse_str("Model").unwrap();
let field_mappings = vec![
(
syn::Ident::new("id", proc_macro2::Span::call_site()),
syn::Ident::new("id", proc_macro2::Span::call_site()),
false,
false,
),
(
syn::Ident::new("owner", proc_macro2::Span::call_site()),
syn::Ident::new("different_name", proc_macro2::Span::call_site()),
false,
true,
),
];
let relation_fields = vec![create_test_relation_info(
"user",
"HasOne",
quote! { user::Schema },
true,
)];
let source_module_path = vec!["crate".to_string()];
let tokens = generate_from_model_with_relations(
&new_type_name,
&source_type,
&field_mappings,
&relation_fields,
&source_module_path,
&[],
);
let output = tokens.to_string();
assert!(output.contains("impl TestSchema"));
}
#[test]
fn test_generate_from_model_with_has_many_inline() {
let new_type_name = syn::Ident::new("UserSchema", proc_macro2::Span::call_site());
let source_type: Type = syn::parse_str("Model").unwrap();
let field_mappings = vec![
(
syn::Ident::new("id", proc_macro2::Span::call_site()),
syn::Ident::new("id", proc_macro2::Span::call_site()),
false,
false,
),
(
syn::Ident::new("memos", proc_macro2::Span::call_site()),
syn::Ident::new("memos", proc_macro2::Span::call_site()),
false,
true,
),
];
let mut rel_info =
create_test_relation_info("memos", "HasMany", quote! { memo::Schema }, false);
rel_info.inline_type_info = Some((
syn::Ident::new("UserSchema_Memos", proc_macro2::Span::call_site()),
vec!["id".to_string(), "title".to_string()],
));
let relation_fields = vec![rel_info];
let source_module_path = vec![
"crate".to_string(),
"models".to_string(),
"user".to_string(),
];
let tokens = generate_from_model_with_relations(
&new_type_name,
&source_type,
&field_mappings,
&relation_fields,
&source_module_path,
&[],
);
let output = tokens.to_string();
assert!(output.contains("impl UserSchema"));
assert!(output.contains(". all (db)"));
assert!(output.contains("into_iter"));
assert!(output.contains("collect"));
}
#[test]
#[serial]
fn test_generate_from_model_needs_parent_stub_with_required_circular() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
let models_dir = src_dir.join("models");
std::fs::create_dir_all(&models_dir).unwrap();
let memo_model = r#"
pub struct Model {
pub id: i32,
pub title: String,
pub user_id: i32,
#[sea_orm(belongs_to = "super::user::Entity", from = "user_id")]
pub user: BelongsTo<super::user::Entity>,
}
"#;
std::fs::write(models_dir.join("memo.rs"), memo_model).unwrap();
let user_model = r#"
pub struct Model {
pub id: i32,
pub name: String,
}
"#;
std::fs::write(models_dir.join("user.rs"), user_model).unwrap();
let original_manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let new_type_name = syn::Ident::new("UserSchema", proc_macro2::Span::call_site());
let source_type: Type = syn::parse_str("crate::models::user::Model").unwrap();
let field_mappings = vec![
(
syn::Ident::new("id", proc_macro2::Span::call_site()),
syn::Ident::new("id", proc_macro2::Span::call_site()),
false,
false,
),
(
syn::Ident::new("name", proc_macro2::Span::call_site()),
syn::Ident::new("name", proc_macro2::Span::call_site()),
false,
false,
),
(
syn::Ident::new("memos", proc_macro2::Span::call_site()),
syn::Ident::new("memos", proc_macro2::Span::call_site()),
false,
true, ),
];
let relation_fields = vec![create_test_relation_info(
"memos",
"HasMany",
quote! { crate::models::memo::Schema },
false,
)];
let source_module_path = vec![
"crate".to_string(),
"models".to_string(),
"user".to_string(),
];
let tokens = generate_from_model_with_relations(
&new_type_name,
&source_type,
&field_mappings,
&relation_fields,
&source_module_path,
&[],
);
unsafe {
if let Some(dir) = original_manifest_dir {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
let output = tokens.to_string();
assert!(output.contains("impl UserSchema"));
assert!(output.contains("from_model"));
assert!(
output.contains("__parent_stub__"),
"Should have parent stub: {}",
output
);
}
#[test]
#[serial]
fn test_generate_from_model_circular_has_one_optional() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
let models_dir = src_dir.join("models");
std::fs::create_dir_all(&models_dir).unwrap();
let profile_model = r#"
pub struct Model {
pub id: i32,
pub bio: String,
pub user: BelongsTo<super::user::Entity>,
}
"#;
std::fs::write(models_dir.join("profile.rs"), profile_model).unwrap();
let original_manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let new_type_name = syn::Ident::new("UserSchema", proc_macro2::Span::call_site());
let source_type: Type = syn::parse_str("crate::models::user::Model").unwrap();
let field_mappings = vec![
(
syn::Ident::new("id", proc_macro2::Span::call_site()),
syn::Ident::new("id", proc_macro2::Span::call_site()),
false,
false,
),
(
syn::Ident::new("profile", proc_macro2::Span::call_site()),
syn::Ident::new("profile", proc_macro2::Span::call_site()),
false,
true,
),
];
let relation_fields = vec![create_test_relation_info(
"profile",
"HasOne",
quote! { crate::models::profile::Schema },
true, )];
let source_module_path = vec![
"crate".to_string(),
"models".to_string(),
"user".to_string(),
];
let tokens = generate_from_model_with_relations(
&new_type_name,
&source_type,
&field_mappings,
&relation_fields,
&source_module_path,
&[],
);
unsafe {
if let Some(dir) = original_manifest_dir {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
let output = tokens.to_string();
assert!(output.contains("impl UserSchema"));
assert!(
output.contains(". map (| r |"),
"Should have map for optional: {}",
output
);
}
#[test]
#[serial]
fn test_generate_from_model_circular_has_one_required() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
let models_dir = src_dir.join("models");
std::fs::create_dir_all(&models_dir).unwrap();
let profile_model = r#"
pub struct Model {
pub id: i32,
pub bio: String,
pub user: BelongsTo<super::user::Entity>,
}
"#;
std::fs::write(models_dir.join("profile.rs"), profile_model).unwrap();
let original_manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let new_type_name = syn::Ident::new("UserSchema", proc_macro2::Span::call_site());
let source_type: Type = syn::parse_str("crate::models::user::Model").unwrap();
let field_mappings = vec![
(
syn::Ident::new("id", proc_macro2::Span::call_site()),
syn::Ident::new("id", proc_macro2::Span::call_site()),
false,
false,
),
(
syn::Ident::new("profile", proc_macro2::Span::call_site()),
syn::Ident::new("profile", proc_macro2::Span::call_site()),
false,
true,
),
];
let relation_fields = vec![create_test_relation_info(
"profile",
"HasOne",
quote! { crate::models::profile::Schema },
false, )];
let source_module_path = vec![
"crate".to_string(),
"models".to_string(),
"user".to_string(),
];
let tokens = generate_from_model_with_relations(
&new_type_name,
&source_type,
&field_mappings,
&relation_fields,
&source_module_path,
&[],
);
unsafe {
if let Some(dir) = original_manifest_dir {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
let output = tokens.to_string();
assert!(output.contains("impl UserSchema"));
assert!(
output.contains("Box :: new"),
"Should have Box::new for required: {}",
output
);
assert!(
output.contains("ok_or_else"),
"Should have ok_or_else: {}",
output
);
}
#[test]
fn test_generate_from_model_unknown_relation_with_inline_type() {
let new_type_name = syn::Ident::new("TestSchema", proc_macro2::Span::call_site());
let source_type: Type = syn::parse_str("Model").unwrap();
let field_mappings = vec![
(
syn::Ident::new("id", proc_macro2::Span::call_site()),
syn::Ident::new("id", proc_macro2::Span::call_site()),
false,
false,
),
(
syn::Ident::new("weird", proc_macro2::Span::call_site()),
syn::Ident::new("weird", proc_macro2::Span::call_site()),
false,
true,
),
];
let mut rel_info = create_test_relation_info(
"weird",
"UnknownRelationType",
quote! { some::Schema },
true,
);
rel_info.inline_type_info = Some((
syn::Ident::new("TestSchema_Weird", proc_macro2::Span::call_site()),
vec!["id".to_string()],
));
let relation_fields = vec![rel_info];
let source_module_path = vec!["crate".to_string()];
let tokens = generate_from_model_with_relations(
&new_type_name,
&source_type,
&field_mappings,
&relation_fields,
&source_module_path,
&[],
);
let output = tokens.to_string();
assert!(output.contains("impl TestSchema"));
assert!(
output.contains("Default :: default ()"),
"Should have Default::default(): {}",
output
);
}
#[test]
#[serial]
fn test_generate_from_model_non_circular_has_one_with_fk_optional() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
let models_dir = src_dir.join("models");
std::fs::create_dir_all(&models_dir).unwrap();
let address_model = r#"
pub struct Model {
pub id: i32,
pub street: String,
pub city_id: i32,
pub city: BelongsTo<super::city::Entity>,
}
"#;
std::fs::write(models_dir.join("address.rs"), address_model).unwrap();
let original_manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let new_type_name = syn::Ident::new("UserSchema", proc_macro2::Span::call_site());
let source_type: Type = syn::parse_str("crate::models::user::Model").unwrap();
let field_mappings = vec![
(
syn::Ident::new("id", proc_macro2::Span::call_site()),
syn::Ident::new("id", proc_macro2::Span::call_site()),
false,
false,
),
(
syn::Ident::new("address", proc_macro2::Span::call_site()),
syn::Ident::new("address", proc_macro2::Span::call_site()),
false,
true,
),
];
let relation_fields = vec![create_test_relation_info(
"address",
"HasOne",
quote! { crate::models::address::Schema },
true, )];
let source_module_path = vec![
"crate".to_string(),
"models".to_string(),
"user".to_string(),
];
let tokens = generate_from_model_with_relations(
&new_type_name,
&source_type,
&field_mappings,
&relation_fields,
&source_module_path,
&[],
);
unsafe {
if let Some(dir) = original_manifest_dir {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
let output = tokens.to_string();
assert!(output.contains("impl UserSchema"));
assert!(
output.contains("from_model (r , db) . await"),
"Should have async from_model: {}",
output
);
}
#[test]
#[serial]
fn test_generate_from_model_non_circular_has_one_with_fk_required() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
let models_dir = src_dir.join("models");
std::fs::create_dir_all(&models_dir).unwrap();
let address_model = r#"
pub struct Model {
pub id: i32,
pub street: String,
pub city_id: i32,
pub city: BelongsTo<super::city::Entity>,
}
"#;
std::fs::write(models_dir.join("address.rs"), address_model).unwrap();
let original_manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let new_type_name = syn::Ident::new("UserSchema", proc_macro2::Span::call_site());
let source_type: Type = syn::parse_str("crate::models::user::Model").unwrap();
let field_mappings = vec![
(
syn::Ident::new("id", proc_macro2::Span::call_site()),
syn::Ident::new("id", proc_macro2::Span::call_site()),
false,
false,
),
(
syn::Ident::new("address", proc_macro2::Span::call_site()),
syn::Ident::new("address", proc_macro2::Span::call_site()),
false,
true,
),
];
let relation_fields = vec![create_test_relation_info(
"address",
"HasOne",
quote! { crate::models::address::Schema },
false, )];
let source_module_path = vec![
"crate".to_string(),
"models".to_string(),
"user".to_string(),
];
let tokens = generate_from_model_with_relations(
&new_type_name,
&source_type,
&field_mappings,
&relation_fields,
&source_module_path,
&[],
);
unsafe {
if let Some(dir) = original_manifest_dir {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
let output = tokens.to_string();
assert!(output.contains("impl UserSchema"));
assert!(
output.contains("Box :: new"),
"Should have Box::new: {}",
output
);
assert!(
output.contains("from_model"),
"Should have from_model: {}",
output
);
assert!(
output.contains("ok_or_else"),
"Should have ok_or_else: {}",
output
);
}
#[test]
#[serial]
fn test_generate_from_model_has_many_with_circular() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
let models_dir = src_dir.join("models");
std::fs::create_dir_all(&models_dir).unwrap();
let memo_model = r#"
pub struct Model {
pub id: i32,
pub title: String,
pub user: BelongsTo<super::user::Entity>,
}
"#;
std::fs::write(models_dir.join("memo.rs"), memo_model).unwrap();
let original_manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let new_type_name = syn::Ident::new("UserSchema", proc_macro2::Span::call_site());
let source_type: Type = syn::parse_str("crate::models::user::Model").unwrap();
let field_mappings = vec![
(
syn::Ident::new("id", proc_macro2::Span::call_site()),
syn::Ident::new("id", proc_macro2::Span::call_site()),
false,
false,
),
(
syn::Ident::new("memos", proc_macro2::Span::call_site()),
syn::Ident::new("memos", proc_macro2::Span::call_site()),
false,
true,
),
];
let relation_fields = vec![create_test_relation_info(
"memos",
"HasMany",
quote! { crate::models::memo::Schema },
false,
)];
let source_module_path = vec![
"crate".to_string(),
"models".to_string(),
"user".to_string(),
];
let tokens = generate_from_model_with_relations(
&new_type_name,
&source_type,
&field_mappings,
&relation_fields,
&source_module_path,
&[],
);
unsafe {
if let Some(dir) = original_manifest_dir {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
let output = tokens.to_string();
assert!(output.contains("impl UserSchema"));
assert!(
output.contains("into_iter ()"),
"Should have into_iter: {}",
output
);
assert!(
output.contains(". map (| r |"),
"Should have map: {}",
output
);
assert!(
output.contains("collect"),
"Should have collect: {}",
output
);
}
#[test]
#[serial]
fn test_generate_from_model_has_many_with_fk_no_circular() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
let models_dir = src_dir.join("models");
std::fs::create_dir_all(&models_dir).unwrap();
let tag_model = r#"
pub struct Model {
pub id: i32,
pub name: String,
pub category_id: i32,
pub category: BelongsTo<super::category::Entity>,
}
"#;
std::fs::write(models_dir.join("tag.rs"), tag_model).unwrap();
let original_manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let new_type_name = syn::Ident::new("UserSchema", proc_macro2::Span::call_site());
let source_type: Type = syn::parse_str("crate::models::user::Model").unwrap();
let field_mappings = vec![
(
syn::Ident::new("id", proc_macro2::Span::call_site()),
syn::Ident::new("id", proc_macro2::Span::call_site()),
false,
false,
),
(
syn::Ident::new("tags", proc_macro2::Span::call_site()),
syn::Ident::new("tags", proc_macro2::Span::call_site()),
false,
true,
),
];
let relation_fields = vec![create_test_relation_info(
"tags",
"HasMany",
quote! { crate::models::tag::Schema },
false,
)];
let source_module_path = vec![
"crate".to_string(),
"models".to_string(),
"user".to_string(),
];
let tokens = generate_from_model_with_relations(
&new_type_name,
&source_type,
&field_mappings,
&relation_fields,
&source_module_path,
&[],
);
unsafe {
if let Some(dir) = original_manifest_dir {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
let output = tokens.to_string();
assert!(output.contains("impl UserSchema"));
assert!(
output.contains("into_iter ()"),
"Should have into_iter: {}",
output
);
assert!(
output.contains(". map (| r |"),
"Should have map: {}",
output
);
assert!(
output.contains("collect"),
"Should have collect: {}",
output
);
}
#[test]
#[serial]
fn test_generate_from_model_inline_type_required() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
let models_dir = src_dir.join("models");
std::fs::create_dir_all(&models_dir).unwrap();
let user_model = r#"
pub struct Model {
pub id: i32,
pub name: String,
}
"#;
std::fs::write(models_dir.join("user.rs"), user_model).unwrap();
let original_manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let new_type_name = syn::Ident::new("MemoSchema", proc_macro2::Span::call_site());
let source_type: Type = syn::parse_str("crate::models::memo::Model").unwrap();
let field_mappings = vec![
(
syn::Ident::new("id", proc_macro2::Span::call_site()),
syn::Ident::new("id", proc_macro2::Span::call_site()),
false,
false,
),
(
syn::Ident::new("user", proc_macro2::Span::call_site()),
syn::Ident::new("user", proc_macro2::Span::call_site()),
false,
true,
),
];
let mut rel_info = create_test_relation_info(
"user",
"BelongsTo",
quote! { crate::models::user::Schema },
false, );
rel_info.inline_type_info = Some((
syn::Ident::new("MemoSchema_User", proc_macro2::Span::call_site()),
vec!["id".to_string(), "name".to_string()],
));
let relation_fields = vec![rel_info];
let source_module_path = vec![
"crate".to_string(),
"models".to_string(),
"memo".to_string(),
];
let tokens = generate_from_model_with_relations(
&new_type_name,
&source_type,
&field_mappings,
&relation_fields,
&source_module_path,
&[],
);
unsafe {
if let Some(dir) = original_manifest_dir {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
let output = tokens.to_string();
assert!(output.contains("impl MemoSchema"));
assert!(
output.contains("Box :: new"),
"Should have Box::new: {}",
output
);
assert!(
output.contains("ok_or_else"),
"Should have ok_or_else: {}",
output
);
}
#[test]
#[serial]
fn test_generate_from_model_parent_stub_all_relation_types() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
let models_dir = src_dir.join("models");
std::fs::create_dir_all(&models_dir).unwrap();
let memo_model = r#"
pub struct Model {
pub id: i32,
pub title: String,
pub user_id: i32,
#[sea_orm(belongs_to = "super::user::Entity", from = "user_id")]
pub user: BelongsTo<super::user::Entity>,
}
"#;
std::fs::write(models_dir.join("memo.rs"), memo_model).unwrap();
let profile_model = r#"
pub struct Model {
pub id: i32,
pub bio: String,
}
"#;
std::fs::write(models_dir.join("profile.rs"), profile_model).unwrap();
let settings_model = r#"
pub struct Model {
pub id: i32,
pub theme: String,
}
"#;
std::fs::write(models_dir.join("settings.rs"), settings_model).unwrap();
let original_manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let new_type_name = syn::Ident::new("UserSchema", proc_macro2::Span::call_site());
let source_type: Type = syn::parse_str("crate::models::user::Model").unwrap();
let field_mappings = vec![
(
syn::Ident::new("id", proc_macro2::Span::call_site()),
syn::Ident::new("id", proc_macro2::Span::call_site()),
false,
false,
),
(
syn::Ident::new("memos", proc_macro2::Span::call_site()),
syn::Ident::new("memos", proc_macro2::Span::call_site()),
false,
true,
),
(
syn::Ident::new("profile", proc_macro2::Span::call_site()),
syn::Ident::new("profile", proc_macro2::Span::call_site()),
false,
true,
),
(
syn::Ident::new("settings", proc_macro2::Span::call_site()),
syn::Ident::new("settings", proc_macro2::Span::call_site()),
false,
true,
),
(
syn::Ident::new("orphan_rel", proc_macro2::Span::call_site()),
syn::Ident::new("orphan_rel", proc_macro2::Span::call_site()),
false,
true,
),
];
let relation_fields = vec![
create_test_relation_info(
"memos",
"HasMany",
quote! { crate::models::memo::Schema },
false,
),
create_test_relation_info(
"profile",
"HasOne",
quote! { crate::models::profile::Schema },
true, ),
create_test_relation_info(
"settings",
"BelongsTo",
quote! { crate::models::settings::Schema },
false, ),
];
let source_module_path = vec![
"crate".to_string(),
"models".to_string(),
"user".to_string(),
];
let tokens = generate_from_model_with_relations(
&new_type_name,
&source_type,
&field_mappings,
&relation_fields,
&source_module_path,
&[],
);
unsafe {
if let Some(dir) = original_manifest_dir {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
let output = tokens.to_string();
assert!(output.contains("impl UserSchema"));
assert!(
output.contains("__parent_stub__"),
"Should have parent stub: {}",
output
);
assert!(
output.contains("memos : vec ! []"),
"Should have memos: vec![]: {}",
output
);
assert!(
output.contains("profile : None") || output.contains("settings : None"),
"Should have None for single relations: {}",
output
);
assert!(
output.contains("Default :: default ()"),
"Should have Default::default() for orphan: {}",
output
);
}
fn create_test_relation_info_full(
field_name: &str,
relation_type: &str,
schema_path: TokenStream,
is_optional: bool,
relation_enum: Option<String>,
fk_column: Option<String>,
via_rel: Option<String>,
) -> RelationFieldInfo {
RelationFieldInfo {
field_name: syn::Ident::new(field_name, proc_macro2::Span::call_site()),
relation_type: relation_type.to_string(),
schema_path,
is_optional,
inline_type_info: None,
relation_enum,
fk_column,
via_rel,
}
}
#[test]
fn test_generate_from_model_has_one_with_relation_enum_optional_with_fk() {
let new_type_name = syn::Ident::new("MemoSchema", proc_macro2::Span::call_site());
let source_type: Type = syn::parse_str("Model").unwrap();
let field_mappings = vec![
(
syn::Ident::new("id", proc_macro2::Span::call_site()),
syn::Ident::new("id", proc_macro2::Span::call_site()),
false,
false,
),
(
syn::Ident::new("target_user", proc_macro2::Span::call_site()),
syn::Ident::new("target_user", proc_macro2::Span::call_site()),
false,
true,
),
];
let relation_fields = vec![create_test_relation_info_full(
"target_user",
"HasOne",
quote! { user::Schema },
true, Some("TargetUser".to_string()), Some("target_user_id".to_string()), None, )];
let source_module_path = vec![
"crate".to_string(),
"models".to_string(),
"memo".to_string(),
];
let tokens = generate_from_model_with_relations(
&new_type_name,
&source_type,
&field_mappings,
&relation_fields,
&source_module_path,
&[],
);
let output = tokens.to_string();
assert!(output.contains("impl MemoSchema"));
assert!(
output.contains("match & model . target_user_id"),
"Should match on FK field: {}",
output
);
assert!(
output.contains("Some (fk_value)"),
"Should have Some(fk_value) arm: {}",
output
);
assert!(
output.contains("find_by_id"),
"Should use find_by_id: {}",
output
);
}
#[test]
fn test_generate_from_model_has_one_with_relation_enum_optional_no_fk() {
let new_type_name = syn::Ident::new("MemoSchema", proc_macro2::Span::call_site());
let source_type: Type = syn::parse_str("Model").unwrap();
let field_mappings = vec![
(
syn::Ident::new("id", proc_macro2::Span::call_site()),
syn::Ident::new("id", proc_macro2::Span::call_site()),
false,
false,
),
(
syn::Ident::new("author", proc_macro2::Span::call_site()),
syn::Ident::new("author", proc_macro2::Span::call_site()),
false,
true,
),
];
let relation_fields = vec![create_test_relation_info_full(
"author",
"HasOne",
quote! { user::Schema },
true, Some("Author".to_string()), None, None, )];
let source_module_path = vec![
"crate".to_string(),
"models".to_string(),
"memo".to_string(),
];
let tokens = generate_from_model_with_relations(
&new_type_name,
&source_type,
&field_mappings,
&relation_fields,
&source_module_path,
&[],
);
let output = tokens.to_string();
assert!(output.contains("impl MemoSchema"));
assert!(
output.contains("Entity :: find_related (Relation :: Author)"),
"Should use find_related with Relation enum: {}",
output
);
}
#[test]
fn test_generate_from_model_belongs_to_with_relation_enum_required_with_fk() {
let new_type_name = syn::Ident::new("CommentSchema", proc_macro2::Span::call_site());
let source_type: Type = syn::parse_str("Model").unwrap();
let field_mappings = vec![
(
syn::Ident::new("id", proc_macro2::Span::call_site()),
syn::Ident::new("id", proc_macro2::Span::call_site()),
false,
false,
),
(
syn::Ident::new("post", proc_macro2::Span::call_site()),
syn::Ident::new("post", proc_macro2::Span::call_site()),
false,
true,
),
];
let relation_fields = vec![create_test_relation_info_full(
"post",
"BelongsTo",
quote! { post::Schema },
false, Some("Post".to_string()), Some("post_id".to_string()), None, )];
let source_module_path = vec![
"crate".to_string(),
"models".to_string(),
"comment".to_string(),
];
let tokens = generate_from_model_with_relations(
&new_type_name,
&source_type,
&field_mappings,
&relation_fields,
&source_module_path,
&[],
);
let output = tokens.to_string();
assert!(output.contains("impl CommentSchema"));
assert!(
output.contains("find_by_id (model . post_id . clone ())"),
"Should use find_by_id with FK: {}",
output
);
}
#[test]
fn test_generate_from_model_belongs_to_with_relation_enum_required_no_fk() {
let new_type_name = syn::Ident::new("CommentSchema", proc_macro2::Span::call_site());
let source_type: Type = syn::parse_str("Model").unwrap();
let field_mappings = vec![
(
syn::Ident::new("id", proc_macro2::Span::call_site()),
syn::Ident::new("id", proc_macro2::Span::call_site()),
false,
false,
),
(
syn::Ident::new("author", proc_macro2::Span::call_site()),
syn::Ident::new("author", proc_macro2::Span::call_site()),
false,
true,
),
];
let relation_fields = vec![create_test_relation_info_full(
"author",
"BelongsTo",
quote! { user::Schema },
false, Some("Author".to_string()), None, None, )];
let source_module_path = vec![
"crate".to_string(),
"models".to_string(),
"comment".to_string(),
];
let tokens = generate_from_model_with_relations(
&new_type_name,
&source_type,
&field_mappings,
&relation_fields,
&source_module_path,
&[],
);
let output = tokens.to_string();
assert!(output.contains("impl CommentSchema"));
assert!(
output.contains("Entity :: find_related (Relation :: Author)"),
"Should use find_related with Relation enum: {}",
output
);
}
#[test]
#[serial]
fn test_generate_from_model_has_many_with_via_rel_fk_found() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
let models_dir = src_dir.join("models");
std::fs::create_dir_all(&models_dir).unwrap();
let notification_model = r#"
pub struct Model {
pub id: i32,
pub message: String,
pub target_user_id: i32,
#[sea_orm(belongs_to = "super::user::Entity", from = "target_user_id", to = "id", relation_enum = "TargetUser")]
pub target_user: BelongsTo<super::user::Entity>,
}
"#;
std::fs::write(models_dir.join("notification.rs"), notification_model).unwrap();
let original_manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let new_type_name = syn::Ident::new("UserSchema", proc_macro2::Span::call_site());
let source_type: Type = syn::parse_str("crate::models::user::Model").unwrap();
let field_mappings = vec![
(
syn::Ident::new("id", proc_macro2::Span::call_site()),
syn::Ident::new("id", proc_macro2::Span::call_site()),
false,
false,
),
(
syn::Ident::new("target_user_notifications", proc_macro2::Span::call_site()),
syn::Ident::new("target_user_notifications", proc_macro2::Span::call_site()),
false,
true,
),
];
let relation_fields = vec![create_test_relation_info_full(
"target_user_notifications",
"HasMany",
quote! { crate::models::notification::Schema },
false,
None,
None,
Some("TargetUser".to_string()), )];
let source_module_path = vec![
"crate".to_string(),
"models".to_string(),
"user".to_string(),
];
let tokens = generate_from_model_with_relations(
&new_type_name,
&source_type,
&field_mappings,
&relation_fields,
&source_module_path,
&[],
);
unsafe {
if let Some(dir) = original_manifest_dir {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
let output = tokens.to_string();
assert!(output.contains("impl UserSchema"));
assert!(
output.contains("TargetUserId"),
"Should have FK column identifier: {}",
output
);
assert!(
output.contains("into_column ()"),
"Should have into_column: {}",
output
);
assert!(
output.contains("eq (model . id . clone ())"),
"Should compare with model.id: {}",
output
);
assert!(
output.contains(". all (db)"),
"Should use .all(db): {}",
output
);
}
#[test]
#[serial]
fn test_generate_from_model_has_many_with_via_rel_fk_not_found() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
let models_dir = src_dir.join("models");
std::fs::create_dir_all(&models_dir).unwrap();
let notification_model = r#"
pub struct Model {
pub id: i32,
pub message: String,
}
"#;
std::fs::write(models_dir.join("notification.rs"), notification_model).unwrap();
let original_manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let new_type_name = syn::Ident::new("UserSchema", proc_macro2::Span::call_site());
let source_type: Type = syn::parse_str("crate::models::user::Model").unwrap();
let field_mappings = vec![
(
syn::Ident::new("id", proc_macro2::Span::call_site()),
syn::Ident::new("id", proc_macro2::Span::call_site()),
false,
false,
),
(
syn::Ident::new("notifications", proc_macro2::Span::call_site()),
syn::Ident::new("notifications", proc_macro2::Span::call_site()),
false,
true,
),
];
let relation_fields = vec![create_test_relation_info_full(
"notifications",
"HasMany",
quote! { crate::models::notification::Schema },
false,
None,
None,
Some("NonExistentRelation".to_string()), )];
let source_module_path = vec![
"crate".to_string(),
"models".to_string(),
"user".to_string(),
];
let tokens = generate_from_model_with_relations(
&new_type_name,
&source_type,
&field_mappings,
&relation_fields,
&source_module_path,
&[],
);
unsafe {
if let Some(dir) = original_manifest_dir {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
let output = tokens.to_string();
assert!(output.contains("impl UserSchema"));
assert!(
output.contains("vec ! []"),
"Should fall back to empty vec: {}",
output
);
}
#[test]
#[serial]
fn test_generate_from_model_has_many_with_relation_enum_fk_found() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
let models_dir = src_dir.join("models");
std::fs::create_dir_all(&models_dir).unwrap();
let comment_model = r#"
pub struct Model {
pub id: i32,
pub content: String,
pub author_id: i32,
#[sea_orm(belongs_to = "super::user::Entity", from = "author_id", to = "id", relation_enum = "AuthorComments")]
pub author: BelongsTo<super::user::Entity>,
}
"#;
std::fs::write(models_dir.join("comment.rs"), comment_model).unwrap();
let original_manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let new_type_name = syn::Ident::new("UserSchema", proc_macro2::Span::call_site());
let source_type: Type = syn::parse_str("crate::models::user::Model").unwrap();
let field_mappings = vec![
(
syn::Ident::new("id", proc_macro2::Span::call_site()),
syn::Ident::new("id", proc_macro2::Span::call_site()),
false,
false,
),
(
syn::Ident::new("author_comments", proc_macro2::Span::call_site()),
syn::Ident::new("author_comments", proc_macro2::Span::call_site()),
false,
true,
),
];
let relation_fields = vec![create_test_relation_info_full(
"author_comments",
"HasMany",
quote! { crate::models::comment::Schema },
false,
Some("AuthorComments".to_string()), None,
None, )];
let source_module_path = vec![
"crate".to_string(),
"models".to_string(),
"user".to_string(),
];
let tokens = generate_from_model_with_relations(
&new_type_name,
&source_type,
&field_mappings,
&relation_fields,
&source_module_path,
&[],
);
unsafe {
if let Some(dir) = original_manifest_dir {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
let output = tokens.to_string();
assert!(output.contains("impl UserSchema"));
assert!(
output.contains("AuthorId"),
"Should have FK column identifier: {}",
output
);
assert!(
output.contains("into_column ()"),
"Should have into_column: {}",
output
);
assert!(
output.contains(". all (db)"),
"Should use .all(db): {}",
output
);
}
#[test]
#[serial]
fn test_generate_from_model_has_many_with_relation_enum_fk_not_found() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
let models_dir = src_dir.join("models");
std::fs::create_dir_all(&models_dir).unwrap();
let post_model = r#"
pub struct Model {
pub id: i32,
pub title: String,
}
"#;
std::fs::write(models_dir.join("post.rs"), post_model).unwrap();
let original_manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let new_type_name = syn::Ident::new("UserSchema", proc_macro2::Span::call_site());
let source_type: Type = syn::parse_str("crate::models::user::Model").unwrap();
let field_mappings = vec![
(
syn::Ident::new("id", proc_macro2::Span::call_site()),
syn::Ident::new("id", proc_macro2::Span::call_site()),
false,
false,
),
(
syn::Ident::new("authored_posts", proc_macro2::Span::call_site()),
syn::Ident::new("authored_posts", proc_macro2::Span::call_site()),
false,
true,
),
];
let relation_fields = vec![create_test_relation_info_full(
"authored_posts",
"HasMany",
quote! { crate::models::post::Schema },
false,
Some("NonExistentRelation".to_string()), None,
None, )];
let source_module_path = vec![
"crate".to_string(),
"models".to_string(),
"user".to_string(),
];
let tokens = generate_from_model_with_relations(
&new_type_name,
&source_type,
&field_mappings,
&relation_fields,
&source_module_path,
&[],
);
unsafe {
if let Some(dir) = original_manifest_dir {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
let output = tokens.to_string();
assert!(output.contains("impl UserSchema"));
assert!(
output.contains("vec ! []"),
"Should fall back to empty vec: {}",
output
);
}
}