telety-impl 0.4.0

Common code for telety. Not intended for public use.
Documentation
use quote::{ToTokens, quote};
use syn::{
    Attribute, Expr, ExprLit, Ident, Lit, MetaNameValue, Path, Token, Visibility, parse::Parse,
    parse_quote, parse2, punctuated::Punctuated, spanned::Spanned as _,
};

use crate::visitor;

pub struct Options {
    pub module_path: Path,
    pub telety_path: Option<Path>,
    pub macro_ident: Option<Ident>,
    pub visibility: Option<Visibility>,
    pub proxy: Option<Path>,
    pub alias_traits: Option<bool>,
}

impl Options {
    pub fn from_attrs(attrs: &[Attribute]) -> syn::Result<Self> {
        let mut args = None;
        for attr in attrs {
            if attr.path().is_ident("telety") {
                #[allow(clippy::collapsible_if, reason = "separate mutating if for clarity")]
                if args
                    .replace(parse2(attr.meta.require_list()?.tokens.clone())?)
                    .is_some()
                {
                    return Err(syn::Error::new(
                        attr.span(),
                        "Only one 'telety' attribute is allowed",
                    ));
                }
            }
        }

        args.ok_or_else(|| {
            syn::Error::new(
                attrs.first().span(),
                "'telety' attribute not found (aliasing the attribute is not supported)",
            )
        })
    }

    pub fn converted_containing_path(&self) -> Path {
        let mut containing_path = self.module_path.clone();
        directed_visit::visit_mut(
            &mut directed_visit::syn::direct::FullDefault,
            &mut visitor::Crateify::new(),
            &mut containing_path,
        );

        containing_path
    }

    pub fn telety_path(&self) -> Path {
        self.telety_path
            .clone()
            .unwrap_or_else(|| parse_quote!(::telety))
    }
}

impl Parse for Options {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        let mut module_path: Path = input.parse()?;
        directed_visit::visit_mut(
            &mut directed_visit::syn::direct::FullDefault,
            &mut visitor::Decrateify::new(),
            &mut module_path,
        );

        let mut telety_path = None;
        let mut macro_ident = None;
        let mut visibility = None;
        let mut proxy = None;
        let mut alias_traits = None;

        if let Some(_comma) = input.parse::<Option<Token![,]>>()? {
            let named_args: Punctuated<MetaNameValue, Token![,]> =
                Punctuated::parse_terminated(input)?;
            for named_arg in named_args {
                if let Some(ident) = named_arg.path.get_ident() {
                    let Expr::Lit(ExprLit {
                        lit: Lit::Str(value),
                        ..
                    }) = &named_arg.value
                    else {
                        return Err(syn::Error::new(
                            named_arg.value.span(),
                            "Expected a string literal",
                        ));
                    };

                    if ident == "telety_path" {
                        telety_path = Some(value.parse()?);
                    } else if ident == "macro_ident" {
                        macro_ident = Some(value.parse()?);
                    } else if ident == "visibility" {
                        visibility = Some(value.parse()?);
                    } else if ident == "proxy" {
                        proxy = Some(value.parse()?);
                    } else if ident == "alias_traits" {
                        if value.value() == "always" {
                            alias_traits = Some(true);
                        } else if value.value() == "never" {
                            alias_traits = Some(false);
                        } else {
                            return Err(syn::Error::new(
                                value.span(),
                                "Expected \"always\" or \"never\"",
                            ));
                        }
                    } else {
                        return Err(syn::Error::new(
                            named_arg.path.span(),
                            "Invalid parameter name",
                        ));
                    }
                } else {
                    return Err(syn::Error::new(
                        named_arg.path.span(),
                        "Expected a parameter name",
                    ));
                }
            }
        }

        Ok(Self {
            module_path,
            telety_path,
            macro_ident,
            visibility,
            proxy,
            alias_traits,
        })
    }
}

impl ToTokens for Options {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        let Self {
            module_path,
            telety_path,
            macro_ident,
            visibility,
            proxy,
            alias_traits,
        } = self;

        // Convert to string literals
        let telety_path = telety_path
            .as_ref()
            .map(ToTokens::to_token_stream)
            .as_ref()
            .map(ToString::to_string)
            .into_iter();
        let macro_ident = macro_ident
            .as_ref()
            .map(ToTokens::to_token_stream)
            .as_ref()
            .map(ToString::to_string)
            .into_iter();
        let visibility = visibility
            .as_ref()
            .map(ToTokens::to_token_stream)
            .as_ref()
            .map(ToString::to_string)
            .into_iter();
        let proxy = proxy
            .as_ref()
            .map(ToTokens::to_token_stream)
            .as_ref()
            .map(ToString::to_string)
            .into_iter();
        let alias_traits = alias_traits
            .as_ref()
            .map(|always| if *always { "always" } else { "never" })
            .into_iter();

        quote!(
            #module_path
            #(, telety_path = #telety_path)*
            #(, macro_ident = #macro_ident)*
            #(, visibility = #visibility)*
            #(, proxy = #proxy)*
            #(, alias_traits = #alias_traits)*
        )
        .to_tokens(tokens);
    }
}