From 604c488d39c0f58f0dbe63111629feaf8ff24d25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leo=20Bl=C3=B6cher?= Date: Fri, 6 Mar 2026 11:35:02 +0000 Subject: [PATCH 1/2] Fix regression in serde argument parsing darling, by default, throws an error when encountering an attribute that is not declared on a struct with `#[derive(FromMeta)]`. We silently swallowed that error and ignored the entire meta attribute, which caused a regression for the Default impls generated by `#[settings]`. To fix this, we now allow unknown fields on `SerdeArgs`. Additionally, we propagate errors from `SerdeArgs` parsing to make debugging such issues more straightforward in the future. --- foundations-macros/src/settings.rs | 57 ++++++++++++++++++------------ foundations/tests/settings.rs | 19 +++++++++- 2 files changed, 52 insertions(+), 24 deletions(-) diff --git a/foundations-macros/src/settings.rs b/foundations-macros/src/settings.rs index e307f06..0c4e2d7 100644 --- a/foundations-macros/src/settings.rs +++ b/foundations-macros/src/settings.rs @@ -73,15 +73,19 @@ impl Parse for Options { } #[derive(FromMeta)] +#[darling(allow_unknown_fields)] struct SerdeArgs { flatten: Flag, default: Option, } impl SerdeArgs { - fn parse_from_attrs(attrs: &[Attribute]) -> Option { - let attr = attrs.iter().find(|a| a.path().is_ident("serde"))?; - Self::from_meta(&attr.meta).ok() + fn parse_from_attrs(attrs: &[Attribute]) -> Result> { + let Some(attr) = attrs.iter().find(|a| a.path().is_ident("serde")) else { + return Ok(None); + }; + + Ok(Some(Self::from_meta(&attr.meta)?)) } } @@ -166,7 +170,7 @@ fn expand_struct(options: Options, item: &mut ItemStruct) -> Result bool { /// Returns whether `attrs` contains `serde(flatten)`. fn is_serde_flattened(attrs: &[Attribute]) -> bool { - SerdeArgs::parse_from_attrs(attrs).is_some_and(|a| a.flatten.is_present()) + matches!( + SerdeArgs::parse_from_attrs(attrs), + Ok(Some(args)) if args.flatten.is_present(), + ) } -fn impl_serde_aware_default(item: &ItemStruct) -> proc_macro2::TokenStream { +fn impl_serde_aware_default(item: &ItemStruct) -> Result { let name = &item.ident; let (impl_generics, ty_generics, where_clause) = item.generics.split_for_impl(); - let initializers = item.fields.iter().map(|field| { - let name = field - .ident - .as_ref() - .expect("should not generate field docs for tuple struct"); + let initializers = item + .fields + .iter() + .map(|field| { + let name = field + .ident + .as_ref() + .expect("should not generate field docs for tuple struct"); - let span = field.ty.span(); + let span = field.ty.span(); - let cfg_attrs = field - .attrs - .iter() - .filter(|attr| attr.path().is_ident("cfg")); + let cfg_attrs = field + .attrs + .iter() + .filter(|attr| attr.path().is_ident("cfg")); - let function_path = SerdeArgs::parse_from_attrs(&field.attrs) - .and_then(|a| Some(a.default?.into_token_stream())) - .unwrap_or_else(|| quote_spanned! { span=> Default::default }); + let function_path = SerdeArgs::parse_from_attrs(&field.attrs)? + .and_then(|a| Some(a.default?.into_token_stream())) + .unwrap_or_else(|| quote_spanned! { span=> Default::default }); - quote_spanned! { span=> #(#cfg_attrs)* #name: #function_path() } - }); + Ok(quote_spanned! { span=> #(#cfg_attrs)* #name: #function_path() }) + }) + .collect::>>()?; - quote! { + Ok(quote! { impl #impl_generics Default for #name #ty_generics #where_clause { fn default() -> Self { Self { #(#initializers,)* } } } - } + }) } #[cfg(test)] diff --git a/foundations/tests/settings.rs b/foundations/tests/settings.rs index 93dd2f6..4ed4d72 100644 --- a/foundations/tests/settings.rs +++ b/foundations/tests/settings.rs @@ -124,6 +124,15 @@ struct WithMap { struct WithOption { /// Optional field optional: Option, + /// A field that is not parsed at all. + #[serde(skip, default = "WithOption::default_a")] + a: u32, +} + +impl WithOption { + fn default_a() -> u32 { + 123 + } } impl Default for NoDefaultStruct { @@ -228,6 +237,10 @@ fn defaults() { assert_eq!(nested_struct.inner.b, 0xb); assert_eq!(nested_struct.x, 1); + + let struct_with_skip = WithOption::default(); + assert!(struct_with_skip.optional.is_none()); + assert_eq!(struct_with_skip.a, 123); } #[test] @@ -259,11 +272,15 @@ fn map() { fn option() { let s = WithOption { optional: Some(NestedStruct { a: 1, b: 2, c: 3 }), + a: 4, }; assert_ser_eq!(s, "data/with_option_some.yaml"); - let s = WithOption { optional: None }; + let s = WithOption { + optional: None, + a: 4, + }; assert_ser_eq!(s, "data/with_option_none.yaml"); } From 2bb2656ad4bebaf68cddecaccc4d79c7ca4bfbf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leo=20Bl=C3=B6cher?= Date: Fri, 6 Mar 2026 11:39:48 +0000 Subject: [PATCH 2/2] Release 5.5.1 --- Cargo.toml | 6 +++--- RELEASE_NOTES.md | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fb75364..13f4373 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ resolver = "3" [workspace.package] -version = "5.5.0" +version = "5.5.1" repository = "https://github.com/cloudflare/foundations" edition = "2024" authors = ["Cloudflare"] @@ -37,8 +37,8 @@ check-cfg = [ [workspace.dependencies] anyhow = "1.0.75" -foundations = { version = "5.5.0", path = "./foundations" } -foundations-macros = { version = "=5.5.0", path = "./foundations-macros", default-features = false } +foundations = { version = "5.5.1", path = "./foundations" } +foundations-macros = { version = "=5.5.1", path = "./foundations-macros", default-features = false } bindgen = { version = "0.72", default-features = false } cc = "1.0" cf-rustracing = "1.2.1" diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 433e9cd..1ae8a16 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,9 @@ +5.5.1 +- 2026-03-06 Fix regression in serde argument parsing + 5.5.0 +- 2026-03-05 Release 5.5.0 - 2026-03-05 Update list of allowed licenses for dependencies - 2026-03-05 chore: Upgrade internal dependency to avoid dupes - 2026-03-05 Mention libclang-dev dependency for bindgen