From 99a9080f4176a51c66be60c05881193bc21da2d0 Mon Sep 17 00:00:00 2001 From: mcitem Date: Thu, 29 Jan 2026 16:45:52 +0800 Subject: [PATCH 1/3] feat(macros): permit passing custom derives to Model or ModelEx --- sea-orm-macros/src/derives/model_ex.rs | 32 ++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/sea-orm-macros/src/derives/model_ex.rs b/sea-orm-macros/src/derives/model_ex.rs index 77b3adb52..44cb8a1b8 100644 --- a/sea-orm-macros/src/derives/model_ex.rs +++ b/sea-orm-macros/src/derives/model_ex.rs @@ -13,12 +13,40 @@ use syn::{ pub fn expand_sea_orm_model(input: ItemStruct, compact: bool) -> syn::Result { let model = input.ident; - let model_attrs = input.attrs; let vis = input.vis; let mut all_fields = input.fields; + let mut model_attrs: Vec = Vec::new(); + let mut model_ex_attrs: Vec = Vec::new(); + + for attr in input.attrs { + let is_model = attr.path().is_ident("sea_orm_model"); + let is_model_ex = attr.path().is_ident("sea_orm_model_ex"); + if is_model || is_model_ex { + attr.parse_nested_meta(|meta| { + let path = &meta.path; + let new_attr: Attribute = if meta.input.peek(syn::token::Paren) { + let content; + syn::parenthesized!(content in meta.input); + let inner: TokenStream = content.parse()?; + parse_quote!( #[#path(#inner)] ) + } else { + parse_quote!( #[#path] ) + }; + if is_model { + model_attrs.push(new_attr); + } else { + model_ex_attrs.push(new_attr); + } + Ok(()) + })?; + } else { + model_attrs.push(attr.clone()); + model_ex_attrs.push(attr); + } + } + let model_ex = Ident::new(&format!("{model}Ex"), model.span()); - let mut model_ex_attrs = model_attrs.clone(); for attr in &mut model_ex_attrs { if attr.path().is_ident("derive") { if let Meta::List(list) = &mut attr.meta { From 67d331bc8d65abc0f05c31b98ca8dba5b5ed7687 Mon Sep 17 00:00:00 2001 From: mcitem Date: Thu, 29 Jan 2026 21:56:15 +0800 Subject: [PATCH 2/3] refactor: transition #[sea_orm_model(..)] to nested #[sea_orm(model_attrs(..))] syntax --- sea-orm-macros/src/derives/model_ex.rs | 57 ++++++++++++++++++-------- 1 file changed, 40 insertions(+), 17 deletions(-) diff --git a/sea-orm-macros/src/derives/model_ex.rs b/sea-orm-macros/src/derives/model_ex.rs index 44cb8a1b8..ed3d81b89 100644 --- a/sea-orm-macros/src/derives/model_ex.rs +++ b/sea-orm-macros/src/derives/model_ex.rs @@ -20,27 +20,50 @@ pub fn expand_sea_orm_model(input: ItemStruct, compact: bool) -> syn::Result = Vec::new(); for attr in input.attrs { - let is_model = attr.path().is_ident("sea_orm_model"); - let is_model_ex = attr.path().is_ident("sea_orm_model_ex"); - if is_model || is_model_ex { - attr.parse_nested_meta(|meta| { + if !attr.path().is_ident("sea_orm") { + model_attrs.push(attr.clone()); + model_ex_attrs.push(attr); + continue; + } + + let mut other_attrs = Punctuated::::new(); + + attr.parse_nested_meta(|meta| { + let is_model = meta.path.is_ident("model_attrs"); + let is_model_ex = meta.path.is_ident("model_ex_attrs"); + + if is_model || is_model_ex { + let content; + syn::parenthesized!(content in meta.input); + use syn::parse::Parse; + let nested_metas = content.parse_terminated(Meta::parse, Comma)?; + for m in nested_metas { + let new_attr: Attribute = parse_quote!( #[#m] ); + if is_model { + model_attrs.push(new_attr); + } else { + model_ex_attrs.push(new_attr); + } + } + } else { let path = &meta.path; - let new_attr: Attribute = if meta.input.peek(syn::token::Paren) { + if meta.input.peek(syn::Token![=]) { + let value: Expr = meta.value()?.parse()?; + other_attrs.push(parse_quote!( #path = #value )); + } else if meta.input.is_empty() || meta.input.peek(Comma) { + other_attrs.push(parse_quote!( #path )); + } else { let content; syn::parenthesized!(content in meta.input); - let inner: TokenStream = content.parse()?; - parse_quote!( #[#path(#inner)] ) - } else { - parse_quote!( #[#path] ) - }; - if is_model { - model_attrs.push(new_attr); - } else { - model_ex_attrs.push(new_attr); + let tokens: TokenStream = content.parse()?; + other_attrs.push(parse_quote!( #path(#tokens) )); } - Ok(()) - })?; - } else { + } + Ok(()) + })?; + + if !other_attrs.is_empty() { + let attr: Attribute = parse_quote!( #[sea_orm(#other_attrs)] ); model_attrs.push(attr.clone()); model_ex_attrs.push(attr); } From 1015c04795b6a968cf58c55e89c7b58a26b357d9 Mon Sep 17 00:00:00 2001 From: mcitem Date: Fri, 30 Jan 2026 09:37:50 +0800 Subject: [PATCH 3/3] test: add tests for macros #[sea_orm(model_attrs(..))] --- tests/derive_model_tests.rs | 76 +++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 tests/derive_model_tests.rs diff --git a/tests/derive_model_tests.rs b/tests/derive_model_tests.rs new file mode 100644 index 000000000..e4eb6cc04 --- /dev/null +++ b/tests/derive_model_tests.rs @@ -0,0 +1,76 @@ +use sea_orm::prelude::{HasMany, HasOne}; + +mod cake { + use sea_orm::prelude::*; + use serde::Serialize; + + #[sea_orm::model] + #[derive(DeriveEntityModel, Debug, Clone, Serialize)] + #[sea_orm(table_name = "cake")] + #[sea_orm(model_attrs(serde(rename_all = "UPPERCASE")))] + #[sea_orm(model_ex_attrs(serde(rename_all = "PascalCase")))] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + #[sea_orm(has_many)] + pub fruits: HasMany, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +mod fruit { + use sea_orm::prelude::*; + use serde::Serialize; + + #[sea_orm::model] + #[derive(DeriveEntityModel, Debug, Clone)] + #[sea_orm( + table_name = "fruit", + model_attrs(derive(Serialize), serde(rename_all = "UPPERCASE")), + model_ex_attrs(derive(Serialize), serde(rename_all = "PascalCase")) + )] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub cake_id: Option, + #[sea_orm(belongs_to, from = "cake_id", to = "id")] + pub cake: HasOne, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +#[test] +fn main() -> Result<(), serde_json::Error> { + use sea_orm::EntityName; + assert_eq!(cake::Entity.table_name(), "cake"); + assert_eq!(fruit::Entity.table_name(), "fruit"); + + assert_eq!(serde_json::to_string(&cake::Model { id: 1 })?, "{\"ID\":1}"); + assert_eq!( + serde_json::to_string(&cake::ModelEx { + id: 1, + fruits: HasMany::Loaded(Vec::new()), + })?, + "{\"Id\":1,\"Fruits\":[]}" + ); + + assert_eq!( + serde_json::to_string(&fruit::Model { + id: 2, + cake_id: Some(1) + })?, + "{\"ID\":2,\"CAKE_ID\":1}" + ); + assert_eq!( + serde_json::to_string(&fruit::ModelEx { + id: 2, + cake_id: Some(1), + cake: HasOne::Unloaded, + })?, + "{\"Id\":2,\"CakeId\":1,\"Cake\":null}" + ); + + Ok(()) +}