accessory 2.1.0

A configurable get/set/get_mut derive macro
Documentation
use macroific::elements::GenericImpl;
use macroific::prelude::*;
use proc_macro2::{Delimiter, Group, Ident, TokenStream};
use quote::{format_ident, quote, ToTokens, TokenStreamExt};
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::{Attribute, DeriveInput, Generics, Token, Type};

use options::*;
use parsed_field::*;

use crate::derive_accessors::final_options::{FinalOptions, Naming};

mod final_options;
pub mod options;
mod parsed_field;

const ATTR_NAME: &str = "access";

pub struct DeriveAccessors {
    fields: Vec<ParsedField>,
    container_opts: ContainerOptions,
    ident: Ident,
    generics: Generics,
}

impl ToTokens for DeriveAccessors {
    fn to_tokens(&self, _: &mut TokenStream) {
        unimplemented!("Use `into_token_stream`")
    }

    fn into_token_stream(self) -> TokenStream
    where
        Self: Sized,
    {
        macro_rules! variations {
            (
                [
                    $final_opts: ident,
                    $container_opts: ident,
                    $naming: ident,
                    $opts: ident,
                    $tokens: ident,
                    $ident: ident,
                    $comments: ident,
                    $ty: ident
                ] => $([$lower: ident $upper: ident $render: ident]),+
                | [$last_lower: ident $last_upper: ident $last_render: ident]
            ) => {
                $(
                    match $final_opts::new(
                        $container_opts.$lower,
                        &$container_opts.defaults.$lower,
                        &$naming::$upper,
                        $opts.$lower,
                        $opts.all.as_ref(),
                        &$container_opts.defaults.all,
                    ) {
                        Some(opts) if !opts.skip => {
                            let a = ::syn::parse_quote!(#[must_use]);
                            render_common(&mut $tokens, &$ident, &$comments, &opts, Some(a));
                            $tokens.extend($render(&$ident, &$ty, opts));
                        },
                        _ => {},
                    }
                )+

                match $final_opts::new(
                    $container_opts.$last_lower,
                    &$container_opts.defaults.$last_lower,
                    &$naming::$last_upper,
                    $opts.$last_lower,
                    $opts.all.as_ref(),
                    &$container_opts.defaults.all,
                ) {
                    Some(opts) if !opts.skip => {
                        render_common(&mut $tokens, &$ident, &$comments, &opts, None);
                        $tokens.extend($last_render($ident, $ty, opts));
                    },
                    _ => {},
                }
            };
        }

        let Self {
            fields,
            container_opts,
            ident,
            mut generics,
        } = self;

        let mut out = {
            if !container_opts.bounds.is_empty() {
                generics
                    .make_where_clause()
                    .predicates
                    .extend(container_opts.bounds);
            }

            let header = GenericImpl::new(generics).with_target(ident);
            quote! {
                #[automatically_derived]
                #[allow(clippy::all)]
                #header
            }
        };

        out.append(Group::new(Delimiter::Brace, {
            let mut tokens = TokenStream::new();

            for field in fields {
                let ParsedField {
                    comments,
                    opts,
                    ident,
                    ty,
                } = field;

                if opts.skip {
                    continue;
                }

                variations!(
                    [FinalOptions, container_opts, Naming, opts, tokens, ident, comments, ty] =>
                    [get GET RENDER_GET],
                    [get_mut GET_MUT RENDER_GET_MUT]
                    | [set SET RENDER_SET]
                );
            }

            tokens
        }));

        out
    }
}

#[allow(clippy::needless_pass_by_value)]
fn render_common(
    tokens: &mut TokenStream,
    ident: &Ident,
    comments: &[Attribute],
    opts: &FinalOptions,
    must_use: Option<Attribute>,
) {
    let vis = &opts.vis;
    tokens.extend(quote! {
        #(#comments)*
        #[inline]
        #must_use
        #vis
    });

    if opts.const_fn {
        tokens.append(Ident::create("const"));
    }

    tokens.append(Ident::create("fn"));

    tokens.append(match (&opts.prefix, &opts.suffix) {
        (Some(p), Some(s)) => format_ident!("{}{ident}{}", p.as_prefix(), s.as_suffix()),
        (Some(p), None) => format_ident!("{}{ident}", p.as_prefix()),
        (None, Some(s)) => format_ident!("{ident}{}", s.as_suffix()),
        (None, None) => ident.clone(),
    });
}

fn arg_ref(owned: bool) -> Option<Token![&]> {
    if owned {
        None
    } else {
        Some(<Token![&]>::default())
    }
}

fn mk_where<T, P>(bounds: Punctuated<T, P>) -> Option<TokenStream>
where
    Punctuated<T, P>: ToTokens,
{
    if bounds.is_empty() {
        None
    } else {
        Some(quote!(where #bounds))
    }
}

fn resolve_ptr_ty(base: &Type, opt: Option<DerefKind>) -> &Type {
    match (opt, base) {
        (Some(_), Type::Ptr(ptr)) => &ptr.elem,
        (_, ty) => ty,
    }
}

type RenderFieldFn = fn(&Ident, &Type, FinalOptions) -> TokenStream;
type LastRenderFieldFn = fn(Ident, Type, FinalOptions) -> TokenStream;

const RENDER_GET: RenderFieldFn = |ident, ty, opts| {
    let arg_ref = arg_ref(opts.owned);

    let val_ref = if opts.cp || opts.owned || opts.as_ref {
        None
    } else if let Some(deref) = opts.ptr_deref {
        let tokens = deref.try_into_tokens().unwrap_or_else(move || quote!(&));
        Some(tokens)
    } else {
        Some(quote!(&))
    };

    let fn_return = if let Some(ty) = opts.ty {
        ty.into_token_stream()
    } else {
        let ty = resolve_ptr_ty(ty, opts.ptr_deref);
        quote!(#val_ref #ty)
    };

    let where_clause = mk_where(opts.bounds);

    let body = if opts.ptr_deref.is_some() {
        quote!(unsafe { #val_ref *self.#ident })
    } else if opts.as_ref {
        quote!(self.#ident.as_ref())
    } else {
        quote!(#val_ref self.#ident)
    };

    quote!((#arg_ref self) -> #fn_return #where_clause { #body })
};

const RENDER_GET_MUT: RenderFieldFn = |ident, ty, opts| {
    let fn_return = if let Some(ty) = opts.ty {
        ty.into_token_stream()
    } else {
        let ty = resolve_ptr_ty(ty, opts.ptr_deref);
        quote!(&mut #ty)
    };

    let where_clause = mk_where(opts.bounds);

    let body = if opts.as_ref {
        quote!(self.#ident.as_mut())
    } else if opts.ptr_deref.is_some() {
        quote!(unsafe { &mut *self.#ident })
    } else {
        quote!(&mut self.#ident)
    };

    quote!((&mut self) -> #fn_return #where_clause { #body })
};

const RENDER_SET: LastRenderFieldFn = |ident, ty, opts| {
    let arg_ref = arg_ref(opts.owned);

    let self_ref = if opts.cp || opts.owned {
        None
    } else {
        Some(quote!(&mut))
    };

    let arg_ty = if let Some(ref ty) = opts.ty {
        ty
    } else {
        resolve_ptr_ty(&ty, opts.ptr_deref)
    };

    let where_clause = mk_where(opts.bounds);

    let assignment = if opts.ptr_deref.is_some() {
        quote! { unsafe { *self.#ident = new_value; } }
    } else {
        quote! { self.#ident = new_value }
    };

    quote! {
        (#arg_ref mut self, new_value: #arg_ty) -> #self_ref Self #where_clause {
            #assignment;
            self
        }
    }
};

impl Parse for DeriveAccessors {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let DeriveInput {
            attrs,
            ident,
            generics,
            data,
            ..
        } = input.parse()?;

        let container_opts = ContainerOptions::from_iter_named(ATTR_NAME, ident.span(), attrs)?;
        let fields = data
            .extract_struct_named()?
            .into_iter()
            .map(ParsedField::try_from);

        Ok(Self {
            fields: fields.collect::<syn::Result<_>>()?,
            container_opts,
            ident,
            generics,
        })
    }
}