Skip to content

Commit

Permalink
Allow specifying arbitrary attributes on the builder struct (#237)
Browse files Browse the repository at this point in the history
* Field: Distinguish field from setter attributes
* Support builder_field_attr(...)
* Support builder_setter_attr(...)
  • Loading branch information
ijackson committed Mar 15, 2022
1 parent f2ffe75 commit f0aebdd
Show file tree
Hide file tree
Showing 8 changed files with 164 additions and 6 deletions.
2 changes: 2 additions & 0 deletions derive_builder/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ derive_builder_macro = { version = "=0.10.2", path = "../derive_builder_macro" }
pretty_assertions = "0.6.1"
rustversion = "1.0.4"
trybuild = "1.0.38"
serde = "1"
serde_json = "1"
25 changes: 25 additions & 0 deletions derive_builder/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,31 @@
//! # fn main() {}
//! ```
//!
//! ### Additional attributes on specifically fields in the builder, or the setter function
//!
//! You can set other attributes on the field in the builder:
//!
//! ```rust
//! # #[macro_use]
//! # extern crate derive_builder;
//! #
//! #[derive(Builder)]
//! #[builder(derive(serde::Serialize))]
//! struct Lorem {
//! #[builder_field_attr(serde(rename="dolor"))]
//! ipsum: String,
//! }
//!
//! # fn main() {
//! let mut show = LoremBuilder::default();
//! show.ipsum("sit".into());
//! assert_eq!(serde_json::to_string(&show).unwrap(), r#"{"dolor":"sit"}"#);
//! # }
//! ```
//!
//! `builder_setter_attr` works the same way,
//! but the contained attribute(s) are applied to the generated setter function.
//!
//! # Error return type from autogenerated `build` function
//!
//! By default, `build` returns an autogenerated error type:
Expand Down
12 changes: 12 additions & 0 deletions derive_builder/tests/compile-fail/builder_field_attr.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#[macro_use]
extern crate derive_builder;

#[derive(Debug, PartialEq, Default, Builder, Clone)]
pub struct Lorem {
ok: String,

#[builder_field_attr(no_such_attribute)]
broken: String,
}

fn main() {}
5 changes: 5 additions & 0 deletions derive_builder/tests/compile-fail/builder_field_attr.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
error: cannot find attribute `no_such_attribute` in this scope
--> tests/compile-fail/builder_field_attr.rs:8:26
|
8 | #[builder_field_attr(no_such_attribute)]
| ^^^^^^^^^^^^^^^^^
16 changes: 16 additions & 0 deletions derive_builder/tests/compile-fail/builder_setter_attr.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#![deny(unused_must_use)]

#[macro_use]
extern crate derive_builder;

#[derive(Debug, PartialEq, Default, Builder, Clone)]
pub struct Lorem {
ok: String,

#[builder_setter_attr(must_use)]
broken: usize,
}

fn main() {
LoremBuilder::default().broken(42);
}
11 changes: 11 additions & 0 deletions derive_builder/tests/compile-fail/builder_setter_attr.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
error: unused return value of `LoremBuilder::broken` that must be used
--> tests/compile-fail/builder_setter_attr.rs:15:5
|
15 | LoremBuilder::default().broken(42);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
note: lint level defined here
--> tests/compile-fail/builder_setter_attr.rs:1:9
|
1 | #![deny(unused_must_use)]
| ^^^^^^^^^^^^^^^
97 changes: 92 additions & 5 deletions derive_builder_core/src/macro_options/darling_opts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ use crate::BuildMethod;

use darling::util::{Flag, PathList};
use darling::{self, FromMeta};
use proc_macro2::Span;
use proc_macro2::{Span, TokenStream};
use syn::parse::{ParseStream, Parser};
use syn::Meta;
use syn::{self, spanned::Spanned, Attribute, Generics, Ident, Path, Visibility};

Expand Down Expand Up @@ -208,10 +209,15 @@ fn field_setter(meta: &Meta) -> darling::Result<FieldLevelSetter> {

/// Data extracted from the fields of the input struct.
#[derive(Debug, Clone, FromField)]
#[darling(attributes(builder), forward_attrs(doc, cfg, allow))]
#[darling(
attributes(builder),
forward_attrs(doc, cfg, allow, builder_field_attr, builder_setter_attr),
and_then = "Self::unnest_attrs"
)]
pub struct Field {
ident: Option<Ident>,
attrs: Vec<Attribute>,
/// Raw input attributes, for consumption by Field::unnest_attrs. Do not use elsewhere.
attrs: Vec<syn::Attribute>,
ty: syn::Type,
/// Field-level override for builder pattern.
/// Note that setting this may force the builder to derive `Clone`.
Expand Down Expand Up @@ -241,6 +247,87 @@ pub struct Field {
try_setter: Flag,
#[darling(default)]
field: FieldMeta,
#[darling(skip)]
field_attrs: Vec<Attribute>,
#[darling(skip)]
setter_attrs: Vec<Attribute>,
}

impl Field {
/// Remove the `builder_field_attr(...)` packaging around an attribute
fn unnest_attrs(mut self) -> darling::Result<Self> {
let mut errors = vec![];

for attr in self.attrs.drain(..) {
let unnest = {
if attr.path.is_ident("builder_field_attr") {
Some(&mut self.field_attrs)
} else if attr.path.is_ident("builder_setter_attr") {
Some(&mut self.setter_attrs)
} else {
None
}
};
if let Some(unnest) = unnest {
match unnest_from_one_attribute(attr) {
Ok(n) => unnest.push(n),
Err(e) => errors.push(e),
}
} else {
self.field_attrs.push(attr.clone());
self.setter_attrs.push(attr);
}
}

if !errors.is_empty() {
return Err(darling::Error::multiple(errors));
}

Ok(self)
}
}

fn unnest_from_one_attribute(attr: syn::Attribute) -> darling::Result<Attribute> {
match &attr.style {
syn::AttrStyle::Outer => (),
syn::AttrStyle::Inner(bang) => {
// We think this error can never actually happen,
// since struct fields don't allow inner attributes.
return Err(darling::Error::unsupported_format(
"builder_field_attr/builder_setter_attr must be an outer attribute",
)
.with_span(bang));
}
};

#[derive(Debug)]
struct ContainedAttribute(syn::Attribute);
impl syn::parse::Parse for ContainedAttribute {
fn parse(input: ParseStream) -> syn::Result<Self> {
// Strip parentheses, and save the span of the parenthesis token
let content;
let paren_token = parenthesized!(content in input);
let wrap_span = paren_token.span;

// Wrap up in #[ ] instead.
let pound = Token![#](wrap_span); // We can't write a literal # inside quote
let content: TokenStream = content.parse()?;
let content = quote_spanned!(wrap_span=> #pound [ #content ]);

let parser = syn::Attribute::parse_outer;
let mut attrs = parser.parse2(content)?.into_iter();
// TryFrom for Array not available in Rust 1.40
// We think this error can never actually happen, since `#[...]` ought to make just one Attribute
let attr = match (attrs.next(), attrs.next()) {
(Some(attr), None) => attr,
_ => return Err(input.error("expected exactly one attribute")),
};
Ok(Self(attr))
}
}

let ContainedAttribute(attr) = syn::parse2(attr.tokens)?;
Ok(attr)
}

impl FlagVisibility for Field {
Expand Down Expand Up @@ -570,7 +657,7 @@ impl<'a> FieldWithDefaults<'a> {
try_setter: self.try_setter(),
visibility: self.setter_vis(),
pattern: self.pattern(),
attrs: &self.field.attrs,
attrs: &self.field.setter_attrs,
ident: self.setter_ident(),
field_ident: self.field_ident(),
field_type: &self.field.ty,
Expand Down Expand Up @@ -608,7 +695,7 @@ impl<'a> FieldWithDefaults<'a> {
field_type: &self.field.ty,
field_enabled: self.field_enabled(),
field_visibility: self.field_vis(),
attrs: &self.field.attrs,
attrs: &self.field.field_attrs,
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion derive_builder_macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ extern crate derive_builder_core;
use proc_macro::TokenStream;

#[doc(hidden)]
#[proc_macro_derive(Builder, attributes(builder))]
#[proc_macro_derive(Builder, attributes(builder, builder_field_attr, builder_setter_attr))]
pub fn derive(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as syn::DeriveInput);
derive_builder_core::builder_for_struct(ast).into()
Expand Down

0 comments on commit f0aebdd

Please sign in to comment.