promkit-derive 0.2.0

A derive macro for promkit
Documentation
extern crate proc_macro;

use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use syn::{parse::Error, parse_macro_input, spanned::Spanned, DeriveInput};

#[proc_macro_derive(Promkit, attributes(form))]
pub fn promkit_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let ast = parse_macro_input!(input as DeriveInput);
    match impl_promkit_derive(&ast) {
        Ok(token) => token.into(),
        Err(e) => e.to_compile_error().into(),
    }
}

mod text_editor;

fn impl_promkit_derive(ast: &DeriveInput) -> Result<TokenStream, Error> {
    let fields = match &ast.data {
        syn::Data::Struct(s) => match &s.fields {
            syn::Fields::Named(fields) => &fields.named,
            syn::Fields::Unnamed(_) => {
                return Err(Error::new(ast.span(), "Not support tuple structs"))
            }
            syn::Fields::Unit => return Err(Error::new(ast.span(), "Not support unit structs")),
        },
        syn::Data::Enum(_) => return Err(Error::new(ast.span(), "Not support enums")),
        syn::Data::Union(_) => return Err(Error::new(ast.span(), "Not support unions")),
    };

    let mut text_editor_states = Vec::new();
    let mut field_assignments = Vec::new();
    let mut field_types = Vec::new();

    for (idx, field) in fields.iter().enumerate() {
        for attr in field.attrs.iter() {
            #[allow(clippy::single_match)]
            match attr.path().get_ident().unwrap().to_string().as_str() {
                "form" => {
                    let state = text_editor::create_state(attr)?;
                    text_editor_states.push(state);

                    let field_ident = field.ident.as_ref().unwrap();
                    let idx_lit = syn::Index::from(idx);

                    match &field.ty {
                        syn::Type::Path(typ) => {
                            let last_segment = typ.path.segments.last().unwrap();
                            match last_segment.ident.to_string().as_str() {
                                "Option" => {
                                    if let syn::PathArguments::AngleBracketed(args) =
                                        &last_segment.arguments
                                    {
                                        if let Some(syn::GenericArgument::Type(inner_type)) =
                                            args.args.first()
                                        {
                                            field_assignments.push(quote! {
                                                self.#field_ident = results[#idx_lit].parse::<#inner_type>().ok();
                                            });
                                            field_types.push(quote! { Option<#inner_type> });
                                        }
                                    }
                                }
                                _ => {
                                    let ty = &field.ty;
                                    field_assignments.push(quote! {
                                        self.#field_ident = results[#idx_lit].parse::<#ty>()?;
                                    });
                                    field_types.push(quote! { #ty });
                                }
                            }
                        }
                        ty => {
                            return Err(Error::new(
                                ty.span(),
                                format!(
                                    "Support only Path for field type but got {}",
                                    ty.to_token_stream(),
                                ),
                            ))
                        }
                    }
                }
                _ => (),
            }
        }
    }

    let name = &ast.ident;
    let combined_states = quote! {
        vec![
            #(#text_editor_states),*
        ]
    };

    Ok(quote! {
        impl #name {
            pub async fn build(&mut self) -> Result<(), Box<dyn std::error::Error>> {
                use promkit::Prompt;

                let states = #combined_states;
                let mut form = promkit::preset::form::Form::new(states);
                let results = form.run().await?;

                #(#field_assignments)*

                Ok(())
            }
        }
    })
}