1use proc_macro::TokenStream;
18use proc_macro_crate::{crate_name, FoundCrate};
19use quote::quote;
20use std::collections::HashSet;
21use syn::{
22 parse_macro_input, Attribute, Data, DeriveInput, Expr, Fields, FnArg, GenericArgument, ItemFn,
23 Lit, LitStr, Meta, PathArguments, ReturnType, Type,
24};
25
26mod api_error;
27mod derive_schema;
28
29fn get_rustapi_path() -> proc_macro2::TokenStream {
34 let rustapi_rs_found = crate_name("rustapi-rs").or_else(|_| crate_name("rustapi_rs"));
35
36 if let Ok(found) = rustapi_rs_found {
37 match found {
38 FoundCrate::Itself => quote! { ::rustapi_rs },
41 FoundCrate::Name(name) => {
42 let normalized = name.replace('-', "_");
43 let ident = syn::Ident::new(&normalized, proc_macro2::Span::call_site());
44 quote! { ::#ident }
45 }
46 }
47 } else {
48 quote! { ::rustapi_rs }
49 }
50}
51
52#[proc_macro_derive(Schema, attributes(schema))]
64pub fn derive_schema(input: TokenStream) -> TokenStream {
65 derive_schema::expand_derive_schema(parse_macro_input!(input as DeriveInput)).into()
66}
67
68#[proc_macro_attribute]
82pub fn schema(_attr: TokenStream, item: TokenStream) -> TokenStream {
83 let input = parse_macro_input!(item as syn::Item);
84 let rustapi_path = get_rustapi_path();
85
86 let (ident, generics) = match &input {
87 syn::Item::Struct(s) => (&s.ident, &s.generics),
88 syn::Item::Enum(e) => (&e.ident, &e.generics),
89 _ => {
90 return syn::Error::new_spanned(
91 &input,
92 "#[rustapi_rs::schema] can only be used on structs or enums",
93 )
94 .to_compile_error()
95 .into();
96 }
97 };
98
99 if !generics.params.is_empty() {
100 return syn::Error::new_spanned(
101 generics,
102 "#[rustapi_rs::schema] does not support generic types",
103 )
104 .to_compile_error()
105 .into();
106 }
107
108 let registrar_ident = syn::Ident::new(
109 &format!("__RUSTAPI_AUTO_SCHEMA_{}", ident),
110 proc_macro2::Span::call_site(),
111 );
112
113 let expanded = quote! {
114 #input
115
116 #[allow(non_upper_case_globals)]
117 #[#rustapi_path::__private::linkme::distributed_slice(#rustapi_path::__private::AUTO_SCHEMAS)]
119 #[linkme(crate = #rustapi_path::__private::linkme)]
120 static #registrar_ident: fn(&mut #rustapi_path::__private::openapi::OpenApiSpec) =
121 |spec: &mut #rustapi_path::__private::openapi::OpenApiSpec| {
122 spec.register_in_place::<#ident>();
123 };
124 };
125
126 debug_output("schema", &expanded);
127 expanded.into()
128}
129
130fn extract_schema_types(ty: &Type, out: &mut Vec<Type>, allow_leaf: bool) {
131 match ty {
132 Type::Reference(r) => extract_schema_types(&r.elem, out, allow_leaf),
133 Type::Path(tp) => {
134 let Some(seg) = tp.path.segments.last() else {
135 return;
136 };
137
138 let ident = seg.ident.to_string();
139
140 let unwrap_first_generic = |out: &mut Vec<Type>| {
141 if let PathArguments::AngleBracketed(args) = &seg.arguments {
142 if let Some(GenericArgument::Type(inner)) = args.args.first() {
143 extract_schema_types(inner, out, true);
144 }
145 }
146 };
147
148 match ident.as_str() {
149 "Json" | "ValidatedJson" | "Created" => {
151 unwrap_first_generic(out);
152 }
153 "WithStatus" => {
155 if let PathArguments::AngleBracketed(args) = &seg.arguments {
156 if let Some(GenericArgument::Type(inner)) = args.args.first() {
157 extract_schema_types(inner, out, true);
158 }
159 }
160 }
161 "Option" | "Result" => {
163 if let PathArguments::AngleBracketed(args) = &seg.arguments {
164 if let Some(GenericArgument::Type(inner)) = args.args.first() {
165 extract_schema_types(inner, out, allow_leaf);
166 }
167 }
168 }
169 _ => {
170 if allow_leaf {
171 out.push(ty.clone());
172 }
173 }
174 }
175 }
176 _ => {}
177 }
178}
179
180fn collect_handler_schema_types(input: &ItemFn) -> Vec<Type> {
181 let mut found: Vec<Type> = Vec::new();
182
183 for arg in &input.sig.inputs {
184 if let FnArg::Typed(pat_ty) = arg {
185 extract_schema_types(&pat_ty.ty, &mut found, false);
186 }
187 }
188
189 if let ReturnType::Type(_, ty) = &input.sig.output {
190 extract_schema_types(ty, &mut found, false);
191 }
192
193 let mut seen = HashSet::<String>::new();
195 found
196 .into_iter()
197 .filter(|t| seen.insert(quote!(#t).to_string()))
198 .collect()
199}
200
201fn collect_path_params(input: &ItemFn) -> Vec<(String, String)> {
205 let mut params = Vec::new();
206
207 for arg in &input.sig.inputs {
208 if let FnArg::Typed(pat_ty) = arg {
209 if let Type::Path(tp) = &*pat_ty.ty {
211 if let Some(seg) = tp.path.segments.last() {
212 if seg.ident == "Path" {
213 if let PathArguments::AngleBracketed(args) = &seg.arguments {
215 if let Some(GenericArgument::Type(inner_ty)) = args.args.first() {
216 if let Some(schema_type) = map_type_to_schema(inner_ty) {
218 if let Some(name) = extract_param_name(&pat_ty.pat) {
226 params.push((name, schema_type));
227 }
228 }
229 }
230 }
231 }
232 }
233 }
234 }
235 }
236
237 params
238}
239
240fn extract_param_name(pat: &syn::Pat) -> Option<String> {
245 match pat {
246 syn::Pat::Ident(ident) => Some(ident.ident.to_string()),
247 syn::Pat::TupleStruct(ts) => {
248 if let Some(first) = ts.elems.first() {
251 extract_param_name(first)
252 } else {
253 None
254 }
255 }
256 _ => None, }
258}
259
260fn map_type_to_schema(ty: &Type) -> Option<String> {
262 match ty {
263 Type::Path(tp) => {
264 if let Some(seg) = tp.path.segments.last() {
265 let ident = seg.ident.to_string();
266 match ident.as_str() {
267 "Uuid" => Some("uuid".to_string()),
268 "String" | "str" => Some("string".to_string()),
269 "bool" => Some("boolean".to_string()),
270 "i8" | "i16" | "i32" | "i64" | "isize" | "u8" | "u16" | "u32" | "u64"
271 | "usize" => Some("integer".to_string()),
272 "f32" | "f64" => Some("number".to_string()),
273 _ => None,
274 }
275 } else {
276 None
277 }
278 }
279 _ => None,
280 }
281}
282
283fn is_debug_enabled() -> bool {
285 std::env::var("RUSTAPI_DEBUG")
286 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
287 .unwrap_or(false)
288}
289
290fn debug_output(name: &str, tokens: &proc_macro2::TokenStream) {
292 if is_debug_enabled() {
293 eprintln!("\n=== RUSTAPI_DEBUG: {} ===", name);
294 eprintln!("{}", tokens);
295 eprintln!("=== END {} ===\n", name);
296 }
297}
298
299fn validate_path_syntax(path: &str, span: proc_macro2::Span) -> Result<(), syn::Error> {
303 if !path.starts_with('/') {
305 return Err(syn::Error::new(
306 span,
307 format!("route path must start with '/', got: \"{}\"", path),
308 ));
309 }
310
311 if path.contains("//") {
313 return Err(syn::Error::new(
314 span,
315 format!(
316 "route path contains empty segment (double slash): \"{}\"",
317 path
318 ),
319 ));
320 }
321
322 let mut brace_depth = 0;
324 let mut param_start = None;
325
326 for (i, ch) in path.char_indices() {
327 match ch {
328 '{' => {
329 if brace_depth > 0 {
330 return Err(syn::Error::new(
331 span,
332 format!(
333 "nested braces are not allowed in route path at position {}: \"{}\"",
334 i, path
335 ),
336 ));
337 }
338 brace_depth += 1;
339 param_start = Some(i);
340 }
341 '}' => {
342 if brace_depth == 0 {
343 return Err(syn::Error::new(
344 span,
345 format!(
346 "unmatched closing brace '}}' at position {} in route path: \"{}\"",
347 i, path
348 ),
349 ));
350 }
351 brace_depth -= 1;
352
353 if let Some(start) = param_start {
355 let param_name = &path[start + 1..i];
356 if param_name.is_empty() {
357 return Err(syn::Error::new(
358 span,
359 format!(
360 "empty parameter name '{{}}' at position {} in route path: \"{}\"",
361 start, path
362 ),
363 ));
364 }
365 if !param_name.chars().all(|c| c.is_alphanumeric() || c == '_') {
367 return Err(syn::Error::new(
368 span,
369 format!(
370 "invalid parameter name '{{{}}}' at position {} - parameter names must contain only alphanumeric characters and underscores: \"{}\"",
371 param_name, start, path
372 ),
373 ));
374 }
375 if param_name
377 .chars()
378 .next()
379 .map(|c| c.is_ascii_digit())
380 .unwrap_or(false)
381 {
382 return Err(syn::Error::new(
383 span,
384 format!(
385 "parameter name '{{{}}}' cannot start with a digit at position {}: \"{}\"",
386 param_name, start, path
387 ),
388 ));
389 }
390 }
391 param_start = None;
392 }
393 _ if brace_depth == 0
395 && !ch.is_alphanumeric() && !"-_./*".contains(ch) =>
397 {
398 return Err(syn::Error::new(
399 span,
400 format!(
401 "invalid character '{}' at position {} in route path: \"{}\"",
402 ch, i, path
403 ),
404 ));
405 }
406 _ if brace_depth == 0 => {}
407 _ => {}
408 }
409 }
410
411 if brace_depth > 0 {
413 return Err(syn::Error::new(
414 span,
415 format!(
416 "unclosed brace '{{' in route path (missing closing '}}'): \"{}\"",
417 path
418 ),
419 ));
420 }
421
422 Ok(())
423}
424
425#[proc_macro_attribute]
443pub fn main(_attr: TokenStream, item: TokenStream) -> TokenStream {
444 let input = parse_macro_input!(item as ItemFn);
445
446 let attrs = &input.attrs;
447 let vis = &input.vis;
448 let sig = &input.sig;
449 let block = &input.block;
450
451 let expanded = quote! {
452 #(#attrs)*
453 #[::tokio::main]
454 #vis #sig {
455 #block
456 }
457 };
458
459 debug_output("main", &expanded);
460
461 TokenStream::from(expanded)
462}
463
464fn is_body_consuming_type(ty: &Type) -> bool {
470 match ty {
471 Type::Path(tp) => {
472 if let Some(seg) = tp.path.segments.last() {
473 matches!(
474 seg.ident.to_string().as_str(),
475 "Json" | "Body" | "ValidatedJson" | "AsyncValidatedJson" | "Multipart"
476 )
477 } else {
478 false
479 }
480 }
481 _ => false,
482 }
483}
484
485fn validate_extractor_order(input: &ItemFn) -> Result<(), syn::Error> {
491 let params: Vec<_> = input
492 .sig
493 .inputs
494 .iter()
495 .filter_map(|arg| {
496 if let FnArg::Typed(pat_ty) = arg {
497 Some(pat_ty)
498 } else {
499 None
500 }
501 })
502 .collect();
503
504 if params.is_empty() {
505 return Ok(());
506 }
507
508 let body_indices: Vec<usize> = params
510 .iter()
511 .enumerate()
512 .filter(|(_, p)| is_body_consuming_type(&p.ty))
513 .map(|(i, _)| i)
514 .collect();
515
516 if body_indices.is_empty() {
517 return Ok(());
518 }
519
520 let last_non_body = params
522 .iter()
523 .enumerate()
524 .filter(|(_, p)| !is_body_consuming_type(&p.ty))
525 .map(|(i, _)| i)
526 .max();
527
528 if let Some(last_non_body_idx) = last_non_body {
530 let first_body_idx = body_indices[0];
531 if first_body_idx < last_non_body_idx {
532 let offending_param = ¶ms[first_body_idx];
533 let ty_name = quote!(#offending_param).to_string();
534 return Err(syn::Error::new_spanned(
535 &offending_param.ty,
536 format!(
537 "Body-consuming extractor must be the LAST parameter.\n\
538 \n\
539 Found `{}` before non-body extractor(s).\n\
540 \n\
541 Body extractors (Json, Body, ValidatedJson, AsyncValidatedJson, Multipart) \
542 consume the request body, which can only be read once. Place them after all \
543 non-body extractors (State, Path, Query, Headers, etc.).\n\
544 \n\
545 Example:\n\
546 \x20 async fn handler(\n\
547 \x20 State(db): State<AppState>, // non-body: OK first\n\
548 \x20 Path(id): Path<i64>, // non-body: OK second\n\
549 \x20 Json(body): Json<CreateUser>, // body: MUST be last\n\
550 \x20 ) -> Result<Json<User>> {{ ... }}",
551 ty_name,
552 ),
553 ));
554 }
555 }
556
557 if body_indices.len() > 1 {
559 let second_body_param = ¶ms[body_indices[1]];
560 return Err(syn::Error::new_spanned(
561 &second_body_param.ty,
562 "Multiple body-consuming extractors detected.\n\
563 \n\
564 Only ONE body-consuming extractor (Json, Body, ValidatedJson, AsyncValidatedJson, \
565 Multipart) is allowed per handler, because the request body can only be consumed once.\n\
566 \n\
567 Remove the extra body extractor or combine the data into a single type.",
568 ));
569 }
570
571 Ok(())
572}
573
574fn generate_route_handler(method: &str, attr: TokenStream, item: TokenStream) -> TokenStream {
576 let path = parse_macro_input!(attr as LitStr);
577 let input = parse_macro_input!(item as ItemFn);
578 let rustapi_path = get_rustapi_path();
579
580 let fn_name = &input.sig.ident;
581 let fn_vis = &input.vis;
582 let fn_attrs = &input.attrs;
583 let fn_async = &input.sig.asyncness;
584 let fn_inputs = &input.sig.inputs;
585 let fn_output = &input.sig.output;
586 let fn_block = &input.block;
587 let fn_generics = &input.sig.generics;
588
589 let schema_types = collect_handler_schema_types(&input);
590
591 let path_value = path.value();
592
593 if let Err(err) = validate_path_syntax(&path_value, path.span()) {
595 return err.to_compile_error().into();
596 }
597
598 if let Err(err) = validate_extractor_order(&input) {
600 return err.to_compile_error().into();
601 }
602
603 let route_fn_name = syn::Ident::new(&format!("{}_route", fn_name), fn_name.span());
605 let auto_route_name = syn::Ident::new(&format!("__AUTO_ROUTE_{}", fn_name), fn_name.span());
607
608 let schema_reg_fn_name =
610 syn::Ident::new(&format!("__{}_register_schemas", fn_name), fn_name.span());
611 let auto_schema_name = syn::Ident::new(&format!("__AUTO_SCHEMA_{}", fn_name), fn_name.span());
612
613 let route_helper = match method {
615 "GET" => quote!(#rustapi_path::get_route),
616 "POST" => quote!(#rustapi_path::post_route),
617 "PUT" => quote!(#rustapi_path::put_route),
618 "PATCH" => quote!(#rustapi_path::patch_route),
619 "DELETE" => quote!(#rustapi_path::delete_route),
620 _ => quote!(#rustapi_path::get_route),
621 };
622
623 let auto_params = collect_path_params(&input);
625
626 let mut chained_calls = quote!();
628
629 for (name, schema) in auto_params {
631 chained_calls = quote! { #chained_calls .param(#name, #schema) };
632 }
633
634 for attr in fn_attrs {
635 if let Some(ident) = attr.path().segments.last().map(|s| &s.ident) {
638 let ident_str = ident.to_string();
639 if ident_str == "tag" {
640 if let Ok(lit) = attr.parse_args::<LitStr>() {
641 let val = lit.value();
642 chained_calls = quote! { #chained_calls .tag(#val) };
643 }
644 } else if ident_str == "summary" {
645 if let Ok(lit) = attr.parse_args::<LitStr>() {
646 let val = lit.value();
647 chained_calls = quote! { #chained_calls .summary(#val) };
648 }
649 } else if ident_str == "description" {
650 if let Ok(lit) = attr.parse_args::<LitStr>() {
651 let val = lit.value();
652 chained_calls = quote! { #chained_calls .description(#val) };
653 }
654 } else if ident_str == "param" {
655 if let Ok(param_args) = attr.parse_args_with(
657 syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
658 ) {
659 let mut param_name: Option<String> = None;
660 let mut param_schema: Option<String> = None;
661
662 for meta in param_args {
663 match &meta {
664 Meta::Path(path) if param_name.is_none() => {
666 if let Some(ident) = path.get_ident() {
667 param_name = Some(ident.to_string());
668 }
669 }
670 Meta::NameValue(nv) => {
672 let key = nv.path.get_ident().map(|i| i.to_string());
673 if let Some(key) = key {
674 if key == "schema" || key == "type" {
675 if let Expr::Lit(lit) = &nv.value {
676 if let Lit::Str(s) = &lit.lit {
677 param_schema = Some(s.value());
678 }
679 }
680 } else if param_name.is_none() {
681 param_name = Some(key);
683 if let Expr::Lit(lit) = &nv.value {
684 if let Lit::Str(s) = &lit.lit {
685 param_schema = Some(s.value());
686 }
687 }
688 }
689 }
690 }
691 _ => {}
692 }
693 }
694
695 if let (Some(pname), Some(pschema)) = (param_name, param_schema) {
696 chained_calls = quote! { #chained_calls .param(#pname, #pschema) };
697 }
698 }
699 } else if ident_str == "errors" {
700 if let Ok(error_args) = attr.parse_args_with(
702 syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
703 ) {
704 for meta in error_args {
705 if let Meta::NameValue(nv) = &meta {
706 let status_str = nv.path.get_ident().map(|i| i.to_string());
709 if let Some(status_key) = status_str {
710 if let Expr::Lit(lit) = &nv.value {
712 if let Lit::Str(s) = &lit.lit {
713 let desc = s.value();
714 chained_calls = quote! {
715 #chained_calls .error_response(#status_key, #desc)
716 };
717 }
718 }
719 }
720 } else if let Meta::List(list) = &meta {
721 let _ = list;
724 }
725 }
726 }
727 if let Ok(ts) = attr.parse_args::<proc_macro2::TokenStream>() {
731 let tokens: Vec<proc_macro2::TokenTree> = ts.into_iter().collect();
732 let mut i = 0;
733 while i < tokens.len() {
734 if let proc_macro2::TokenTree::Literal(lit) = &tokens[i] {
736 let lit_str = lit.to_string();
737 if let Ok(status_code) = lit_str.parse::<u16>() {
738 if i + 2 < tokens.len() {
740 if let proc_macro2::TokenTree::Punct(p) = &tokens[i + 1] {
741 if p.as_char() == '=' {
742 if let proc_macro2::TokenTree::Literal(desc_lit) =
743 &tokens[i + 2]
744 {
745 let desc_str = desc_lit.to_string();
746 let desc = desc_str.trim_matches('"').to_string();
748 chained_calls = quote! {
749 #chained_calls .error_response(#status_code, #desc)
750 };
751 i += 3;
752 if i < tokens.len() {
754 if let proc_macro2::TokenTree::Punct(p) =
755 &tokens[i]
756 {
757 if p.as_char() == ',' {
758 i += 1;
759 }
760 }
761 }
762 continue;
763 }
764 }
765 }
766 }
767 }
768 }
769 i += 1;
770 }
771 }
772 }
773 }
774 }
775
776 let expanded = quote! {
777 #(#fn_attrs)*
779 #fn_vis #fn_async fn #fn_name #fn_generics (#fn_inputs) #fn_output #fn_block
780
781 #[doc(hidden)]
783 #fn_vis fn #route_fn_name() -> #rustapi_path::Route {
784 #route_helper(#path_value, #fn_name)
785 #chained_calls
786 }
787
788 #[doc(hidden)]
792 #[allow(non_upper_case_globals)]
793 #[#rustapi_path::__private::linkme::distributed_slice(#rustapi_path::__private::AUTO_ROUTES)]
794 #[linkme(crate = #rustapi_path::__private::linkme)]
795 static #auto_route_name: fn() -> #rustapi_path::Route = #route_fn_name;
796
797 #[doc(hidden)]
799 #[allow(non_snake_case)]
800 fn #schema_reg_fn_name(spec: &mut #rustapi_path::__private::openapi::OpenApiSpec) {
801 #( spec.register_in_place::<#schema_types>(); )*
802 }
803
804 #[doc(hidden)]
808 #[allow(non_upper_case_globals)]
809 #[#rustapi_path::__private::linkme::distributed_slice(#rustapi_path::__private::AUTO_SCHEMAS)]
811 #[linkme(crate = #rustapi_path::__private::linkme)]
812 static #auto_schema_name: fn(&mut #rustapi_path::__private::openapi::OpenApiSpec) = #schema_reg_fn_name;
813 };
814
815 debug_output(&format!("{} {}", method, path_value), &expanded);
816
817 TokenStream::from(expanded)
818}
819
820#[proc_macro_attribute]
836pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream {
837 generate_route_handler("GET", attr, item)
838}
839
840#[proc_macro_attribute]
842pub fn post(attr: TokenStream, item: TokenStream) -> TokenStream {
843 generate_route_handler("POST", attr, item)
844}
845
846#[proc_macro_attribute]
848pub fn put(attr: TokenStream, item: TokenStream) -> TokenStream {
849 generate_route_handler("PUT", attr, item)
850}
851
852#[proc_macro_attribute]
854pub fn patch(attr: TokenStream, item: TokenStream) -> TokenStream {
855 generate_route_handler("PATCH", attr, item)
856}
857
858#[proc_macro_attribute]
860pub fn delete(attr: TokenStream, item: TokenStream) -> TokenStream {
861 generate_route_handler("DELETE", attr, item)
862}
863
864#[proc_macro_attribute]
880pub fn tag(attr: TokenStream, item: TokenStream) -> TokenStream {
881 let tag = parse_macro_input!(attr as LitStr);
882 let input = parse_macro_input!(item as ItemFn);
883
884 let attrs = &input.attrs;
885 let vis = &input.vis;
886 let sig = &input.sig;
887 let block = &input.block;
888 let tag_value = tag.value();
889
890 let expanded = quote! {
892 #[doc = concat!("**Tag:** ", #tag_value)]
893 #(#attrs)*
894 #vis #sig #block
895 };
896
897 TokenStream::from(expanded)
898}
899
900#[proc_macro_attribute]
912pub fn summary(attr: TokenStream, item: TokenStream) -> TokenStream {
913 let summary = parse_macro_input!(attr as LitStr);
914 let input = parse_macro_input!(item as ItemFn);
915
916 let attrs = &input.attrs;
917 let vis = &input.vis;
918 let sig = &input.sig;
919 let block = &input.block;
920 let summary_value = summary.value();
921
922 let expanded = quote! {
924 #[doc = #summary_value]
925 #(#attrs)*
926 #vis #sig #block
927 };
928
929 TokenStream::from(expanded)
930}
931
932#[proc_macro_attribute]
944pub fn description(attr: TokenStream, item: TokenStream) -> TokenStream {
945 let desc = parse_macro_input!(attr as LitStr);
946 let input = parse_macro_input!(item as ItemFn);
947
948 let attrs = &input.attrs;
949 let vis = &input.vis;
950 let sig = &input.sig;
951 let block = &input.block;
952 let desc_value = desc.value();
953
954 let expanded = quote! {
956 #[doc = ""]
957 #[doc = #desc_value]
958 #(#attrs)*
959 #vis #sig #block
960 };
961
962 TokenStream::from(expanded)
963}
964
965#[proc_macro_attribute]
997pub fn param(_attr: TokenStream, item: TokenStream) -> TokenStream {
998 item
1001}
1002
1003#[proc_macro_attribute]
1027pub fn errors(_attr: TokenStream, item: TokenStream) -> TokenStream {
1028 item
1031}
1032
1033#[derive(Debug)]
1039struct ValidationRuleInfo {
1040 rule_type: String,
1041 params: Vec<(String, String)>,
1042 message: Option<String>,
1043 groups: Vec<String>,
1044}
1045
1046fn parse_validate_attrs(attrs: &[Attribute]) -> Vec<ValidationRuleInfo> {
1048 let mut rules = Vec::new();
1049
1050 for attr in attrs {
1051 if !attr.path().is_ident("validate") {
1052 continue;
1053 }
1054
1055 if let Ok(meta) = attr.parse_args::<Meta>() {
1057 if let Some(rule) = parse_validate_meta(&meta) {
1058 rules.push(rule);
1059 }
1060 } else if let Ok(nested) = attr
1061 .parse_args_with(syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated)
1062 {
1063 for meta in nested {
1064 if let Some(rule) = parse_validate_meta(&meta) {
1065 rules.push(rule);
1066 }
1067 }
1068 }
1069 }
1070
1071 rules
1072}
1073
1074fn parse_validate_meta(meta: &Meta) -> Option<ValidationRuleInfo> {
1076 match meta {
1077 Meta::Path(path) => {
1078 let ident = path.get_ident()?.to_string();
1080 Some(ValidationRuleInfo {
1081 rule_type: ident,
1082 params: Vec::new(),
1083 message: None,
1084 groups: Vec::new(),
1085 })
1086 }
1087 Meta::List(list) => {
1088 let rule_type = list.path.get_ident()?.to_string();
1090 let mut params = Vec::new();
1091 let mut message = None;
1092 let mut groups = Vec::new();
1093
1094 if let Ok(nested) = list.parse_args_with(
1096 syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
1097 ) {
1098 for nested_meta in nested {
1099 if let Meta::NameValue(nv) = &nested_meta {
1100 let key = nv.path.get_ident()?.to_string();
1101
1102 if key == "groups" {
1103 let vec = expr_to_string_vec(&nv.value);
1104 groups.extend(vec);
1105 } else if let Some(value) = expr_to_string(&nv.value) {
1106 if key == "message" {
1107 message = Some(value);
1108 } else if key == "group" {
1109 groups.push(value);
1110 } else {
1111 params.push((key, value));
1112 }
1113 }
1114 } else if let Meta::Path(path) = &nested_meta {
1115 if let Some(ident) = path.get_ident() {
1117 params.push((ident.to_string(), "true".to_string()));
1118 }
1119 }
1120 }
1121 }
1122
1123 Some(ValidationRuleInfo {
1124 rule_type,
1125 params,
1126 message,
1127 groups,
1128 })
1129 }
1130 Meta::NameValue(nv) => {
1131 let rule_type = nv.path.get_ident()?.to_string();
1133 let value = expr_to_string(&nv.value)?;
1134
1135 Some(ValidationRuleInfo {
1136 rule_type: rule_type.clone(),
1137 params: vec![(rule_type.clone(), value)],
1138 message: None,
1139 groups: Vec::new(),
1140 })
1141 }
1142 }
1143}
1144
1145fn expr_to_string(expr: &Expr) -> Option<String> {
1147 match expr {
1148 Expr::Lit(lit) => match &lit.lit {
1149 Lit::Str(s) => Some(s.value()),
1150 Lit::Int(i) => Some(i.base10_digits().to_string()),
1151 Lit::Float(f) => Some(f.base10_digits().to_string()),
1152 Lit::Bool(b) => Some(b.value.to_string()),
1153 _ => None,
1154 },
1155 _ => None,
1156 }
1157}
1158
1159fn expr_to_string_vec(expr: &Expr) -> Vec<String> {
1161 match expr {
1162 Expr::Array(arr) => {
1163 let mut result = Vec::new();
1164 for elem in &arr.elems {
1165 if let Some(s) = expr_to_string(elem) {
1166 result.push(s);
1167 }
1168 }
1169 result
1170 }
1171 _ => {
1172 if let Some(s) = expr_to_string(expr) {
1173 vec![s]
1174 } else {
1175 Vec::new()
1176 }
1177 }
1178 }
1179}
1180
1181fn get_validate_path() -> proc_macro2::TokenStream {
1191 let rustapi_rs_found = crate_name("rustapi-rs").or_else(|_| crate_name("rustapi_rs"));
1192
1193 if let Ok(found) = rustapi_rs_found {
1194 match found {
1195 FoundCrate::Itself => {
1196 quote! { ::rustapi_rs::__private::validate }
1197 }
1198 FoundCrate::Name(name) => {
1199 let normalized = name.replace('-', "_");
1200 let ident = syn::Ident::new(&normalized, proc_macro2::Span::call_site());
1201 quote! { ::#ident::__private::validate }
1202 }
1203 }
1204 } else if let Ok(found) =
1205 crate_name("rustapi-validate").or_else(|_| crate_name("rustapi_validate"))
1206 {
1207 match found {
1208 FoundCrate::Itself => quote! { crate },
1209 FoundCrate::Name(name) => {
1210 let normalized = name.replace('-', "_");
1211 let ident = syn::Ident::new(&normalized, proc_macro2::Span::call_site());
1212 quote! { ::#ident }
1213 }
1214 }
1215 } else {
1216 quote! { ::rustapi_validate }
1218 }
1219}
1220
1221fn get_core_path() -> proc_macro2::TokenStream {
1227 let rustapi_rs_found = crate_name("rustapi-rs").or_else(|_| crate_name("rustapi_rs"));
1228
1229 if let Ok(found) = rustapi_rs_found {
1230 match found {
1231 FoundCrate::Itself => quote! { ::rustapi_rs::__private::core },
1232 FoundCrate::Name(name) => {
1233 let normalized = name.replace('-', "_");
1234 let ident = syn::Ident::new(&normalized, proc_macro2::Span::call_site());
1235 quote! { ::#ident::__private::core }
1236 }
1237 }
1238 } else if let Ok(found) = crate_name("rustapi-core").or_else(|_| crate_name("rustapi_core")) {
1239 match found {
1240 FoundCrate::Itself => quote! { crate },
1241 FoundCrate::Name(name) => {
1242 let normalized = name.replace('-', "_");
1243 let ident = syn::Ident::new(&normalized, proc_macro2::Span::call_site());
1244 quote! { ::#ident }
1245 }
1246 }
1247 } else {
1248 quote! { ::rustapi_core }
1249 }
1250}
1251
1252fn get_async_trait_path() -> proc_macro2::TokenStream {
1258 let rustapi_rs_found = crate_name("rustapi-rs").or_else(|_| crate_name("rustapi_rs"));
1259
1260 if let Ok(found) = rustapi_rs_found {
1261 match found {
1262 FoundCrate::Itself => {
1263 quote! { ::rustapi_rs::__private::async_trait }
1264 }
1265 FoundCrate::Name(name) => {
1266 let normalized = name.replace('-', "_");
1267 let ident = syn::Ident::new(&normalized, proc_macro2::Span::call_site());
1268 quote! { ::#ident::__private::async_trait }
1269 }
1270 }
1271 } else if let Ok(found) = crate_name("async-trait").or_else(|_| crate_name("async_trait")) {
1272 match found {
1273 FoundCrate::Itself => quote! { crate },
1274 FoundCrate::Name(name) => {
1275 let normalized = name.replace('-', "_");
1276 let ident = syn::Ident::new(&normalized, proc_macro2::Span::call_site());
1277 quote! { ::#ident }
1278 }
1279 }
1280 } else {
1281 quote! { ::async_trait }
1282 }
1283}
1284
1285fn generate_rule_validation(
1286 field_name: &str,
1287 _field_type: &Type,
1288 rule: &ValidationRuleInfo,
1289 validate_path: &proc_macro2::TokenStream,
1290) -> proc_macro2::TokenStream {
1291 let field_ident = syn::Ident::new(field_name, proc_macro2::Span::call_site());
1292 let field_name_str = field_name;
1293
1294 let group_check = if rule.groups.is_empty() {
1296 quote! { true }
1297 } else {
1298 let group_names = rule.groups.iter().map(|g| g.as_str());
1299 quote! {
1300 {
1301 let rule_groups = [#(#validate_path::v2::ValidationGroup::from(#group_names)),*];
1302 rule_groups.iter().any(|g| g.matches(&group))
1303 }
1304 }
1305 };
1306
1307 let validation_logic = match rule.rule_type.as_str() {
1308 "email" => {
1309 let message = rule
1310 .message
1311 .as_ref()
1312 .map(|m| quote! { .with_message(#m) })
1313 .unwrap_or_default();
1314 quote! {
1315 {
1316 let rule = #validate_path::v2::EmailRule::new() #message;
1317 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1318 errors.add(#field_name_str, e);
1319 }
1320 }
1321 }
1322 }
1323 "length" => {
1324 let min = rule
1325 .params
1326 .iter()
1327 .find(|(k, _)| k == "min")
1328 .and_then(|(_, v)| v.parse::<usize>().ok());
1329 let max = rule
1330 .params
1331 .iter()
1332 .find(|(k, _)| k == "max")
1333 .and_then(|(_, v)| v.parse::<usize>().ok());
1334 let message = rule
1335 .message
1336 .as_ref()
1337 .map(|m| quote! { .with_message(#m) })
1338 .unwrap_or_default();
1339
1340 let rule_creation = match (min, max) {
1341 (Some(min), Some(max)) => {
1342 quote! { #validate_path::v2::LengthRule::new(#min, #max) }
1343 }
1344 (Some(min), None) => quote! { #validate_path::v2::LengthRule::min(#min) },
1345 (None, Some(max)) => quote! { #validate_path::v2::LengthRule::max(#max) },
1346 (None, None) => quote! { #validate_path::v2::LengthRule::new(0, usize::MAX) },
1347 };
1348
1349 quote! {
1350 {
1351 let rule = #rule_creation #message;
1352 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1353 errors.add(#field_name_str, e);
1354 }
1355 }
1356 }
1357 }
1358 "range" => {
1359 let min = rule
1360 .params
1361 .iter()
1362 .find(|(k, _)| k == "min")
1363 .map(|(_, v)| v.clone());
1364 let max = rule
1365 .params
1366 .iter()
1367 .find(|(k, _)| k == "max")
1368 .map(|(_, v)| v.clone());
1369 let message = rule
1370 .message
1371 .as_ref()
1372 .map(|m| quote! { .with_message(#m) })
1373 .unwrap_or_default();
1374
1375 let rule_creation = match (min, max) {
1377 (Some(min), Some(max)) => {
1378 let min_lit: proc_macro2::TokenStream = min.parse().unwrap();
1379 let max_lit: proc_macro2::TokenStream = max.parse().unwrap();
1380 quote! { #validate_path::v2::RangeRule::new(#min_lit, #max_lit) }
1381 }
1382 (Some(min), None) => {
1383 let min_lit: proc_macro2::TokenStream = min.parse().unwrap();
1384 quote! { #validate_path::v2::RangeRule::min(#min_lit) }
1385 }
1386 (None, Some(max)) => {
1387 let max_lit: proc_macro2::TokenStream = max.parse().unwrap();
1388 quote! { #validate_path::v2::RangeRule::max(#max_lit) }
1389 }
1390 (None, None) => {
1391 return quote! {};
1392 }
1393 };
1394
1395 quote! {
1396 {
1397 let rule = #rule_creation #message;
1398 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1399 errors.add(#field_name_str, e);
1400 }
1401 }
1402 }
1403 }
1404 "regex" => {
1405 let pattern = rule
1406 .params
1407 .iter()
1408 .find(|(k, _)| k == "regex" || k == "pattern")
1409 .map(|(_, v)| v.clone())
1410 .unwrap_or_default();
1411 let message = rule
1412 .message
1413 .as_ref()
1414 .map(|m| quote! { .with_message(#m) })
1415 .unwrap_or_default();
1416
1417 quote! {
1418 {
1419 let rule = #validate_path::v2::RegexRule::new(#pattern) #message;
1420 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1421 errors.add(#field_name_str, e);
1422 }
1423 }
1424 }
1425 }
1426 "url" => {
1427 let message = rule
1428 .message
1429 .as_ref()
1430 .map(|m| quote! { .with_message(#m) })
1431 .unwrap_or_default();
1432 quote! {
1433 {
1434 let rule = #validate_path::v2::UrlRule::new() #message;
1435 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1436 errors.add(#field_name_str, e);
1437 }
1438 }
1439 }
1440 }
1441 "required" => {
1442 let message = rule
1443 .message
1444 .as_ref()
1445 .map(|m| quote! { .with_message(#m) })
1446 .unwrap_or_default();
1447 quote! {
1448 {
1449 let rule = #validate_path::v2::RequiredRule::new() #message;
1450 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1451 errors.add(#field_name_str, e);
1452 }
1453 }
1454 }
1455 }
1456 "credit_card" => {
1457 let message = rule
1458 .message
1459 .as_ref()
1460 .map(|m| quote! { .with_message(#m) })
1461 .unwrap_or_default();
1462 quote! {
1463 {
1464 let rule = #validate_path::v2::CreditCardRule::new() #message;
1465 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1466 errors.add(#field_name_str, e);
1467 }
1468 }
1469 }
1470 }
1471 "ip" => {
1472 let v4 = rule.params.iter().any(|(k, _)| k == "v4");
1473 let v6 = rule.params.iter().any(|(k, _)| k == "v6");
1474
1475 let rule_creation = if v4 && !v6 {
1476 quote! { #validate_path::v2::IpRule::v4() }
1477 } else if !v4 && v6 {
1478 quote! { #validate_path::v2::IpRule::v6() }
1479 } else {
1480 quote! { #validate_path::v2::IpRule::new() }
1481 };
1482
1483 let message = rule
1484 .message
1485 .as_ref()
1486 .map(|m| quote! { .with_message(#m) })
1487 .unwrap_or_default();
1488
1489 quote! {
1490 {
1491 let rule = #rule_creation #message;
1492 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1493 errors.add(#field_name_str, e);
1494 }
1495 }
1496 }
1497 }
1498 "phone" => {
1499 let message = rule
1500 .message
1501 .as_ref()
1502 .map(|m| quote! { .with_message(#m) })
1503 .unwrap_or_default();
1504 quote! {
1505 {
1506 let rule = #validate_path::v2::PhoneRule::new() #message;
1507 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1508 errors.add(#field_name_str, e);
1509 }
1510 }
1511 }
1512 }
1513 "contains" => {
1514 let needle = rule
1515 .params
1516 .iter()
1517 .find(|(k, _)| k == "needle")
1518 .map(|(_, v)| v.clone())
1519 .unwrap_or_default();
1520
1521 let message = rule
1522 .message
1523 .as_ref()
1524 .map(|m| quote! { .with_message(#m) })
1525 .unwrap_or_default();
1526
1527 quote! {
1528 {
1529 let rule = #validate_path::v2::ContainsRule::new(#needle) #message;
1530 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1531 errors.add(#field_name_str, e);
1532 }
1533 }
1534 }
1535 }
1536 _ => {
1537 quote! {}
1539 }
1540 };
1541
1542 quote! {
1543 if #group_check {
1544 #validation_logic
1545 }
1546 }
1547}
1548
1549fn generate_async_rule_validation(
1551 field_name: &str,
1552 rule: &ValidationRuleInfo,
1553 validate_path: &proc_macro2::TokenStream,
1554) -> proc_macro2::TokenStream {
1555 let field_ident = syn::Ident::new(field_name, proc_macro2::Span::call_site());
1556 let field_name_str = field_name;
1557
1558 let group_check = if rule.groups.is_empty() {
1560 quote! { true }
1561 } else {
1562 let group_names = rule.groups.iter().map(|g| g.as_str());
1563 quote! {
1564 {
1565 let rule_groups = [#(#validate_path::v2::ValidationGroup::from(#group_names)),*];
1566 rule_groups.iter().any(|g| g.matches(&group))
1567 }
1568 }
1569 };
1570
1571 let validation_logic = match rule.rule_type.as_str() {
1572 "async_unique" => {
1573 let table = rule
1574 .params
1575 .iter()
1576 .find(|(k, _)| k == "table")
1577 .map(|(_, v)| v.clone())
1578 .unwrap_or_default();
1579 let column = rule
1580 .params
1581 .iter()
1582 .find(|(k, _)| k == "column")
1583 .map(|(_, v)| v.clone())
1584 .unwrap_or_default();
1585 let message = rule
1586 .message
1587 .as_ref()
1588 .map(|m| quote! { .with_message(#m) })
1589 .unwrap_or_default();
1590
1591 quote! {
1592 {
1593 let rule = #validate_path::v2::AsyncUniqueRule::new(#table, #column) #message;
1594 if let Err(e) = #validate_path::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
1595 errors.add(#field_name_str, e);
1596 }
1597 }
1598 }
1599 }
1600 "async_exists" => {
1601 let table = rule
1602 .params
1603 .iter()
1604 .find(|(k, _)| k == "table")
1605 .map(|(_, v)| v.clone())
1606 .unwrap_or_default();
1607 let column = rule
1608 .params
1609 .iter()
1610 .find(|(k, _)| k == "column")
1611 .map(|(_, v)| v.clone())
1612 .unwrap_or_default();
1613 let message = rule
1614 .message
1615 .as_ref()
1616 .map(|m| quote! { .with_message(#m) })
1617 .unwrap_or_default();
1618
1619 quote! {
1620 {
1621 let rule = #validate_path::v2::AsyncExistsRule::new(#table, #column) #message;
1622 if let Err(e) = #validate_path::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
1623 errors.add(#field_name_str, e);
1624 }
1625 }
1626 }
1627 }
1628 "async_api" => {
1629 let endpoint = rule
1630 .params
1631 .iter()
1632 .find(|(k, _)| k == "endpoint")
1633 .map(|(_, v)| v.clone())
1634 .unwrap_or_default();
1635 let message = rule
1636 .message
1637 .as_ref()
1638 .map(|m| quote! { .with_message(#m) })
1639 .unwrap_or_default();
1640
1641 quote! {
1642 {
1643 let rule = #validate_path::v2::AsyncApiRule::new(#endpoint) #message;
1644 if let Err(e) = #validate_path::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
1645 errors.add(#field_name_str, e);
1646 }
1647 }
1648 }
1649 }
1650 "custom_async" => {
1651 let function_path = rule
1653 .params
1654 .iter()
1655 .find(|(k, _)| k == "custom_async" || k == "function")
1656 .map(|(_, v)| v.clone())
1657 .unwrap_or_default();
1658
1659 if function_path.is_empty() {
1660 quote! {}
1662 } else {
1663 let func: syn::Path = syn::parse_str(&function_path).unwrap();
1664 let message_handling = if let Some(msg) = &rule.message {
1665 quote! {
1666 let e = #validate_path::v2::RuleError::new("custom_async", #msg);
1667 errors.add(#field_name_str, e);
1668 }
1669 } else {
1670 quote! {
1671 errors.add(#field_name_str, e);
1672 }
1673 };
1674
1675 quote! {
1676 {
1677 if let Err(e) = #func(&self.#field_ident, ctx).await {
1679 #message_handling
1680 }
1681 }
1682 }
1683 }
1684 }
1685 _ => {
1686 quote! {}
1688 }
1689 };
1690
1691 quote! {
1692 if #group_check {
1693 #validation_logic
1694 }
1695 }
1696}
1697
1698fn is_async_rule(rule: &ValidationRuleInfo) -> bool {
1700 matches!(
1701 rule.rule_type.as_str(),
1702 "async_unique" | "async_exists" | "async_api" | "custom_async"
1703 )
1704}
1705
1706#[proc_macro_derive(Validate, attributes(validate))]
1729pub fn derive_validate(input: TokenStream) -> TokenStream {
1730 let input = parse_macro_input!(input as DeriveInput);
1731 let name = &input.ident;
1732 let generics = &input.generics;
1733 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
1734
1735 let fields = match &input.data {
1737 Data::Struct(data) => match &data.fields {
1738 Fields::Named(fields) => &fields.named,
1739 _ => {
1740 return syn::Error::new_spanned(
1741 &input,
1742 "Validate can only be derived for structs with named fields",
1743 )
1744 .to_compile_error()
1745 .into();
1746 }
1747 },
1748 _ => {
1749 return syn::Error::new_spanned(&input, "Validate can only be derived for structs")
1750 .to_compile_error()
1751 .into();
1752 }
1753 };
1754
1755 let validate_path = get_validate_path();
1757 let core_path = get_core_path();
1758 let async_trait_path = get_async_trait_path();
1759
1760 let mut sync_validations = Vec::new();
1762 let mut async_validations = Vec::new();
1763 let mut has_async_rules = false;
1764
1765 for field in fields {
1766 let field_name = field.ident.as_ref().unwrap().to_string();
1767 let field_type = &field.ty;
1768 let rules = parse_validate_attrs(&field.attrs);
1769
1770 for rule in &rules {
1771 if is_async_rule(rule) {
1772 has_async_rules = true;
1773 let validation = generate_async_rule_validation(&field_name, rule, &validate_path);
1774 async_validations.push(validation);
1775 } else {
1776 let validation =
1777 generate_rule_validation(&field_name, field_type, rule, &validate_path);
1778 sync_validations.push(validation);
1779 }
1780 }
1781 }
1782
1783 let validate_impl = quote! {
1785 impl #impl_generics #validate_path::v2::Validate for #name #ty_generics #where_clause {
1786 fn validate_with_group(&self, group: #validate_path::v2::ValidationGroup) -> Result<(), #validate_path::v2::ValidationErrors> {
1787 let mut errors = #validate_path::v2::ValidationErrors::new();
1788
1789 #(#sync_validations)*
1790
1791 errors.into_result()
1792 }
1793 }
1794 };
1795
1796 let async_validate_impl = if has_async_rules {
1798 quote! {
1799 #[#async_trait_path::async_trait]
1800 impl #impl_generics #validate_path::v2::AsyncValidate for #name #ty_generics #where_clause {
1801 async fn validate_async_with_group(&self, ctx: &#validate_path::v2::ValidationContext, group: #validate_path::v2::ValidationGroup) -> Result<(), #validate_path::v2::ValidationErrors> {
1802 let mut errors = #validate_path::v2::ValidationErrors::new();
1803
1804 #(#async_validations)*
1805
1806 errors.into_result()
1807 }
1808 }
1809 }
1810 } else {
1811 quote! {
1813 #[#async_trait_path::async_trait]
1814 impl #impl_generics #validate_path::v2::AsyncValidate for #name #ty_generics #where_clause {
1815 async fn validate_async_with_group(&self, _ctx: &#validate_path::v2::ValidationContext, _group: #validate_path::v2::ValidationGroup) -> Result<(), #validate_path::v2::ValidationErrors> {
1816 Ok(())
1817 }
1818 }
1819 }
1820 };
1821
1822 let validatable_impl = quote! {
1825 impl #impl_generics #core_path::validation::Validatable for #name #ty_generics #where_clause {
1826 fn do_validate(&self) -> Result<(), #core_path::ApiError> {
1827 match #validate_path::v2::Validate::validate(self) {
1828 Ok(_) => Ok(()),
1829 Err(e) => Err(#core_path::validation::convert_v2_errors(e)),
1830 }
1831 }
1832 }
1833 };
1834
1835 let expanded = quote! {
1836 #validate_impl
1837 #async_validate_impl
1838 #validatable_impl
1839 };
1840
1841 debug_output("Validate derive", &expanded);
1842
1843 TokenStream::from(expanded)
1844}
1845
1846#[proc_macro_derive(ApiError, attributes(error))]
1865pub fn derive_api_error(input: TokenStream) -> TokenStream {
1866 api_error::expand_derive_api_error(input)
1867}
1868
1869#[proc_macro_derive(TypedPath, attributes(typed_path))]
1886pub fn derive_typed_path(input: TokenStream) -> TokenStream {
1887 let input = parse_macro_input!(input as DeriveInput);
1888 let name = &input.ident;
1889 let generics = &input.generics;
1890 let rustapi_path = get_rustapi_path();
1891 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
1892
1893 let mut path_str = None;
1895 for attr in &input.attrs {
1896 if attr.path().is_ident("typed_path") {
1897 if let Ok(lit) = attr.parse_args::<LitStr>() {
1898 path_str = Some(lit.value());
1899 }
1900 }
1901 }
1902
1903 let path = match path_str {
1904 Some(p) => p,
1905 None => {
1906 return syn::Error::new_spanned(
1907 &input,
1908 "#[derive(TypedPath)] requires a #[typed_path(\"...\")] attribute",
1909 )
1910 .to_compile_error()
1911 .into();
1912 }
1913 };
1914
1915 if let Err(err) = validate_path_syntax(&path, proc_macro2::Span::call_site()) {
1917 return err.to_compile_error().into();
1918 }
1919
1920 let mut format_string = String::new();
1923 let mut format_args = Vec::new();
1924
1925 let mut chars = path.chars().peekable();
1926 while let Some(ch) = chars.next() {
1927 if ch == '{' {
1928 let mut param_name = String::new();
1929 while let Some(&c) = chars.peek() {
1930 if c == '}' {
1931 chars.next(); break;
1933 }
1934 param_name.push(chars.next().unwrap());
1935 }
1936
1937 if param_name.is_empty() {
1938 return syn::Error::new_spanned(
1939 &input,
1940 "Empty path parameter not allowed in typed_path",
1941 )
1942 .to_compile_error()
1943 .into();
1944 }
1945
1946 format_string.push_str("{}");
1947 let ident = syn::Ident::new(¶m_name, proc_macro2::Span::call_site());
1948 format_args.push(quote! { self.#ident });
1949 } else {
1950 format_string.push(ch);
1951 }
1952 }
1953
1954 let expanded = quote! {
1955 impl #impl_generics #rustapi_path::prelude::TypedPath for #name #ty_generics #where_clause {
1956 const PATH: &'static str = #path;
1957
1958 fn to_uri(&self) -> String {
1959 format!(#format_string, #(#format_args),*)
1960 }
1961 }
1962 };
1963
1964 debug_output("TypedPath derive", &expanded);
1965 TokenStream::from(expanded)
1966}