Skip to content

Commit 8fb052f

Browse files
authored
feat(macros): permit passing custom derives to Model or ModelEx (#2933)
This change allows for granular control over the attributes on both `Model` and `ModelEx`. For instance, it enables users to customize the generated TypeScript interface names when using `ts-rs` with `ModelEx`. ### Usage ```rs #[sea_orm::model] #[derive(TS, ...)] // Apply attributes specifically to the generated Model struct #[sea_orm(model_attrs(ts(rename = "Fruit")))] // Apply attributes specifically to the generated ModelEx struct #[sea_orm(model_ex_attrs(ts(rename = "FruitEx")))] struct Model { // ... } ``` **The code above expands to:** ```rs // ... #[derive(TS, ...)] #[ts(rename = "Fruit")] struct Model { // ... } // ... #[derive(TS, ...)] #[ts(rename = "FruitEx")] struct ModelEx { // ... } ``` --- * [Full Example: SeaORM 2.0 and ts-rs Integration](https://github.com/mcitem/sea-orm-mini-app)
1 parent 9a59e1e commit 8fb052f

File tree

2 files changed

+129
-2
lines changed

2 files changed

+129
-2
lines changed

sea-orm-macros/src/derives/model_ex.rs

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,63 @@ use syn::{
1313

1414
pub fn expand_sea_orm_model(input: ItemStruct, compact: bool) -> syn::Result<TokenStream> {
1515
let model = input.ident;
16-
let model_attrs = input.attrs;
1716
let vis = input.vis;
1817
let mut all_fields = input.fields;
1918

19+
let mut model_attrs: Vec<Attribute> = Vec::new();
20+
let mut model_ex_attrs: Vec<Attribute> = Vec::new();
21+
22+
for attr in input.attrs {
23+
if !attr.path().is_ident("sea_orm") {
24+
model_attrs.push(attr.clone());
25+
model_ex_attrs.push(attr);
26+
continue;
27+
}
28+
29+
let mut other_attrs = Punctuated::<Meta, Comma>::new();
30+
31+
attr.parse_nested_meta(|meta| {
32+
let is_model = meta.path.is_ident("model_attrs");
33+
let is_model_ex = meta.path.is_ident("model_ex_attrs");
34+
35+
if is_model || is_model_ex {
36+
let content;
37+
syn::parenthesized!(content in meta.input);
38+
use syn::parse::Parse;
39+
let nested_metas = content.parse_terminated(Meta::parse, Comma)?;
40+
for m in nested_metas {
41+
let new_attr: Attribute = parse_quote!( #[#m] );
42+
if is_model {
43+
model_attrs.push(new_attr);
44+
} else {
45+
model_ex_attrs.push(new_attr);
46+
}
47+
}
48+
} else {
49+
let path = &meta.path;
50+
if meta.input.peek(syn::Token![=]) {
51+
let value: Expr = meta.value()?.parse()?;
52+
other_attrs.push(parse_quote!( #path = #value ));
53+
} else if meta.input.is_empty() || meta.input.peek(Comma) {
54+
other_attrs.push(parse_quote!( #path ));
55+
} else {
56+
let content;
57+
syn::parenthesized!(content in meta.input);
58+
let tokens: TokenStream = content.parse()?;
59+
other_attrs.push(parse_quote!( #path(#tokens) ));
60+
}
61+
}
62+
Ok(())
63+
})?;
64+
65+
if !other_attrs.is_empty() {
66+
let attr: Attribute = parse_quote!( #[sea_orm(#other_attrs)] );
67+
model_attrs.push(attr.clone());
68+
model_ex_attrs.push(attr);
69+
}
70+
}
71+
2072
let model_ex = Ident::new(&format!("{model}Ex"), model.span());
21-
let mut model_ex_attrs = model_attrs.clone();
2273
for attr in &mut model_ex_attrs {
2374
if attr.path().is_ident("derive") {
2475
if let Meta::List(list) = &mut attr.meta {

tests/derive_model_tests.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
use sea_orm::prelude::{HasMany, HasOne};
2+
3+
mod cake {
4+
use sea_orm::prelude::*;
5+
use serde::Serialize;
6+
7+
#[sea_orm::model]
8+
#[derive(DeriveEntityModel, Debug, Clone, Serialize)]
9+
#[sea_orm(table_name = "cake")]
10+
#[sea_orm(model_attrs(serde(rename_all = "UPPERCASE")))]
11+
#[sea_orm(model_ex_attrs(serde(rename_all = "PascalCase")))]
12+
pub struct Model {
13+
#[sea_orm(primary_key)]
14+
pub id: i32,
15+
#[sea_orm(has_many)]
16+
pub fruits: HasMany<super::fruit::Entity>,
17+
}
18+
19+
impl ActiveModelBehavior for ActiveModel {}
20+
}
21+
22+
mod fruit {
23+
use sea_orm::prelude::*;
24+
use serde::Serialize;
25+
26+
#[sea_orm::model]
27+
#[derive(DeriveEntityModel, Debug, Clone)]
28+
#[sea_orm(
29+
table_name = "fruit",
30+
model_attrs(derive(Serialize), serde(rename_all = "UPPERCASE")),
31+
model_ex_attrs(derive(Serialize), serde(rename_all = "PascalCase"))
32+
)]
33+
pub struct Model {
34+
#[sea_orm(primary_key)]
35+
pub id: i32,
36+
pub cake_id: Option<i32>,
37+
#[sea_orm(belongs_to, from = "cake_id", to = "id")]
38+
pub cake: HasOne<super::cake::Entity>,
39+
}
40+
41+
impl ActiveModelBehavior for ActiveModel {}
42+
}
43+
44+
#[test]
45+
fn main() -> Result<(), serde_json::Error> {
46+
use sea_orm::EntityName;
47+
assert_eq!(cake::Entity.table_name(), "cake");
48+
assert_eq!(fruit::Entity.table_name(), "fruit");
49+
50+
assert_eq!(serde_json::to_string(&cake::Model { id: 1 })?, "{\"ID\":1}");
51+
assert_eq!(
52+
serde_json::to_string(&cake::ModelEx {
53+
id: 1,
54+
fruits: HasMany::Loaded(Vec::new()),
55+
})?,
56+
"{\"Id\":1,\"Fruits\":[]}"
57+
);
58+
59+
assert_eq!(
60+
serde_json::to_string(&fruit::Model {
61+
id: 2,
62+
cake_id: Some(1)
63+
})?,
64+
"{\"ID\":2,\"CAKE_ID\":1}"
65+
);
66+
assert_eq!(
67+
serde_json::to_string(&fruit::ModelEx {
68+
id: 2,
69+
cake_id: Some(1),
70+
cake: HasOne::Unloaded,
71+
})?,
72+
"{\"Id\":2,\"CakeId\":1,\"Cake\":null}"
73+
);
74+
75+
Ok(())
76+
}

0 commit comments

Comments
 (0)