feat: add support for named enum variant fields (#15)

This commit is contained in:
2026-01-24 14:09:07 +01:00
parent 017080244a
commit ce70ea4365
5 changed files with 132 additions and 48 deletions

View File

@@ -1,55 +1,70 @@
use crate::utils::type_lifetimes_to_static;
use proc_macro2::{Ident, Span, TokenStream};
use quote::quote;
use syn::{parse_quote, ItemStruct};
use syn::{spanned::Spanned, Error, Fields, ItemEnum, ItemStruct};
use crate::utils::{check_field_for_default_expr, ATTR_NAME, DEFAULT_FN_PREFIX};
pub(crate) fn expand_struct(mut item: ItemStruct) -> proc_macro::TokenStream {
let mut inline_fns: Vec<TokenStream> = vec![];
let mut default_fns = vec![];
for (i, field) in item.fields.iter_mut().enumerate() {
for (j, attr) in field.attrs.iter_mut().enumerate() {
if !attr.path().is_ident("serde_inline_default") {
continue;
default_fns.extend(check_field_for_default_expr(field, || {
format!("{}_{}_Field{}", DEFAULT_FN_PREFIX, item.ident, i)
}));
}
quote! {
#( #default_fns )*
#item
}
.into()
}
pub(crate) fn expand_enum(mut item: ItemEnum) -> proc_macro::TokenStream {
let mut default_fns = vec![];
for (i, variant) in item.variants.iter_mut().enumerate() {
if variant.attrs.iter().any(|a| a.path().is_ident(ATTR_NAME)) {
return Error::new(
variant.span(),
format!(
"#[{}] can only be used on named enum variant fields",
ATTR_NAME
),
)
.to_compile_error()
.into();
}
let fields = match &mut variant.fields {
Fields::Named(fields) => fields,
_ => {
return Error::new(
variant.span(),
format!(
"#[{}] can only be used on named enum variant fields",
ATTR_NAME
),
)
.to_compile_error()
.into()
}
};
let default: TokenStream = attr.parse_args().unwrap();
// copy all the same #[cfg] conditional compilations flags for the field onto our built
// default function.
// otherwise, it's possible to create a constructor for a type that may be filtered by
// the same #[cfg]'s, breaking compilation
let cfg_attrs = field.attrs.iter().filter(|a| a.path().is_ident("cfg"));
let fn_name_lit = format!("__serde_inline_default_{}_{}", item.ident, i);
let fn_name_ident = Ident::new(&fn_name_lit, Span::call_site());
let mut return_type = field.ty.clone();
// replace lifetimes with 'static.
// the built default function / default values in general can only be static as they're
// generated without reference to the parent struct
type_lifetimes_to_static(&mut return_type);
inline_fns.push(quote! {
#[doc(hidden)]
#[allow(non_snake_case)]
#( #cfg_attrs )*
fn #fn_name_ident () -> #return_type {
#default
}
});
field.attrs.remove(j);
field
.attrs
.insert(j, parse_quote!( #[serde(default = #fn_name_lit)] ));
break;
for (j, field) in fields.named.iter_mut().enumerate() {
default_fns.extend(check_field_for_default_expr(field, || {
format!(
"{}_{}_Variant{}_Field{}",
DEFAULT_FN_PREFIX, item.ident, i, j
)
}));
}
}
let expanded = quote! {
#( #inline_fns )*
quote! {
#( #default_fns )*
#item
};
expanded.into()
}
.into()
}

View File

@@ -37,6 +37,7 @@ pub fn serde_inline_default(_attr: TokenStream, input: TokenStream) -> TokenStre
match item {
Item::Struct(s) => expand::expand_struct(s),
_ => panic!("can only be used on structs"),
Item::Enum(e) => expand::expand_enum(e),
_ => panic!("can only be used on structs and enums"),
}
}

View File

@@ -1,4 +1,9 @@
use syn::{parse_quote, GenericArgument, PathArguments, Type};
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::{parse_quote, Field, GenericArgument, Ident, PathArguments, Type};
pub(crate) const ATTR_NAME: &str = "serde_inline_default";
pub(crate) const DEFAULT_FN_PREFIX: &str = "__serde_inline_default";
pub(crate) fn type_lifetimes_to_static(ty: &mut Type) {
match ty {
@@ -38,3 +43,45 @@ pub(crate) fn type_lifetimes_to_static(ty: &mut Type) {
_ => (),
}
}
pub(crate) fn check_field_for_default_expr(
field: &mut Field,
identifier_fn: impl FnOnce() -> String,
) -> Option<TokenStream> {
for (i, attr) in field.attrs.iter_mut().enumerate() {
if !attr.path().is_ident(ATTR_NAME) {
continue;
}
let default_expr: TokenStream = attr.parse_args().unwrap();
// copy all the same #[cfg] conditional compilations flags for the field onto our built
// default function.
// otherwise, it's possible to create a constructor for a type that may be filtered by
// the same #[cfg]'s, breaking compilation
let cfg_attrs = field.attrs.iter().filter(|a| a.path().is_ident("cfg"));
let default_fn_lit = identifier_fn();
let default_fn_ident = Ident::new(&default_fn_lit, Span::call_site());
let mut return_type = field.ty.clone();
// replace lifetimes with 'static.
// the built default function / default values in general can only be static as they're
// generated without reference to the parent struct
type_lifetimes_to_static(&mut return_type);
let default_fn_expr = quote! {
#[doc(hidden)]
#[allow(non_snake_case)]
#( #cfg_attrs )*
fn #default_fn_ident () -> #return_type {
#default_expr
}
};
field.attrs[i] = parse_quote!( #[serde(default = #default_fn_lit)] );
return Some(default_fn_expr);
}
None
}