Created
May 28, 2025 03:25
-
-
Save NanamiNakano/c3220ec52c6c5fd072cf928cc65c8e9f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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