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 && !ch.is_alphanumeric() && !"-_./*".contains(ch) =>
396 {
397 return Err(syn::Error::new(
398 span,
399 format!(
400 "invalid character '{}' at position {} in route path: \"{}\"",
401 ch, i, path
402 ),
403 ));
404 }
405 _ if brace_depth == 0 => {}
406 _ => {}
407 }
408 }
409
410 if brace_depth > 0 {
412 return Err(syn::Error::new(
413 span,
414 format!(
415 "unclosed brace '{{' in route path (missing closing '}}'): \"{}\"",
416 path
417 ),
418 ));
419 }
420
421 Ok(())
422}
423
424#[proc_macro_attribute]
442pub fn main(_attr: TokenStream, item: TokenStream) -> TokenStream {
443 let input = parse_macro_input!(item as ItemFn);
444
445 let attrs = &input.attrs;
446 let vis = &input.vis;
447 let sig = &input.sig;
448 let block = &input.block;
449
450 let expanded = quote! {
451 #(#attrs)*
452 #[::tokio::main]
453 #vis #sig {
454 #block
455 }
456 };
457
458 debug_output("main", &expanded);
459
460 TokenStream::from(expanded)
461}
462
463fn is_body_consuming_type(ty: &Type) -> bool {
469 match ty {
470 Type::Path(tp) => {
471 if let Some(seg) = tp.path.segments.last() {
472 matches!(
473 seg.ident.to_string().as_str(),
474 "Json" | "Body" | "ValidatedJson" | "AsyncValidatedJson" | "Multipart"
475 )
476 } else {
477 false
478 }
479 }
480 _ => false,
481 }
482}
483
484fn validate_extractor_order(input: &ItemFn) -> Result<(), syn::Error> {
490 let params: Vec<_> = input
491 .sig
492 .inputs
493 .iter()
494 .filter_map(|arg| {
495 if let FnArg::Typed(pat_ty) = arg {
496 Some(pat_ty)
497 } else {
498 None
499 }
500 })
501 .collect();
502
503 if params.is_empty() {
504 return Ok(());
505 }
506
507 let body_indices: Vec<usize> = params
509 .iter()
510 .enumerate()
511 .filter(|(_, p)| is_body_consuming_type(&p.ty))
512 .map(|(i, _)| i)
513 .collect();
514
515 if body_indices.is_empty() {
516 return Ok(());
517 }
518
519 let last_non_body = params
521 .iter()
522 .enumerate()
523 .filter(|(_, p)| !is_body_consuming_type(&p.ty))
524 .map(|(i, _)| i)
525 .max();
526
527 if let Some(last_non_body_idx) = last_non_body {
529 let first_body_idx = body_indices[0];
530 if first_body_idx < last_non_body_idx {
531 let offending_param = ¶ms[first_body_idx];
532 let ty_name = quote!(#offending_param).to_string();
533 return Err(syn::Error::new_spanned(
534 &offending_param.ty,
535 format!(
536 "Body-consuming extractor must be the LAST parameter.\n\
537 \n\
538 Found `{}` before non-body extractor(s).\n\
539 \n\
540 Body extractors (Json, Body, ValidatedJson, AsyncValidatedJson, Multipart) \
541 consume the request body, which can only be read once. Place them after all \
542 non-body extractors (State, Path, Query, Headers, etc.).\n\
543 \n\
544 Example:\n\
545 \x20 async fn handler(\n\
546 \x20 State(db): State<AppState>, // non-body: OK first\n\
547 \x20 Path(id): Path<i64>, // non-body: OK second\n\
548 \x20 Json(body): Json<CreateUser>, // body: MUST be last\n\
549 \x20 ) -> Result<Json<User>> {{ ... }}",
550 ty_name,
551 ),
552 ));
553 }
554 }
555
556 if body_indices.len() > 1 {
558 let second_body_param = ¶ms[body_indices[1]];
559 return Err(syn::Error::new_spanned(
560 &second_body_param.ty,
561 "Multiple body-consuming extractors detected.\n\
562 \n\
563 Only ONE body-consuming extractor (Json, Body, ValidatedJson, AsyncValidatedJson, \
564 Multipart) is allowed per handler, because the request body can only be consumed once.\n\
565 \n\
566 Remove the extra body extractor or combine the data into a single type.",
567 ));
568 }
569
570 Ok(())
571}
572
573fn generate_route_handler(method: &str, attr: TokenStream, item: TokenStream) -> TokenStream {
575 let path = parse_macro_input!(attr as LitStr);
576 let input = parse_macro_input!(item as ItemFn);
577 let rustapi_path = get_rustapi_path();
578
579 let fn_name = &input.sig.ident;
580 let fn_vis = &input.vis;
581 let fn_attrs = &input.attrs;
582 let fn_async = &input.sig.asyncness;
583 let fn_inputs = &input.sig.inputs;
584 let fn_output = &input.sig.output;
585 let fn_block = &input.block;
586 let fn_generics = &input.sig.generics;
587
588 let schema_types = collect_handler_schema_types(&input);
589
590 let path_value = path.value();
591
592 if let Err(err) = validate_path_syntax(&path_value, path.span()) {
594 return err.to_compile_error().into();
595 }
596
597 if let Err(err) = validate_extractor_order(&input) {
599 return err.to_compile_error().into();
600 }
601
602 let route_fn_name = syn::Ident::new(&format!("{}_route", fn_name), fn_name.span());
604 let auto_route_name = syn::Ident::new(&format!("__AUTO_ROUTE_{}", fn_name), fn_name.span());
606
607 let schema_reg_fn_name =
609 syn::Ident::new(&format!("__{}_register_schemas", fn_name), fn_name.span());
610 let auto_schema_name = syn::Ident::new(&format!("__AUTO_SCHEMA_{}", fn_name), fn_name.span());
611
612 let route_helper = match method {
614 "GET" => quote!(#rustapi_path::get_route),
615 "POST" => quote!(#rustapi_path::post_route),
616 "PUT" => quote!(#rustapi_path::put_route),
617 "PATCH" => quote!(#rustapi_path::patch_route),
618 "DELETE" => quote!(#rustapi_path::delete_route),
619 _ => quote!(#rustapi_path::get_route),
620 };
621
622 let auto_params = collect_path_params(&input);
624
625 let mut chained_calls = quote!();
627
628 for (name, schema) in auto_params {
630 chained_calls = quote! { #chained_calls .param(#name, #schema) };
631 }
632
633 for attr in fn_attrs {
634 if let Some(ident) = attr.path().segments.last().map(|s| &s.ident) {
637 let ident_str = ident.to_string();
638 if ident_str == "tag" {
639 if let Ok(lit) = attr.parse_args::<LitStr>() {
640 let val = lit.value();
641 chained_calls = quote! { #chained_calls .tag(#val) };
642 }
643 } else if ident_str == "summary" {
644 if let Ok(lit) = attr.parse_args::<LitStr>() {
645 let val = lit.value();
646 chained_calls = quote! { #chained_calls .summary(#val) };
647 }
648 } else if ident_str == "description" {
649 if let Ok(lit) = attr.parse_args::<LitStr>() {
650 let val = lit.value();
651 chained_calls = quote! { #chained_calls .description(#val) };
652 }
653 } else if ident_str == "param" {
654 if let Ok(param_args) = attr.parse_args_with(
656 syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
657 ) {
658 let mut param_name: Option<String> = None;
659 let mut param_schema: Option<String> = None;
660
661 for meta in param_args {
662 match &meta {
663 Meta::Path(path) if param_name.is_none() => {
665 if let Some(ident) = path.get_ident() {
666 param_name = Some(ident.to_string());
667 }
668 }
669 Meta::NameValue(nv) => {
671 let key = nv.path.get_ident().map(|i| i.to_string());
672 if let Some(key) = key {
673 if key == "schema" || key == "type" {
674 if let Expr::Lit(lit) = &nv.value {
675 if let Lit::Str(s) = &lit.lit {
676 param_schema = Some(s.value());
677 }
678 }
679 } else if param_name.is_none() {
680 param_name = Some(key);
682 if let Expr::Lit(lit) = &nv.value {
683 if let Lit::Str(s) = &lit.lit {
684 param_schema = Some(s.value());
685 }
686 }
687 }
688 }
689 }
690 _ => {}
691 }
692 }
693
694 if let (Some(pname), Some(pschema)) = (param_name, param_schema) {
695 chained_calls = quote! { #chained_calls .param(#pname, #pschema) };
696 }
697 }
698 } else if ident_str == "errors" {
699 if let Ok(error_args) = attr.parse_args_with(
701 syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
702 ) {
703 for meta in error_args {
704 if let Meta::NameValue(nv) = &meta {
705 let status_str = nv.path.get_ident().map(|i| i.to_string());
708 if let Some(status_key) = status_str {
709 if let Expr::Lit(lit) = &nv.value {
711 if let Lit::Str(s) = &lit.lit {
712 let desc = s.value();
713 chained_calls = quote! {
714 #chained_calls .error_response(#status_key, #desc)
715 };
716 }
717 }
718 }
719 } else if let Meta::List(list) = &meta {
720 let _ = list;
723 }
724 }
725 }
726 if let Ok(ts) = attr.parse_args::<proc_macro2::TokenStream>() {
730 let tokens: Vec<proc_macro2::TokenTree> = ts.into_iter().collect();
731 let mut i = 0;
732 while i < tokens.len() {
733 if let proc_macro2::TokenTree::Literal(lit) = &tokens[i] {
735 let lit_str = lit.to_string();
736 if let Ok(status_code) = lit_str.parse::<u16>() {
737 if i + 2 < tokens.len() {
739 if let proc_macro2::TokenTree::Punct(p) = &tokens[i + 1] {
740 if p.as_char() == '=' {
741 if let proc_macro2::TokenTree::Literal(desc_lit) =
742 &tokens[i + 2]
743 {
744 let desc_str = desc_lit.to_string();
745 let desc = desc_str.trim_matches('"').to_string();
747 chained_calls = quote! {
748 #chained_calls .error_response(#status_code, #desc)
749 };
750 i += 3;
751 if i < tokens.len() {
753 if let proc_macro2::TokenTree::Punct(p) =
754 &tokens[i]
755 {
756 if p.as_char() == ',' {
757 i += 1;
758 }
759 }
760 }
761 continue;
762 }
763 }
764 }
765 }
766 }
767 }
768 i += 1;
769 }
770 }
771 }
772 }
773 }
774
775 let expanded = quote! {
776 #(#fn_attrs)*
778 #fn_vis #fn_async fn #fn_name #fn_generics (#fn_inputs) #fn_output #fn_block
779
780 #[doc(hidden)]
782 #fn_vis fn #route_fn_name() -> #rustapi_path::Route {
783 #route_helper(#path_value, #fn_name)
784 #chained_calls
785 }
786
787 #[doc(hidden)]
789 #[allow(non_upper_case_globals)]
790 #[#rustapi_path::__private::linkme::distributed_slice(#rustapi_path::__private::AUTO_ROUTES)]
791 #[linkme(crate = #rustapi_path::__private::linkme)]
792 static #auto_route_name: fn() -> #rustapi_path::Route = #route_fn_name;
793
794 #[doc(hidden)]
796 #[allow(non_snake_case)]
797 fn #schema_reg_fn_name(spec: &mut #rustapi_path::__private::openapi::OpenApiSpec) {
798 #( spec.register_in_place::<#schema_types>(); )*
799 }
800
801 #[doc(hidden)]
802 #[allow(non_upper_case_globals)]
803 #[#rustapi_path::__private::linkme::distributed_slice(#rustapi_path::__private::AUTO_SCHEMAS)]
804 #[linkme(crate = #rustapi_path::__private::linkme)]
805 static #auto_schema_name: fn(&mut #rustapi_path::__private::openapi::OpenApiSpec) = #schema_reg_fn_name;
806 };
807
808 debug_output(&format!("{} {}", method, path_value), &expanded);
809
810 TokenStream::from(expanded)
811}
812
813#[proc_macro_attribute]
829pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream {
830 generate_route_handler("GET", attr, item)
831}
832
833#[proc_macro_attribute]
835pub fn post(attr: TokenStream, item: TokenStream) -> TokenStream {
836 generate_route_handler("POST", attr, item)
837}
838
839#[proc_macro_attribute]
841pub fn put(attr: TokenStream, item: TokenStream) -> TokenStream {
842 generate_route_handler("PUT", attr, item)
843}
844
845#[proc_macro_attribute]
847pub fn patch(attr: TokenStream, item: TokenStream) -> TokenStream {
848 generate_route_handler("PATCH", attr, item)
849}
850
851#[proc_macro_attribute]
853pub fn delete(attr: TokenStream, item: TokenStream) -> TokenStream {
854 generate_route_handler("DELETE", attr, item)
855}
856
857#[proc_macro_attribute]
873pub fn tag(attr: TokenStream, item: TokenStream) -> TokenStream {
874 let tag = parse_macro_input!(attr as LitStr);
875 let input = parse_macro_input!(item as ItemFn);
876
877 let attrs = &input.attrs;
878 let vis = &input.vis;
879 let sig = &input.sig;
880 let block = &input.block;
881 let tag_value = tag.value();
882
883 let expanded = quote! {
885 #[doc = concat!("**Tag:** ", #tag_value)]
886 #(#attrs)*
887 #vis #sig #block
888 };
889
890 TokenStream::from(expanded)
891}
892
893#[proc_macro_attribute]
905pub fn summary(attr: TokenStream, item: TokenStream) -> TokenStream {
906 let summary = parse_macro_input!(attr as LitStr);
907 let input = parse_macro_input!(item as ItemFn);
908
909 let attrs = &input.attrs;
910 let vis = &input.vis;
911 let sig = &input.sig;
912 let block = &input.block;
913 let summary_value = summary.value();
914
915 let expanded = quote! {
917 #[doc = #summary_value]
918 #(#attrs)*
919 #vis #sig #block
920 };
921
922 TokenStream::from(expanded)
923}
924
925#[proc_macro_attribute]
937pub fn description(attr: TokenStream, item: TokenStream) -> TokenStream {
938 let desc = parse_macro_input!(attr as LitStr);
939 let input = parse_macro_input!(item as ItemFn);
940
941 let attrs = &input.attrs;
942 let vis = &input.vis;
943 let sig = &input.sig;
944 let block = &input.block;
945 let desc_value = desc.value();
946
947 let expanded = quote! {
949 #[doc = ""]
950 #[doc = #desc_value]
951 #(#attrs)*
952 #vis #sig #block
953 };
954
955 TokenStream::from(expanded)
956}
957
958#[proc_macro_attribute]
990pub fn param(_attr: TokenStream, item: TokenStream) -> TokenStream {
991 item
994}
995
996#[proc_macro_attribute]
1020pub fn errors(_attr: TokenStream, item: TokenStream) -> TokenStream {
1021 item
1024}
1025
1026#[derive(Debug)]
1032struct ValidationRuleInfo {
1033 rule_type: String,
1034 params: Vec<(String, String)>,
1035 message: Option<String>,
1036 groups: Vec<String>,
1037}
1038
1039fn parse_validate_attrs(attrs: &[Attribute]) -> Vec<ValidationRuleInfo> {
1041 let mut rules = Vec::new();
1042
1043 for attr in attrs {
1044 if !attr.path().is_ident("validate") {
1045 continue;
1046 }
1047
1048 if let Ok(meta) = attr.parse_args::<Meta>() {
1050 if let Some(rule) = parse_validate_meta(&meta) {
1051 rules.push(rule);
1052 }
1053 } else if let Ok(nested) = attr
1054 .parse_args_with(syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated)
1055 {
1056 for meta in nested {
1057 if let Some(rule) = parse_validate_meta(&meta) {
1058 rules.push(rule);
1059 }
1060 }
1061 }
1062 }
1063
1064 rules
1065}
1066
1067fn parse_validate_meta(meta: &Meta) -> Option<ValidationRuleInfo> {
1069 match meta {
1070 Meta::Path(path) => {
1071 let ident = path.get_ident()?.to_string();
1073 Some(ValidationRuleInfo {
1074 rule_type: ident,
1075 params: Vec::new(),
1076 message: None,
1077 groups: Vec::new(),
1078 })
1079 }
1080 Meta::List(list) => {
1081 let rule_type = list.path.get_ident()?.to_string();
1083 let mut params = Vec::new();
1084 let mut message = None;
1085 let mut groups = Vec::new();
1086
1087 if let Ok(nested) = list.parse_args_with(
1089 syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
1090 ) {
1091 for nested_meta in nested {
1092 if let Meta::NameValue(nv) = &nested_meta {
1093 let key = nv.path.get_ident()?.to_string();
1094
1095 if key == "groups" {
1096 let vec = expr_to_string_vec(&nv.value);
1097 groups.extend(vec);
1098 } else if let Some(value) = expr_to_string(&nv.value) {
1099 if key == "message" {
1100 message = Some(value);
1101 } else if key == "group" {
1102 groups.push(value);
1103 } else {
1104 params.push((key, value));
1105 }
1106 }
1107 } else if let Meta::Path(path) = &nested_meta {
1108 if let Some(ident) = path.get_ident() {
1110 params.push((ident.to_string(), "true".to_string()));
1111 }
1112 }
1113 }
1114 }
1115
1116 Some(ValidationRuleInfo {
1117 rule_type,
1118 params,
1119 message,
1120 groups,
1121 })
1122 }
1123 Meta::NameValue(nv) => {
1124 let rule_type = nv.path.get_ident()?.to_string();
1126 let value = expr_to_string(&nv.value)?;
1127
1128 Some(ValidationRuleInfo {
1129 rule_type: rule_type.clone(),
1130 params: vec![(rule_type.clone(), value)],
1131 message: None,
1132 groups: Vec::new(),
1133 })
1134 }
1135 }
1136}
1137
1138fn expr_to_string(expr: &Expr) -> Option<String> {
1140 match expr {
1141 Expr::Lit(lit) => match &lit.lit {
1142 Lit::Str(s) => Some(s.value()),
1143 Lit::Int(i) => Some(i.base10_digits().to_string()),
1144 Lit::Float(f) => Some(f.base10_digits().to_string()),
1145 Lit::Bool(b) => Some(b.value.to_string()),
1146 _ => None,
1147 },
1148 _ => None,
1149 }
1150}
1151
1152fn expr_to_string_vec(expr: &Expr) -> Vec<String> {
1154 match expr {
1155 Expr::Array(arr) => {
1156 let mut result = Vec::new();
1157 for elem in &arr.elems {
1158 if let Some(s) = expr_to_string(elem) {
1159 result.push(s);
1160 }
1161 }
1162 result
1163 }
1164 _ => {
1165 if let Some(s) = expr_to_string(expr) {
1166 vec![s]
1167 } else {
1168 Vec::new()
1169 }
1170 }
1171 }
1172}
1173
1174fn get_validate_path() -> proc_macro2::TokenStream {
1184 let rustapi_rs_found = crate_name("rustapi-rs").or_else(|_| crate_name("rustapi_rs"));
1185
1186 if let Ok(found) = rustapi_rs_found {
1187 match found {
1188 FoundCrate::Itself => {
1189 quote! { ::rustapi_rs::__private::validate }
1190 }
1191 FoundCrate::Name(name) => {
1192 let normalized = name.replace('-', "_");
1193 let ident = syn::Ident::new(&normalized, proc_macro2::Span::call_site());
1194 quote! { ::#ident::__private::validate }
1195 }
1196 }
1197 } else if let Ok(found) =
1198 crate_name("rustapi-validate").or_else(|_| crate_name("rustapi_validate"))
1199 {
1200 match found {
1201 FoundCrate::Itself => quote! { crate },
1202 FoundCrate::Name(name) => {
1203 let normalized = name.replace('-', "_");
1204 let ident = syn::Ident::new(&normalized, proc_macro2::Span::call_site());
1205 quote! { ::#ident }
1206 }
1207 }
1208 } else {
1209 quote! { ::rustapi_validate }
1211 }
1212}
1213
1214fn get_core_path() -> proc_macro2::TokenStream {
1220 let rustapi_rs_found = crate_name("rustapi-rs").or_else(|_| crate_name("rustapi_rs"));
1221
1222 if let Ok(found) = rustapi_rs_found {
1223 match found {
1224 FoundCrate::Itself => quote! { ::rustapi_rs::__private::core },
1225 FoundCrate::Name(name) => {
1226 let normalized = name.replace('-', "_");
1227 let ident = syn::Ident::new(&normalized, proc_macro2::Span::call_site());
1228 quote! { ::#ident::__private::core }
1229 }
1230 }
1231 } else if let Ok(found) = crate_name("rustapi-core").or_else(|_| crate_name("rustapi_core")) {
1232 match found {
1233 FoundCrate::Itself => quote! { crate },
1234 FoundCrate::Name(name) => {
1235 let normalized = name.replace('-', "_");
1236 let ident = syn::Ident::new(&normalized, proc_macro2::Span::call_site());
1237 quote! { ::#ident }
1238 }
1239 }
1240 } else {
1241 quote! { ::rustapi_core }
1242 }
1243}
1244
1245fn get_async_trait_path() -> proc_macro2::TokenStream {
1251 let rustapi_rs_found = crate_name("rustapi-rs").or_else(|_| crate_name("rustapi_rs"));
1252
1253 if let Ok(found) = rustapi_rs_found {
1254 match found {
1255 FoundCrate::Itself => {
1256 quote! { ::rustapi_rs::__private::async_trait }
1257 }
1258 FoundCrate::Name(name) => {
1259 let normalized = name.replace('-', "_");
1260 let ident = syn::Ident::new(&normalized, proc_macro2::Span::call_site());
1261 quote! { ::#ident::__private::async_trait }
1262 }
1263 }
1264 } else if let Ok(found) = crate_name("async-trait").or_else(|_| crate_name("async_trait")) {
1265 match found {
1266 FoundCrate::Itself => quote! { crate },
1267 FoundCrate::Name(name) => {
1268 let normalized = name.replace('-', "_");
1269 let ident = syn::Ident::new(&normalized, proc_macro2::Span::call_site());
1270 quote! { ::#ident }
1271 }
1272 }
1273 } else {
1274 quote! { ::async_trait }
1275 }
1276}
1277
1278fn generate_rule_validation(
1279 field_name: &str,
1280 _field_type: &Type,
1281 rule: &ValidationRuleInfo,
1282 validate_path: &proc_macro2::TokenStream,
1283) -> proc_macro2::TokenStream {
1284 let field_ident = syn::Ident::new(field_name, proc_macro2::Span::call_site());
1285 let field_name_str = field_name;
1286
1287 let group_check = if rule.groups.is_empty() {
1289 quote! { true }
1290 } else {
1291 let group_names = rule.groups.iter().map(|g| g.as_str());
1292 quote! {
1293 {
1294 let rule_groups = [#(#validate_path::v2::ValidationGroup::from(#group_names)),*];
1295 rule_groups.iter().any(|g| g.matches(&group))
1296 }
1297 }
1298 };
1299
1300 let validation_logic = match rule.rule_type.as_str() {
1301 "email" => {
1302 let message = rule
1303 .message
1304 .as_ref()
1305 .map(|m| quote! { .with_message(#m) })
1306 .unwrap_or_default();
1307 quote! {
1308 {
1309 let rule = #validate_path::v2::EmailRule::new() #message;
1310 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1311 errors.add(#field_name_str, e);
1312 }
1313 }
1314 }
1315 }
1316 "length" => {
1317 let min = rule
1318 .params
1319 .iter()
1320 .find(|(k, _)| k == "min")
1321 .and_then(|(_, v)| v.parse::<usize>().ok());
1322 let max = rule
1323 .params
1324 .iter()
1325 .find(|(k, _)| k == "max")
1326 .and_then(|(_, v)| v.parse::<usize>().ok());
1327 let message = rule
1328 .message
1329 .as_ref()
1330 .map(|m| quote! { .with_message(#m) })
1331 .unwrap_or_default();
1332
1333 let rule_creation = match (min, max) {
1334 (Some(min), Some(max)) => {
1335 quote! { #validate_path::v2::LengthRule::new(#min, #max) }
1336 }
1337 (Some(min), None) => quote! { #validate_path::v2::LengthRule::min(#min) },
1338 (None, Some(max)) => quote! { #validate_path::v2::LengthRule::max(#max) },
1339 (None, None) => quote! { #validate_path::v2::LengthRule::new(0, usize::MAX) },
1340 };
1341
1342 quote! {
1343 {
1344 let rule = #rule_creation #message;
1345 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1346 errors.add(#field_name_str, e);
1347 }
1348 }
1349 }
1350 }
1351 "range" => {
1352 let min = rule
1353 .params
1354 .iter()
1355 .find(|(k, _)| k == "min")
1356 .map(|(_, v)| v.clone());
1357 let max = rule
1358 .params
1359 .iter()
1360 .find(|(k, _)| k == "max")
1361 .map(|(_, v)| v.clone());
1362 let message = rule
1363 .message
1364 .as_ref()
1365 .map(|m| quote! { .with_message(#m) })
1366 .unwrap_or_default();
1367
1368 let rule_creation = match (min, max) {
1370 (Some(min), Some(max)) => {
1371 let min_lit: proc_macro2::TokenStream = min.parse().unwrap();
1372 let max_lit: proc_macro2::TokenStream = max.parse().unwrap();
1373 quote! { #validate_path::v2::RangeRule::new(#min_lit, #max_lit) }
1374 }
1375 (Some(min), None) => {
1376 let min_lit: proc_macro2::TokenStream = min.parse().unwrap();
1377 quote! { #validate_path::v2::RangeRule::min(#min_lit) }
1378 }
1379 (None, Some(max)) => {
1380 let max_lit: proc_macro2::TokenStream = max.parse().unwrap();
1381 quote! { #validate_path::v2::RangeRule::max(#max_lit) }
1382 }
1383 (None, None) => {
1384 return quote! {};
1385 }
1386 };
1387
1388 quote! {
1389 {
1390 let rule = #rule_creation #message;
1391 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1392 errors.add(#field_name_str, e);
1393 }
1394 }
1395 }
1396 }
1397 "regex" => {
1398 let pattern = rule
1399 .params
1400 .iter()
1401 .find(|(k, _)| k == "regex" || k == "pattern")
1402 .map(|(_, v)| v.clone())
1403 .unwrap_or_default();
1404 let message = rule
1405 .message
1406 .as_ref()
1407 .map(|m| quote! { .with_message(#m) })
1408 .unwrap_or_default();
1409
1410 quote! {
1411 {
1412 let rule = #validate_path::v2::RegexRule::new(#pattern) #message;
1413 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1414 errors.add(#field_name_str, e);
1415 }
1416 }
1417 }
1418 }
1419 "url" => {
1420 let message = rule
1421 .message
1422 .as_ref()
1423 .map(|m| quote! { .with_message(#m) })
1424 .unwrap_or_default();
1425 quote! {
1426 {
1427 let rule = #validate_path::v2::UrlRule::new() #message;
1428 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1429 errors.add(#field_name_str, e);
1430 }
1431 }
1432 }
1433 }
1434 "required" => {
1435 let message = rule
1436 .message
1437 .as_ref()
1438 .map(|m| quote! { .with_message(#m) })
1439 .unwrap_or_default();
1440 quote! {
1441 {
1442 let rule = #validate_path::v2::RequiredRule::new() #message;
1443 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1444 errors.add(#field_name_str, e);
1445 }
1446 }
1447 }
1448 }
1449 "credit_card" => {
1450 let message = rule
1451 .message
1452 .as_ref()
1453 .map(|m| quote! { .with_message(#m) })
1454 .unwrap_or_default();
1455 quote! {
1456 {
1457 let rule = #validate_path::v2::CreditCardRule::new() #message;
1458 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1459 errors.add(#field_name_str, e);
1460 }
1461 }
1462 }
1463 }
1464 "ip" => {
1465 let v4 = rule.params.iter().any(|(k, _)| k == "v4");
1466 let v6 = rule.params.iter().any(|(k, _)| k == "v6");
1467
1468 let rule_creation = if v4 && !v6 {
1469 quote! { #validate_path::v2::IpRule::v4() }
1470 } else if !v4 && v6 {
1471 quote! { #validate_path::v2::IpRule::v6() }
1472 } else {
1473 quote! { #validate_path::v2::IpRule::new() }
1474 };
1475
1476 let message = rule
1477 .message
1478 .as_ref()
1479 .map(|m| quote! { .with_message(#m) })
1480 .unwrap_or_default();
1481
1482 quote! {
1483 {
1484 let rule = #rule_creation #message;
1485 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1486 errors.add(#field_name_str, e);
1487 }
1488 }
1489 }
1490 }
1491 "phone" => {
1492 let message = rule
1493 .message
1494 .as_ref()
1495 .map(|m| quote! { .with_message(#m) })
1496 .unwrap_or_default();
1497 quote! {
1498 {
1499 let rule = #validate_path::v2::PhoneRule::new() #message;
1500 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1501 errors.add(#field_name_str, e);
1502 }
1503 }
1504 }
1505 }
1506 "contains" => {
1507 let needle = rule
1508 .params
1509 .iter()
1510 .find(|(k, _)| k == "needle")
1511 .map(|(_, v)| v.clone())
1512 .unwrap_or_default();
1513
1514 let message = rule
1515 .message
1516 .as_ref()
1517 .map(|m| quote! { .with_message(#m) })
1518 .unwrap_or_default();
1519
1520 quote! {
1521 {
1522 let rule = #validate_path::v2::ContainsRule::new(#needle) #message;
1523 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1524 errors.add(#field_name_str, e);
1525 }
1526 }
1527 }
1528 }
1529 _ => {
1530 quote! {}
1532 }
1533 };
1534
1535 quote! {
1536 if #group_check {
1537 #validation_logic
1538 }
1539 }
1540}
1541
1542fn generate_async_rule_validation(
1544 field_name: &str,
1545 rule: &ValidationRuleInfo,
1546 validate_path: &proc_macro2::TokenStream,
1547) -> proc_macro2::TokenStream {
1548 let field_ident = syn::Ident::new(field_name, proc_macro2::Span::call_site());
1549 let field_name_str = field_name;
1550
1551 let group_check = if rule.groups.is_empty() {
1553 quote! { true }
1554 } else {
1555 let group_names = rule.groups.iter().map(|g| g.as_str());
1556 quote! {
1557 {
1558 let rule_groups = [#(#validate_path::v2::ValidationGroup::from(#group_names)),*];
1559 rule_groups.iter().any(|g| g.matches(&group))
1560 }
1561 }
1562 };
1563
1564 let validation_logic = match rule.rule_type.as_str() {
1565 "async_unique" => {
1566 let table = rule
1567 .params
1568 .iter()
1569 .find(|(k, _)| k == "table")
1570 .map(|(_, v)| v.clone())
1571 .unwrap_or_default();
1572 let column = rule
1573 .params
1574 .iter()
1575 .find(|(k, _)| k == "column")
1576 .map(|(_, v)| v.clone())
1577 .unwrap_or_default();
1578 let message = rule
1579 .message
1580 .as_ref()
1581 .map(|m| quote! { .with_message(#m) })
1582 .unwrap_or_default();
1583
1584 quote! {
1585 {
1586 let rule = #validate_path::v2::AsyncUniqueRule::new(#table, #column) #message;
1587 if let Err(e) = #validate_path::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
1588 errors.add(#field_name_str, e);
1589 }
1590 }
1591 }
1592 }
1593 "async_exists" => {
1594 let table = rule
1595 .params
1596 .iter()
1597 .find(|(k, _)| k == "table")
1598 .map(|(_, v)| v.clone())
1599 .unwrap_or_default();
1600 let column = rule
1601 .params
1602 .iter()
1603 .find(|(k, _)| k == "column")
1604 .map(|(_, v)| v.clone())
1605 .unwrap_or_default();
1606 let message = rule
1607 .message
1608 .as_ref()
1609 .map(|m| quote! { .with_message(#m) })
1610 .unwrap_or_default();
1611
1612 quote! {
1613 {
1614 let rule = #validate_path::v2::AsyncExistsRule::new(#table, #column) #message;
1615 if let Err(e) = #validate_path::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
1616 errors.add(#field_name_str, e);
1617 }
1618 }
1619 }
1620 }
1621 "async_api" => {
1622 let endpoint = rule
1623 .params
1624 .iter()
1625 .find(|(k, _)| k == "endpoint")
1626 .map(|(_, v)| v.clone())
1627 .unwrap_or_default();
1628 let message = rule
1629 .message
1630 .as_ref()
1631 .map(|m| quote! { .with_message(#m) })
1632 .unwrap_or_default();
1633
1634 quote! {
1635 {
1636 let rule = #validate_path::v2::AsyncApiRule::new(#endpoint) #message;
1637 if let Err(e) = #validate_path::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
1638 errors.add(#field_name_str, e);
1639 }
1640 }
1641 }
1642 }
1643 "custom_async" => {
1644 let function_path = rule
1646 .params
1647 .iter()
1648 .find(|(k, _)| k == "custom_async" || k == "function")
1649 .map(|(_, v)| v.clone())
1650 .unwrap_or_default();
1651
1652 if function_path.is_empty() {
1653 quote! {}
1655 } else {
1656 let func: syn::Path = syn::parse_str(&function_path).unwrap();
1657 let message_handling = if let Some(msg) = &rule.message {
1658 quote! {
1659 let e = #validate_path::v2::RuleError::new("custom_async", #msg);
1660 errors.add(#field_name_str, e);
1661 }
1662 } else {
1663 quote! {
1664 errors.add(#field_name_str, e);
1665 }
1666 };
1667
1668 quote! {
1669 {
1670 if let Err(e) = #func(&self.#field_ident, ctx).await {
1672 #message_handling
1673 }
1674 }
1675 }
1676 }
1677 }
1678 _ => {
1679 quote! {}
1681 }
1682 };
1683
1684 quote! {
1685 if #group_check {
1686 #validation_logic
1687 }
1688 }
1689}
1690
1691fn is_async_rule(rule: &ValidationRuleInfo) -> bool {
1693 matches!(
1694 rule.rule_type.as_str(),
1695 "async_unique" | "async_exists" | "async_api" | "custom_async"
1696 )
1697}
1698
1699#[proc_macro_derive(Validate, attributes(validate))]
1722pub fn derive_validate(input: TokenStream) -> TokenStream {
1723 let input = parse_macro_input!(input as DeriveInput);
1724 let name = &input.ident;
1725 let generics = &input.generics;
1726 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
1727
1728 let fields = match &input.data {
1730 Data::Struct(data) => match &data.fields {
1731 Fields::Named(fields) => &fields.named,
1732 _ => {
1733 return syn::Error::new_spanned(
1734 &input,
1735 "Validate can only be derived for structs with named fields",
1736 )
1737 .to_compile_error()
1738 .into();
1739 }
1740 },
1741 _ => {
1742 return syn::Error::new_spanned(&input, "Validate can only be derived for structs")
1743 .to_compile_error()
1744 .into();
1745 }
1746 };
1747
1748 let validate_path = get_validate_path();
1750 let core_path = get_core_path();
1751 let async_trait_path = get_async_trait_path();
1752
1753 let mut sync_validations = Vec::new();
1755 let mut async_validations = Vec::new();
1756 let mut has_async_rules = false;
1757
1758 for field in fields {
1759 let field_name = field.ident.as_ref().unwrap().to_string();
1760 let field_type = &field.ty;
1761 let rules = parse_validate_attrs(&field.attrs);
1762
1763 for rule in &rules {
1764 if is_async_rule(rule) {
1765 has_async_rules = true;
1766 let validation = generate_async_rule_validation(&field_name, rule, &validate_path);
1767 async_validations.push(validation);
1768 } else {
1769 let validation =
1770 generate_rule_validation(&field_name, field_type, rule, &validate_path);
1771 sync_validations.push(validation);
1772 }
1773 }
1774 }
1775
1776 let validate_impl = quote! {
1778 impl #impl_generics #validate_path::v2::Validate for #name #ty_generics #where_clause {
1779 fn validate_with_group(&self, group: #validate_path::v2::ValidationGroup) -> Result<(), #validate_path::v2::ValidationErrors> {
1780 let mut errors = #validate_path::v2::ValidationErrors::new();
1781
1782 #(#sync_validations)*
1783
1784 errors.into_result()
1785 }
1786 }
1787 };
1788
1789 let async_validate_impl = if has_async_rules {
1791 quote! {
1792 #[#async_trait_path::async_trait]
1793 impl #impl_generics #validate_path::v2::AsyncValidate for #name #ty_generics #where_clause {
1794 async fn validate_async_with_group(&self, ctx: &#validate_path::v2::ValidationContext, group: #validate_path::v2::ValidationGroup) -> Result<(), #validate_path::v2::ValidationErrors> {
1795 let mut errors = #validate_path::v2::ValidationErrors::new();
1796
1797 #(#async_validations)*
1798
1799 errors.into_result()
1800 }
1801 }
1802 }
1803 } else {
1804 quote! {
1806 #[#async_trait_path::async_trait]
1807 impl #impl_generics #validate_path::v2::AsyncValidate for #name #ty_generics #where_clause {
1808 async fn validate_async_with_group(&self, _ctx: &#validate_path::v2::ValidationContext, _group: #validate_path::v2::ValidationGroup) -> Result<(), #validate_path::v2::ValidationErrors> {
1809 Ok(())
1810 }
1811 }
1812 }
1813 };
1814
1815 let validatable_impl = quote! {
1818 impl #impl_generics #core_path::validation::Validatable for #name #ty_generics #where_clause {
1819 fn do_validate(&self) -> Result<(), #core_path::ApiError> {
1820 match #validate_path::v2::Validate::validate(self) {
1821 Ok(_) => Ok(()),
1822 Err(e) => Err(#core_path::validation::convert_v2_errors(e)),
1823 }
1824 }
1825 }
1826 };
1827
1828 let expanded = quote! {
1829 #validate_impl
1830 #async_validate_impl
1831 #validatable_impl
1832 };
1833
1834 debug_output("Validate derive", &expanded);
1835
1836 TokenStream::from(expanded)
1837}
1838
1839#[proc_macro_derive(ApiError, attributes(error))]
1858pub fn derive_api_error(input: TokenStream) -> TokenStream {
1859 api_error::expand_derive_api_error(input)
1860}
1861
1862#[proc_macro_derive(TypedPath, attributes(typed_path))]
1879pub fn derive_typed_path(input: TokenStream) -> TokenStream {
1880 let input = parse_macro_input!(input as DeriveInput);
1881 let name = &input.ident;
1882 let generics = &input.generics;
1883 let rustapi_path = get_rustapi_path();
1884 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
1885
1886 let mut path_str = None;
1888 for attr in &input.attrs {
1889 if attr.path().is_ident("typed_path") {
1890 if let Ok(lit) = attr.parse_args::<LitStr>() {
1891 path_str = Some(lit.value());
1892 }
1893 }
1894 }
1895
1896 let path = match path_str {
1897 Some(p) => p,
1898 None => {
1899 return syn::Error::new_spanned(
1900 &input,
1901 "#[derive(TypedPath)] requires a #[typed_path(\"...\")] attribute",
1902 )
1903 .to_compile_error()
1904 .into();
1905 }
1906 };
1907
1908 if let Err(err) = validate_path_syntax(&path, proc_macro2::Span::call_site()) {
1910 return err.to_compile_error().into();
1911 }
1912
1913 let mut format_string = String::new();
1916 let mut format_args = Vec::new();
1917
1918 let mut chars = path.chars().peekable();
1919 while let Some(ch) = chars.next() {
1920 if ch == '{' {
1921 let mut param_name = String::new();
1922 while let Some(&c) = chars.peek() {
1923 if c == '}' {
1924 chars.next(); break;
1926 }
1927 param_name.push(chars.next().unwrap());
1928 }
1929
1930 if param_name.is_empty() {
1931 return syn::Error::new_spanned(
1932 &input,
1933 "Empty path parameter not allowed in typed_path",
1934 )
1935 .to_compile_error()
1936 .into();
1937 }
1938
1939 format_string.push_str("{}");
1940 let ident = syn::Ident::new(¶m_name, proc_macro2::Span::call_site());
1941 format_args.push(quote! { self.#ident });
1942 } else {
1943 format_string.push(ch);
1944 }
1945 }
1946
1947 let expanded = quote! {
1948 impl #impl_generics #rustapi_path::prelude::TypedPath for #name #ty_generics #where_clause {
1949 const PATH: &'static str = #path;
1950
1951 fn to_uri(&self) -> String {
1952 format!(#format_string, #(#format_args),*)
1953 }
1954 }
1955 };
1956
1957 debug_output("TypedPath derive", &expanded);
1958 TokenStream::from(expanded)
1959}