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)]
118 #[linkme(crate = #rustapi_path::__private::linkme)]
119 static #registrar_ident: fn(&mut #rustapi_path::__private::openapi::OpenApiSpec) =
120 |spec: &mut #rustapi_path::__private::openapi::OpenApiSpec| {
121 spec.register_in_place::<#ident>();
122 };
123 };
124
125 debug_output("schema", &expanded);
126 expanded.into()
127}
128
129fn extract_schema_types(ty: &Type, out: &mut Vec<Type>, allow_leaf: bool) {
130 match ty {
131 Type::Reference(r) => extract_schema_types(&r.elem, out, allow_leaf),
132 Type::Path(tp) => {
133 let Some(seg) = tp.path.segments.last() else {
134 return;
135 };
136
137 let ident = seg.ident.to_string();
138
139 let unwrap_first_generic = |out: &mut Vec<Type>| {
140 if let PathArguments::AngleBracketed(args) = &seg.arguments {
141 if let Some(GenericArgument::Type(inner)) = args.args.first() {
142 extract_schema_types(inner, out, true);
143 }
144 }
145 };
146
147 match ident.as_str() {
148 "Json" | "ValidatedJson" | "Created" => {
150 unwrap_first_generic(out);
151 }
152 "WithStatus" => {
154 if let PathArguments::AngleBracketed(args) = &seg.arguments {
155 if let Some(GenericArgument::Type(inner)) = args.args.first() {
156 extract_schema_types(inner, out, true);
157 }
158 }
159 }
160 "Option" | "Result" => {
162 if let PathArguments::AngleBracketed(args) = &seg.arguments {
163 if let Some(GenericArgument::Type(inner)) = args.args.first() {
164 extract_schema_types(inner, out, allow_leaf);
165 }
166 }
167 }
168 _ => {
169 if allow_leaf {
170 out.push(ty.clone());
171 }
172 }
173 }
174 }
175 _ => {}
176 }
177}
178
179fn collect_handler_schema_types(input: &ItemFn) -> Vec<Type> {
180 let mut found: Vec<Type> = Vec::new();
181
182 for arg in &input.sig.inputs {
183 if let FnArg::Typed(pat_ty) = arg {
184 extract_schema_types(&pat_ty.ty, &mut found, false);
185 }
186 }
187
188 if let ReturnType::Type(_, ty) = &input.sig.output {
189 extract_schema_types(ty, &mut found, false);
190 }
191
192 let mut seen = HashSet::<String>::new();
194 found
195 .into_iter()
196 .filter(|t| seen.insert(quote!(#t).to_string()))
197 .collect()
198}
199
200fn collect_path_params(input: &ItemFn) -> Vec<(String, String)> {
204 let mut params = Vec::new();
205
206 for arg in &input.sig.inputs {
207 if let FnArg::Typed(pat_ty) = arg {
208 if let Type::Path(tp) = &*pat_ty.ty {
210 if let Some(seg) = tp.path.segments.last() {
211 if seg.ident == "Path" {
212 if let PathArguments::AngleBracketed(args) = &seg.arguments {
214 if let Some(GenericArgument::Type(inner_ty)) = args.args.first() {
215 if let Some(schema_type) = map_type_to_schema(inner_ty) {
217 if let Some(name) = extract_param_name(&pat_ty.pat) {
225 params.push((name, schema_type));
226 }
227 }
228 }
229 }
230 }
231 }
232 }
233 }
234 }
235
236 params
237}
238
239fn extract_param_name(pat: &syn::Pat) -> Option<String> {
244 match pat {
245 syn::Pat::Ident(ident) => Some(ident.ident.to_string()),
246 syn::Pat::TupleStruct(ts) => {
247 if let Some(first) = ts.elems.first() {
250 extract_param_name(first)
251 } else {
252 None
253 }
254 }
255 _ => None, }
257}
258
259fn map_type_to_schema(ty: &Type) -> Option<String> {
261 match ty {
262 Type::Path(tp) => {
263 if let Some(seg) = tp.path.segments.last() {
264 let ident = seg.ident.to_string();
265 match ident.as_str() {
266 "Uuid" => Some("uuid".to_string()),
267 "String" | "str" => Some("string".to_string()),
268 "bool" => Some("boolean".to_string()),
269 "i8" | "i16" | "i32" | "i64" | "isize" | "u8" | "u16" | "u32" | "u64"
270 | "usize" => Some("integer".to_string()),
271 "f32" | "f64" => Some("number".to_string()),
272 _ => None,
273 }
274 } else {
275 None
276 }
277 }
278 _ => None,
279 }
280}
281
282fn is_debug_enabled() -> bool {
284 std::env::var("RUSTAPI_DEBUG")
285 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
286 .unwrap_or(false)
287}
288
289fn debug_output(name: &str, tokens: &proc_macro2::TokenStream) {
291 if is_debug_enabled() {
292 eprintln!("\n=== RUSTAPI_DEBUG: {} ===", name);
293 eprintln!("{}", tokens);
294 eprintln!("=== END {} ===\n", name);
295 }
296}
297
298fn validate_path_syntax(path: &str, span: proc_macro2::Span) -> Result<(), syn::Error> {
302 if !path.starts_with('/') {
304 return Err(syn::Error::new(
305 span,
306 format!("route path must start with '/', got: \"{}\"", path),
307 ));
308 }
309
310 if path.contains("//") {
312 return Err(syn::Error::new(
313 span,
314 format!(
315 "route path contains empty segment (double slash): \"{}\"",
316 path
317 ),
318 ));
319 }
320
321 let mut brace_depth = 0;
323 let mut param_start = None;
324
325 for (i, ch) in path.char_indices() {
326 match ch {
327 '{' => {
328 if brace_depth > 0 {
329 return Err(syn::Error::new(
330 span,
331 format!(
332 "nested braces are not allowed in route path at position {}: \"{}\"",
333 i, path
334 ),
335 ));
336 }
337 brace_depth += 1;
338 param_start = Some(i);
339 }
340 '}' => {
341 if brace_depth == 0 {
342 return Err(syn::Error::new(
343 span,
344 format!(
345 "unmatched closing brace '}}' at position {} in route path: \"{}\"",
346 i, path
347 ),
348 ));
349 }
350 brace_depth -= 1;
351
352 if let Some(start) = param_start {
354 let param_name = &path[start + 1..i];
355 if param_name.is_empty() {
356 return Err(syn::Error::new(
357 span,
358 format!(
359 "empty parameter name '{{}}' at position {} in route path: \"{}\"",
360 start, path
361 ),
362 ));
363 }
364 if !param_name.chars().all(|c| c.is_alphanumeric() || c == '_') {
366 return Err(syn::Error::new(
367 span,
368 format!(
369 "invalid parameter name '{{{}}}' at position {} - parameter names must contain only alphanumeric characters and underscores: \"{}\"",
370 param_name, start, path
371 ),
372 ));
373 }
374 if param_name
376 .chars()
377 .next()
378 .map(|c| c.is_ascii_digit())
379 .unwrap_or(false)
380 {
381 return Err(syn::Error::new(
382 span,
383 format!(
384 "parameter name '{{{}}}' cannot start with a digit at position {}: \"{}\"",
385 param_name, start, path
386 ),
387 ));
388 }
389 }
390 param_start = None;
391 }
392 _ if brace_depth == 0 => {
394 if !ch.is_alphanumeric() && !"-_./*".contains(ch) {
396 return Err(syn::Error::new(
397 span,
398 format!(
399 "invalid character '{}' at position {} in route path: \"{}\"",
400 ch, i, path
401 ),
402 ));
403 }
404 }
405 _ => {}
406 }
407 }
408
409 if brace_depth > 0 {
411 return Err(syn::Error::new(
412 span,
413 format!(
414 "unclosed brace '{{' in route path (missing closing '}}'): \"{}\"",
415 path
416 ),
417 ));
418 }
419
420 Ok(())
421}
422
423#[proc_macro_attribute]
441pub fn main(_attr: TokenStream, item: TokenStream) -> TokenStream {
442 let input = parse_macro_input!(item as ItemFn);
443
444 let attrs = &input.attrs;
445 let vis = &input.vis;
446 let sig = &input.sig;
447 let block = &input.block;
448
449 let expanded = quote! {
450 #(#attrs)*
451 #[::tokio::main]
452 #vis #sig {
453 #block
454 }
455 };
456
457 debug_output("main", &expanded);
458
459 TokenStream::from(expanded)
460}
461
462fn is_body_consuming_type(ty: &Type) -> bool {
468 match ty {
469 Type::Path(tp) => {
470 if let Some(seg) = tp.path.segments.last() {
471 matches!(
472 seg.ident.to_string().as_str(),
473 "Json" | "Body" | "ValidatedJson" | "AsyncValidatedJson" | "Multipart"
474 )
475 } else {
476 false
477 }
478 }
479 _ => false,
480 }
481}
482
483fn validate_extractor_order(input: &ItemFn) -> Result<(), syn::Error> {
489 let params: Vec<_> = input
490 .sig
491 .inputs
492 .iter()
493 .filter_map(|arg| {
494 if let FnArg::Typed(pat_ty) = arg {
495 Some(pat_ty)
496 } else {
497 None
498 }
499 })
500 .collect();
501
502 if params.is_empty() {
503 return Ok(());
504 }
505
506 let body_indices: Vec<usize> = params
508 .iter()
509 .enumerate()
510 .filter(|(_, p)| is_body_consuming_type(&p.ty))
511 .map(|(i, _)| i)
512 .collect();
513
514 if body_indices.is_empty() {
515 return Ok(());
516 }
517
518 let last_non_body = params
520 .iter()
521 .enumerate()
522 .filter(|(_, p)| !is_body_consuming_type(&p.ty))
523 .map(|(i, _)| i)
524 .max();
525
526 if let Some(last_non_body_idx) = last_non_body {
528 let first_body_idx = body_indices[0];
529 if first_body_idx < last_non_body_idx {
530 let offending_param = ¶ms[first_body_idx];
531 let ty_name = quote!(#offending_param).to_string();
532 return Err(syn::Error::new_spanned(
533 &offending_param.ty,
534 format!(
535 "Body-consuming extractor must be the LAST parameter.\n\
536 \n\
537 Found `{}` before non-body extractor(s).\n\
538 \n\
539 Body extractors (Json, Body, ValidatedJson, AsyncValidatedJson, Multipart) \
540 consume the request body, which can only be read once. Place them after all \
541 non-body extractors (State, Path, Query, Headers, etc.).\n\
542 \n\
543 Example:\n\
544 \x20 async fn handler(\n\
545 \x20 State(db): State<AppState>, // non-body: OK first\n\
546 \x20 Path(id): Path<i64>, // non-body: OK second\n\
547 \x20 Json(body): Json<CreateUser>, // body: MUST be last\n\
548 \x20 ) -> Result<Json<User>> {{ ... }}",
549 ty_name,
550 ),
551 ));
552 }
553 }
554
555 if body_indices.len() > 1 {
557 let second_body_param = ¶ms[body_indices[1]];
558 return Err(syn::Error::new_spanned(
559 &second_body_param.ty,
560 "Multiple body-consuming extractors detected.\n\
561 \n\
562 Only ONE body-consuming extractor (Json, Body, ValidatedJson, AsyncValidatedJson, \
563 Multipart) is allowed per handler, because the request body can only be consumed once.\n\
564 \n\
565 Remove the extra body extractor or combine the data into a single type.",
566 ));
567 }
568
569 Ok(())
570}
571
572fn generate_route_handler(method: &str, attr: TokenStream, item: TokenStream) -> TokenStream {
574 let path = parse_macro_input!(attr as LitStr);
575 let input = parse_macro_input!(item as ItemFn);
576 let rustapi_path = get_rustapi_path();
577
578 let fn_name = &input.sig.ident;
579 let fn_vis = &input.vis;
580 let fn_attrs = &input.attrs;
581 let fn_async = &input.sig.asyncness;
582 let fn_inputs = &input.sig.inputs;
583 let fn_output = &input.sig.output;
584 let fn_block = &input.block;
585 let fn_generics = &input.sig.generics;
586
587 let schema_types = collect_handler_schema_types(&input);
588
589 let path_value = path.value();
590
591 if let Err(err) = validate_path_syntax(&path_value, path.span()) {
593 return err.to_compile_error().into();
594 }
595
596 if let Err(err) = validate_extractor_order(&input) {
598 return err.to_compile_error().into();
599 }
600
601 let route_fn_name = syn::Ident::new(&format!("{}_route", fn_name), fn_name.span());
603 let auto_route_name = syn::Ident::new(&format!("__AUTO_ROUTE_{}", fn_name), fn_name.span());
605
606 let schema_reg_fn_name =
608 syn::Ident::new(&format!("__{}_register_schemas", fn_name), fn_name.span());
609 let auto_schema_name = syn::Ident::new(&format!("__AUTO_SCHEMA_{}", fn_name), fn_name.span());
610
611 let route_helper = match method {
613 "GET" => quote!(#rustapi_path::get_route),
614 "POST" => quote!(#rustapi_path::post_route),
615 "PUT" => quote!(#rustapi_path::put_route),
616 "PATCH" => quote!(#rustapi_path::patch_route),
617 "DELETE" => quote!(#rustapi_path::delete_route),
618 _ => quote!(#rustapi_path::get_route),
619 };
620
621 let auto_params = collect_path_params(&input);
623
624 let mut chained_calls = quote!();
626
627 for (name, schema) in auto_params {
629 chained_calls = quote! { #chained_calls .param(#name, #schema) };
630 }
631
632 for attr in fn_attrs {
633 if let Some(ident) = attr.path().segments.last().map(|s| &s.ident) {
636 let ident_str = ident.to_string();
637 if ident_str == "tag" {
638 if let Ok(lit) = attr.parse_args::<LitStr>() {
639 let val = lit.value();
640 chained_calls = quote! { #chained_calls .tag(#val) };
641 }
642 } else if ident_str == "summary" {
643 if let Ok(lit) = attr.parse_args::<LitStr>() {
644 let val = lit.value();
645 chained_calls = quote! { #chained_calls .summary(#val) };
646 }
647 } else if ident_str == "description" {
648 if let Ok(lit) = attr.parse_args::<LitStr>() {
649 let val = lit.value();
650 chained_calls = quote! { #chained_calls .description(#val) };
651 }
652 } else if ident_str == "param" {
653 if let Ok(param_args) = attr.parse_args_with(
655 syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
656 ) {
657 let mut param_name: Option<String> = None;
658 let mut param_schema: Option<String> = None;
659
660 for meta in param_args {
661 match &meta {
662 Meta::Path(path) => {
664 if param_name.is_none() {
665 if let Some(ident) = path.get_ident() {
666 param_name = Some(ident.to_string());
667 }
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)]
790 #[allow(non_upper_case_globals)]
791 #[#rustapi_path::__private::linkme::distributed_slice(#rustapi_path::__private::AUTO_ROUTES)]
792 #[linkme(crate = #rustapi_path::__private::linkme)]
793 static #auto_route_name: fn() -> #rustapi_path::Route = #route_fn_name;
794
795 #[doc(hidden)]
797 #[allow(non_snake_case)]
798 fn #schema_reg_fn_name(spec: &mut #rustapi_path::__private::openapi::OpenApiSpec) {
799 #( spec.register_in_place::<#schema_types>(); )*
800 }
801
802 #[doc(hidden)]
803 #[allow(non_upper_case_globals)]
804 #[#rustapi_path::__private::linkme::distributed_slice(#rustapi_path::__private::AUTO_SCHEMAS)]
805 #[linkme(crate = #rustapi_path::__private::linkme)]
806 static #auto_schema_name: fn(&mut #rustapi_path::__private::openapi::OpenApiSpec) = #schema_reg_fn_name;
807 };
808
809 debug_output(&format!("{} {}", method, path_value), &expanded);
810
811 TokenStream::from(expanded)
812}
813
814#[proc_macro_attribute]
830pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream {
831 generate_route_handler("GET", attr, item)
832}
833
834#[proc_macro_attribute]
836pub fn post(attr: TokenStream, item: TokenStream) -> TokenStream {
837 generate_route_handler("POST", attr, item)
838}
839
840#[proc_macro_attribute]
842pub fn put(attr: TokenStream, item: TokenStream) -> TokenStream {
843 generate_route_handler("PUT", attr, item)
844}
845
846#[proc_macro_attribute]
848pub fn patch(attr: TokenStream, item: TokenStream) -> TokenStream {
849 generate_route_handler("PATCH", attr, item)
850}
851
852#[proc_macro_attribute]
854pub fn delete(attr: TokenStream, item: TokenStream) -> TokenStream {
855 generate_route_handler("DELETE", attr, item)
856}
857
858#[proc_macro_attribute]
874pub fn tag(attr: TokenStream, item: TokenStream) -> TokenStream {
875 let tag = parse_macro_input!(attr as LitStr);
876 let input = parse_macro_input!(item as ItemFn);
877
878 let attrs = &input.attrs;
879 let vis = &input.vis;
880 let sig = &input.sig;
881 let block = &input.block;
882 let tag_value = tag.value();
883
884 let expanded = quote! {
886 #[doc = concat!("**Tag:** ", #tag_value)]
887 #(#attrs)*
888 #vis #sig #block
889 };
890
891 TokenStream::from(expanded)
892}
893
894#[proc_macro_attribute]
906pub fn summary(attr: TokenStream, item: TokenStream) -> TokenStream {
907 let summary = parse_macro_input!(attr as LitStr);
908 let input = parse_macro_input!(item as ItemFn);
909
910 let attrs = &input.attrs;
911 let vis = &input.vis;
912 let sig = &input.sig;
913 let block = &input.block;
914 let summary_value = summary.value();
915
916 let expanded = quote! {
918 #[doc = #summary_value]
919 #(#attrs)*
920 #vis #sig #block
921 };
922
923 TokenStream::from(expanded)
924}
925
926#[proc_macro_attribute]
938pub fn description(attr: TokenStream, item: TokenStream) -> TokenStream {
939 let desc = parse_macro_input!(attr as LitStr);
940 let input = parse_macro_input!(item as ItemFn);
941
942 let attrs = &input.attrs;
943 let vis = &input.vis;
944 let sig = &input.sig;
945 let block = &input.block;
946 let desc_value = desc.value();
947
948 let expanded = quote! {
950 #[doc = ""]
951 #[doc = #desc_value]
952 #(#attrs)*
953 #vis #sig #block
954 };
955
956 TokenStream::from(expanded)
957}
958
959#[proc_macro_attribute]
991pub fn param(_attr: TokenStream, item: TokenStream) -> TokenStream {
992 item
995}
996
997#[proc_macro_attribute]
1021pub fn errors(_attr: TokenStream, item: TokenStream) -> TokenStream {
1022 item
1025}
1026
1027#[derive(Debug)]
1033struct ValidationRuleInfo {
1034 rule_type: String,
1035 params: Vec<(String, String)>,
1036 message: Option<String>,
1037 groups: Vec<String>,
1038}
1039
1040fn parse_validate_attrs(attrs: &[Attribute]) -> Vec<ValidationRuleInfo> {
1042 let mut rules = Vec::new();
1043
1044 for attr in attrs {
1045 if !attr.path().is_ident("validate") {
1046 continue;
1047 }
1048
1049 if let Ok(meta) = attr.parse_args::<Meta>() {
1051 if let Some(rule) = parse_validate_meta(&meta) {
1052 rules.push(rule);
1053 }
1054 } else if let Ok(nested) = attr
1055 .parse_args_with(syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated)
1056 {
1057 for meta in nested {
1058 if let Some(rule) = parse_validate_meta(&meta) {
1059 rules.push(rule);
1060 }
1061 }
1062 }
1063 }
1064
1065 rules
1066}
1067
1068fn parse_validate_meta(meta: &Meta) -> Option<ValidationRuleInfo> {
1070 match meta {
1071 Meta::Path(path) => {
1072 let ident = path.get_ident()?.to_string();
1074 Some(ValidationRuleInfo {
1075 rule_type: ident,
1076 params: Vec::new(),
1077 message: None,
1078 groups: Vec::new(),
1079 })
1080 }
1081 Meta::List(list) => {
1082 let rule_type = list.path.get_ident()?.to_string();
1084 let mut params = Vec::new();
1085 let mut message = None;
1086 let mut groups = Vec::new();
1087
1088 if let Ok(nested) = list.parse_args_with(
1090 syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
1091 ) {
1092 for nested_meta in nested {
1093 if let Meta::NameValue(nv) = &nested_meta {
1094 let key = nv.path.get_ident()?.to_string();
1095
1096 if key == "groups" {
1097 let vec = expr_to_string_vec(&nv.value);
1098 groups.extend(vec);
1099 } else if let Some(value) = expr_to_string(&nv.value) {
1100 if key == "message" {
1101 message = Some(value);
1102 } else if key == "group" {
1103 groups.push(value);
1104 } else {
1105 params.push((key, value));
1106 }
1107 }
1108 } else if let Meta::Path(path) = &nested_meta {
1109 if let Some(ident) = path.get_ident() {
1111 params.push((ident.to_string(), "true".to_string()));
1112 }
1113 }
1114 }
1115 }
1116
1117 Some(ValidationRuleInfo {
1118 rule_type,
1119 params,
1120 message,
1121 groups,
1122 })
1123 }
1124 Meta::NameValue(nv) => {
1125 let rule_type = nv.path.get_ident()?.to_string();
1127 let value = expr_to_string(&nv.value)?;
1128
1129 Some(ValidationRuleInfo {
1130 rule_type: rule_type.clone(),
1131 params: vec![(rule_type.clone(), value)],
1132 message: None,
1133 groups: Vec::new(),
1134 })
1135 }
1136 }
1137}
1138
1139fn expr_to_string(expr: &Expr) -> Option<String> {
1141 match expr {
1142 Expr::Lit(lit) => match &lit.lit {
1143 Lit::Str(s) => Some(s.value()),
1144 Lit::Int(i) => Some(i.base10_digits().to_string()),
1145 Lit::Float(f) => Some(f.base10_digits().to_string()),
1146 Lit::Bool(b) => Some(b.value.to_string()),
1147 _ => None,
1148 },
1149 _ => None,
1150 }
1151}
1152
1153fn expr_to_string_vec(expr: &Expr) -> Vec<String> {
1155 match expr {
1156 Expr::Array(arr) => {
1157 let mut result = Vec::new();
1158 for elem in &arr.elems {
1159 if let Some(s) = expr_to_string(elem) {
1160 result.push(s);
1161 }
1162 }
1163 result
1164 }
1165 _ => {
1166 if let Some(s) = expr_to_string(expr) {
1167 vec![s]
1168 } else {
1169 Vec::new()
1170 }
1171 }
1172 }
1173}
1174
1175fn get_validate_path() -> proc_macro2::TokenStream {
1185 let rustapi_rs_found = crate_name("rustapi-rs").or_else(|_| crate_name("rustapi_rs"));
1186
1187 if let Ok(found) = rustapi_rs_found {
1188 match found {
1189 FoundCrate::Itself => {
1190 quote! { ::rustapi_rs::__private::validate }
1191 }
1192 FoundCrate::Name(name) => {
1193 let normalized = name.replace('-', "_");
1194 let ident = syn::Ident::new(&normalized, proc_macro2::Span::call_site());
1195 quote! { ::#ident::__private::validate }
1196 }
1197 }
1198 } else if let Ok(found) =
1199 crate_name("rustapi-validate").or_else(|_| crate_name("rustapi_validate"))
1200 {
1201 match found {
1202 FoundCrate::Itself => quote! { crate },
1203 FoundCrate::Name(name) => {
1204 let normalized = name.replace('-', "_");
1205 let ident = syn::Ident::new(&normalized, proc_macro2::Span::call_site());
1206 quote! { ::#ident }
1207 }
1208 }
1209 } else {
1210 quote! { ::rustapi_validate }
1212 }
1213}
1214
1215fn get_core_path() -> proc_macro2::TokenStream {
1221 let rustapi_rs_found = crate_name("rustapi-rs").or_else(|_| crate_name("rustapi_rs"));
1222
1223 if let Ok(found) = rustapi_rs_found {
1224 match found {
1225 FoundCrate::Itself => quote! { ::rustapi_rs::__private::core },
1226 FoundCrate::Name(name) => {
1227 let normalized = name.replace('-', "_");
1228 let ident = syn::Ident::new(&normalized, proc_macro2::Span::call_site());
1229 quote! { ::#ident::__private::core }
1230 }
1231 }
1232 } else if let Ok(found) = crate_name("rustapi-core").or_else(|_| crate_name("rustapi_core")) {
1233 match found {
1234 FoundCrate::Itself => quote! { crate },
1235 FoundCrate::Name(name) => {
1236 let normalized = name.replace('-', "_");
1237 let ident = syn::Ident::new(&normalized, proc_macro2::Span::call_site());
1238 quote! { ::#ident }
1239 }
1240 }
1241 } else {
1242 quote! { ::rustapi_core }
1243 }
1244}
1245
1246fn get_async_trait_path() -> proc_macro2::TokenStream {
1252 let rustapi_rs_found = crate_name("rustapi-rs").or_else(|_| crate_name("rustapi_rs"));
1253
1254 if let Ok(found) = rustapi_rs_found {
1255 match found {
1256 FoundCrate::Itself => {
1257 quote! { ::rustapi_rs::__private::async_trait }
1258 }
1259 FoundCrate::Name(name) => {
1260 let normalized = name.replace('-', "_");
1261 let ident = syn::Ident::new(&normalized, proc_macro2::Span::call_site());
1262 quote! { ::#ident::__private::async_trait }
1263 }
1264 }
1265 } else if let Ok(found) = crate_name("async-trait").or_else(|_| crate_name("async_trait")) {
1266 match found {
1267 FoundCrate::Itself => quote! { crate },
1268 FoundCrate::Name(name) => {
1269 let normalized = name.replace('-', "_");
1270 let ident = syn::Ident::new(&normalized, proc_macro2::Span::call_site());
1271 quote! { ::#ident }
1272 }
1273 }
1274 } else {
1275 quote! { ::async_trait }
1276 }
1277}
1278
1279fn generate_rule_validation(
1280 field_name: &str,
1281 _field_type: &Type,
1282 rule: &ValidationRuleInfo,
1283 validate_path: &proc_macro2::TokenStream,
1284) -> proc_macro2::TokenStream {
1285 let field_ident = syn::Ident::new(field_name, proc_macro2::Span::call_site());
1286 let field_name_str = field_name;
1287
1288 let group_check = if rule.groups.is_empty() {
1290 quote! { true }
1291 } else {
1292 let group_names = rule.groups.iter().map(|g| g.as_str());
1293 quote! {
1294 {
1295 let rule_groups = [#(#validate_path::v2::ValidationGroup::from(#group_names)),*];
1296 rule_groups.iter().any(|g| g.matches(&group))
1297 }
1298 }
1299 };
1300
1301 let validation_logic = match rule.rule_type.as_str() {
1302 "email" => {
1303 let message = rule
1304 .message
1305 .as_ref()
1306 .map(|m| quote! { .with_message(#m) })
1307 .unwrap_or_default();
1308 quote! {
1309 {
1310 let rule = #validate_path::v2::EmailRule::new() #message;
1311 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1312 errors.add(#field_name_str, e);
1313 }
1314 }
1315 }
1316 }
1317 "length" => {
1318 let min = rule
1319 .params
1320 .iter()
1321 .find(|(k, _)| k == "min")
1322 .and_then(|(_, v)| v.parse::<usize>().ok());
1323 let max = rule
1324 .params
1325 .iter()
1326 .find(|(k, _)| k == "max")
1327 .and_then(|(_, v)| v.parse::<usize>().ok());
1328 let message = rule
1329 .message
1330 .as_ref()
1331 .map(|m| quote! { .with_message(#m) })
1332 .unwrap_or_default();
1333
1334 let rule_creation = match (min, max) {
1335 (Some(min), Some(max)) => {
1336 quote! { #validate_path::v2::LengthRule::new(#min, #max) }
1337 }
1338 (Some(min), None) => quote! { #validate_path::v2::LengthRule::min(#min) },
1339 (None, Some(max)) => quote! { #validate_path::v2::LengthRule::max(#max) },
1340 (None, None) => quote! { #validate_path::v2::LengthRule::new(0, usize::MAX) },
1341 };
1342
1343 quote! {
1344 {
1345 let rule = #rule_creation #message;
1346 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1347 errors.add(#field_name_str, e);
1348 }
1349 }
1350 }
1351 }
1352 "range" => {
1353 let min = rule
1354 .params
1355 .iter()
1356 .find(|(k, _)| k == "min")
1357 .map(|(_, v)| v.clone());
1358 let max = rule
1359 .params
1360 .iter()
1361 .find(|(k, _)| k == "max")
1362 .map(|(_, v)| v.clone());
1363 let message = rule
1364 .message
1365 .as_ref()
1366 .map(|m| quote! { .with_message(#m) })
1367 .unwrap_or_default();
1368
1369 let rule_creation = match (min, max) {
1371 (Some(min), Some(max)) => {
1372 let min_lit: proc_macro2::TokenStream = min.parse().unwrap();
1373 let max_lit: proc_macro2::TokenStream = max.parse().unwrap();
1374 quote! { #validate_path::v2::RangeRule::new(#min_lit, #max_lit) }
1375 }
1376 (Some(min), None) => {
1377 let min_lit: proc_macro2::TokenStream = min.parse().unwrap();
1378 quote! { #validate_path::v2::RangeRule::min(#min_lit) }
1379 }
1380 (None, Some(max)) => {
1381 let max_lit: proc_macro2::TokenStream = max.parse().unwrap();
1382 quote! { #validate_path::v2::RangeRule::max(#max_lit) }
1383 }
1384 (None, None) => {
1385 return quote! {};
1386 }
1387 };
1388
1389 quote! {
1390 {
1391 let rule = #rule_creation #message;
1392 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1393 errors.add(#field_name_str, e);
1394 }
1395 }
1396 }
1397 }
1398 "regex" => {
1399 let pattern = rule
1400 .params
1401 .iter()
1402 .find(|(k, _)| k == "regex" || k == "pattern")
1403 .map(|(_, v)| v.clone())
1404 .unwrap_or_default();
1405 let message = rule
1406 .message
1407 .as_ref()
1408 .map(|m| quote! { .with_message(#m) })
1409 .unwrap_or_default();
1410
1411 quote! {
1412 {
1413 let rule = #validate_path::v2::RegexRule::new(#pattern) #message;
1414 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1415 errors.add(#field_name_str, e);
1416 }
1417 }
1418 }
1419 }
1420 "url" => {
1421 let message = rule
1422 .message
1423 .as_ref()
1424 .map(|m| quote! { .with_message(#m) })
1425 .unwrap_or_default();
1426 quote! {
1427 {
1428 let rule = #validate_path::v2::UrlRule::new() #message;
1429 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1430 errors.add(#field_name_str, e);
1431 }
1432 }
1433 }
1434 }
1435 "required" => {
1436 let message = rule
1437 .message
1438 .as_ref()
1439 .map(|m| quote! { .with_message(#m) })
1440 .unwrap_or_default();
1441 quote! {
1442 {
1443 let rule = #validate_path::v2::RequiredRule::new() #message;
1444 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1445 errors.add(#field_name_str, e);
1446 }
1447 }
1448 }
1449 }
1450 "credit_card" => {
1451 let message = rule
1452 .message
1453 .as_ref()
1454 .map(|m| quote! { .with_message(#m) })
1455 .unwrap_or_default();
1456 quote! {
1457 {
1458 let rule = #validate_path::v2::CreditCardRule::new() #message;
1459 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1460 errors.add(#field_name_str, e);
1461 }
1462 }
1463 }
1464 }
1465 "ip" => {
1466 let v4 = rule.params.iter().any(|(k, _)| k == "v4");
1467 let v6 = rule.params.iter().any(|(k, _)| k == "v6");
1468
1469 let rule_creation = if v4 && !v6 {
1470 quote! { #validate_path::v2::IpRule::v4() }
1471 } else if !v4 && v6 {
1472 quote! { #validate_path::v2::IpRule::v6() }
1473 } else {
1474 quote! { #validate_path::v2::IpRule::new() }
1475 };
1476
1477 let message = rule
1478 .message
1479 .as_ref()
1480 .map(|m| quote! { .with_message(#m) })
1481 .unwrap_or_default();
1482
1483 quote! {
1484 {
1485 let rule = #rule_creation #message;
1486 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1487 errors.add(#field_name_str, e);
1488 }
1489 }
1490 }
1491 }
1492 "phone" => {
1493 let message = rule
1494 .message
1495 .as_ref()
1496 .map(|m| quote! { .with_message(#m) })
1497 .unwrap_or_default();
1498 quote! {
1499 {
1500 let rule = #validate_path::v2::PhoneRule::new() #message;
1501 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1502 errors.add(#field_name_str, e);
1503 }
1504 }
1505 }
1506 }
1507 "contains" => {
1508 let needle = rule
1509 .params
1510 .iter()
1511 .find(|(k, _)| k == "needle")
1512 .map(|(_, v)| v.clone())
1513 .unwrap_or_default();
1514
1515 let message = rule
1516 .message
1517 .as_ref()
1518 .map(|m| quote! { .with_message(#m) })
1519 .unwrap_or_default();
1520
1521 quote! {
1522 {
1523 let rule = #validate_path::v2::ContainsRule::new(#needle) #message;
1524 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1525 errors.add(#field_name_str, e);
1526 }
1527 }
1528 }
1529 }
1530 _ => {
1531 quote! {}
1533 }
1534 };
1535
1536 quote! {
1537 if #group_check {
1538 #validation_logic
1539 }
1540 }
1541}
1542
1543fn generate_async_rule_validation(
1545 field_name: &str,
1546 rule: &ValidationRuleInfo,
1547 validate_path: &proc_macro2::TokenStream,
1548) -> proc_macro2::TokenStream {
1549 let field_ident = syn::Ident::new(field_name, proc_macro2::Span::call_site());
1550 let field_name_str = field_name;
1551
1552 let group_check = if rule.groups.is_empty() {
1554 quote! { true }
1555 } else {
1556 let group_names = rule.groups.iter().map(|g| g.as_str());
1557 quote! {
1558 {
1559 let rule_groups = [#(#validate_path::v2::ValidationGroup::from(#group_names)),*];
1560 rule_groups.iter().any(|g| g.matches(&group))
1561 }
1562 }
1563 };
1564
1565 let validation_logic = match rule.rule_type.as_str() {
1566 "async_unique" => {
1567 let table = rule
1568 .params
1569 .iter()
1570 .find(|(k, _)| k == "table")
1571 .map(|(_, v)| v.clone())
1572 .unwrap_or_default();
1573 let column = rule
1574 .params
1575 .iter()
1576 .find(|(k, _)| k == "column")
1577 .map(|(_, v)| v.clone())
1578 .unwrap_or_default();
1579 let message = rule
1580 .message
1581 .as_ref()
1582 .map(|m| quote! { .with_message(#m) })
1583 .unwrap_or_default();
1584
1585 quote! {
1586 {
1587 let rule = #validate_path::v2::AsyncUniqueRule::new(#table, #column) #message;
1588 if let Err(e) = #validate_path::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
1589 errors.add(#field_name_str, e);
1590 }
1591 }
1592 }
1593 }
1594 "async_exists" => {
1595 let table = rule
1596 .params
1597 .iter()
1598 .find(|(k, _)| k == "table")
1599 .map(|(_, v)| v.clone())
1600 .unwrap_or_default();
1601 let column = rule
1602 .params
1603 .iter()
1604 .find(|(k, _)| k == "column")
1605 .map(|(_, v)| v.clone())
1606 .unwrap_or_default();
1607 let message = rule
1608 .message
1609 .as_ref()
1610 .map(|m| quote! { .with_message(#m) })
1611 .unwrap_or_default();
1612
1613 quote! {
1614 {
1615 let rule = #validate_path::v2::AsyncExistsRule::new(#table, #column) #message;
1616 if let Err(e) = #validate_path::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
1617 errors.add(#field_name_str, e);
1618 }
1619 }
1620 }
1621 }
1622 "async_api" => {
1623 let endpoint = rule
1624 .params
1625 .iter()
1626 .find(|(k, _)| k == "endpoint")
1627 .map(|(_, v)| v.clone())
1628 .unwrap_or_default();
1629 let message = rule
1630 .message
1631 .as_ref()
1632 .map(|m| quote! { .with_message(#m) })
1633 .unwrap_or_default();
1634
1635 quote! {
1636 {
1637 let rule = #validate_path::v2::AsyncApiRule::new(#endpoint) #message;
1638 if let Err(e) = #validate_path::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
1639 errors.add(#field_name_str, e);
1640 }
1641 }
1642 }
1643 }
1644 "custom_async" => {
1645 let function_path = rule
1647 .params
1648 .iter()
1649 .find(|(k, _)| k == "custom_async" || k == "function")
1650 .map(|(_, v)| v.clone())
1651 .unwrap_or_default();
1652
1653 if function_path.is_empty() {
1654 quote! {}
1656 } else {
1657 let func: syn::Path = syn::parse_str(&function_path).unwrap();
1658 let message_handling = if let Some(msg) = &rule.message {
1659 quote! {
1660 let e = #validate_path::v2::RuleError::new("custom_async", #msg);
1661 errors.add(#field_name_str, e);
1662 }
1663 } else {
1664 quote! {
1665 errors.add(#field_name_str, e);
1666 }
1667 };
1668
1669 quote! {
1670 {
1671 if let Err(e) = #func(&self.#field_ident, ctx).await {
1673 #message_handling
1674 }
1675 }
1676 }
1677 }
1678 }
1679 _ => {
1680 quote! {}
1682 }
1683 };
1684
1685 quote! {
1686 if #group_check {
1687 #validation_logic
1688 }
1689 }
1690}
1691
1692fn is_async_rule(rule: &ValidationRuleInfo) -> bool {
1694 matches!(
1695 rule.rule_type.as_str(),
1696 "async_unique" | "async_exists" | "async_api" | "custom_async"
1697 )
1698}
1699
1700#[proc_macro_derive(Validate, attributes(validate))]
1723pub fn derive_validate(input: TokenStream) -> TokenStream {
1724 let input = parse_macro_input!(input as DeriveInput);
1725 let name = &input.ident;
1726 let generics = &input.generics;
1727 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
1728
1729 let fields = match &input.data {
1731 Data::Struct(data) => match &data.fields {
1732 Fields::Named(fields) => &fields.named,
1733 _ => {
1734 return syn::Error::new_spanned(
1735 &input,
1736 "Validate can only be derived for structs with named fields",
1737 )
1738 .to_compile_error()
1739 .into();
1740 }
1741 },
1742 _ => {
1743 return syn::Error::new_spanned(&input, "Validate can only be derived for structs")
1744 .to_compile_error()
1745 .into();
1746 }
1747 };
1748
1749 let validate_path = get_validate_path();
1751 let core_path = get_core_path();
1752 let async_trait_path = get_async_trait_path();
1753
1754 let mut sync_validations = Vec::new();
1756 let mut async_validations = Vec::new();
1757 let mut has_async_rules = false;
1758
1759 for field in fields {
1760 let field_name = field.ident.as_ref().unwrap().to_string();
1761 let field_type = &field.ty;
1762 let rules = parse_validate_attrs(&field.attrs);
1763
1764 for rule in &rules {
1765 if is_async_rule(rule) {
1766 has_async_rules = true;
1767 let validation = generate_async_rule_validation(&field_name, rule, &validate_path);
1768 async_validations.push(validation);
1769 } else {
1770 let validation =
1771 generate_rule_validation(&field_name, field_type, rule, &validate_path);
1772 sync_validations.push(validation);
1773 }
1774 }
1775 }
1776
1777 let validate_impl = quote! {
1779 impl #impl_generics #validate_path::v2::Validate for #name #ty_generics #where_clause {
1780 fn validate_with_group(&self, group: #validate_path::v2::ValidationGroup) -> Result<(), #validate_path::v2::ValidationErrors> {
1781 let mut errors = #validate_path::v2::ValidationErrors::new();
1782
1783 #(#sync_validations)*
1784
1785 errors.into_result()
1786 }
1787 }
1788 };
1789
1790 let async_validate_impl = if has_async_rules {
1792 quote! {
1793 #[#async_trait_path::async_trait]
1794 impl #impl_generics #validate_path::v2::AsyncValidate for #name #ty_generics #where_clause {
1795 async fn validate_async_with_group(&self, ctx: &#validate_path::v2::ValidationContext, group: #validate_path::v2::ValidationGroup) -> Result<(), #validate_path::v2::ValidationErrors> {
1796 let mut errors = #validate_path::v2::ValidationErrors::new();
1797
1798 #(#async_validations)*
1799
1800 errors.into_result()
1801 }
1802 }
1803 }
1804 } else {
1805 quote! {
1807 #[#async_trait_path::async_trait]
1808 impl #impl_generics #validate_path::v2::AsyncValidate for #name #ty_generics #where_clause {
1809 async fn validate_async_with_group(&self, _ctx: &#validate_path::v2::ValidationContext, _group: #validate_path::v2::ValidationGroup) -> Result<(), #validate_path::v2::ValidationErrors> {
1810 Ok(())
1811 }
1812 }
1813 }
1814 };
1815
1816 let validatable_impl = quote! {
1819 impl #impl_generics #core_path::validation::Validatable for #name #ty_generics #where_clause {
1820 fn do_validate(&self) -> Result<(), #core_path::ApiError> {
1821 match #validate_path::v2::Validate::validate(self) {
1822 Ok(_) => Ok(()),
1823 Err(e) => Err(#core_path::validation::convert_v2_errors(e)),
1824 }
1825 }
1826 }
1827 };
1828
1829 let expanded = quote! {
1830 #validate_impl
1831 #async_validate_impl
1832 #validatable_impl
1833 };
1834
1835 debug_output("Validate derive", &expanded);
1836
1837 TokenStream::from(expanded)
1838}
1839
1840#[proc_macro_derive(ApiError, attributes(error))]
1859pub fn derive_api_error(input: TokenStream) -> TokenStream {
1860 api_error::expand_derive_api_error(input)
1861}
1862
1863#[proc_macro_derive(TypedPath, attributes(typed_path))]
1880pub fn derive_typed_path(input: TokenStream) -> TokenStream {
1881 let input = parse_macro_input!(input as DeriveInput);
1882 let name = &input.ident;
1883 let generics = &input.generics;
1884 let rustapi_path = get_rustapi_path();
1885 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
1886
1887 let mut path_str = None;
1889 for attr in &input.attrs {
1890 if attr.path().is_ident("typed_path") {
1891 if let Ok(lit) = attr.parse_args::<LitStr>() {
1892 path_str = Some(lit.value());
1893 }
1894 }
1895 }
1896
1897 let path = match path_str {
1898 Some(p) => p,
1899 None => {
1900 return syn::Error::new_spanned(
1901 &input,
1902 "#[derive(TypedPath)] requires a #[typed_path(\"...\")] attribute",
1903 )
1904 .to_compile_error()
1905 .into();
1906 }
1907 };
1908
1909 if let Err(err) = validate_path_syntax(&path, proc_macro2::Span::call_site()) {
1911 return err.to_compile_error().into();
1912 }
1913
1914 let mut format_string = String::new();
1917 let mut format_args = Vec::new();
1918
1919 let mut chars = path.chars().peekable();
1920 while let Some(ch) = chars.next() {
1921 if ch == '{' {
1922 let mut param_name = String::new();
1923 while let Some(&c) = chars.peek() {
1924 if c == '}' {
1925 chars.next(); break;
1927 }
1928 param_name.push(chars.next().unwrap());
1929 }
1930
1931 if param_name.is_empty() {
1932 return syn::Error::new_spanned(
1933 &input,
1934 "Empty path parameter not allowed in typed_path",
1935 )
1936 .to_compile_error()
1937 .into();
1938 }
1939
1940 format_string.push_str("{}");
1941 let ident = syn::Ident::new(¶m_name, proc_macro2::Span::call_site());
1942 format_args.push(quote! { self.#ident });
1943 } else {
1944 format_string.push(ch);
1945 }
1946 }
1947
1948 let expanded = quote! {
1949 impl #impl_generics #rustapi_path::prelude::TypedPath for #name #ty_generics #where_clause {
1950 const PATH: &'static str = #path;
1951
1952 fn to_uri(&self) -> String {
1953 format!(#format_string, #(#format_args),*)
1954 }
1955 }
1956 };
1957
1958 debug_output("TypedPath derive", &expanded);
1959 TokenStream::from(expanded)
1960}