Skip to content

Commit dc7f97b

Browse files
authored
feat: Extend DeriveIntoActiveModel (#2961)
* Add default and set functionality to DeriveIntoActiveModel macros * Add simple comments to avoid adding more functions Remove redundant mut skip Run cargo fmt * Add foreign ingnore test * Add draft docs for DeriveIntoActiveModel * Fix silent fail on invalid rust in default attribute Move field attribute parsing into a function * Error on dublicate `default` attributes * Add comments and internal docs Rename sets to set_fields * Add fill as an alias for `set` container attribute * Add a test both variants of writing `set` attribute * Add bare `default` variant support * Format docs * Allow `default` attribute for enums other than Option<T> through `.into()` * Add a test for external CustomOption implmentations
1 parent 5e109bd commit dc7f97b

5 files changed

Lines changed: 783 additions & 12 deletions

File tree

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

Lines changed: 166 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,48 @@ enum Error {
99
Syn(syn::Error),
1010
}
1111

12+
/// Matches all potential ways to convert struct fields into ActiveModel ones
13+
pub(super) enum IntoActiveModelField {
14+
/// `IntoActiveValue::into_active_value(self.field).into()`
15+
Normal(syn::Ident),
16+
/// Option<T> with fallback: `Some(v) => Set(v).into(), None => Set(expr).into()`
17+
WithDefault { ident: syn::Ident, expr: syn::Expr },
18+
}
19+
20+
impl IntoActiveModelField {
21+
pub(super) fn ident(&self) -> &syn::Ident {
22+
match self {
23+
IntoActiveModelField::Normal(ident) => ident,
24+
IntoActiveModelField::WithDefault { ident, .. } => ident,
25+
}
26+
}
27+
}
28+
29+
/// Contains all the information extracted from the input struct and its attributes
30+
/// needed to generate the `IntoActiveModel` trait implementation.
1231
pub(super) struct DeriveIntoActiveModel {
32+
/// The identifier of the input struct
1333
pub ident: syn::Ident,
34+
/// Optional explicit ActiveModel type specified via `#[sea_orm(active_model = "Type")]`
1435
pub active_model: Option<syn::Type>,
15-
pub fields: Vec<syn::Ident>,
36+
/// handles provided struct fields
37+
pub fields: Vec<IntoActiveModelField>,
38+
/// handles fields set by #[sea_orm(set(field = expr))]
39+
pub set_fields: Vec<(syn::Ident, syn::Expr)>,
40+
/// require all fields specified, no `..default::Default()`
41+
pub exhaustive: bool,
1642
}
1743

1844
impl DeriveIntoActiveModel {
45+
/// This function finds attributes relevant for this macros:
46+
/// Container attributes (#[sea_orm(...)]) on the struct for:
47+
/// - active_model: explicit ActiveModel type
48+
/// - exhaustive: require all fields to be set
49+
/// - set/fill(...): provided values for ommited fields
50+
///
51+
/// Field attributes (#[sea_orm(...)]) with:
52+
/// - ignore/skip: exclude from conversion
53+
/// - default: fallback value for Option<T> fields
1954
fn new(input: syn::DeriveInput) -> Result<Self, Error> {
2055
let fields = match input.data {
2156
syn::Data::Struct(syn::DataStruct {
@@ -26,30 +61,65 @@ impl DeriveIntoActiveModel {
2661
};
2762

2863
let mut active_model = None;
64+
let mut set_fields = Vec::new();
65+
let mut exhaustive = false;
2966

3067
for attr in input.attrs.iter() {
3168
if !attr.path().is_ident("sea_orm") {
3269
continue;
3370
}
3471

72+
// Parse container attributes: #[sea_orm(...)]
73+
// Supports:
74+
// - active_model = "Type": explicitly specify the ActiveModel type
75+
// - exhaustive: require all ActiveModel fields to be explicitly set
76+
// - set(field = expr, ...): provide default values for fields not in the input struct
3577
if let Ok(list) = attr.parse_args_with(Punctuated::<Meta, Comma>::parse_terminated) {
3678
for meta in list {
79+
// Parse active_model attribute: #[sea_orm(active_model = "MyActiveModel")]
3780
if let Some(s) = meta.get_as_kv("active_model") {
3881
active_model = Some(syn::parse_str::<syn::Type>(&s).map_err(Error::Syn)?);
3982
}
83+
// Parse exhaustive flag: #[sea_orm(exhaustive)]
84+
// When set, prevents using Default::default() for unspecified fields
85+
if meta.exists("exhaustive") {
86+
exhaustive = true;
87+
}
88+
// Parse set/fill attribute: #[sea_orm(set(field1 = expr1, field2 = expr2, ...))]
89+
// Collects field assignments to be included in the generated ActiveModel
90+
if let Meta::List(meta_list) = &meta {
91+
if meta_list.path.is_ident("set") || meta_list.path.is_ident("fill") {
92+
let nested = meta_list
93+
.parse_args_with(Punctuated::<Meta, Comma>::parse_terminated)
94+
.map_err(Error::Syn)?;
95+
for nested_meta in nested {
96+
if let Some(val) = nested_meta.get_as_kv_with_ident() {
97+
let (ident, expr_str) = val;
98+
let expr = syn::parse_str::<syn::Expr>(&expr_str)
99+
.map_err(Error::Syn)?;
100+
set_fields.push((ident, expr));
101+
}
102+
}
103+
}
104+
}
40105
}
41106
}
42107
}
43108

44-
let field_idents = fields
45-
.iter()
46-
.map(|field| field.ident.as_ref().unwrap().clone())
47-
.collect();
109+
// Field attributes
110+
let mut field_idents: Vec<IntoActiveModelField> = Vec::new();
111+
for field in fields.iter() {
112+
if let Some(f) = parse_field(field)? {
113+
field_idents.push(f);
114+
}
115+
}
48116

49117
Ok(Self {
50118
ident: input.ident,
51119
active_model,
52120
fields: field_idents,
121+
set_fields,
122+
exhaustive,
53123
})
54124
}
55125

@@ -59,17 +129,21 @@ impl DeriveIntoActiveModel {
59129
Ok(expanded_impl_into_active_model)
60130
}
61131

132+
/// Generates the implementation of `IntoActiveModel` trait for the input struct
62133
pub(super) fn impl_into_active_model(&self) -> TokenStream {
63134
let Self {
64135
ident,
65136
active_model,
66137
fields,
138+
set_fields,
139+
exhaustive,
67140
} = self;
68141

69142
let mut active_model_ident = active_model
70143
.clone()
71144
.unwrap_or_else(|| syn::parse_str::<syn::Type>("ActiveModel").unwrap());
72145

146+
// Create a type alias for qualified types
73147
let type_alias_definition = if is_qualified_type(&active_model_ident) {
74148
let type_alias = format_ident!("ActiveModelFor{ident}");
75149
let type_def = quote!( type #type_alias = #active_model_ident; );
@@ -90,28 +164,111 @@ impl DeriveIntoActiveModel {
90164
quote!()
91165
};
92166

93-
let expanded_fields = fields.iter().map(|field_ident| {
167+
let field_idents: Vec<_> = fields.iter().map(|f| f.ident()).collect();
168+
169+
// Generate field conversion code based on field type
170+
let expanded_fields = fields.iter().map(|field| match field {
171+
IntoActiveModelField::Normal(ident) => quote!(
172+
sea_orm::IntoActiveValue::<_>::into_active_value(self.#ident).into()
173+
),
174+
IntoActiveModelField::WithDefault { ident, expr } => quote!({
175+
match self.#ident.into() {
176+
Some(v) => sea_orm::ActiveValue::Set(v).into(),
177+
None => sea_orm::ActiveValue::Set(#expr).into(),
178+
}
179+
}),
180+
});
181+
182+
// Add custom field assignments from #[sea_orm(set(field = expr))]
183+
let (set_idents, set_exprs): (Vec<_>, Vec<_>) = set_fields.iter().cloned().unzip();
184+
let expanded_sets = set_exprs.iter().map(|expr| {
94185
quote!(
95-
sea_orm::IntoActiveValue::<_>::into_active_value(self.#field_ident).into()
186+
sea_orm::ActiveValue::Set(#expr)
96187
)
97188
});
98189

190+
// Add defaults(Unset) unless exhaustive mode is enabled
191+
let rest = if *exhaustive {
192+
quote!()
193+
} else {
194+
quote!(..::std::default::Default::default())
195+
};
196+
99197
quote!(
100198
#type_alias_definition
101199

102200
#[automatically_derived]
103201
impl sea_orm::IntoActiveModel<#active_model_ident> for #ident {
104202
fn into_active_model(self) -> #active_model_ident {
105203
#active_model_ident {
106-
#( #fields: #expanded_fields, )*
107-
..::std::default::Default::default()
204+
#( #field_idents: #expanded_fields, )*
205+
#( #set_idents: #expanded_sets, )*
206+
#rest
108207
}
109208
}
110209
}
111210
)
112211
}
113212
}
114213

214+
/// Parse field-level attributes on each struct field
215+
/// Supports:
216+
/// - ignore or skip: exclude the field from conversion
217+
/// - default = "expr": provide a fallback value for Option<T> fields (Some(v) => Set(v), None => Set(expr))
218+
fn parse_field(field: &syn::Field) -> Result<Option<IntoActiveModelField>, Error> {
219+
let ident = field.ident.as_ref().unwrap().clone();
220+
// Default expression for this field
221+
let mut default_expr: Option<syn::Expr> = None;
222+
223+
for attr in field.attrs.iter() {
224+
if !attr.path().is_ident("sea_orm") {
225+
continue;
226+
}
227+
228+
// Parse the attribute arguments: #[sea_orm(...)]
229+
if let Ok(list) = attr.parse_args_with(Punctuated::<Meta, Comma>::parse_terminated) {
230+
for meta in list.iter() {
231+
// Check for ignore/skip: #[sea_orm(ignore)] or #[sea_orm(skip)]
232+
if meta.exists("ignore") || meta.exists("skip") {
233+
return Ok(None);
234+
}
235+
// Check for bare default: #[sea_orm(default)]
236+
if meta.exists("default") {
237+
if default_expr.is_some() {
238+
return Err(Error::Syn(syn::Error::new_spanned(
239+
meta,
240+
"duplicate `default` attribute",
241+
)));
242+
}
243+
let expr: syn::Expr = syn::parse_quote!(::core::default::Default::default());
244+
default_expr = Some(expr);
245+
continue; // Skip next default check
246+
}
247+
// Check for default value: #[sea_orm(default = "expr")]
248+
if let Some(expr_str) = meta.get_as_kv("default") {
249+
// Error on duplicate `default`
250+
if default_expr.is_some() {
251+
return Err(Error::Syn(syn::Error::new_spanned(
252+
meta,
253+
"duplicate `default` attribute",
254+
)));
255+
}
256+
// Parse the expression string into a syn::Expr
257+
let expr = syn::parse_str::<syn::Expr>(&expr_str).map_err(Error::Syn)?;
258+
default_expr = Some(expr);
259+
}
260+
}
261+
}
262+
}
263+
264+
// Finnaly match and return appropriate field type
265+
if let Some(expr) = default_expr {
266+
Ok(Some(IntoActiveModelField::WithDefault { ident, expr }))
267+
} else {
268+
Ok(Some(IntoActiveModelField::Normal(ident)))
269+
}
270+
}
271+
115272
/// Method to derive the ActiveModel from the [ActiveModelTrait](sea_orm::ActiveModelTrait)
116273
pub fn expand_into_active_model(input: syn::DeriveInput) -> syn::Result<TokenStream> {
117274
let ident_span = input.ident.span();

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use syn::{
88
use super::from_query_result::{
99
DeriveFromQueryResult, FromQueryResultItem, ItemType as FqrItemType,
1010
};
11-
use super::into_active_model::DeriveIntoActiveModel;
11+
use super::into_active_model::{DeriveIntoActiveModel, IntoActiveModelField};
1212
use super::util::GetMeta;
1313

1414
#[derive(Debug)]
@@ -235,9 +235,11 @@ impl DerivePartialModel {
235235
ColumnAs::Nested { .. } => None,
236236
ColumnAs::Skip(_) => None,
237237
}
238-
.cloned()
238+
.map(|f| IntoActiveModelField::Normal(f.clone()))
239239
})
240240
.collect(),
241+
set_fields: Vec::new(),
242+
exhaustive: false,
241243
}
242244
.impl_into_active_model()
243245
} else {

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ pub(crate) const RUST_SPECIAL_KEYWORDS: [&str; 3] = ["crate", "Self", "self"];
198198
pub(crate) trait GetMeta {
199199
fn exists(&self, k: &str) -> bool;
200200
fn get_as_kv(&self, k: &str) -> Option<String>;
201+
fn get_as_kv_with_ident(&self) -> Option<(Ident, String)>;
201202
}
202203

203204
impl GetMeta for Meta {
@@ -228,6 +229,24 @@ impl GetMeta for Meta {
228229
None
229230
}
230231
}
232+
233+
fn get_as_kv_with_ident(&self) -> Option<(Ident, String)> {
234+
let Meta::NameValue(MetaNameValue {
235+
path,
236+
value: syn::Expr::Lit(exprlit),
237+
..
238+
}) = self
239+
else {
240+
return None;
241+
};
242+
243+
let syn::Lit::Str(litstr) = &exprlit.lit else {
244+
return None;
245+
};
246+
247+
path.get_ident()
248+
.map(|ident| (ident.clone(), litstr.value()))
249+
}
231250
}
232251

233252
#[cfg(test)]

0 commit comments

Comments
 (0)