sark-gen 0.13.0

Sark proc-macro generators
Documentation
use proc_macro2::{Ident, TokenStream};
use quote::{format_ident, quote};
use syn::{Error, GenericArgument, Index, LitStr, PathArguments, Type, TypePath, Visibility};

use super::plan::Meta;
use crate::{
    model::{AppDispatchInput, AppRouteInput},
    route_compiler::Method,
};

#[derive(Clone, Copy, PartialEq, Eq)]
pub(super) enum RouteKind {
    Sync,
    Fiber,
    Stream,
}

pub(super) struct Entry {
    pub(super) route: TypePath,
    pub(super) path: LitStr,
    pub(super) meta: Meta,
    pub(super) wraps: Vec<TypePath>,
    pub(super) kind: RouteKind,
    pub(super) capacity: Option<syn::Expr>,
}

pub(super) struct Gen {
    pub(super) vis: Visibility,
    pub(super) name: Ident,
    pub(super) state_ty: Type,
    pub(super) route_specs: Vec<Entry>,
    pub(super) routes: Vec<TypePath>,
    pub(super) idx: Vec<Index>,
    pub(super) route_bounds: Vec<TokenStream>,
}

impl Gen {
    pub(super) fn new(input: AppDispatchInput) -> syn::Result<Self> {
        let vis = input.vis;
        let name = input.name;
        let state_ty = normalize_state_lifetimes(input.state_ty);
        let route_specs: Vec<Entry> = input
            .routes
            .into_iter()
            .map(build_route_entry)
            .collect::<syn::Result<_>>()?;
        let routes: Vec<TypePath> = route_specs
            .iter()
            .map(|entry| entry.route.clone())
            .collect();
        let idx: Vec<Index> = (0..routes.len()).map(Index::from).collect();
        let route_bounds = build_route_bounds(&route_specs, &state_ty);

        Ok(Self {
            vis,
            name,
            state_ty,
            route_specs,
            routes,
            idx,
            route_bounds,
        })
    }
}

fn build_route_bounds(entries: &[Entry], state_ty: &Type) -> Vec<TokenStream> {
    entries
        .iter()
        .map(|entry| {
            let route = &entry.route;
            let (kind, invoke) = match entry.kind {
                RouteKind::Sync => (
                    quote!(sark::service::manifold::Sync),
                    quote!(sark::service::manifold::Route<#state_ty>),
                ),
                RouteKind::Fiber => (
                    quote!(sark::service::manifold::NativeFiber),
                    quote!(sark::service::manifold::TaskRoute<'d, #state_ty>),
                ),
                RouteKind::Stream => (
                    quote!(sark::service::manifold::NativeStream),
                    quote!(sark::service::manifold::Route<#state_ty>),
                ),
            };
            quote! {
                #route: sark::service::RouteSpec<Kind = #kind> + #invoke,
            }
        })
        .collect()
}

fn normalize_state_lifetimes(mut ty: Type) -> Type {
    use syn::visit_mut::VisitMut;
    struct Rewrite;
    impl VisitMut for Rewrite {
        fn visit_lifetime_mut(&mut self, lt: &mut syn::Lifetime) {
            if lt.ident != "static" {
                lt.ident = format_ident!("d");
            }
        }
    }
    VisitMut::visit_type_mut(&mut Rewrite, &mut ty);
    ty
}

fn build_route_entry(entry: AppRouteInput) -> syn::Result<Entry> {
    let path = entry.path;
    let method = Method::parse(&entry.method.to_string()).ok_or_else(|| {
        Error::new_spanned(
            &entry.method,
            "unsupported method; use one of GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS",
        )
    })?;
    let meta = Meta { method };
    let (route, kind, capacity) = unpack_route(entry.route)?;
    Ok(Entry {
        route,
        path,
        meta,
        wraps: entry.wraps,
        kind,
        capacity,
    })
}

fn unpack_route(route: TypePath) -> syn::Result<(TypePath, RouteKind, Option<syn::Expr>)> {
    use syn::Expr;
    let Some(segment) = route.path.segments.last() else {
        return Ok((route, RouteKind::Sync, None));
    };
    let kind = match segment.ident.to_string().as_str() {
        "__SarkAsyncRoute" => RouteKind::Fiber,
        "__SarkStreamRoute" => RouteKind::Stream,
        _ => return Ok((route, RouteKind::Sync, None)),
    };
    let PathArguments::AngleBracketed(arguments) = &segment.arguments else {
        return Err(Error::new_spanned(route, "invalid route storage metadata"));
    };
    let mut arguments = arguments.args.iter();
    let Some(GenericArgument::Type(Type::Path(route_type))) = arguments.next() else {
        return Err(Error::new_spanned(route, "invalid route type metadata"));
    };
    let Some(GenericArgument::Const(capacity)) = arguments.next() else {
        return Err(Error::new_spanned(route, "invalid route capacity metadata"));
    };
    if arguments.next().is_some() {
        return Err(Error::new_spanned(route, "invalid route storage metadata"));
    }
    let capacity = match capacity {
        Expr::Block(block) if block.block.stmts.len() == 1 => {
            use syn::Stmt;
            let Stmt::Expr(capacity, None) = &block.block.stmts[0] else {
                return Err(Error::new_spanned(block, "invalid route capacity"));
            };
            capacity.clone()
        }
        capacity => capacity.clone(),
    };
    Ok((route_type.clone(), kind, Some(capacity)))
}