Skip to main content

serde_const_default/
lib.rs

1//! Serde defaults backed by const expressions.
2//!
3//! Serde's `#[serde(default = "...")]` field attribute expects a path to a
4//! zero-argument function. This crate lets fields name the expression directly
5//! and rewrites that expression into the helper function Serde already knows
6//! how to call.
7//!
8//! Use [`serde_const_default`] on the struct, then mark fields with either
9//! `#[const_default = EXPR]` or `#[const_default_from(EXPR)]`.
10//!
11//! ```
12//! use serde::Deserialize;
13//! use serde_const_default::serde_const_default;
14//!
15//! const SOME_VALUE: u8 = 7;
16//! const DEFAULT_NAME: &str = "anonymous";
17//!
18//! #[serde_const_default]
19//! #[derive(Deserialize)]
20//! struct Config {
21//!     #[const_default = SOME_VALUE]
22//!     count: u8,
23//!
24//!     #[const_default_from(DEFAULT_NAME)]
25//!     name: String,
26//! }
27//!
28//! let value: Config = serde_json::from_str("{}").unwrap();
29//! assert_eq!(value.count, 7);
30//! assert_eq!(value.name, "anonymous");
31//! ```
32
33use proc_macro::TokenStream;
34use quote::{format_ident, quote};
35use syn::punctuated::Punctuated;
36use syn::{
37    Attribute, Error, Expr, Field, Fields, ItemStruct, LitStr, Meta, Token, parse_macro_input,
38    parse_quote,
39};
40
41/// Enables const-expression defaults for fields on a named struct.
42///
43/// The macro consumes field attributes using the following forms:
44///
45/// - `#[const_default = EXPR]`, which generates a `const fn` that returns
46///   `EXPR` directly.
47/// - `#[const_default_from(EXPR)]`, which generates a default function that
48///   returns `::core::convert::From::from(EXPR)`.
49///
50/// The generated helper functions are wired back into Serde with
51/// `#[serde(default = "...")]`, so the struct should also derive or implement
52/// Serde deserialization in the usual way.
53///
54/// ```
55/// use serde::Deserialize;
56/// use serde_const_default::serde_const_default;
57///
58/// const PORT: u16 = 8080;
59///
60/// #[serde_const_default]
61/// #[derive(Deserialize)]
62/// struct Settings {
63///     #[const_default = PORT]
64///     port: u16,
65/// }
66///
67/// let settings: Settings = serde_json::from_str("{}").unwrap();
68/// assert_eq!(settings.port, 8080);
69/// ```
70///
71/// Use `const_default_from` when the expression's type needs a `From`
72/// conversion into the field type:
73///
74/// ```
75/// use serde::Deserialize;
76/// use serde_const_default::serde_const_default;
77///
78/// const DEFAULT_NAME: &str = "guest";
79///
80/// #[serde_const_default]
81/// #[derive(Deserialize)]
82/// struct Settings {
83///     #[const_default_from(DEFAULT_NAME)]
84///     name: String,
85/// }
86///
87/// let settings: Settings = serde_json::from_str("{}").unwrap();
88/// assert_eq!(settings.name, "guest");
89/// ```
90#[proc_macro_attribute]
91pub fn serde_const_default(attr: TokenStream, item: TokenStream) -> TokenStream {
92    if !attr.is_empty() {
93        return Error::new_spanned(
94            proc_macro2::TokenStream::from(attr),
95            "`serde_const_default` does not accept arguments",
96        )
97        .to_compile_error()
98        .into();
99    }
100
101    let item = parse_macro_input!(item as ItemStruct);
102    expand_serde_const_default(item)
103        .unwrap_or_else(Error::into_compile_error)
104        .into()
105}
106
107fn expand_serde_const_default(mut item: ItemStruct) -> Result<proc_macro2::TokenStream, Error> {
108    let mut helpers = Vec::new();
109    let struct_ident = item.ident.clone();
110
111    match &mut item.fields {
112        Fields::Named(fields) => {
113            for (index, field) in fields.named.iter_mut().enumerate() {
114                if let Some(helper) = rewrite_field(&struct_ident, index, field)? {
115                    helpers.push(helper);
116                }
117            }
118        }
119        Fields::Unnamed(fields) => {
120            for field in &fields.unnamed {
121                if has_const_default_attr(field) {
122                    // Helper names are derived from field identifiers, so keep
123                    // tuple structs unsupported until the API has a clear
124                    // naming rule for generated defaults.
125                    return Err(Error::new_spanned(
126                        field,
127                        "`serde_const_default` only supports named fields",
128                    ));
129                }
130            }
131        }
132        Fields::Unit => {}
133    }
134
135    Ok(quote! {
136        #(#helpers)*
137        #item
138    })
139}
140
141fn rewrite_field(
142    struct_ident: &syn::Ident,
143    index: usize,
144    field: &mut Field,
145) -> Result<Option<proc_macro2::TokenStream>, Error> {
146    let Some(default) = take_const_default_attr(&mut field.attrs)? else {
147        return Ok(None);
148    };
149
150    if let Some(attr) = field.attrs.iter().find(|attr| serde_attr_has_default(attr)) {
151        // Serde accepts only one default source. Rejecting this explicitly
152        // keeps the user's chosen default from depending on attribute order.
153        return Err(Error::new_spanned(
154            attr,
155            "`const_default` cannot be combined with `#[serde(default)]`",
156        ));
157    }
158
159    let Some(field_ident) = field.ident.as_ref() else {
160        return Err(Error::new_spanned(
161            &*field,
162            "`serde_const_default` only supports named fields",
163        ));
164    };
165    let helper_ident = format_ident!(
166        "__serde_const_default_{}_{}_{}",
167        struct_ident,
168        field_ident,
169        index
170    );
171    let helper_name = LitStr::new(&helper_ident.to_string(), helper_ident.span());
172    let ty = &field.ty;
173    let expr = default.expr;
174    let (function_qualifier, body) = match default.kind {
175        DefaultKind::Plain => (quote! { const }, quote! { #expr }),
176        DefaultKind::From => (quote! {}, quote! { ::core::convert::From::from(#expr) }),
177    };
178
179    // Serde's derive macro reads default helpers through a string literal path,
180    // so the custom field attribute is replaced with the equivalent Serde
181    // attribute before the struct is emitted.
182    field.attrs.push(parse_quote! {
183        #[serde(default = #helper_name)]
184    });
185
186    Ok(Some(quote! {
187        #[allow(non_snake_case)]
188        #function_qualifier fn #helper_ident() -> #ty {
189            #body
190        }
191    }))
192}
193
194fn take_const_default_attr(attrs: &mut Vec<Attribute>) -> Result<Option<ConstDefault>, Error> {
195    let mut default = None;
196    let mut error = None::<Error>;
197    let mut retained = Vec::with_capacity(attrs.len());
198
199    for attr in attrs.drain(..) {
200        // Remove our custom attributes from the field. If they survive the
201        // expansion, rustc validates them as ordinary attributes and rejects
202        // useful expressions such as `#[const_default = SOME_CONST]`.
203        match parse_const_default_attr(&attr)? {
204            Some(parsed) => {
205                if default.is_some() {
206                    let duplicate_error = Error::new_spanned(
207                        attr,
208                        "only one const default attribute is allowed per field",
209                    );
210
211                    if let Some(error) = &mut error {
212                        error.combine(duplicate_error);
213                    } else {
214                        error = Some(duplicate_error);
215                    }
216                } else {
217                    default = Some(parsed);
218                }
219            }
220            None => retained.push(attr),
221        }
222    }
223
224    *attrs = retained;
225
226    if let Some(error) = error {
227        Err(error)
228    } else {
229        Ok(default)
230    }
231}
232
233fn parse_const_default_attr(attr: &Attribute) -> Result<Option<ConstDefault>, Error> {
234    if attr.path().is_ident("const_default") {
235        let Meta::NameValue(name_value) = &attr.meta else {
236            return Err(Error::new_spanned(
237                attr,
238                "expected `#[const_default = EXPR]`",
239            ));
240        };
241
242        return Ok(Some(ConstDefault {
243            kind: DefaultKind::Plain,
244            expr: name_value.value.clone(),
245        }));
246    }
247
248    if attr.path().is_ident("const_default_from") {
249        return Ok(Some(ConstDefault {
250            kind: DefaultKind::From,
251            expr: attr.parse_args::<Expr>()?,
252        }));
253    }
254
255    Ok(None)
256}
257
258/// Checks tuple-struct fields without fully parsing their defaults.
259fn has_const_default_attr(field: &Field) -> bool {
260    field.attrs.iter().any(|attr| {
261        attr.path().is_ident("const_default") || attr.path().is_ident("const_default_from")
262    })
263}
264
265/// Returns true when a Serde field attribute already configures a default.
266fn serde_attr_has_default(attr: &Attribute) -> bool {
267    if !attr.path().is_ident("serde") {
268        return false;
269    }
270
271    let Meta::List(list) = &attr.meta else {
272        return false;
273    };
274
275    list.parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)
276        .map(|metas| {
277            metas.iter().any(|meta| match meta {
278                Meta::Path(path) => path.is_ident("default"),
279                Meta::NameValue(name_value) => name_value.path.is_ident("default"),
280                Meta::List(list) => list.path.is_ident("default"),
281            })
282        })
283        .unwrap_or(false)
284}
285
286/// Parsed representation of a field-level const default attribute.
287struct ConstDefault {
288    kind: DefaultKind,
289    expr: Expr,
290}
291
292/// Selects whether the generated helper is a direct const default or a runtime
293/// conversion default.
294enum DefaultKind {
295    Plain,
296    From,
297}