a3s-boot-macros 0.1.2

Attribute macros for a3s-boot
Documentation
use syn::parse::ParseStream;
use syn::{Attribute, GenericArgument, Ident, PathArguments, Result, Token, Type};

pub(crate) fn set_once<T>(slot: &mut Option<T>, value: T, name: Ident) -> Result<()> {
    if slot.is_some() {
        let message = format!("duplicate `{name}` option");
        return Err(syn::Error::new_spanned(&name, message));
    }
    *slot = Some(value);
    Ok(())
}

pub(crate) fn parse_optional_comma(input: ParseStream<'_>) -> Result<()> {
    if input.is_empty() {
        return Ok(());
    }
    input.parse::<Token![,]>()?;
    Ok(())
}

pub(crate) fn push_error(slot: &mut Option<syn::Error>, error: syn::Error) {
    if let Some(existing) = slot {
        existing.combine(error);
    } else {
        *slot = Some(error);
    }
}

pub(crate) fn is_type_ident(ty: &Type, ident: &str) -> bool {
    let Type::Path(type_path) = ty else {
        return false;
    };
    type_path
        .path
        .segments
        .last()
        .is_some_and(|segment| segment.ident == ident)
}

pub(crate) fn expect_no_extractor_args(attr: &Attribute, name: &str) -> Result<()> {
    match &attr.meta {
        syn::Meta::Path(_) => Ok(()),
        _ => Err(syn::Error::new_spanned(
            attr,
            format!("#[{name}] does not accept arguments"),
        )),
    }
}

pub(crate) fn option_inner_type(ty: &Type) -> Option<Type> {
    let Type::Path(type_path) = ty else {
        return None;
    };
    if type_path.qself.is_some() {
        return None;
    }

    let segment = type_path.path.segments.last()?;
    if segment.ident != "Option" {
        return None;
    }

    let PathArguments::AngleBracketed(arguments) = &segment.arguments else {
        return None;
    };
    if arguments.args.len() != 1 {
        return None;
    }

    match arguments.args.first()? {
        GenericArgument::Type(inner) => Some(inner.clone()),
        _ => None,
    }
}