predawn-macro 0.9.0

Macros for predawn
Documentation
use from_attr::{AttrsValue, FromAttr, FromIdent};
use http::HeaderName;
use proc_macro2::TokenStream;
use quote::quote;
use quote_use::quote_use;
use syn::{parse_quote, spanned::Spanned, Attribute, DeriveInput, Ident, LitStr};

use crate::util;

#[derive(FromAttr)]
#[attribute(idents = [api_key])]
struct ApiKeyAttr {
    rename: Option<String>,
    #[attribute(rename = "in")]
    location: Location,
    name: LitStr,
}

#[derive(FromIdent)]
enum Location {
    Query,
    Header,
    Cookie,
}

impl Location {
    fn as_ident(&self) -> Ident {
        match self {
            Location::Query => parse_quote!(Query),
            Location::Header => parse_quote!(Header),
            Location::Cookie => parse_quote!(Cookie),
        }
    }
}

#[derive(FromAttr)]
#[attribute(idents = [http])]
struct HttpAttr {
    rename: Option<String>,
    scheme: AuthScheme,
    bearer_format: Option<String>,
}

#[derive(FromIdent)]
enum AuthScheme {
    Basic,
    Bearer,
    Digest,
    Dpop,
    Gnap,
    Hoba,
    Mutual,
}

impl AuthScheme {
    fn as_str(&self) -> &'static str {
        match self {
            AuthScheme::Basic => "Basic",
            AuthScheme::Bearer => "Bearer",
            AuthScheme::Digest => "Digest",
            AuthScheme::Dpop => "DPoP",
            AuthScheme::Gnap => "GNAP",
            AuthScheme::Hoba => "HOBA",
            AuthScheme::Mutual => "Mutual",
        }
    }
}

pub(crate) fn generate(input: DeriveInput) -> syn::Result<TokenStream> {
    let DeriveInput {
        attrs: ty_attrs,
        ident,
        ..
    } = input;

    let mut path_and_expand = None;
    let mut errors = Vec::new();

    macro_rules! expand_scheme {
        ($ty:ty, $ident:ident) => {
            match <$ty as FromAttr>::from_attributes(&ty_attrs) {
                Ok(Some(AttrsValue {
                    attrs,
                    value: api_key_attr,
                })) => {
                    match &path_and_expand {
                        Some((path, _)) => {
                            let msg = format!("already defined as {:?}", path);

                            attrs.iter().for_each(|attr| {
                                errors.push(syn::Error::new(attr.span(), &msg));
                            });
                        }
                        None => match $ident(&ty_attrs, &ident, api_key_attr) {
                            Ok(expand) => {
                                path_and_expand = Some((attrs.first().unwrap().path(), expand));
                            }
                            Err(e) => errors.push(e),
                        },
                    };
                }
                Ok(None) => {}
                Err(AttrsValue { value: e, .. }) => errors.push(e),
            }
        };
    }

    expand_scheme!(ApiKeyAttr, generate_api_key);
    expand_scheme!(HttpAttr, generate_http);

    if path_and_expand.is_none() {
        errors.push(syn::Error::new(
            ident.span(),
            "missing `#[api_key(..)]` or `#[http(..)]` attribute",
        ));
    }

    if let Some(e) = errors.into_iter().reduce(|mut a, b| {
        a.combine(b);
        a
    }) {
        return Err(e);
    }

    let (_, expand) = path_and_expand.unwrap();

    Ok(expand)
}

fn generate_api_key(
    attrs: &[Attribute],
    ident: &Ident,
    api_key: ApiKeyAttr,
) -> syn::Result<TokenStream> {
    let ApiKeyAttr {
        rename,
        location,
        name,
    } = api_key;

    let name = match location {
        Location::Header => {
            let name_str = name.value();

            match HeaderName::from_bytes(name_str.as_bytes()) {
                Ok(header_name) => header_name.as_str().to_string(),
                Err(e) => return Err(syn::Error::new(name.span(), e)),
            }
        }
        _ => name.value(),
    };

    let ident_str = rename.unwrap_or_else(|| ident.to_string());

    let location = location.as_ident();

    let description = util::extract_description(attrs);
    let description = if description.is_empty() {
        quote! { None }
    } else {
        let description = util::generate_string_expr(&description);
        quote! { Some(#description) }
    };

    let expand = quote_use! {
        # use core::default::Default;
        # use std::string::ToString;
        # use predawn::SecurityScheme;
        # use predawn::openapi::{self, APIKeyLocation};

        impl SecurityScheme for #ident {
            const NAME: &'static str = #ident_str;

            fn create() -> openapi::SecurityScheme {
                openapi::SecurityScheme::APIKey {
                    location: APIKeyLocation::#location,
                    name: ToString::to_string(#name),
                    description: #description,
                    extensions: Default::default(),
                }
            }
        }
    };

    Ok(expand)
}

fn generate_http(attrs: &[Attribute], ident: &Ident, http: HttpAttr) -> syn::Result<TokenStream> {
    let HttpAttr {
        rename,
        scheme,
        bearer_format,
    } = http;

    let ident_str = rename.unwrap_or_else(|| ident.to_string());

    let scheme = scheme.as_str();

    let bearer_format = match bearer_format {
        Some(f) if !f.is_empty() => {
            quote!(::core::option::Option::Some(::std::string::ToString::to_string(#f)))
        }
        _ => quote!(::core::option::Option::None),
    };

    let description = util::extract_description(attrs);
    let description = if description.is_empty() {
        quote! { None }
    } else {
        let description = util::generate_string_expr(&description);
        quote! { Some(#description) }
    };

    let expand = quote_use! {
        # use core::default::Default;
        # use std::string::ToString;
        # use predawn::SecurityScheme;
        # use predawn::openapi::{self, APIKeyLocation};

        impl SecurityScheme for #ident {
            const NAME: &'static str = #ident_str;

            fn create() -> openapi::SecurityScheme {
                openapi::SecurityScheme::HTTP {
                    scheme: ToString::to_string(#scheme),
                    bearer_format: #bearer_format,
                    description: #description,
                    extensions: Default::default(),
                }
            }
        }
    };

    Ok(expand)
}