builder_option_derive 0.1.0

A macro to generate builder class for a struct
Documentation
#![deny(missing_docs)]

//! This crate contains a proc-macro `Builder` that creates a builder for an annotated struct.
//! It's in a separate crate, because Rust does not allow for `proc-macro` crates to contain
//! macros-by-example.
//!
//! See [builder_option](http://crates.io/crates/builder_option) for details.
//!
use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Fields, Ident, Type};

#[proc_macro_derive(Builder)]
/// A macro to create a corresponding builder for an annotated struct
pub fn builder_derive(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    let name = input.ident;
    let builder_name = Ident::new(&format!("{}Builder", name), Span::def_site());

    let fields = match input.data {
        Data::Struct(ref data_struct) => match data_struct.fields {
            Fields::Named(ref fields_named) => &fields_named.named,
            _ => {
                return syn::Error::new_spanned(
                    &data_struct.fields,
                    "Builder can only be derived for structs with named fields",
                )
                .to_compile_error()
                .into();
            }
        },
        _ => {
            return syn::Error::new_spanned(&name, "Builder can only be derived for structs")
                .to_compile_error()
                .into();
        }
    };

    fn inner_type(ty: &Type) -> (bool, &Type) {
        if let Type::Path(type_path) = ty {
            if let Some(segment) = type_path.path.segments.first() {
                if segment.ident == "Option" {
                    if let syn::PathArguments::AngleBracketed(ref angle_bracketed) =
                        segment.arguments
                    {
                        if let Some(syn::GenericArgument::Type(ref inner_ty)) =
                            angle_bracketed.args.first()
                        {
                            return (true, inner_ty);
                        }
                    }
                }
            }
        }
        (false, ty)
    }

    let builder_fields = fields.iter().map(|f| {
        let name = &f.ident;
        let ty = &f.ty;
        let (_, inner_ty) = inner_type(ty);

        quote! {
            #name: std::option::Option<#inner_ty>,
        }
    });

    let builder_init = fields.iter().map(|f| {
        let name = &f.ident;
        quote! {
            #name: None,
        }
    });

    let build_fields = fields.iter().map(|f| {
        let name = &f.ident;
        let ty = &f.ty;
        let field_name_str = name.as_ref().unwrap().to_string();
        let (is_option, _) = inner_type(ty);

        if is_option {
            quote! {
                #name: self.#name.clone(),
            }
        } else {
            quote! {
                #name: self.#name.clone().ok_or_else(|| format!("Field '{}' is required", #field_name_str))?,
            }
        }
    });

    let builder_methods = fields.iter().map(|f| {
        let name = &f.ident;
        let ty = &f.ty;
        let (_, inner_ty) = inner_type(ty);
        let method_name = syn::Ident::new(
            &format!("with_{}", name.as_ref().unwrap()),
            Span::call_site(),
        );

        quote! {
            pub fn #method_name(&mut self, #name: #inner_ty) -> &mut Self {
                self.#name = std::option::Option::Some(#name);
                self
            }
        }
    });

    let expanded = quote! {
        #[derive(Debug)]
        pub struct #builder_name {
             #(#builder_fields)*
        }

        impl #builder_name {
           #(#builder_methods)*

           pub fn build(&self) -> Result<#name, std::boxed::Box<dyn std::error::Error>> {
                Ok(#name {
                    #(#build_fields)*
                })
           }
        }

        impl #name {
            pub fn builder() -> #builder_name {
                #builder_name {
                    #(#builder_init)*
                }
            }
        }
    };

    TokenStream::from(expanded)
}