a3s-boot-macros 0.1.2

Attribute macros for a3s-boot
Documentation
use quote::quote;
use syn::{Attribute, Ident, LitStr, Result, Type};

#[derive(Clone)]
pub(crate) struct UploadedFileExtractor {
    pub(crate) name: LitStr,
}

pub(crate) struct UploadOpenApiField {
    pub(crate) name: LitStr,
    pub(crate) multiple: bool,
    pub(crate) required: bool,
}

pub(crate) fn parse_uploaded_file_extractor(
    attr: &Attribute,
    name: &str,
) -> Result<UploadedFileExtractor> {
    attr.parse_args::<LitStr>()
        .map(|name| UploadedFileExtractor { name })
        .map_err(|_| {
            syn::Error::new_spanned(
                attr,
                format!("#[{name}] requires a multipart field-name string literal"),
            )
        })
}

pub(crate) fn uploaded_file_extractor_tokens(
    ident: Ident,
    ty: Box<Type>,
    spec: UploadedFileExtractor,
    optional: bool,
) -> proc_macro2::TokenStream {
    let name = spec.name;
    if optional {
        quote! {
            let #ident: #ty = __a3s_boot_multipart_form.file(#name).cloned();
        }
    } else {
        quote! {
            let #ident: #ty = __a3s_boot_multipart_form
                .file(#name)
                .cloned()
                .ok_or_else(|| ::a3s_boot::BootError::BadRequest(
                    format!("missing uploaded file: {}", #name)
                ))?;
        }
    }
}

pub(crate) fn uploaded_files_extractor_tokens(
    ident: Ident,
    ty: Box<Type>,
    spec: UploadedFileExtractor,
) -> proc_macro2::TokenStream {
    let name = spec.name;
    quote! {
        let #ident: #ty = __a3s_boot_multipart_form
            .files_by_name(#name)
            .into_iter()
            .cloned()
            .collect();
    }
}

pub(crate) fn multipart_openapi_token(
    fields: Vec<UploadOpenApiField>,
) -> Option<proc_macro2::TokenStream> {
    if fields.is_empty() {
        return None;
    }

    let mut properties = Vec::new();
    let mut required = Vec::new();

    for field in fields {
        let name = field.name;
        if field.multiple {
            properties.push(quote! {
                (
                    #name,
                    ::a3s_boot::OpenApiSchema::array(
                        ::a3s_boot::OpenApiSchema::binary_file()
                    )
                )
            });
        } else {
            properties.push(quote! {
                (#name, ::a3s_boot::OpenApiSchema::binary_file())
            });
        }

        if field.required {
            required.push(quote!(#name));
        }
    }

    let required = if required.is_empty() {
        quote!(::std::iter::empty::<&str>())
    } else {
        quote!([#(#required),*])
    };

    Some(quote! {
        with_request_body_content_type(
            "multipart/form-data",
            ::a3s_boot::OpenApiSchema::object_with_properties(
                [#(#properties),*],
                #required,
            )
        )
    })
}