1use proc_macro::TokenStream;
45use proc_macro2::Span;
46use proc_macro2::TokenStream as TokenStream2;
47use quote::format_ident;
48use quote::quote;
49use syn::Ident;
50use syn::ItemFn;
51use syn::LitStr;
52use syn::Token;
53use syn::Type;
54use syn::parse::Parse;
55use syn::parse::ParseStream;
56use syn::parse_macro_input;
57use syn::parse_str;
58
59struct RouteArgs {
60 method: Ident,
61 path: LitStr,
62 name_override: Option<Ident>,
63}
64
65impl Parse for RouteArgs {
66 fn parse(input: ParseStream) -> syn::Result<Self> {
67 let method: Ident = input.parse()?;
68 input.parse::<Token![,]>()?;
69 let path: LitStr = input.parse()?;
70 let name_override = parse_optional_name(input)?;
71 Ok(Self {
72 method,
73 path,
74 name_override,
75 })
76 }
77}
78
79struct ShortcutArgs {
80 path: LitStr,
81 name_override: Option<Ident>,
82}
83
84impl Parse for ShortcutArgs {
85 fn parse(input: ParseStream) -> syn::Result<Self> {
86 let path: LitStr = input.parse()?;
87 let name_override = parse_optional_name(input)?;
88 Ok(Self {
89 path,
90 name_override,
91 })
92 }
93}
94
95fn parse_optional_name(input: ParseStream) -> syn::Result<Option<Ident>> {
98 if input.is_empty() {
99 return Ok(None);
100 }
101 input.parse::<Token![,]>()?;
102 if input.is_empty() {
103 return Ok(None);
104 }
105 let key: Ident = input.parse()?;
106 if key != "name" {
107 return Err(syn::Error::new(key.span(), "expected `name = \"...\"`"));
108 }
109 input.parse::<Token![=]>()?;
110 let lit: LitStr = input.parse()?;
111 let ident: Ident = parse_str(&lit.value())
112 .map_err(|e| syn::Error::new(lit.span(), format!("invalid struct name: {e}")))?;
113 Ok(Some(ident))
114}
115
116struct PathParam {
117 name: Ident,
118 ty: Type,
119}
120
121fn parse_path(path: &str, span: Span) -> syn::Result<(String, Vec<PathParam>)> {
129 if !path.is_ascii() {
136 return Err(syn::Error::new(
137 span,
138 "route path must be ASCII (RFC 3986); percent-encode any non-ASCII characters",
139 ));
140 }
141 let mut stripped = String::with_capacity(path.len());
142 let mut typed = Vec::new();
143 let bytes = path.as_bytes();
144 let mut i = 0;
145 while i < bytes.len() {
146 let c = bytes[i];
147 if c == b'}' {
148 return Err(syn::Error::new(
152 span,
153 "unexpected '}' in path (no matching '{')",
154 ));
155 }
156 if c != b'{' {
157 stripped.push(c as char);
158 i += 1;
159 continue;
160 }
161 let close = (i + 1..bytes.len())
162 .find(|&j| bytes[j] == b'}')
163 .ok_or_else(|| syn::Error::new(span, "unclosed '{' in path"))?;
164 let inner = &path[i + 1..close];
165 if let Some((name_str, ty_str)) = inner.split_once(':') {
166 let name: Ident = parse_str(name_str.trim()).map_err(|e| {
167 syn::Error::new(
168 span,
169 format!("invalid placeholder name '{}': {e}", name_str.trim()),
170 )
171 })?;
172 let ty: Type = parse_str(ty_str.trim()).map_err(|e| {
173 syn::Error::new(
174 span,
175 format!("invalid placeholder type '{}': {e}", ty_str.trim()),
176 )
177 })?;
178 stripped.push('{');
179 stripped.push_str(&name.to_string());
180 stripped.push('}');
181 typed.push(PathParam { name, ty });
182 } else {
183 let name: Ident = parse_str(inner.trim()).map_err(|e| {
184 syn::Error::new(
185 span,
186 format!("invalid placeholder name '{}': {e}", inner.trim()),
187 )
188 })?;
189 stripped.push('{');
190 stripped.push_str(&name.to_string());
191 stripped.push('}');
192 }
193 i = close + 1;
194 }
195 Ok((stripped, typed))
196}
197
198fn pascal_case(s: &str) -> String {
201 let mut out = String::with_capacity(s.len());
202 let mut next_upper = true;
203 for ch in s.chars() {
204 if ch == '_' {
205 next_upper = true;
206 } else if next_upper {
207 out.extend(ch.to_uppercase());
208 next_upper = false;
209 } else {
210 out.push(ch);
211 }
212 }
213 out
214}
215
216fn expand_route(
223 method: Ident,
224 path: LitStr,
225 name_override: Option<Ident>,
226 func: ItemFn,
227) -> TokenStream {
228 let span = path.span();
229 let path_str = path.value();
230 let (stripped, params) = match parse_path(&path_str, span) {
231 Ok(v) => v,
232 Err(e) => return e.to_compile_error().into(),
233 };
234
235 let fn_name = &func.sig.ident;
236 let registrar_suffix = {
242 let key = format!("{method}_{path_str}");
243 let mut hash: u64 = 0xcbf2_9ce4_8422_2325; for byte in key.as_bytes() {
245 hash ^= u64::from(*byte);
246 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
247 }
248 format!("{hash:016X}")
249 };
250 let registrar_ident = format_ident!(
251 "__TAKO_REGISTER_{}_{}",
252 fn_name.to_string().to_uppercase(),
253 registrar_suffix,
254 span = fn_name.span()
255 );
256
257 if params.is_empty() {
259 if let Some(struct_name) = name_override {
263 let expanded: TokenStream2 = quote! {
264 pub struct #struct_name;
265
266 impl #struct_name {
267 pub const METHOD: ::tako::Method = ::tako::Method::#method;
268 pub const PATH: &'static str = #stripped;
269 }
270
271 #[::tako::__private::linkme::distributed_slice(::tako::router::TAKO_ROUTES)]
272 #[linkme(crate = ::tako::__private::linkme)]
273 static #registrar_ident: fn(&mut ::tako::router::Router) = |__router| {
274 __router.route(#struct_name::METHOD, #struct_name::PATH, #fn_name);
275 };
276
277 #func
278 };
279 return expanded.into();
280 }
281
282 let expanded: TokenStream2 = quote! {
283 #[::tako::__private::linkme::distributed_slice(::tako::router::TAKO_ROUTES)]
284 #[linkme(crate = ::tako::__private::linkme)]
285 static #registrar_ident: fn(&mut ::tako::router::Router) = |__router| {
286 __router.route(::tako::Method::#method, #stripped, #fn_name);
287 };
288
289 #func
290 };
291 return expanded.into();
292 }
293
294 let struct_name = name_override.unwrap_or_else(|| {
295 format_ident!(
296 "{}Params",
297 pascal_case(&fn_name.to_string()),
298 span = fn_name.span()
299 )
300 });
301
302 let field_idents: Vec<&Ident> = params.iter().map(|p| &p.name).collect();
303 let field_names_str: Vec<String> = params.iter().map(|p| p.name.to_string()).collect();
304 let field_types: Vec<&Type> = params.iter().map(|p| &p.ty).collect();
305
306 let expanded: TokenStream2 = quote! {
307 pub struct #struct_name {
308 #(pub #field_idents: #field_types,)*
309 }
310
311 impl #struct_name {
312 pub const METHOD: ::tako::Method = ::tako::Method::#method;
313 pub const PATH: &'static str = #stripped;
314 }
315
316 impl ::tako::extractors::typed_params::TypedParamsStruct for #struct_name {
317 fn from_path_params(
318 __pp: &::tako::extractors::params::PathParams,
319 ) -> ::core::result::Result<Self, ::tako::extractors::typed_params::TypedParamsError> {
320 ::core::result::Result::Ok(Self {
321 #(
322 #field_idents: {
323 let __raw = __pp
324 .0
325 .iter()
326 .find(|(__k, _)| __k.as_str() == #field_names_str)
327 .map(|(_, __v)| __v.as_str())
328 .ok_or(::tako::extractors::typed_params::TypedParamsError::MissingField(
329 #field_names_str,
330 ))?;
331 <#field_types as ::core::str::FromStr>::from_str(__raw).map_err(|__e| {
332 ::tako::extractors::typed_params::TypedParamsError::Parse(
333 #field_names_str,
334 __e.to_string(),
335 )
336 })?
337 },
338 )*
339 })
340 }
341 }
342
343 #[::tako::__private::linkme::distributed_slice(::tako::router::TAKO_ROUTES)]
344 #[linkme(crate = ::tako::__private::linkme)]
345 static #registrar_ident: fn(&mut ::tako::router::Router) = |__router| {
346 __router.route(#struct_name::METHOD, #struct_name::PATH, #fn_name);
347 };
348
349 #func
350 };
351
352 expanded.into()
353}
354
355fn shortcut(method_name: &'static str, attr: TokenStream, item: TokenStream) -> TokenStream {
357 let ShortcutArgs {
358 path,
359 name_override,
360 } = parse_macro_input!(attr as ShortcutArgs);
361 let func = parse_macro_input!(item as ItemFn);
362 let method = Ident::new(method_name, Span::call_site());
363 expand_route(method, path, name_override, func)
364}
365
366#[proc_macro_attribute]
367pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
368 let RouteArgs {
369 method,
370 path,
371 name_override,
372 } = parse_macro_input!(attr as RouteArgs);
373 let func = parse_macro_input!(item as ItemFn);
374 expand_route(method, path, name_override, func)
375}
376
377#[proc_macro_attribute]
379pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream {
380 shortcut("GET", attr, item)
381}
382
383#[proc_macro_attribute]
385pub fn post(attr: TokenStream, item: TokenStream) -> TokenStream {
386 shortcut("POST", attr, item)
387}
388
389#[proc_macro_attribute]
391pub fn put(attr: TokenStream, item: TokenStream) -> TokenStream {
392 shortcut("PUT", attr, item)
393}
394
395#[proc_macro_attribute]
397pub fn delete(attr: TokenStream, item: TokenStream) -> TokenStream {
398 shortcut("DELETE", attr, item)
399}
400
401#[proc_macro_attribute]
403pub fn patch(attr: TokenStream, item: TokenStream) -> TokenStream {
404 shortcut("PATCH", attr, item)
405}