a3s-boot-macros 0.1.2

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

pub(super) struct RouteArgs {
    pub(super) path: LitStr,
    pub(super) status: Option<LitInt>,
    pub(super) raw: Option<Ident>,
}

impl RouteArgs {
    pub(super) fn explicit_status<'a>(
        &'a self,
        http_code: Option<&'a LitInt>,
    ) -> Result<Option<&'a LitInt>> {
        match (&self.status, http_code) {
            (Some(_), Some(http_code)) => Err(syn::Error::new_spanned(
                http_code,
                "route status cannot be set with both `status = ...` and #[http_code(...)]",
            )),
            (Some(status), None) => Ok(Some(status)),
            (None, Some(http_code)) => Ok(Some(http_code)),
            (None, None) => Ok(None),
        }
    }
}

pub(super) fn status_value(status: Option<&LitInt>) -> Result<proc_macro2::TokenStream> {
    let Some(status) = status else {
        return Ok(quote!(200));
    };
    let value = status.base10_parse::<u16>()?;
    Ok(quote!(#value))
}

impl Parse for RouteArgs {
    fn parse(input: ParseStream<'_>) -> Result<Self> {
        let path = input.parse::<LitStr>()?;
        let mut status = None;
        let mut raw = None;

        if !input.is_empty() {
            while !input.is_empty() {
                input.parse::<Token![,]>()?;
                let name = input.parse::<Ident>()?;

                if name == "status" {
                    if status.is_some() {
                        return Err(syn::Error::new_spanned(name, "duplicate `status` option"));
                    }
                    input.parse::<Token![=]>()?;
                    status = Some(input.parse::<LitInt>()?);
                } else if name == "raw" {
                    if raw.is_some() {
                        return Err(syn::Error::new_spanned(name, "duplicate `raw` option"));
                    }
                    raw = Some(name);
                } else {
                    return Err(syn::Error::new_spanned(
                        name,
                        "expected `status = <u16>` or `raw`",
                    ));
                }
            }
        }

        if !input.is_empty() {
            return Err(input.error("unexpected route attribute arguments"));
        }

        Ok(Self { path, status, raw })
    }
}

pub(super) struct RouteSpec {
    pub(super) kind: RouteKind,
    pub(super) args: RouteArgs,
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub(super) enum RouteKind {
    All,
    Get,
    Sse,
    Post,
    Put,
    Patch,
    Delete,
    Options,
    Head,
    GetJson,
    PostJson,
    PutJson,
    PatchJson,
    DeleteJson,
}

impl RouteKind {
    pub(super) fn from_attribute(attr: &Attribute) -> Option<Self> {
        let ident = attr.path().segments.last()?.ident.to_string();
        match ident.as_str() {
            "all" => Some(Self::All),
            "get" => Some(Self::Get),
            "sse" => Some(Self::Sse),
            "post" => Some(Self::Post),
            "put" => Some(Self::Put),
            "patch" => Some(Self::Patch),
            "delete" => Some(Self::Delete),
            "options" => Some(Self::Options),
            "head" => Some(Self::Head),
            "get_json" => Some(Self::GetJson),
            "post_json" => Some(Self::PostJson),
            "put_json" => Some(Self::PutJson),
            "patch_json" => Some(Self::PatchJson),
            "delete_json" => Some(Self::DeleteJson),
            _ => None,
        }
    }

    pub(super) fn raw_builder_ident(self) -> Ident {
        match self {
            Self::All => format_ident!("all"),
            Self::Get => format_ident!("get"),
            Self::Sse => format_ident!("get"),
            Self::Post => format_ident!("post"),
            Self::Put => format_ident!("put"),
            Self::Patch => format_ident!("patch"),
            Self::Delete => format_ident!("delete"),
            Self::Options => format_ident!("options"),
            Self::Head => format_ident!("head"),
            Self::GetJson => format_ident!("get"),
            Self::PostJson => format_ident!("post"),
            Self::PutJson => format_ident!("put"),
            Self::PatchJson => format_ident!("patch"),
            Self::DeleteJson => format_ident!("delete"),
        }
    }

    pub(super) fn json_builder_ident(self) -> Option<Ident> {
        match self {
            Self::All => Some(format_ident!("all_json_with_status")),
            Self::Get | Self::GetJson => Some(format_ident!("get_json_with_status")),
            Self::Post | Self::PostJson => Some(format_ident!("post_json_with_status")),
            Self::Put | Self::PutJson => Some(format_ident!("put_json_with_status")),
            Self::Patch | Self::PatchJson => Some(format_ident!("patch_json_with_status")),
            Self::Delete | Self::DeleteJson => Some(format_ident!("delete_json_with_status")),
            Self::Sse | Self::Options | Self::Head => None,
        }
    }

    pub(super) fn is_explicit_json(self) -> bool {
        matches!(
            self,
            Self::GetJson | Self::PostJson | Self::PutJson | Self::PatchJson | Self::DeleteJson
        )
    }

    pub(super) fn flavor(self, raw: bool) -> RouteFlavor {
        if matches!(self, Self::Sse) {
            return RouteFlavor::Sse;
        }

        if raw {
            return RouteFlavor::Raw;
        }

        match self {
            Self::Sse => RouteFlavor::Sse,
            Self::All | Self::Get | Self::GetJson | Self::Delete | Self::DeleteJson => {
                RouteFlavor::JsonRequest
            }
            Self::Post
            | Self::PostJson
            | Self::Put
            | Self::PutJson
            | Self::Patch
            | Self::PatchJson => RouteFlavor::JsonBody,
            Self::Options | Self::Head => RouteFlavor::Raw,
        }
    }
}

#[derive(Clone, Copy)]
pub(super) enum RouteFlavor {
    Sse,
    Raw,
    JsonRequest,
    JsonBody,
}