Skip to content

feat: support partial_attr for struct fields #44

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ toml = { version = "0.8", optional = true }

[dev-dependencies]
pretty_assertions = "1.2.1"
derive_more = { version = "2.0.1", features = ["debug"] }
Copy link
Contributor Author

@aschey aschey Apr 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I unfortunately had to add a new dev dependency just for the test because I couldn't think of any derive macros in the standard library that use attributes on struct fields.



[package.metadata.docs.rs]
Expand Down
2 changes: 1 addition & 1 deletion macro/src/gen/meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub(super) fn gen(input: &ir::Input) -> TokenStream {
let name = f.name.to_string();
let doc = &f.doc;
let kind = match &f.kind {
FieldKind::Nested { ty } => {
FieldKind::Nested { ty, partial_attr: _ } => {
quote! {
confique::meta::FieldKind::Nested { meta: &<#ty as confique::Config>::META }
}
Expand Down
21 changes: 18 additions & 3 deletions macro/src/gen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,10 +190,12 @@ fn gen_parts_for_field(f: &ir::Field, input: &ir::Input, parts: &mut Parts) {

match &f.kind {
// ----- Nested -------------------------------------------------------------
FieldKind::Nested { ty } => {
FieldKind::Nested { ty, partial_attr } => {
let ty_span = ty.span();
let field_ty = quote_spanned! {ty_span=> <#ty as confique::Config>::Partial };
let partial_attr = attr_expression_to_tokens(partial_attr.as_ref());
parts.struct_fields.push(quote! {
#partial_attr
#[serde(default = "confique::Partial::empty")]
#field_visibility #field_name: #field_ty,
});
Expand All @@ -211,7 +213,14 @@ fn gen_parts_for_field(f: &ir::Field, input: &ir::Input, parts: &mut Parts) {


// ----- Leaf ---------------------------------------------------------------
FieldKind::Leaf { kind, deserialize_with, validate, env, parse_env } => {
FieldKind::Leaf {
kind,
deserialize_with,
validate,
env,
parse_env,
partial_attr,
} => {
let inner_ty = kind.inner_ty();

// This has an ugly name to avoid clashing with imported names.
Expand Down Expand Up @@ -315,7 +324,8 @@ fn gen_parts_for_field(f: &ir::Field, input: &ir::Input, parts: &mut Parts) {
let main = quote_spanned! {field_name.span()=>
#field_visibility #field_name: std::option::Option<#inner_ty>,
};
quote! { #attr #main }
let partial_attr = attr_expression_to_tokens(partial_attr.as_ref());
quote! { #attr #partial_attr #main }
});


Expand Down Expand Up @@ -364,6 +374,11 @@ fn gen_parts_for_field(f: &ir::Field, input: &ir::Input, parts: &mut Parts) {
}
}

/// Generates a valid attribute expression from a path expression or empty value
fn attr_expression_to_tokens(attr_expression: Option<&TokenStream>) -> TokenStream {
attr_expression.map(|p| quote!(#[#p])).unwrap_or_default()
}

/// Returns the names of the module and struct for the partial type:
/// `(mod_name, struct_name)`.
fn partial_names(original_name: &Ident) -> (Ident, Ident) {
Expand Down
2 changes: 2 additions & 0 deletions macro/src/ir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,14 @@ pub(crate) enum FieldKind {
deserialize_with: Option<syn::Path>,
parse_env: Option<syn::Path>,
validate: Option<FieldValidator>,
partial_attr: Option<TokenStream>,
kind: LeafKind,
},

/// A nested configuration. The type is never `Option<_>`.
Nested {
ty: syn::Type,
partial_attr: Option<TokenStream>
},
}

Expand Down
14 changes: 13 additions & 1 deletion macro/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ impl Field {
}
}

FieldKind::Nested { ty: field.ty }
FieldKind::Nested { ty: field.ty, partial_attr: attrs.partial_attr }
} else {
if attrs.env.is_none() && attrs.parse_env.is_some() {
return err("cannot specify `parse_env` attribute without the `env` attribute");
Expand All @@ -164,6 +164,7 @@ impl Field {
deserialize_with: attrs.deserialize_with,
parse_env: attrs.parse_env,
validate: attrs.validate,
partial_attr: attrs.partial_attr,
kind,
}
};
Expand All @@ -187,6 +188,7 @@ struct FieldAttrs {
deserialize_with: Option<syn::Path>,
parse_env: Option<syn::Path>,
validate: Option<FieldValidator>,
partial_attr: Option<TokenStream>,
}

enum FieldAttr {
Expand All @@ -196,6 +198,7 @@ enum FieldAttr {
DeserializeWith(syn::Path),
ParseEnv(syn::Path),
Validate(FieldValidator),
PartialAttr(TokenStream),
}

impl FieldAttrs {
Expand Down Expand Up @@ -244,6 +247,10 @@ impl FieldAttrs {
duplicate_if!(out.validate.is_some());
out.validate = Some(path);
}
FieldAttr::PartialAttr(partial_attr) => {
duplicate_if!(out.partial_attr.is_some());
out.partial_attr = Some(partial_attr);
}
}
}
}
Expand All @@ -261,6 +268,7 @@ impl FieldAttr {
Self::ParseEnv(_) => "parse_env",
Self::DeserializeWith(_) => "deserialize_with",
Self::Validate(_) => "validate",
Self::PartialAttr(_) => "partial_attr",
}
}
}
Expand Down Expand Up @@ -329,6 +337,10 @@ impl Parse for FieldAttr {
))
}
}
"partial_attr" => {
let group: Group = input.parse()?;
Ok(Self::PartialAttr(group.stream()))
}

_ => Err(syn::Error::new(ident.span(), "unknown confique attribute")),
}
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,8 @@ pub use crate::{
/// For example, `#[config(partial_attr(derive(Clone)))]` can be used to make
/// the partial type implement `Clone`.
///
/// This attribute can also be applied to struct fields.
///
///
/// # What the macro generates
///
Expand Down
54 changes: 54 additions & 0 deletions tests/partial_props.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use confique::Config;
use pretty_assertions::assert_eq;

#[test]
fn partial_props() {
#[allow(dead_code)]
#[derive(Config)]
#[config(partial_attr(derive(derive_more::Debug)))]
struct Foo {
#[config(default = 1, partial_attr(debug("test {bar:?}")))]
bar: u32,
}

use confique_partial_foo::PartialFoo;
let partial_foo = PartialFoo { bar: Some(1) };
assert_eq!(
"PartialFoo { bar: test Some(1) }",
format!("{partial_foo:?}")
);
}

#[test]
fn partial_props_nested() {
mod foo {
use confique::Config;
pub use confique_partial_bar::PartialBar;
pub use confique_partial_foo::PartialFoo;

#[allow(dead_code)]
#[derive(Config)]
#[config(partial_attr(derive(derive_more::Debug)))]
pub struct Foo {
#[config(default = 1, partial_attr(debug("test {bar:?}")))]
bar: u32,
}

#[allow(dead_code)]
#[derive(Config)]
#[config(partial_attr(derive(derive_more::Debug)))]
pub struct Bar {
#[config(nested, partial_attr(debug("test2 {foo2:?}")))]
foo2: Foo,
}
}

use foo::*;

let partial_foo = PartialFoo { bar: Some(1) };
let partial_bar = PartialBar { foo2: partial_foo };
assert_eq!(
"PartialBar { foo2: test2 PartialFoo { bar: test Some(1) } }",
format!("{partial_bar:?}")
);
}
Loading