a3s-boot-macros 0.1.2

Attribute macros for a3s-boot
Documentation
use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::{Attribute, LitStr, Result, Token};

use crate::expect_no_extractor_args;

pub(in crate::controller) fn take_controller_version_attrs(
    attrs: &[Attribute],
) -> (Vec<Attribute>, ControllerVersionAttrs, Vec<syn::Error>) {
    let mut clean_attrs = Vec::new();
    let mut version = ControllerVersionAttrs::default();
    let mut errors = Vec::new();

    for attr in attrs {
        let Some(kind) = VersionAttrKind::from_attribute(attr) else {
            clean_attrs.push(attr.clone());
            continue;
        };

        match VersionSpec::from_attribute(kind, attr) {
            Ok(spec) => {
                if version.spec.is_some() {
                    errors.push(syn::Error::new_spanned(
                        attr,
                        "controller impl blocks can use at most one version attribute",
                    ));
                } else {
                    version.spec = Some(spec);
                }
            }
            Err(error) => errors.push(error),
        }
    }

    (clean_attrs, version, errors)
}

pub(in crate::controller) fn take_route_version_attrs(
    attrs: &[Attribute],
) -> (Vec<Attribute>, Option<VersionSpec>, Vec<syn::Error>) {
    let mut clean_attrs = Vec::new();
    let mut spec = None;
    let mut errors = Vec::new();

    for attr in attrs {
        let Some(kind) = VersionAttrKind::from_attribute(attr) else {
            clean_attrs.push(attr.clone());
            continue;
        };

        match VersionSpec::from_attribute(kind, attr) {
            Ok(parsed) => {
                if spec.is_some() {
                    errors.push(syn::Error::new_spanned(
                        attr,
                        "route methods can use at most one version attribute",
                    ));
                } else {
                    spec = Some(parsed);
                }
            }
            Err(error) => errors.push(error),
        }
    }

    (clean_attrs, spec, errors)
}

#[derive(Default)]
pub(in crate::controller) struct ControllerVersionAttrs {
    spec: Option<VersionSpec>,
}

impl ControllerVersionAttrs {
    pub(in crate::controller) fn tokens(&self) -> Vec<proc_macro2::TokenStream> {
        self.spec.iter().map(VersionSpec::token).collect()
    }
}

#[derive(Clone)]
pub(in crate::controller) enum VersionSpec {
    Version(LitStr),
    Versions(Vec<LitStr>),
    Neutral,
}

impl VersionSpec {
    fn from_attribute(kind: VersionAttrKind, attr: &Attribute) -> Result<Self> {
        match kind {
            VersionAttrKind::Version => {
                attr.parse_args::<LitStr>().map(Self::Version).map_err(|_| {
                    syn::Error::new_spanned(attr, "#[version] requires one string literal argument")
                })
            }
            VersionAttrKind::Versions => {
                let values = attr.parse_args::<VersionList>().map_err(|_| {
                    syn::Error::new_spanned(
                        attr,
                        "#[versions] requires one or more string literal arguments",
                    )
                })?;
                if values.0.is_empty() {
                    Err(syn::Error::new_spanned(
                        attr,
                        "#[versions] requires one or more string literal arguments",
                    ))
                } else {
                    Ok(Self::Versions(values.0))
                }
            }
            VersionAttrKind::Neutral => {
                expect_no_extractor_args(attr, "version_neutral")?;
                Ok(Self::Neutral)
            }
        }
    }

    pub(in crate::controller) fn token(&self) -> proc_macro2::TokenStream {
        match self {
            Self::Version(version) => quote!(with_version(#version)),
            Self::Versions(versions) => quote!(with_versions([#(#versions),*])),
            Self::Neutral => quote!(version_neutral()),
        }
    }
}

#[derive(Clone, Copy)]
enum VersionAttrKind {
    Version,
    Versions,
    Neutral,
}

impl VersionAttrKind {
    fn from_attribute(attr: &Attribute) -> Option<Self> {
        let ident = attr.path().segments.last()?.ident.to_string();
        match ident.as_str() {
            "version" => Some(Self::Version),
            "versions" => Some(Self::Versions),
            "version_neutral" => Some(Self::Neutral),
            _ => None,
        }
    }
}

struct VersionList(Vec<LitStr>);

impl Parse for VersionList {
    fn parse(input: ParseStream<'_>) -> Result<Self> {
        let values = Punctuated::<LitStr, Token![,]>::parse_terminated(input)?
            .into_iter()
            .collect();
        Ok(Self(values))
    }
}