serverkit-macros 0.2.0

Internal procedural macros for ServerKit
Documentation
use std::{fmt::Write, str::FromStr};

use proc_macro::{TokenStream, TokenTree};

mod schema;

#[proc_macro]
pub fn impl_handlers(input: TokenStream) -> TokenStream {
    match expand(input) {
        Ok(output) => output,
        Err(message) => compile_error(&message),
    }
}

#[proc_macro]
pub fn impl_routes(input: TokenStream) -> TokenStream {
    match expand_routes(input) {
        Ok(output) => output,
        Err(message) => compile_error(&message),
    }
}

#[proc_macro_derive(Schema, attributes(schema))]
pub fn derive_schema(input: TokenStream) -> TokenStream {
    match schema::expand(input) {
        Ok(output) => output,
        Err(message) => compile_error(&message),
    }
}

fn expand(input: TokenStream) -> Result<TokenStream, String> {
    let maximum = parse_maximum(input)?;
    let mut output = String::new();

    for arity in 1..=maximum {
        output.push_str(&handler_invocation(arity)?);
    }

    TokenStream::from_str(&output).map_err(|error| error.to_string())
}

fn expand_routes(input: TokenStream) -> Result<TokenStream, String> {
    let maximum = parse_maximum(input)?;
    let mut output = String::new();

    for arity in 1..=maximum {
        output.push_str("impl_route_tuple!(");
        for index in 1..=arity {
            if index > 1 {
                output.push(',');
            }
            write!(output, "R{index}").map_err(|error| error.to_string())?;
        }
        output.push_str(");");
    }

    TokenStream::from_str(&output).map_err(|error| error.to_string())
}

fn parse_maximum(input: TokenStream) -> Result<usize, String> {
    let mut tokens = input.into_iter();

    let literal = match tokens.next() {
        Some(TokenTree::Literal(literal)) => literal,
        _ => return Err("expected one positive decimal integer".to_owned()),
    };

    if tokens.next().is_some() {
        return Err("expected one positive decimal integer".to_owned());
    }

    let maximum = literal
        .to_string()
        .parse::<usize>()
        .map_err(|_| "expected one positive decimal integer".to_owned())?;

    if maximum == 0 {
        return Err("handler arity must be greater than zero".to_owned());
    }

    Ok(maximum)
}

fn handler_invocation(arity: usize) -> Result<String, String> {
    let mut invocation = String::from("impl_handler!([");

    for index in 0..arity.saturating_sub(1) {
        if index > 0 {
            invocation.push(',');
        }

        write!(invocation, "(A{index},a{index})").map_err(|error| error.to_string())?;
    }

    let last = arity - 1;
    write!(invocation, "];(A{last},a{last}));").map_err(|error| error.to_string())?;

    Ok(invocation)
}

fn compile_error(message: &str) -> TokenStream {
    TokenStream::from_str(&format!("compile_error!({message:?});")).unwrap_or_default()
}