struct-builder 0.3.0

Derive a builder for your structs
Documentation
use crate::struct_builder::{BuilderContext, GenericsContext};
use proc_macro2::TokenStream;
use quote::ToTokens;
use syn::punctuated::Punctuated;
use syn::{parse_quote, Expr, Field, FieldValue, Fields, Index, ItemImpl, ItemStruct, Token, Type};

pub struct ImplSubjectFnBuilder {
    ctx: BuilderContext,
    fields: Fields
}

impl From<&ItemStruct> for ImplSubjectFnBuilder {
    fn from(value: &ItemStruct) -> Self {
        let ctx: BuilderContext = value.into();
        let fields = value.fields.clone();
        
        Self { ctx, fields }
    }
}

impl ToTokens for ImplSubjectFnBuilder {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        let BuilderContext { 
            subject,
            params,
            params_argument,
            builder,
            builder_subject_field,
            generics,
            fields_metadata,
            ..
        } = &self.ctx;
        
        let optional_expr: Option<Expr> = match &self.fields {
            Fields::Named(named_fields) => {
                let punctuated_fields = named_fields.named
                    .iter()
                    .map::<FieldValue, _>(|field| {
                        let field_ident = field.ident.as_ref().expect("named field missing ident");
                        if is_required(&field) {
                            parse_quote! { #field_ident: #params_argument.#field_ident }
                        } else {
                            parse_quote! { #field_ident: ::std::option::Option::None }
                        }
                    })
                    .collect::<Punctuated<FieldValue, Token![,]>>();
                
                Some(parse_quote! { Self { #punctuated_fields } })
            },
            
            Fields::Unnamed(unnamed_fields) => {
                let mut next_index = 0;
                let punctuated_fields = unnamed_fields.unnamed
                    .iter()
                    .map::<Expr, _>(|field|
                        if is_required(&field) {
                            let index = Index::from(next_index);
                            next_index += 1;
                            parse_quote! { #params_argument.#index }
                        } else {
                            parse_quote! { ::std::option::Option::None }
                        }
                    )
                    .collect::<Punctuated<Expr, Token![,]>>();
                
                Some(parse_quote! { Self(#punctuated_fields) })
            },
            
            Fields::Unit => None
        };

        if let Some(expr) = optional_expr {
            let GenericsContext {
                generics_def,
                generics_expr,
                where_clause
            } = &generics;

            let include_params_generics = fields_metadata.generic_required_fields_count > 0;
            
            let item_impl: ItemImpl = if include_params_generics {
                parse_quote! {
                    impl #generics_def #subject #generics_expr #where_clause {
                        pub fn builder(#params_argument: #params #generics_expr) -> #builder #generics_expr {
                            #builder {
                                #builder_subject_field: #expr
                            }
                        }
                    }
                }
            } else {
                parse_quote! {
                    impl #generics_def #subject #generics_expr #where_clause {
                        pub fn builder(#params_argument: #params) -> #builder #generics_expr {
                            #builder {
                                #builder_subject_field: #expr
                            }
                        }
                    }
                }
            };

            item_impl.to_tokens(tokens);   
        }
    }
}

pub fn is_required(field: &Field) -> bool {
    match &field.ty {
        Type::Path(path_type) => path_type.path.segments
            .last()
            .map(|seg| seg.ident != "Option")
            .unwrap_or(true),
        
        _ => true
    }
}

#[cfg(test)]
mod tests {
    use crate::components::impl_subject_fn_builder::ImplSubjectFnBuilder;
    use crate::test_util::{sample_named_item_struct, sample_unit_item_struct, sample_unnamed_item_struct};
    use proc_macro2::TokenStream;
    use quote::ToTokens;
    use syn::{parse_quote, ItemImpl};

    #[test]
    fn test_with_named_fields() {
        let item_struct = sample_named_item_struct();
        let expected: ItemImpl = parse_quote! {
            impl<T, I: Send, W> MyStruct<T, I, W>
            where
                W: Sync
            {
                pub fn builder(params: MyStructParams<T, I, W>) -> MyStructBuilder<T, I, W> {
                    MyStructBuilder {
                        inner: Self {
                            public_field: params.public_field,
                            private_field: params.private_field,
                            optional: ::std::option::Option::None,
                            test: ::std::option::Option::None,
                            test2: ::std::option::Option::None,
                            dynamic: params.dynamic,
                            dynamic2: params.dynamic2,
                            generic: params.generic,
                            generic_inline: params.generic_inline,
                            generic_where: params.generic_where
                        }
                    }
                }
            }
        };
        
        let impl_subject_fn_builder = ImplSubjectFnBuilder::from(&item_struct);

        assert_eq!(
            impl_subject_fn_builder.to_token_stream().to_string(),
            expected.to_token_stream().to_string()
        );
    }

    #[test]
    fn test_with_unnamed_fields() {
        let item_struct = sample_unnamed_item_struct();
        let expected: ItemImpl = parse_quote! {
            impl<T, I: Send, W> MyStruct<T, I, W>
            where
                W: Sync
            {
                pub fn builder(params: MyStructParams<T, I, W>) -> MyStructBuilder<T, I, W> {
                    MyStructBuilder {
                        inner: Self(
                            params.0,
                            params.1,
                            ::std::option::Option::None,
                            ::std::option::Option::None,
                            ::std::option::Option::None,
                            params.2,
                            params.3,
                            params.4,
                            params.5,
                            params.6
                        )
                    }
                }
            }
        };

        let subject_impl = ImplSubjectFnBuilder::from(&item_struct);

        assert_eq!(
            subject_impl.to_token_stream().to_string(),
            expected.to_token_stream().to_string()
        );
    }
    
    #[test]
    fn test_with_unit_struct() {
        let item_struct = sample_unit_item_struct();
        
        let subject_impl = ImplSubjectFnBuilder::from(&item_struct);
        
        assert_eq!(
            subject_impl.to_token_stream().to_string(),
            TokenStream::new().to_string()
        );
    }
}