doido-controller-macros 0.0.6

Proc-macros for Doido's controller attribute, before_action/after_action filters, and the routes! routing DSL.
Documentation
use syn::{
    braced, bracketed,
    parse::{Parse, ParseStream},
    Expr, Ident, LitStr, Result, Token,
};

pub enum RouteDecl {
    Method {
        method: String,
        path: LitStr,
        handler: Expr,
    },
    Resources {
        resource_name: Ident,
        controller: Ident,
        filter: ResourceFilter,
    },
    Namespace {
        name: Ident,
        body: RoutesInput,
    },
    Scope {
        path_prefix: LitStr,
        body: RoutesInput,
    },
}

pub enum ResourceFilter {
    All,
    Only(Vec<String>),
    Except(Vec<String>),
}

pub struct RoutesInput {
    pub decls: Vec<RouteDecl>,
}

fn parse_action_list(input: ParseStream) -> Result<Vec<String>> {
    let content;
    bracketed!(content in input);
    let mut actions = Vec::new();
    while !content.is_empty() {
        let ident: Ident = content.parse()?;
        actions.push(ident.to_string());
        let _comma: Option<Token![,]> = content.parse().ok();
    }
    Ok(actions)
}

impl Parse for RoutesInput {
    fn parse(input: ParseStream) -> Result<Self> {
        let mut decls = Vec::new();
        while !input.is_empty() {
            let macro_ident: Ident = input.parse()?;
            let _bang: Token![!] = input.parse()?;
            let content;
            syn::parenthesized!(content in input);
            let _semi: Option<Token![;]> = input.parse().ok();

            match macro_ident.to_string().as_str() {
                "namespace" => {
                    let name: Ident = content.parse()?;
                    let _comma: Token![,] = content.parse()?;
                    let inner;
                    braced!(inner in content);
                    let body: RoutesInput = inner.parse()?;
                    decls.push(RouteDecl::Namespace { name, body });
                }
                "scope" => {
                    let path_prefix: LitStr = content.parse()?;
                    let _comma: Token![,] = content.parse()?;
                    let inner;
                    braced!(inner in content);
                    let body: RoutesInput = inner.parse()?;
                    decls.push(RouteDecl::Scope { path_prefix, body });
                }
                "resources" => {
                    let resource_name: Ident = content.parse()?;
                    let _comma: Token![,] = content.parse()?;
                    let controller: Ident = content.parse()?;
                    let filter = if content.is_empty() {
                        ResourceFilter::All
                    } else {
                        let _comma: Token![,] = content.parse()?;
                        let key: Ident = content.parse()?;
                        let _colon: Token![:] = content.parse()?;
                        let actions = parse_action_list(&content)?;
                        match key.to_string().as_str() {
                            "only" => ResourceFilter::Only(actions),
                            "except" => ResourceFilter::Except(actions),
                            other => {
                                return Err(syn::Error::new(
                                    key.span(),
                                    format!("unknown option: {other}"),
                                ))
                            }
                        }
                    };
                    decls.push(RouteDecl::Resources {
                        resource_name,
                        controller,
                        filter,
                    });
                }
                method @ ("get" | "post" | "put" | "patch" | "delete") => {
                    let path: LitStr = content.parse()?;
                    let _comma: Token![,] = content.parse()?;
                    let handler: Expr = content.parse()?;
                    decls.push(RouteDecl::Method {
                        method: method.to_string(),
                        path,
                        handler,
                    });
                }
                other => {
                    return Err(syn::Error::new(
                        macro_ident.span(),
                        format!("unknown macro: {other}!"),
                    ))
                }
            }
        }
        Ok(RoutesInput { decls })
    }
}