build_it/
lib.rs

1//! This crate provides a derive-macro to generate the builder pattern for a struct.
2//! The builder implementation contains a method for each field of the struct, ignoring fields with
3//! the #[skip] attribute.
4//! Each field to generate a method for must be of type Option<T>. If any field is not of type
5//! Option<T>, and doesn't have the #[skip] attribute, the macro will panic.
6//!
7//!
8//! # Examples
9//! ```
10//! use build_it::Builder;
11//! #[derive(Default, Builder)]
12//! struct MyAwesomeStruct {
13//!     name: Option<String>,
14//!     pub age: Option<u32>,
15//!     #[build_it(skip)]
16//!     address: String,
17//!
18//!     /// The `#[build_it(rename = "new_name")]` attribute can be used to rename the builder
19//!     /// method. In this case, the builder method will be called `new_name` instead of
20//!     /// `renamed`:
21//!     /// `let builder = MyAwesomeStruct::default().new_name("Alice".to_string());`
22//!     #[build_it(rename = "new_name")]
23//!     renamed: Option<String>,
24//!
25//!     /// The `#[build_it(into)]` attribute can be used to allow the builder method to accept
26//!     /// types that can be converted into the field type. In this case, the builder method will
27//!     /// accept a `&str` instead of a `String`:
28//!     /// `let builder = MyAwesomeStruct::default().name_into("Alice");`
29//!     #[build_it(into)]
30//!     name_into: Option<String>,
31//!
32//!     #[build_it(skip)]
33//!     // NOTE: While the `#[skip]` attribute is still supported, it is deprecated in favor of
34//!     // the `#[build_it(skip)]` attribute.
35//!     pub phone: Option<String>,
36//! }
37//! let builder = MyAwesomeStruct::default()
38//!     .name("Alice".to_string())
39//!     .age(42);
40//!     // Note that `address` and `phone` do not have builder methods because of the #[skip]
41//!     // attribute.
42//! assert_eq!(builder.name, Some("Alice".to_string()));
43//! assert_eq!(builder.age, Some(42));
44//!
45//! // These fields are skipped, so they're value will still be the default value.
46//! assert_eq!(builder.address, String::default());
47//! assert_eq!(builder.phone, None);
48//!```
49//!
50//! The generated builder methods will also display the field's documentation:
51//! ```
52//! use build_it::Builder;
53//! #[derive(Default, Builder)]
54//! struct MyAwesomeStruct {
55//!     /// Name of the person
56//!     name: Option<String>,
57//!     /// Age of the person
58//!     age: Option<u32>,
59//! }
60//! ```
61//! This will generate the following builder methods:
62//! ```
63//! # struct MyAwesomeStruct {
64//! #     name: Option<String>,
65//! #     age: Option<u32>,
66//! # }
67//! impl MyAwesomeStruct {
68//!    /// Name of the person
69//!     pub fn name(mut self, name: String) -> Self {
70//!         self.name = Some(name);
71//!         self
72//!     }
73//!     /// Age of the person
74//!     pub fn age(mut self, age: u32) -> Self {
75//!         self.age = Some(age);
76//!         self
77//!     }
78//! }
79//!
80
81use proc_macro::TokenStream;
82use proc_macro2::Span;
83use quote::{quote, ToTokens};
84use syn::{parse_macro_input, spanned::Spanned, DeriveInput};
85
86type Fields = syn::punctuated::Punctuated<syn::Field, syn::token::Comma>;
87
88#[proc_macro_derive(Builder, attributes(build_it, skip))]
89/// Derive the builder pattern for a struct.
90///
91/// The builder implementation contains a method for each field of the struct, ignoring fields with
92/// a #[skip] attribute.
93/// Each field to generate a method for must be of type Option<T>.
94///
95/// # Example
96///
97/// The following struct:
98/// ```
99/// # use build_it::Builder;
100/// #[derive(Builder)]
101/// struct SimpleStruct {
102///    name: Option<String>,
103///    age: Option<u32>,
104///    #[build_it(skip)]
105///    address: String,
106/// }
107/// ```
108/// will generate the following implementation:
109/// ```
110/// # struct SimpleStruct {
111/// #    name: Option<String>,
112/// #    age: Option<u32>,
113/// # }
114/// impl SimpleStruct {
115///    pub fn name(mut self, name: String) -> Self {
116///        self.name = Some(name);
117///        self
118///     }
119///     pub fn age(mut self, age: u32) -> Self {
120///         self.age = Some(age);
121///         self
122///     }
123/// }
124/// ```
125pub fn derive_builder(input: TokenStream) -> TokenStream {
126    // Parse the input tokens into a syntax tree
127    let input = parse_macro_input!(input as DeriveInput);
128    let global_attr = parse_global_attr(&input);
129    let data = match input.data {
130        syn::Data::Struct(ref data) => Ok(data),
131        syn::Data::Enum(ref data) => Err(syn::Error::new(
132            data.enum_token.span(),
133            "Builder derive does not work on enums",
134        )),
135        syn::Data::Union(ref data) => Err(syn::Error::new(
136            data.union_token.span(),
137            "Builder derive does not work on unions",
138        )),
139    };
140    if let Err(err) = data {
141        return err.to_compile_error().into();
142    }
143    let data = data.expect("data is a struct");
144
145    let fields = match data.fields {
146        syn::Fields::Named(ref fields) => &fields.named,
147        syn::Fields::Unit => return quote! {}.into(),
148        syn::Fields::Unnamed(ref fields) => {
149            return syn::Error::new(
150                fields.span(),
151                "Builder derive only works on structs with named fields",
152            )
153            .to_compile_error()
154            .into();
155        }
156    };
157
158    generate_builder_impl(&input, &global_attr, fields).into()
159}
160
161/// Generate the builder implementation for a struct.
162/// The builder implementation contains a method for each field of the struct, ignoring fields with
163/// a #[build_it(skip)] attribute.
164///
165/// # Example
166///
167/// For a struct with fields `name: Option<String>` and `age: Option<u32>`, the generated
168/// implementation is:
169/// ```
170/// # struct SimpleStruct {
171/// #    name: Option<String>,
172/// #    age: Option<u32>,
173/// # }
174/// impl SimpleStruct {
175///    pub fn name(mut self, name: String) -> Self {
176///        self.name = Some(name);
177///        self
178///     }
179///     pub fn age(mut self, age: u32) -> Self {
180///         self.age = Some(age);
181///         self
182///     }
183/// }
184/// ```
185fn generate_builder_impl(
186    input: &DeriveInput,
187    global_attr: &GlobalAttr,
188    fields: &Fields,
189) -> proc_macro2::TokenStream {
190    let name = &input.ident;
191    let generics = &input.generics;
192    let methods = fields
193        .iter()
194        .map(|f| generate_builder_method(f, global_attr));
195    quote! {
196        impl #generics #name #generics {
197            #(#methods)*
198        }
199    }
200}
201
202/// Generate the builder method for a field.
203/// The method has the same name as the field and takes the field type by value.
204///
205/// # Example
206///
207/// For a field `name: Option<String>`, the generated method is:
208/// ```
209/// # struct SimpleStruct {
210/// #    name: Option<String>,
211/// # }
212/// # impl SimpleStruct {
213/// pub fn name(mut self, name: String) -> Self {
214///    self.name = Some(name);
215///    self
216/// }
217/// # }
218/// ```
219fn generate_builder_method(
220    field: &syn::Field,
221    global_attr: &GlobalAttr,
222) -> proc_macro2::TokenStream {
223    // Skip fields with a #[skip] attribute
224    // NOTE: This is deprecated in favor of the `build_it` attribute
225    if field.attrs.iter().any(|attr| attr.path().is_ident("skip")) {
226        return quote! {};
227    }
228
229    let attr = parse_attr(field);
230    if attr.skip {
231        return quote! {};
232    }
233
234    let field_name = field.ident.as_ref().unwrap();
235    let fn_name = syn::Ident::new(
236        &attr.rename.unwrap_or(field_name.to_string()),
237        Span::call_site(),
238    );
239    let field_ty = get_inner_type(&field.ty);
240    if field_ty.is_none() {
241        return syn::Error::new(
242            field.span(),
243            "Builder only works on Option<T> fields. Consider using #[skip] to skip fields that should not be optional.",
244        )
245        .to_compile_error();
246    }
247    let field_ty = field_ty.expect("field type is an Option<T>");
248
249    let docs = field.attrs.iter().filter_map(|attr| {
250        if attr.path().is_ident("doc") {
251            Some(attr.clone())
252        } else {
253            None
254        }
255    });
256    if attr.into || global_attr.into {
257        quote! {
258            #(#docs)*
259            pub fn #fn_name(mut self, #field_name: impl core::convert::Into<#field_ty>) -> Self {
260                self.#field_name = Some(#field_name.into());
261                self
262            }
263        }
264    } else {
265        quote! {
266            #(#docs)*
267            pub fn #fn_name(mut self, #field_name: #field_ty) -> Self {
268                self.#field_name = Some(#field_name);
269                self
270            }
271        }
272    }
273}
274
275#[derive(Default)]
276struct GlobalAttr {
277    into: bool,
278}
279
280fn parse_global_attr(input: &DeriveInput) -> GlobalAttr {
281    let mut result = GlobalAttr::default();
282    let attr = input
283        .attrs
284        .iter()
285        .find(|attr| attr.path().is_ident("build_it"));
286    if let Some(attr) = attr {
287        attr.parse_nested_meta(|meta| {
288            if meta.path.is_ident("into") {
289                result.into = true;
290            }
291            Ok(())
292        })
293        .expect("Failed to parse global build_it attribute");
294    }
295    result
296}
297
298#[derive(Default)]
299struct Attr {
300    skip: bool,
301    into: bool,
302    rename: Option<String>,
303}
304
305fn parse_attr(field: &syn::Field) -> Attr {
306    let attr = field
307        .attrs
308        .iter()
309        .find(|attr| attr.path().is_ident("build_it"));
310    let mut result = Attr::default();
311    if let Some(attr) = attr {
312        attr.parse_nested_meta(|meta| {
313            if meta.path.is_ident("skip") {
314                result.skip = true;
315            } else if meta.path.is_ident("into") {
316                result.into = true;
317            } else if meta.path.is_ident("rename") {
318                let content = meta.value().expect("Expected a value");
319                let lit: syn::LitStr = content.parse()?;
320                result.rename = Some(lit.value());
321            }
322            Ok(())
323        })
324        .expect("Failed to parse build_it attribute");
325    }
326    result
327}
328
329/// Get the inner type of an Option<T> type.
330fn get_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
331    if let syn::Type::Path(ref type_path) = ty {
332        if let Some(segment) = type_path.path.segments.first() {
333            // Check if the type is an Option
334            if segment.ident == "Option" {
335                // Get the type inside the Option: the first generic argument
336                if let syn::PathArguments::AngleBracketed(ref args) = segment.arguments {
337                    if let Some(syn::GenericArgument::Type(ref ty)) = args.args.first() {
338                        return Some(ty);
339                    }
340                }
341            }
342        }
343    }
344    None
345}