Support non-flag attributes in #[builder(on(...))]
#152
Labels
feature request
A new feature is requested
#[builder(on(...))]
#152
Currently, there is a
#[builder(on(pattern, attrs))]
attribute that supportsinto
configuration. With that one it's possible to enable#[builder(into)]
for all members like this:The
_
is a pattern that matches all members. However, it's possible to match only specific types like this:This way only
x1
will get a#[builder(into)]
because its typeString
matches the pattern. To match any member ofBox
type the following pattern could be used:It's also possible to specify multiple
on(...)
clauses:This syntax of
#[builder(on(...))]
works quite well for attributes that are boolean flags.However, using it for attributes that can have more states than
true/false
is more complex. For example,#[builder(default)]
is not just a boolean. It can accept any arbitrary expression:#[builder(default = 2 + 2)]
. So then there is a question of how#[builder(on(...))]
should behave when there are multipleon(...)
directives that match the same member.There are several ways to approach the problem.
Prioritization based on pattern specificity
We could obviously say that the pattern
_
is more general thanBox<_>
orString
. Therefore we could say that if there ison(String, default = ...)
and#[builder(_, default)]
, then theon(String, default = ...)
clause wins and thus its configuration is applied.However, evaluating specificity is not as easy as that. Suppose, for example, there is this case:
What
default
config should be applied in this case? Both of the patternsBTreeMap<_, u32>
andBTreeMap<u32, _>
seem to be at the same level of specificity as for me. So... there is no one obvious way to resolve this configuration if we specialized based on pattern specificity.So this approach doesn't work.
Prioritization based on relative ordering
We can say that
on(...)
directives behave like match arms in amatch
where the scrutinee is the member. I.e. it semantically works like this:Then the answer for the problematic
BTreeMap<_, u32>/BTreeMap<u32, _>
case becomes obvious. The first pattern that we declared syntactically higher in the code wins. This requires the developer to manually arrange theon(...)
directives in the order from the most specific to the least specific according to their taste.However, this becomes inconvenient when multiple attributes are involved. For example:
In this case the first
on(String, into)
will match the memberx1
. It specifies onlyinto
attribute, and thusx1
will get#[builder(into)]
, but it won't get#[builder(default)]
, which may or may not be the desired behavior.Maybe instead, we could say that different
into
anddefault
attributes are matched separately. So, for example,on(String, into)
short circuits forinto
, but this directive is ignored fordefault
because it doesn't mention it. If the user wants to explicitly disabledefault
forString
s they should writeon(String, into, reset(default))
.Extended pattern syntax and attributes that change the member's type and other matched properties
In version
3.0
of bon, there will be a new attribute called#[builder(transparent)]
. This attribute applies only to members of typeOption<T>
. It disables their special handling such that there is only one setter generated that accepts theOption<T>
value directly, and that setter is required to call just like for any other member not marked with#[builder(default)]
.There was a request for applying such an attribute at the top level with
#[builder(on(...))]
as well already (#35 (comment)). However, the problem is that this attribute changes the underlying type of the member. It changes it fromT
toOption<T>
.Currently, the type matching ignores the
Option<...>
wrapper. This allows the following to work:All the members
x1
,x2
,x3
have#[builder(into)]
automatically configured for them. We didn't have to writeon(String, into), on(Option<String>, into)
. The "underlying" type of the member is matched instead. What are the underlying types in this case? Here they are:x1
- this is a required member and its underlying type is the type of the field itself i.e.String
x2
- this is an optional member and its underlying type is the type under theOption<...>
i.e.String
.x3
- this is an optional member with adefault
and its underlying type is the type of the fieeld itself i.e.String
.This way the "optional", "default", "required" behaviors are treated as separate parameters of the member. Member's underlying type is always independent of those parameters. This makes the match against the underlying type of the member stable. So if the user changes a required member of type
String
toOption<String>
the match still occurs.However, if we add
#[builder(transparent)]
into this, then the reasoning becomes more complex:Now, the member
x2
becomes a required member with the underlying type ofOption<String>
. Therefore, the directiveon(String, into)
no longer matches it.Then, if we support
transparent
in theon(...)
directive itself, then the order in which we evaluate theon(...)
directives becomes even more important. For example:In this case, we assume that we first apply
transparent
to all members, and only after that do we evaluate theon(String, into)
. So after the first pass of applyingtransparent
thex2
member's underlying type changes toOption<String>
, and thus it no longer matches theon(String, into)
.Then, we could say that if we change the order of
on(...)
directives here, things change like this:The first pass applies
into
to all members with the underlyingString
type. The second pass appliestransparent
to the memberx2
.Summing up
We need to generalize all of this into a consistent simple algorithm that could be explained and understood by the developers. Here is how it could be described:
All
on(...)
directives are evaluated in order of their declaration.Once the first matching
on(...)
directive sets a config for a specific parameter of the member all other configs for this parameter in the remainingon(...)
directives will be ignored. But... member-level configuration always takes precedence.Example:
The
reset(...)
directive can be used to explicitly request the "factory reset" for the config parameter so thaton(...)
directives below don't override its value. Example:The
on(...)
directives that are declared earlier (higher in code) change the properties of the member and these changes influence the matching for theon(...)
directives that come later (lower in code).Example:
Future posibilities
In the future, there can be more complex syntax for patterns in
on(pattern, attrs)
. We could allow selecting members not only by their type, but also by other properites:on(prefix = foo, ...)
select members with names that start withfoo
on(required(), ...)
select all reequired memberson(has(into), ...)
select members that have#[buider(into)]
applied to themA note for the community from the maintainers
Please vote on this issue by adding a 👍 reaction to help the maintainers with prioritizing it. You may add a comment describing your real use case related to this issue for us to better understand the problem domain.
The text was updated successfully, but these errors were encountered: