rust-utils-macros 0.1.0

Procedural macros for the rust-utils crate
Documentation
use super::{
    some_or_compile_err,
    ok_or_compile_err,
    quote_compile_err,
    gen_doc_attrs,
    ChainableOptions
};
use proc_macro2::TokenStream as TokenStream2;
use syn::{
    parse::Parser,
    parse_quote,
    punctuated::Punctuated,
    Attribute, Expr,
    FnArg, Ident,
    ImplItemFn,
    ItemStruct,
    Lit, Meta,
    Token, Type
};
use quote::{
    format_ident,
    quote, ToTokens
};
use if_chain::if_chain;

#[derive(Clone, Default)]
struct GetterOptions {
    // create a getter method?
    enabled: bool,

    // create a mutable getter method?
    enabled_mut: bool,

    // copy the value instead of return a reference
    copy_val: bool,

    // doc attributes
    doc_attrs: Vec<Attribute>
}

#[derive(Clone, Default)]
struct SetterOptions {
    // create a getter method?
    enabled: bool,

    // use an Into impl for the input type?
    use_into_impl: bool,

    // doc attributes
    doc_attrs: Vec<Attribute>
}

#[derive(Clone)]
struct EncapsulatedField {
    // the field name
    name: Ident,

    // the field type
    field_type: Type,

    // options for generating a getter method
    getter_opts: GetterOptions,

    // options for generating a setter method
    setter_opts: SetterOptions,

    // create a chainable method?
    chainable_opts: Option<ChainableOptions>
}

impl EncapsulatedField {
    fn new(name: Ident, field_type: Type) -> Self {
        Self {
            name,
            field_type,
            setter_opts: SetterOptions::default(),
            getter_opts: GetterOptions::default(),
            chainable_opts: None
        }
    }
}

pub fn encapsulated_struct(mut struct_def: ItemStruct) -> TokenStream2 {
    let mut fields = Vec::new();
    let mut methods: Vec<ImplItemFn> = Vec::new();
    let vis = &struct_def.vis;
    let type_name = &struct_def.ident;
    let (impl_generics, ty_generics, where_clause) = struct_def.generics.split_for_impl();

    for field in &mut struct_def.fields {
        let name = some_or_compile_err!(field.ident.clone(), "Tuple structs are not supported");
        let mut enc_field = EncapsulatedField::new(name, field.ty.clone());
        let mut new_attrs = Vec::new();
        let mut setter_doc = String::new();

        for attr in field.attrs.drain(..) {
            let mut my_attr = false;

            if let Meta::Path(path) = &attr.meta {
                let name = ok_or_compile_err!(path.require_ident());

                if name == "getter" {
                    my_attr = true;
                    enc_field.getter_opts.enabled = true;
                }
                else if name == "setter" {
                    my_attr = true;
                    enc_field.setter_opts.enabled = true;
                }
                else if name == "chainable" {
                    my_attr = true;
                    let mut chainable_opts = ok_or_compile_err!(ChainableOptions::parse(true, TokenStream2::new()));
                    if chainable_opts.doc_attrs.is_empty() {
                        setter_doc.push_str("\n\nChainable variant");
                        chainable_opts.doc_attrs = gen_doc_attrs(&setter_doc);
                    }

                    enc_field.chainable_opts = Some(chainable_opts);
                }
            }
            else if let Meta::List(list) = &attr.meta {
                let name = ok_or_compile_err!(list.path.require_ident());

                if name == "getter" {
                    enc_field.getter_opts.enabled = true;
                    my_attr = true;

                    let options = ok_or_compile_err!(<Punctuated<Meta, Token![,]>>::parse_terminated.parse2(list.tokens.clone()));

                    for option in options {
                        if let Meta::Path(path) = &option {
                            let name = ok_or_compile_err!(path.require_ident());
                            if name == "mutable" {
                                enc_field.getter_opts.enabled_mut = true;
                            }
                            else if name == "copy_val" {
                                enc_field.getter_opts.copy_val = true;
                            }
                            else {
                                return quote_compile_err!(
                                    "Invalid option! Specify \"mutable\" to generate \
                                    a mutable getter method, \"copy_val\" to return \
                                    the value instead of a reference, or \"doc\" to generate documentation"
                                )
                            }
                        }
                        else if let Meta::NameValue(meta_val) = &option {
                            let name = ok_or_compile_err!(meta_val.path.require_ident());

                            if name == "doc" {
                                if_chain! {
                                    if let Expr::Lit(literal) = &meta_val.value;
                                    if let Lit::Str(str_val) = &literal.lit;

                                    then {
                                        enc_field.getter_opts.doc_attrs = gen_doc_attrs(str_val.value());
                                    }
                                    else {
                                        return quote_compile_err!("Invalid input!");
                                    }
                                }
                            }
                            else {
                                return quote_compile_err!(
                                    "Invalid option! Specify \"mutable\" to generate \
                                    a mutable getter method, \"copy_val\" to return \
                                    the value instead of a reference, or \"doc\" to \
                                    generate documentation"
                                )
                            }
                        }
                        else {
                            return quote_compile_err!("Invalid input!");
                        }
                    }
                }
                else if name == "setter" {
                    my_attr = true;
                    enc_field.setter_opts.enabled = true;

                    let options = ok_or_compile_err!(<Punctuated<Meta, Token![,]>>::parse_terminated.parse2(list.tokens.clone()));

                    for option in options {
                        if let Meta::Path(path) = &option {
                            let name = ok_or_compile_err!(path.require_ident());
                            if name == "use_into_impl" {
                                enc_field.setter_opts.use_into_impl = true;
                            }
                            else {
                                return quote_compile_err!(
                                    "Invalid option! Specify \"use_into_impl\" \
                                    to generate an Into implementation for the \
                                    setter method"
                                );
                            }
                        }
                        else if let Meta::NameValue(meta_val) = &option {
                            let name = ok_or_compile_err!(meta_val.path.require_ident());

                            if name == "doc" {
                                if_chain! {
                                    if let Expr::Lit(literal) = &meta_val.value;
                                    if let Lit::Str(str_val) = &literal.lit;

                                    then {
                                        setter_doc = str_val.value();
                                        enc_field.setter_opts.doc_attrs = gen_doc_attrs(str_val.value());
                                    }
                                    else {
                                        return quote_compile_err!("Invalid input!");
                                    }
                                }
                            }
                            else {
                                return quote_compile_err!(
                                    "Invalid option! Specify \
                                    \"use_into_impl\" to generate \
                                    an Into implementation for \
                                    the setter method or \"doc\" to \
                                    generate documentation"
                                )
                            }
                        }
                    }
                }
                else if name == "chainable" {
                    my_attr = true;
                    let mut chainable_opts = ok_or_compile_err!(ChainableOptions::parse(true, list.tokens.clone()));
                    if chainable_opts.doc_attrs.is_empty() {
                        setter_doc.push_str("\n\nChainable variant");
                        chainable_opts.doc_attrs = gen_doc_attrs(&setter_doc);
                    }

                    enc_field.chainable_opts = Some(chainable_opts);
                
                }
            }

            if !my_attr {
                new_attrs.push(attr);
            }
        }

        field.attrs = new_attrs;
        fields.push(enc_field);
    }

    for field in &fields {
        let name = &field.name;
        let field_type = &field.field_type;

        if field.setter_opts.enabled {
            let fn_name = format_ident!("set_{name}");

            let in_arg: FnArg = if field.setter_opts.use_into_impl {
                parse_quote!(#name:impl core::convert::Into<#field_type>)
            }
            else {
                parse_quote!(#name:#field_type)
            };

            let arg = if field.setter_opts.use_into_impl {
                parse_quote!(#name.into())
            }
            else {
                name.to_token_stream()
            };

            let setter_doc_attrs = &field.setter_opts.doc_attrs;
            methods.push(
                parse_quote! {
                    #(#setter_doc_attrs)*
                    #vis fn #fn_name(&mut self, #in_arg) {
                        self.#name = #arg;
                    }
                }
            );
        }

        if field.getter_opts.enabled {
            let fn_name = format_ident!("get_{name}");
            let getter_doc_attrs = &field.getter_opts.doc_attrs;

            let ret_type: Type = if field.getter_opts.copy_val {
                parse_quote!(#field_type)
            }
            else {
                parse_quote!(&#field_type)
            };

            let ret_stmt: TokenStream2 = if field.getter_opts.copy_val {
                parse_quote!(self.#name)
            }
            else {
                parse_quote!(&self.#name)
            };

            methods.push(
                parse_quote! {
                    #(#getter_doc_attrs)*
                    #vis fn #fn_name(&self) -> #ret_type {
                        #ret_stmt
                    }
                }
            );
        }

        if field.getter_opts.enabled_mut {
            let fn_name = format_ident!("get_{name}_mut");
            let getter_doc_attrs = &field.getter_opts.doc_attrs;

            methods.push(
                parse_quote! {
                    #(#getter_doc_attrs)*
                    #vis fn #fn_name(&mut self) -> &mut #field_type {
                        &mut self.#name
                    }
                }
            );
        }

        if let Some(chainable_opts) = &field.chainable_opts {
            methods.push(chainable_opts.gen_method_from_field(name, field_type, vis))
        }
    }

    quote! {
        #struct_def

        impl #impl_generics #type_name #ty_generics #where_clause {
            #(#methods)*
        }
    }
}