bon_macros/builder/builder_gen/member/config/
mod.rs

1mod blanket;
2mod getter;
3mod setters;
4mod with;
5
6pub(crate) use blanket::*;
7pub(crate) use getter::*;
8pub(crate) use setters::*;
9pub(crate) use with::*;
10
11use super::MemberOrigin;
12use crate::parsing::SpannedKey;
13use crate::util::prelude::*;
14use std::fmt;
15
16#[derive(Debug, darling::FromAttributes)]
17#[darling(attributes(builder))]
18pub(crate) struct MemberConfig {
19    /// Assign a default value to the member it it's not specified.
20    ///
21    /// An optional expression can be provided to set the value for the member,
22    /// otherwise its [`Default`] trait impl will be used.
23    #[darling(with = parse_optional_expr, map = Some)]
24    pub(crate) default: Option<SpannedKey<Option<syn::Expr>>>,
25
26    /// Make the member a private field in the builder struct.
27    /// This is useful when the user needs to add custom fields to the builder,
28    /// that they would use in the custom methods they add to the builder.
29    ///
30    /// This is similar to `skip`. The difference is that `field` is evaluated
31    /// inside of the starting function, and stored in the builder. Its initialization
32    /// expression thus has access to all `start_fn` parameters. It must be declared
33    /// strictly after `#[builder(start_fn)]` members (if any) or right at the top of
34    /// the members list.
35    #[darling(with = parse_optional_expr, map = Some)]
36    pub(crate) field: Option<SpannedKey<Option<syn::Expr>>>,
37
38    /// Make the member gettable. [`GetterConfig`] specifies the signature for
39    /// the getter.
40    ///
41    /// This takes the same attributes as the setter fns; `name`, `vis`, and `doc`
42    /// and produces a getter method that returns the value of the member.
43    /// By default, the value is returned by a shared reference (&T).
44    pub(crate) getter: Option<SpannedKey<GetterConfig>>,
45
46    /// Accept the value for the member in the finishing function parameters.
47    pub(crate) finish_fn: darling::util::Flag,
48
49    /// Enables an `Into` conversion for the setter method.
50    pub(crate) into: darling::util::Flag,
51
52    /// Rename the name exposed in the builder API.
53    pub(crate) name: Option<syn::Ident>,
54
55    /// Allows setting the value for the member repeatedly. This reduces the
56    /// number of type states and thus increases the compilation performance.
57    ///
58    /// However, this also means that unintended overwrites won't be caught
59    /// at compile time. Measure the compilation time before and after enabling
60    /// this option to see if it's worth it.
61    pub(crate) overwritable: darling::util::Flag,
62
63    /// Disables the special handling for a member of type `Option<T>`. The
64    /// member no longer has the default of `None`. It also becomes a required
65    /// member unless a separate `#[builder(default = ...)]` attribute is
66    /// also specified.
67    pub(crate) required: darling::util::Flag,
68
69    /// Configurations for the setter methods.
70    #[darling(with = crate::parsing::parse_non_empty_paren_meta_list)]
71    pub(crate) setters: Option<SettersConfig>,
72
73    /// Skip generating a setter method for this member.
74    ///
75    /// An optional expression can be provided to set the value for the member,
76    /// otherwise its  [`Default`] trait impl will be used.
77    #[darling(with = parse_optional_expr, map = Some)]
78    pub(crate) skip: Option<SpannedKey<Option<syn::Expr>>>,
79
80    /// Accept the value for the member in the starting function parameters.
81    pub(crate) start_fn: darling::util::Flag,
82
83    /// Customize the setter signature and body with a custom closure or a well-known
84    /// function. The closure/function must return the value of the type of the member,
85    /// or optionally a `Result<_>` type where `_` is used to mark the type of
86    /// the member. In this case the generated setters will be fallible
87    /// (they'll propagate the `Result`).
88    pub(crate) with: Option<SpannedKey<WithConfig>>,
89}
90
91#[derive(PartialEq, Eq, Clone, Copy)]
92enum ParamName {
93    Default,
94    Field,
95    Getter,
96    FinishFn,
97    Into,
98    Name,
99    Overwritable,
100    Required,
101    Setters,
102    Skip,
103    StartFn,
104    With,
105}
106
107impl fmt::Display for ParamName {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        let str = match self {
110            Self::Default => "default",
111            Self::Field => "field",
112            Self::Getter => "getter",
113            Self::FinishFn => "finish_fn",
114            Self::Into => "into",
115            Self::Name => "name",
116            Self::Overwritable => "overwritable",
117            Self::Required => "required",
118            Self::Setters => "setters",
119            Self::Skip => "skip",
120            Self::StartFn => "start_fn",
121            Self::With => "with",
122        };
123        f.write_str(str)
124    }
125}
126
127impl MemberConfig {
128    fn validate_mutually_exclusive(
129        &self,
130        attr_name: ParamName,
131        attr_span: Span,
132        mutually_exclusive: &[ParamName],
133    ) -> Result<()> {
134        self.validate_compat(attr_name, attr_span, mutually_exclusive, true)
135    }
136
137    fn validate_mutually_allowed(
138        &self,
139        attr_name: ParamName,
140        attr_span: Span,
141        mutually_allowed: &[ParamName],
142    ) -> Result<()> {
143        self.validate_compat(attr_name, attr_span, mutually_allowed, false)
144    }
145
146    fn validate_compat(
147        &self,
148        attr_name: ParamName,
149        attr_span: Span,
150        patterns: &[ParamName],
151        mutually_exclusive: bool,
152    ) -> Result<()> {
153        let conflicting: Vec<_> = self
154            .specified_param_names()
155            .filter(|name| *name != attr_name && patterns.contains(name) == mutually_exclusive)
156            .collect();
157
158        if conflicting.is_empty() {
159            return Ok(());
160        }
161
162        let conflicting = conflicting
163            .iter()
164            .map(|name| format!("`{name}`"))
165            .join(", ");
166
167        bail!(
168            &attr_span,
169            "`{attr_name}` attribute can't be specified together with {conflicting}",
170        );
171    }
172
173    fn specified_param_names(&self) -> impl Iterator<Item = ParamName> {
174        let Self {
175            default,
176            field,
177            getter,
178            finish_fn,
179            into,
180            name,
181            overwritable,
182            required,
183            setters,
184            skip,
185            start_fn,
186            with,
187        } = self;
188
189        let attrs = [
190            (default.is_some(), ParamName::Default),
191            (field.is_some(), ParamName::Field),
192            (getter.is_some(), ParamName::Getter),
193            (finish_fn.is_present(), ParamName::FinishFn),
194            (into.is_present(), ParamName::Into),
195            (name.is_some(), ParamName::Name),
196            (overwritable.is_present(), ParamName::Overwritable),
197            (required.is_present(), ParamName::Required),
198            (setters.is_some(), ParamName::Setters),
199            (skip.is_some(), ParamName::Skip),
200            (start_fn.is_present(), ParamName::StartFn),
201            (with.is_some(), ParamName::With),
202        ];
203
204        attrs
205            .into_iter()
206            .filter(|(is_present, _)| *is_present)
207            .map(|(_, name)| name)
208    }
209
210    pub(crate) fn validate(&self, origin: MemberOrigin) -> Result {
211        if !cfg!(feature = "experimental-overwritable") && self.overwritable.is_present() {
212            bail!(
213                &self.overwritable.span(),
214                "🔬 `overwritable` attribute is experimental and requires \
215                 \"experimental-overwritable\" cargo feature to be enabled; \
216                 we would be glad to make this attribute stable if you find it useful; \
217                 please leave a 👍 reaction under the issue https://github.com/elastio/bon/issues/149 \
218                 to help us measure the demand for this feature; it would be \
219                 double-awesome if you could also describe your use case in \
220                 a comment under the issue for us to understand how it's used \
221                 in practice",
222            );
223        }
224
225        if let Some(getter) = &self.getter {
226            if !cfg!(feature = "experimental-getter") {
227                bail!(
228                    &getter.key,
229                    "🔬 `getter` attribute is experimental and requires \
230                    \"experimental-getter\" cargo feature to be enabled; \
231                    if you find the current design of this attribute already \
232                    solid please leave a 👍 reaction under the issue \
233                    https://github.com/elastio/bon/issues/225; if you have \
234                    any feedback, then feel free to leave a comment under that issue",
235                );
236            }
237
238            self.validate_mutually_exclusive(
239                ParamName::Getter,
240                getter.key.span(),
241                &[ParamName::Overwritable],
242            )?;
243        }
244
245        if self.start_fn.is_present() {
246            self.validate_mutually_allowed(
247                ParamName::StartFn,
248                self.start_fn.span(),
249                // TODO: add support for `#[builder(getter)]` with `start_fn`
250                &[ParamName::Into],
251            )?;
252        }
253
254        if self.finish_fn.is_present() {
255            self.validate_mutually_allowed(
256                ParamName::FinishFn,
257                self.finish_fn.span(),
258                &[ParamName::Into],
259            )?;
260        }
261
262        if let Some(field) = &self.field {
263            self.validate_mutually_allowed(ParamName::Field, field.key.span(), &[])?;
264        }
265
266        if let Some(skip) = &self.skip {
267            match origin {
268                MemberOrigin::FnArg => {
269                    bail!(
270                        &skip.key.span(),
271                        "`skip` attribute is not supported on function arguments; \
272                        use a local variable instead.",
273                    );
274                }
275                MemberOrigin::StructField => {}
276            }
277
278            if let Some(Some(_expr)) = self.default.as_deref() {
279                bail!(
280                    &skip.key.span(),
281                    "`skip` attribute can't be specified with the `default` attribute; \
282                    if you wanted to specify a value for the member, then use \
283                    the following syntax instead `#[builder(skip = value)]`",
284                );
285            }
286
287            self.validate_mutually_allowed(ParamName::Skip, skip.key.span(), &[])?;
288        }
289
290        if let Some(with) = &self.with {
291            self.validate_mutually_exclusive(ParamName::With, with.key.span(), &[ParamName::Into])?;
292        }
293
294        Ok(())
295    }
296}
297
298fn parse_optional_expr(meta: &syn::Meta) -> Result<SpannedKey<Option<syn::Expr>>> {
299    match meta {
300        syn::Meta::Path(path) => SpannedKey::new(path, None),
301        syn::Meta::List(_) => Err(Error::unsupported_format("list").with_span(meta)),
302        syn::Meta::NameValue(meta) => SpannedKey::new(&meta.path, Some(meta.value.clone())),
303    }
304}