cgp-macro-lib 0.7.0

Context-generic programming core component macros implemented as a library.
Documentation
use std::collections::BTreeMap;

use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::token::{Comma, For};
use syn::{
    AngleBracketedGenericArguments, Error, GenericArgument, ItemImpl, ItemStruct, Path,
    PathArguments, Type, parse_quote,
};

use crate::derive_provider::replace_provider_in_generics;
use crate::parse::SimpleType;

pub fn derive_provider_struct(provider_impl: &ItemImpl) -> syn::Result<ItemStruct> {
    let impl_self_type = &provider_impl.self_ty;

    let provider_type: SimpleType = syn::parse2(quote!( #impl_self_type ))?;

    let provider_name = &provider_type.name;

    let provider_field = match &provider_type.generics {
        Some(generics) => {
            let args = &generics.args;
            quote! {
                #generics
                ( pub ::core::marker::PhantomData<( #args )>  )
            }
        }
        None => TokenStream::new(),
    };

    let provider_struct = syn::parse2(quote! {
        pub struct #provider_name #provider_field;
    })?;

    Ok(provider_struct)
}

pub fn derive_is_provider_for(
    component_name: &Type,
    provider_impl: &ItemImpl,
) -> syn::Result<ItemImpl> {
    let provider_path = provider_impl
        .trait_
        .as_ref()
        .ok_or_else(|| {
            Error::new(
                provider_impl.span(),
                "provider impl should contain trait path",
            )
        })?
        .1
        .segments
        .last()
        .ok_or_else(|| {
            Error::new(
                provider_impl.span(),
                "provider impl should contain trait path containing generic parameters",
            )
        })?;

    let provider_map = BTreeMap::from([(provider_path.ident.clone(), component_name.clone())]);

    let is_provider_generics: AngleBracketedGenericArguments = match &provider_path.arguments {
        PathArguments::AngleBracketed(generics) => {
            let mut generic_args = generics.clone().args.into_iter();

            let mut is_provider_params: Punctuated<GenericArgument, Comma> = Punctuated::default();

            let mut context_arg: Option<GenericArgument> = None;

            for arg in generic_args.by_ref() {
                if let GenericArgument::Lifetime(life) = arg {
                    // Lifetime params are forced to be pushed to the front of a provider trait.
                    // Skip those and put them in the form of `Life<'a>` inside the IsProviderFor params
                    is_provider_params.push(parse_quote! { Life<#life> })
                } else {
                    // Find the first non-lifetime context type argument and break
                    context_arg = Some(arg);
                    break;
                }
            }

            let context_arg = context_arg.ok_or_else(|| {
                Error::new(
                    provider_impl.span(),
                    "provider impl should contain trait path containing at least one generic parameter",
                )
            })?;

            is_provider_params.extend(generic_args);

            parse_quote!( < #component_name, #context_arg, ( #is_provider_params ) > )
        }
        _ => {
            return Err(Error::new(
                provider_impl.span(),
                "provider impl should contain trait path containing generic parameters",
            ));
        }
    };

    let is_provider_path: Path = parse_quote!( IsProviderFor #is_provider_generics );

    let mut is_provider_impl = provider_impl.clone();

    is_provider_impl.attrs.clear();
    is_provider_impl.items.clear();
    is_provider_impl.defaultness = None;
    is_provider_impl.unsafety = None;

    is_provider_impl.trait_ = Some((None, is_provider_path, For(Span::call_site())));

    replace_provider_in_generics(&provider_map, &mut is_provider_impl.generics);

    Ok(is_provider_impl)
}