From ce70ea43658c861aaf9faf58c0ad760be637470d Mon Sep 17 00:00:00 2001 From: bytedream Date: Sat, 24 Jan 2026 14:09:07 +0100 Subject: [PATCH] feat: add support for named enum variant fields (#15) --- src/expand.rs | 99 +++++++++++-------- src/lib.rs | 3 +- src/utils.rs | 49 ++++++++- tests/enum.rs | 20 ++++ ...test_serde_inline_default.rs => struct.rs} | 9 +- 5 files changed, 132 insertions(+), 48 deletions(-) create mode 100644 tests/enum.rs rename tests/{test_serde_inline_default.rs => struct.rs} (94%) diff --git a/src/expand.rs b/src/expand.rs index c1bab27..9bc677a 100644 --- a/src/expand.rs +++ b/src/expand.rs @@ -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 = 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() } diff --git a/src/lib.rs b/src/lib.rs index 1c09256..a3934ba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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"), } } diff --git a/src/utils.rs b/src/utils.rs index 266398a..e6e93ef 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -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 { + 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 +} diff --git a/tests/enum.rs b/tests/enum.rs new file mode 100644 index 0000000..7d911e0 --- /dev/null +++ b/tests/enum.rs @@ -0,0 +1,20 @@ +use serde::Deserialize; +use serde_inline_default::serde_inline_default; +use serde_json::json; + +#[test] +fn enum_default() { + #[serde_inline_default] + #[derive(Debug, PartialEq, Eq, Deserialize)] + #[serde(untagged)] + enum Test { + VariantWithFields { + #[serde_inline_default(255)] + test_int: u8, + }, + } + + let enum_test: Test = serde_json::from_value(json!({"VariantWithFields": {}})).unwrap(); + + assert_eq!(enum_test, Test::VariantWithFields { test_int: 255 }) +} diff --git a/tests/test_serde_inline_default.rs b/tests/struct.rs similarity index 94% rename from tests/test_serde_inline_default.rs rename to tests/struct.rs index ed5208f..06454e3 100644 --- a/tests/test_serde_inline_default.rs +++ b/tests/struct.rs @@ -1,10 +1,11 @@ +use std::borrow::Cow; + use serde::Deserialize; use serde_inline_default::serde_inline_default; use serde_json::json; -use std::borrow::Cow; #[test] -fn test_serde_inline_default() { +fn struct_normal() { fn native_default() -> u32 { 69 } @@ -31,7 +32,7 @@ fn test_serde_inline_default() { } #[test] -fn test_lifetime() { +fn lifetime() { #[serde_inline_default] #[derive(Deserialize)] struct LifetimeTest<'a> { @@ -46,7 +47,7 @@ fn test_lifetime() { #[test] #[allow(dead_code)] -fn test_conditional_compilation() { +fn conditional_compilation() { #[cfg(debug_assertions)] #[derive(Deserialize)] struct TypeA(u8);