use proc_macro2::TokenStream;
use quote::quote;
use syn::{
Error, Expr, LitStr, Result, Token,
parse::{Parse, ParseStream},
punctuated::Punctuated,
};
pub(crate) fn expand(input: TokenStream) -> Result<TokenStream> {
let mut input = syn::parse2::<RouterInput>(input)?;
input.sort_and_validate_routes()?;
Ok(input.into_tokens())
}
struct RouterInput {
fallback: Expr,
routes: Vec<RouteInput>,
}
impl RouterInput {
fn sort_and_validate_routes(&mut self) -> Result<()> {
self.routes
.sort_by(|left, right| left.key_value.cmp(&right.key_value));
if let Some(pair) = self
.routes
.windows(2)
.find(|pair| pair[0].key_value == pair[1].key_value)
{
return Err(Error::new(
pair[1].key.span(),
format!("duplicate route key {:?}", pair[1].key_value),
));
}
Ok(())
}
fn into_tokens(self) -> TokenStream {
let Self { fallback, routes } = self;
let routes = routes.iter().map(RouteInput::to_tokens);
quote! {
::const_router::__internal::new_router(#fallback(), &[#(#routes),*])
}
}
}
impl Parse for RouterInput {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let fallback = input.parse::<Expr>()?;
let routes = if input.is_empty() {
Vec::new()
} else {
input.parse::<Token![,]>()?;
Punctuated::<RouteInput, Token![,]>::parse_terminated(input)?
.into_iter()
.collect()
};
Ok(Self { fallback, routes })
}
}
struct RouteInput {
handler: Expr,
key: LitStr,
key_value: String,
}
impl RouteInput {
fn to_tokens(&self) -> TokenStream {
let key = &self.key;
let handler = &self.handler;
quote! {
::const_router::__internal::new_route(#key, #handler())
}
}
}
impl Parse for RouteInput {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let key = input.parse::<LitStr>()?;
input.parse::<Token![=>]>()?;
let handler = input.parse::<Expr>()?;
let key_value = key.value();
Ok(Self {
handler,
key,
key_value,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use quote::quote;
fn expand_to_string(input: TokenStream) -> String {
expand(input)
.expect("router input should expand")
.to_string()
}
#[test]
fn expands_fallback_only_router() {
let expanded = expand_to_string(quote! { fallback_handler });
assert!(expanded.contains("new_router"));
assert!(expanded.contains("fallback_handler ()"));
assert!(!expanded.contains("new_route ("));
}
#[test]
fn sorts_routes_by_key_before_expanding() {
let expanded = expand_to_string(quote! {
fallback_handler,
"/zeta" => zeta_handler,
"/alpha" => alpha_handler,
"/middle" => middle_handler,
});
let alpha = expanded.find("\"/alpha\"").expect("alpha route exists");
let middle = expanded.find("\"/middle\"").expect("middle route exists");
let zeta = expanded.find("\"/zeta\"").expect("zeta route exists");
assert!(alpha < middle);
assert!(middle < zeta);
}
#[test]
fn accepts_trailing_route_comma() {
let expanded = expand_to_string(quote! {
fallback_handler,
"/alpha" => alpha_handler,
});
assert!(expanded.contains("\"/alpha\""));
assert!(expanded.contains("alpha_handler ()"));
}
#[test]
fn rejects_duplicate_route_keys_after_sorting() {
let error = expand(quote! {
fallback_handler,
"/same" => first_handler,
"/different" => different_handler,
"/same" => second_handler,
})
.expect_err("duplicate keys should be rejected");
assert_eq!(error.to_string(), "duplicate route key \"/same\"");
}
}