I am currently exploring how to use const generics in the combination with builders in rust. My plan is that it should be possible to check at compile time if all fields of a builder have been set and only then allow the building. This is not especially usefull most of the times but I ran into a strange error, where the cargo expand code works but the 'normal' code doesn't. I have the following Rust code:
use macros::Builder;
#[derive(Builder)]
struct User{
username: String,
password: String,
}
pub fn main() {
UserBuilder::new().set_username("username".into()).set_password("password".into()).build();
}
Which results in:
error[E0599]: no method named `set_username` found for struct `strict_builder::UserBuilder` in the current scope
--> src\main.rs:10:24
|
3 | #[derive(Builder)]
| ------- method `set_username` not found for this struct
...
10 | UserBuilder::new().set_username("username".into()).set_password("password".into()).build();
| ^^^^^^^^^^^^ method not found in `UserBuilder<false, false>`
For more information about this error, try `rustc --explain E0599`.
error: could not compile `testing` (bin "testing") due to 1 previous error
But when I run cargo expand:
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
use macros::Builder;
struct User {
username: String,
password: String,
}
mod strict_builder {
use super::User;
pub struct UserBuilder<
const USERNAME_CONFIGURED: bool,
const PASSWORD_CONFIGURED: bool,
> {
username: Option<String>,
password: Option<String>,
}
impl UserBuilder<false, false> {
pub fn new() -> Self {
Self {
username: None,
password: None,
}
}
}
impl UserBuilder<true, true> {
pub fn build(self) -> User {
unsafe {
User {
username: self.username.unwrap_unchecked(),
password: self.password.unwrap_unchecked(),
}
}
}
}
impl<const X_0: bool> UserBuilder<false, X_0> {
pub fn set_username(self, username: String) -> UserBuilder<true, X_0> {
UserBuilder::<true, X_0> {
username: Some(username),
password: self.password,
}
}
}
impl<const X_0: bool> UserBuilder<X_0, false> {
pub fn set_password(self, password: String) -> UserBuilder<X_0, true> {
UserBuilder::<X_0, true> {
username: self.username,
password: Some(password),
}
}
}
}
pub use strict_builder::*;
pub fn main() {
UserBuilder::new()
.set_username("username".into())
.set_password("password".into())
.build();
}
And run this, it works just fine!
The macro code is:
#![feature(proc_macro_def_site)]
use proc_macro::{Span, TokenStream};
use quote::quote;
use syn::{Data, DeriveInput, parse_macro_input};
#[proc_macro_derive(Builder)]
pub fn builder(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let builder_name = syn::Ident::new(&format!("{}Builder", name), Span::def_site().into());
let Data::Struct(data_struct) = input.data else {
panic!("Only structs are supported by Builder");
};
let field_idents = data_struct.fields.iter().map(|field| {
field.ident.as_ref().expect("Fields of the struct have to be named to create a builder!")
}).collect::<Vec<_>>();
let consts = field_idents.iter().map(
|ident| {
let const_name = syn::Ident::new(
&format!("{}_CONFIGURED", to_const_case(&ident.to_string())),
Span::def_site().into(),
);
quote! {
const #const_name : bool,
}
}).collect::<Vec<_>>();
let mut impls = Vec::new();
let mut generic_index = 0;
let field_count = data_struct.fields.len();
let mut consts_generics = Vec::new();
for field in 0..field_count - 1 {
let new_ident = syn::Ident::new(&format!("X_{}", field), Span::def_site().into());
consts_generics.push(quote! {const #new_ident: bool,});
}
for field in &data_struct.fields {
let field_ident = &field
.ident
.as_ref()
.expect("Fields of the struct have to be named to create a builder!");
// <X_0, ..., X_N-1, false, X_N+1, ...>
let gen_consts_bev = (0..field_count).map(|index| {
if generic_index == index {
quote! {false,}
} else {
let index = if index >= generic_index { index - 1 } else { index };
let new_ident = syn::Ident::new(&format!("X_{}", index), Span::def_site().into());
quote! {#new_ident,}
}
});
// <X_0, ..., X_N-1, true, X_N+1, ...>
let gen_consts_after = (0..field_count)
.map(|index| {
if generic_index == index {
quote! {true,}
} else {
let index = if index >= generic_index { index - 1 } else { index };
let new_ident =
syn::Ident::new(&format!("X_{}", index), Span::def_site().into());
quote! {#new_ident,}
}
})
.collect::<Vec<_>>();
// field0: self.field_0, ..., field_n-1: field_n-1, field_n: Some(field_n), field_n+1: self.field_n+1, ...
let field_setters = &data_struct
.fields
.iter()
.map(|set_field| {
let set_field_ident = set_field
.ident
.as_ref()
.expect("Fields of the struct have to be named to create a builder!");
if set_field.ident == field.ident {
quote! {
#set_field_ident: Some(#field_ident),
}
} else {
quote! {
#set_field_ident: self.#set_field_ident,
}
}
})
.collect::<Vec<_>>();
let function_ident =
syn::Ident::new(&format!("set_{}", field_ident), Span::def_site().into());
let field_ty = &field.ty;
impls.push(quote! {
impl <#(#consts_generics)*> #builder_name<#(#gen_consts_bev)*> {
pub fn #function_ident(self, #field_ident: #field_ty) -> #builder_name< #(#gen_consts_after)*> {
#builder_name::<#(#gen_consts_after)*> {
#(#field_setters)*
}
}
}
});
generic_index += 1;
}
let optional_fields = &data_struct
.fields
.iter()
.map(|field| {
let field_ident = field.ident.as_ref().unwrap();
let field_type = &field.ty;
quote! {
#field_ident: Option<#field_type>,
}
})
.collect::<Vec<_>>();
let all_generics_false = (0..field_count).map(|_| {
quote! {false, }
});
let all_generics_true = (0..field_count).map(|_| {
quote! {true,}
});
let all_fields_empty = field_idents.iter().map(|field_ident| {
quote! {
#field_ident: None,
}
});
let copy_all_fields = field_idents.iter().map(|field_ident| {
quote! {
#field_ident: self.#field_ident.unwrap_unchecked(),
}
});
quote! {
mod strict_builder {
use super::#name;
pub struct #builder_name <#(#consts)*>{
#(#optional_fields)*
}
impl #builder_name <#(#all_generics_false)*>{
pub fn new() -> Self {
Self {
#(#all_fields_empty)*
}
}
}
impl #builder_name <#(#all_generics_true)*>{
pub fn build(self) -> #name {
unsafe {
#name {
#(#copy_all_fields)*
}
}
}
}
#(#impls)*
}
pub use strict_builder::*;
}
.into()
}
fn to_const_case(str: &str) -> String {
let mut out = String::with_capacity(str.len());
if str.len() == 1 {
return str.to_uppercase().to_string();
}
out += &str.chars().next().unwrap().to_uppercase().to_string();
for c in str.chars().skip(1) {
if c.is_uppercase() {
out.push('_');
}
out += &c.to_uppercase().to_string();
}
out
}
This is my first ever macro, it's not pretty but I just wanna play around a liitle.
That's what happens when you use unstable features (or really, any feature at all) without reading the documentation and fully understanding it.
Span::def_site()
opts for macros 2.0 hygiene, also called opaque hygiene or def site hygiene, which means that identifier created with this span will never resolve to/when written outside the macro.
Use Span::mixed_site()
or Span::call_site()
instead.