Skip to content

Instantly share code, notes, and snippets.

@NanamiNakano
Created May 28, 2025 03:25
Show Gist options
  • Select an option

  • Save NanamiNakano/c3220ec52c6c5fd072cf928cc65c8e9f to your computer and use it in GitHub Desktop.

Select an option

Save NanamiNakano/c3220ec52c6c5fd072cf928cc65c8e9f to your computer and use it in GitHub Desktop.
use darling::FromMeta;
use proc_macro::TokenStream;
use proc_macro2::{Ident, Span};
use quote::quote;
use syn::PathArguments::AngleBracketed;
use syn::{parse_macro_input, Data, DeriveInput, GenericArgument, PathSegment, Type, TypePath};
#[derive(FromMeta)]
struct BuilderOpts {
each: String,
}
struct Field {
name: Ident,
optional: bool,
each: bool,
builder: Ident,
field_type: Type,
}
#[proc_macro_derive(Builder, attributes(builder))]
pub fn derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let struct_ident = &input.ident;
let builder_ident = Ident::new(&format!("{}Builder", struct_ident), Span::call_site());
let input_fields = match &input.data {
Data::Struct(data) => data.clone().fields,
_ => {
return syn::Error::new_spanned(&input, "Builder can only be derived for structs")
.to_compile_error()
.into();
}
};
let mut fields = vec![];
for input_field in input_fields.iter() {
let Some(ref input_ident) = input_field.ident else {
return syn::Error::new_spanned(&input, "Unnamed field is not supported")
.to_compile_error()
.into();
};
let input_field_type = &input_field.ty;
let Type::Path(TypePath { path, qself: _ }) = input_field_type else {
fields.push(Field {
name: input_ident.clone(),
optional: false,
each: false,
builder: Ident::new(&input_ident.clone().to_string(), Span::call_site()),
field_type: input_field_type.clone(),
});
continue;
};
let last_seg = path.segments.last().expect("Except last segment");
let field_optional = last_seg.ident == "Option";
let field_type = if field_optional {
strip_segment(last_seg)
.expect("Except has inner type")
.first()
.expect("Except one inner type")
.clone()
} else {
input_field_type.clone()
};
let builder_attrs: Vec<_> = input_field
.attrs
.iter()
.filter(|attr| attr.path().is_ident("builder"))
.collect();
if !builder_attrs.is_empty() {
let attr = *builder_attrs.first().expect("Except attribute");
let Ok(opts) = BuilderOpts::from_meta(&attr.meta) else {
return syn::Error::new_spanned(&attr.meta, "expected `builder(each = \"...\")`")
.to_compile_error()
.into();
};
let field_type = strip_type(field_type);
fields.push(Field {
name: input_ident.clone(),
optional: field_optional,
each: true,
builder: Ident::new(&opts.each, Span::call_site()),
field_type,
});
} else {
fields.push(Field {
name: input_ident.clone(),
optional: field_optional,
each: false,
builder: Ident::new(&input_ident.clone().to_string(), Span::call_site()),
field_type,
});
};
}
let mut option_fields = Vec::with_capacity(fields.len());
let mut none_fields = Vec::with_capacity(fields.len());
let mut setters = Vec::with_capacity(fields.len());
let mut extractors = vec![];
let mut fillers = Vec::with_capacity(fields.len());
for field in fields {
let Field {
name,
optional,
each,
builder,
field_type,
} = field;
if each {
none_fields.push(quote! {
#name: ::std::option::Option::Some(vec![]),
});
option_fields.push(quote! {
#name: ::std::option::Option<::std::vec::Vec<#field_type>>,
});
setters.push(quote! {
fn #builder(&mut self, #builder: #field_type) -> &mut Self {
if let ::std::option::Option::Some(vec) = &mut self.#name {
vec.push(#builder);
} else {
let vec = vec![#builder];
self.#name = ::std::option::Option::Some(vec);
}
self
}
})
} else {
none_fields.push(quote! {
#name: ::std::option::Option::None,
});
option_fields.push(quote! {
#name: ::std::option::Option<#field_type>,
});
setters.push(quote! {
fn #name(&mut self, #name: #field_type) -> &mut Self {
self.#name = ::std::option::Option::Some(#name);
self
}
});
};
if optional {
fillers.push(quote! {
#name: self.#name.clone(),
})
} else {
extractors.push(quote! {
let ::std::option::Option::Some(#name) = self.#name.clone() else {
return ::std::result::Result::Err(::std::boxed::Box::from("Field can not be empty"));
};
});
fillers.push(quote! {
#name,
})
}
}
let expand = quote! {
pub struct #builder_ident {
#(#option_fields)*
}
impl #builder_ident {
#(#setters)*
pub fn build(&mut self) -> ::std::result::Result<#struct_ident, ::std::boxed::Box<dyn ::std::error::Error>> {
#(#extractors)*
return ::std::result::Result::Ok(#struct_ident {
#(#fillers)*
})
}
}
impl #struct_ident {
pub fn builder() -> #builder_ident {
#builder_ident {
#(#none_fields)*
}
}
}
};
TokenStream::from(expand)
}
fn strip_segment(path: &PathSegment) -> Option<Vec<Type>> {
match &path.arguments {
AngleBracketed(bracketed_args) => {
let mut striped = vec![];
for arg in &bracketed_args.args {
let GenericArgument::Type(striped_type) = arg else {
continue;
};
striped.push(striped_type.clone());
}
Some(striped)
}
_ => None,
}
}
fn strip_type(bracketed_type: Type) -> Type {
let Type::Path(path) = bracketed_type else {
return bracketed_type;
};
let last_seg = path.path.segments.last().expect("Except last segment");
strip_segment(last_seg)
.expect("Except has inner type")
.first()
.expect("Except one inner type")
.clone()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment