rkt_codegen 0.6.0

Procedural macros for the rkt web framework.
Documentation
use devise::{
    ext::{SpanDiagnosticExt, TypeExt},
    *,
};
use proc_macro2::TokenStream;
use quote::ToTokens;

use crate::exports::*;
use crate::http_codegen::{ContentType, Status};
use crate::syn_ext::{GenericsExt as _, TypeExt as _};

#[derive(Debug, Default, FromMeta)]
struct ItemAttr {
    content_type: Option<SpanWrapped<ContentType>>,
    status: Option<SpanWrapped<Status>>,
}

#[derive(Default, FromMeta)]
struct FieldAttr {
    ignore: bool,
}

pub fn derive_responder(input: proc_macro::TokenStream) -> TokenStream {
    let impl_tokens = quote!(impl<'r, 'o: 'r> #_response::Responder<'r, 'o>);
    DeriveGenerator::build_for(input, impl_tokens)
        .support(Support::Struct | Support::Enum | Support::Lifetime | Support::Type)
        .replace_generic(1, 0)
        .type_bound_mapper(
            MapperBuild::new()
                .try_enum_map(|m, e| mapper::enum_null(m, e))
                .try_fields_map(|_, fields| {
                    let generic_idents = fields.parent.input().generics().type_idents();
                    let lifetime = |ty: &syn::Type| syn::Lifetime::new("'o", ty.span());
                    let mut types = fields
                        .iter()
                        .map(|f| (f, &f.field.inner.ty))
                        .map(|(f, ty)| (f, ty.with_replaced_lifetimes(lifetime(ty))));

                    let mut bounds = vec![];
                    if let Some((_, ty)) = types.next() {
                        if !ty.is_concrete(&generic_idents) {
                            let span = ty.span();
                            bounds.push(quote_spanned!(span => #ty: #_response::Responder<'r, 'o>));
                        }
                    }

                    for (f, ty) in types {
                        let attr =
                            FieldAttr::one_from_attrs("response", &f.attrs)?.unwrap_or_default();
                        if ty.is_concrete(&generic_idents) || attr.ignore {
                            continue;
                        }

                        bounds.push(quote_spanned! { ty.span() =>
                            #ty: ::std::convert::Into<#_http::Header<'o>>
                        });
                    }

                    Ok(quote!(#(#bounds,)*))
                }),
        )
        .validator(
            ValidatorBuild::new()
                .input_validate(|_, i| match i.generics().lifetimes().count() > 1 {
                    true => Err(i.generics().span().error("only one lifetime is supported")),
                    false => Ok(()),
                })
                .fields_validate(|_, fields| match fields.is_empty() {
                    true => Err(fields.span().error("need at least one field")),
                    false => Ok(()),
                }),
        )
        .inner_mapper(
            MapperBuild::new()
                .with_output(|_, output| {
                    quote! {
                        fn respond_to(self, __req: &'r #Request<'_>) -> #_response::Result<'o> {
                            #output
                        }
                    }
                })
                .try_fields_map(|_, fields| {
                    fn set_header_tokens<T: ToTokens + Spanned>(item: T) -> TokenStream {
                        quote_spanned!(item.span() => __res.set_header(#item);)
                    }

                    let attr = ItemAttr::one_from_attrs("response", fields.parent.attrs())?
                        .unwrap_or_default();

                    let responder = fields
                        .iter()
                        .next()
                        .map(|f| {
                            let (accessor, ty) = (f.accessor(), f.ty.with_stripped_lifetimes());
                            quote_spanned! { f.span() =>
                                let mut __res = <#ty as #_response::Responder>::respond_to(
                                    #accessor, __req
                                )?;
                            }
                        })
                        .expect("have at least one field");

                    let mut headers = vec![];
                    for field in fields.iter().skip(1) {
                        let attr = FieldAttr::one_from_attrs("response", &field.attrs)?
                            .unwrap_or_default();

                        if !attr.ignore {
                            headers.push(set_header_tokens(field.accessor()));
                        }
                    }

                    let content_type = attr.content_type.map(set_header_tokens);
                    let status = attr
                        .status
                        .map(|status| quote_spanned!(status.span() => __res.set_status(#status);));

                    Ok(quote! {
                        #responder
                        #(#headers)*
                        #content_type
                        #status
                        #_Ok(__res)
                    })
                }),
        )
        .to_tokens()
}