use proc_macro::TokenStream;
use proc_macro2::Span;
use proc_macro2::TokenStream as TokenStream2;
use quote::format_ident;
use quote::quote;
use syn::Ident;
use syn::ItemFn;
use syn::LitStr;
use syn::Token;
use syn::Type;
use syn::parse::Parse;
use syn::parse::ParseStream;
use syn::parse_macro_input;
use syn::parse_str;
struct RouteArgs {
method: Ident,
path: LitStr,
name_override: Option<Ident>,
}
impl Parse for RouteArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let method: Ident = input.parse()?;
input.parse::<Token![,]>()?;
let path: LitStr = input.parse()?;
let name_override = parse_optional_name(input)?;
Ok(Self {
method,
path,
name_override,
})
}
}
struct ShortcutArgs {
path: LitStr,
name_override: Option<Ident>,
}
impl Parse for ShortcutArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let path: LitStr = input.parse()?;
let name_override = parse_optional_name(input)?;
Ok(Self {
path,
name_override,
})
}
}
fn parse_optional_name(input: ParseStream) -> syn::Result<Option<Ident>> {
if input.is_empty() {
return Ok(None);
}
input.parse::<Token![,]>()?;
if input.is_empty() {
return Ok(None);
}
let key: Ident = input.parse()?;
if key != "name" {
return Err(syn::Error::new(key.span(), "expected `name = \"...\"`"));
}
input.parse::<Token![=]>()?;
let lit: LitStr = input.parse()?;
let ident: Ident = parse_str(&lit.value())
.map_err(|e| syn::Error::new(lit.span(), format!("invalid struct name: {e}")))?;
Ok(Some(ident))
}
struct PathParam {
name: Ident,
ty: Type,
}
fn parse_path(path: &str, span: Span) -> syn::Result<(String, Vec<PathParam>)> {
if !path.is_ascii() {
return Err(syn::Error::new(
span,
"route path must be ASCII (RFC 3986); percent-encode any non-ASCII characters",
));
}
let mut stripped = String::with_capacity(path.len());
let mut typed = Vec::new();
let bytes = path.as_bytes();
let mut i = 0;
while i < bytes.len() {
let c = bytes[i];
if c == b'}' {
return Err(syn::Error::new(
span,
"unexpected '}' in path (no matching '{')",
));
}
if c != b'{' {
stripped.push(c as char);
i += 1;
continue;
}
let close = (i + 1..bytes.len())
.find(|&j| bytes[j] == b'}')
.ok_or_else(|| syn::Error::new(span, "unclosed '{' in path"))?;
let inner = &path[i + 1..close];
if let Some((name_str, ty_str)) = inner.split_once(':') {
let name: Ident = parse_str(name_str.trim()).map_err(|e| {
syn::Error::new(
span,
format!("invalid placeholder name '{}': {e}", name_str.trim()),
)
})?;
let ty: Type = parse_str(ty_str.trim()).map_err(|e| {
syn::Error::new(
span,
format!("invalid placeholder type '{}': {e}", ty_str.trim()),
)
})?;
stripped.push('{');
stripped.push_str(&name.to_string());
stripped.push('}');
typed.push(PathParam { name, ty });
} else {
let name: Ident = parse_str(inner.trim()).map_err(|e| {
syn::Error::new(
span,
format!("invalid placeholder name '{}': {e}", inner.trim()),
)
})?;
stripped.push('{');
stripped.push_str(&name.to_string());
stripped.push('}');
}
i = close + 1;
}
Ok((stripped, typed))
}
fn pascal_case(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut next_upper = true;
for ch in s.chars() {
if ch == '_' {
next_upper = true;
} else if next_upper {
out.extend(ch.to_uppercase());
next_upper = false;
} else {
out.push(ch);
}
}
out
}
fn expand_route(
method: Ident,
path: LitStr,
name_override: Option<Ident>,
func: ItemFn,
) -> TokenStream {
let span = path.span();
let path_str = path.value();
let (stripped, params) = match parse_path(&path_str, span) {
Ok(v) => v,
Err(e) => return e.to_compile_error().into(),
};
let fn_name = &func.sig.ident;
let registrar_suffix = {
let key = format!("{method}_{path_str}");
let mut hash: u64 = 0xcbf2_9ce4_8422_2325; for byte in key.as_bytes() {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
format!("{hash:016X}")
};
let registrar_ident = format_ident!(
"__TAKO_REGISTER_{}_{}",
fn_name.to_string().to_uppercase(),
registrar_suffix,
span = fn_name.span()
);
if params.is_empty() {
if let Some(struct_name) = name_override {
let expanded: TokenStream2 = quote! {
pub struct #struct_name;
impl #struct_name {
pub const METHOD: ::tako::Method = ::tako::Method::#method;
pub const PATH: &'static str = #stripped;
}
#[::tako::__private::linkme::distributed_slice(::tako::router::TAKO_ROUTES)]
#[linkme(crate = ::tako::__private::linkme)]
static #registrar_ident: fn(&mut ::tako::router::Router) = |__router| {
__router.route(#struct_name::METHOD, #struct_name::PATH, #fn_name);
};
#func
};
return expanded.into();
}
let expanded: TokenStream2 = quote! {
#[::tako::__private::linkme::distributed_slice(::tako::router::TAKO_ROUTES)]
#[linkme(crate = ::tako::__private::linkme)]
static #registrar_ident: fn(&mut ::tako::router::Router) = |__router| {
__router.route(::tako::Method::#method, #stripped, #fn_name);
};
#func
};
return expanded.into();
}
let struct_name = name_override.unwrap_or_else(|| {
format_ident!(
"{}Params",
pascal_case(&fn_name.to_string()),
span = fn_name.span()
)
});
let field_idents: Vec<&Ident> = params.iter().map(|p| &p.name).collect();
let field_names_str: Vec<String> = params.iter().map(|p| p.name.to_string()).collect();
let field_types: Vec<&Type> = params.iter().map(|p| &p.ty).collect();
let expanded: TokenStream2 = quote! {
pub struct #struct_name {
#(pub #field_idents: #field_types,)*
}
impl #struct_name {
pub const METHOD: ::tako::Method = ::tako::Method::#method;
pub const PATH: &'static str = #stripped;
}
impl ::tako::extractors::typed_params::TypedParamsStruct for #struct_name {
fn from_path_params(
__pp: &::tako::extractors::params::PathParams,
) -> ::core::result::Result<Self, ::tako::extractors::typed_params::TypedParamsError> {
::core::result::Result::Ok(Self {
#(
#field_idents: {
let __raw = __pp
.0
.iter()
.find(|(__k, _)| __k.as_str() == #field_names_str)
.map(|(_, __v)| __v.as_str())
.ok_or(::tako::extractors::typed_params::TypedParamsError::MissingField(
#field_names_str,
))?;
<#field_types as ::core::str::FromStr>::from_str(__raw).map_err(|__e| {
::tako::extractors::typed_params::TypedParamsError::Parse(
#field_names_str,
__e.to_string(),
)
})?
},
)*
})
}
}
#[::tako::__private::linkme::distributed_slice(::tako::router::TAKO_ROUTES)]
#[linkme(crate = ::tako::__private::linkme)]
static #registrar_ident: fn(&mut ::tako::router::Router) = |__router| {
__router.route(#struct_name::METHOD, #struct_name::PATH, #fn_name);
};
#func
};
expanded.into()
}
fn shortcut(method_name: &'static str, attr: TokenStream, item: TokenStream) -> TokenStream {
let ShortcutArgs {
path,
name_override,
} = parse_macro_input!(attr as ShortcutArgs);
let func = parse_macro_input!(item as ItemFn);
let method = Ident::new(method_name, Span::call_site());
expand_route(method, path, name_override, func)
}
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
let RouteArgs {
method,
path,
name_override,
} = parse_macro_input!(attr as RouteArgs);
let func = parse_macro_input!(item as ItemFn);
expand_route(method, path, name_override, func)
}
#[proc_macro_attribute]
pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream {
shortcut("GET", attr, item)
}
#[proc_macro_attribute]
pub fn post(attr: TokenStream, item: TokenStream) -> TokenStream {
shortcut("POST", attr, item)
}
#[proc_macro_attribute]
pub fn put(attr: TokenStream, item: TokenStream) -> TokenStream {
shortcut("PUT", attr, item)
}
#[proc_macro_attribute]
pub fn delete(attr: TokenStream, item: TokenStream) -> TokenStream {
shortcut("DELETE", attr, item)
}
#[proc_macro_attribute]
pub fn patch(attr: TokenStream, item: TokenStream) -> TokenStream {
shortcut("PATCH", attr, item)
}