notionrs_macro 0.2.0

Macro for generating Notion API client
Documentation
use quote::quote;
use syn::{Data, DeriveInput, Expr, Fields, Lit, Meta, MetaNameValue, Type};

pub fn generate_setters(input: DeriveInput) -> proc_macro::TokenStream {
    let struct_name = input.ident;

    let fields = match input.data {
        Data::Struct(data) => match data.fields {
            Fields::Named(fields) => fields.named.into_iter().collect::<Vec<_>>(),
            _ => vec![],
        },
        _ => panic!("Setter can only be used with structs"),
    };

    let field_setters = fields.iter().map(|f| {
        let field_name = &f.ident;
        let field_ty = &f.ty;
        let attribute = &f.attrs;

        let comment = generate_comment(f);

        if is_skip(attribute) {
            quote! {}
        } else if is_string_type(field_ty) {
            quote! {
                #comment
                pub fn #field_name<T>(mut self, #field_name: T) -> Self
                where
                    T: AsRef<str>,
                {
                    self.#field_name = #field_name.as_ref().to_string();
                    self
                }
            }
        } else if is_option_type(field_ty) {
            let inner_ty = if let Type::Path(type_path) = field_ty {
                if let Some(first_segment) = type_path.path.segments.first() {
                    if let syn::PathArguments::AngleBracketed(args) = &first_segment.arguments {
                        if let syn::GenericArgument::Type(ty) = args.args.first().unwrap() {
                            ty
                        } else {
                            panic!("Option type must have a generic argument");
                        }
                    } else {
                        panic!("Option type must have a generic argument");
                    }
                } else {
                    panic!("Option type must have a generic argument");
                }
            } else {
                panic!("Option type must have a generic argument");
            };

            if is_string_type(inner_ty) {
                quote! {
                    #comment
                    pub fn #field_name<T>(mut self, #field_name: T) -> Self
                    where
                        T: AsRef<str>,{
                        self.#field_name = Some(#field_name.as_ref().to_string());
                        self
                    }
                }
            } else {
                quote! {
                    #comment
                    pub fn #field_name(mut self, #field_name: #inner_ty) -> Self {
                        self.#field_name = Some(#field_name);
                        self
                    }
                }
            }
        } else {
            quote! {
                #comment
                pub fn #field_name(mut self, #field_name: #field_ty) -> Self {
                    self.#field_name = #field_name;
                    self
                }
            }
        }
    });

    let expanded = quote! {
        impl #struct_name {
            #(#field_setters)*
        }
    };

    proc_macro::TokenStream::from(expanded)
}

fn generate_comment(f: &syn::Field) -> proc_macro2::TokenStream {
    let field_name = &f.ident;
    let setter_comment = format!(
        "Set the value of the `{}` field.",
        field_name.as_ref().unwrap()
    );

    let field_original_comment = f.attrs.iter().filter_map(|attr| {
        if attr.path().is_ident("doc") {
            if let Meta::NameValue(MetaNameValue {
                path: _,
                eq_token: _,
                value: Expr::Lit(comment),
            }) = &attr.meta
            {
                if let Lit::Str(comment) = &comment.lit {
                    let comment = comment.value();
                    return Some(quote! {
                        #[doc = #comment]
                    });
                }
            }

            return None;
        }
        None
    });

    let comment = quote! {
        #[doc = #setter_comment]
        #[doc = ""]
        #[doc = "---"]
        #[doc = ""]
        #(#field_original_comment)*
    };

    comment
}

fn is_option_type(ty: &Type) -> bool {
    if let Type::Path(type_path) = ty {
        if let Some(first_segment) = type_path.path.segments.first() {
            return first_segment.ident == "Option";
        }
    }
    false
}

fn is_string_type(ty: &Type) -> bool {
    if let Type::Path(type_path) = ty {
        if let Some(segment) = type_path.path.segments.first() {
            return segment.ident == "String";
        }
    }
    false
}

fn is_skip(attrs: &Vec<syn::Attribute>) -> bool {
    let flag = attrs.iter().any(|attr| attr.path().is_ident("skip"));
    flag
}