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 == "mcp" {
655 if let Ok(mcp_args) = attr.parse_args_with(
658 syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
659 ) {
660 let mut skip = quote! { None };
661 let mut readonly = quote! { None };
662 let mut write = quote! { None };
663 let mut require = quote! { None };
664
665 for meta in mcp_args {
666 match &meta {
667 Meta::Path(path) => {
668 if let Some(ident) = path.get_ident() {
669 let s = ident.to_string().to_lowercase();
670 if s == "skip" {
671 skip = quote! { Some(true) };
672 } else if s == "readonly" {
673 readonly = quote! { Some(true) };
674 } else if s == "write" {
675 write = quote! { Some(true) };
676 }
677 }
678 }
679 Meta::NameValue(nv) => {
680 let key = nv.path.get_ident().map(|i| i.to_string().to_lowercase());
681 if key.as_deref() == Some("require") {
682 if let Expr::Lit(lit) = &nv.value {
683 if let Lit::Str(s) = &lit.lit {
684 let val = s.value();
685 require = quote! { Some(#val.to_string()) };
686 }
687 }
688 }
689 }
690 _ => {}
691 }
692 }
693
694 chained_calls = quote! {
695 #chained_calls .mcp( #rustapi_path::__private::openapi::McpOperation {
696 skip: #skip,
697 readonly: #readonly,
698 write: #write,
699 require: #require,
700 })
701 };
702 }
703 } else if ident_str == "param" {
704 if let Ok(param_args) = attr.parse_args_with(
706 syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
707 ) {
708 let mut param_name: Option<String> = None;
709 let mut param_schema: Option<String> = None;
710
711 for meta in param_args {
712 match &meta {
713 Meta::Path(path) if param_name.is_none() => {
715 if let Some(ident) = path.get_ident() {
716 param_name = Some(ident.to_string());
717 }
718 }
719 Meta::NameValue(nv) => {
721 let key = nv.path.get_ident().map(|i| i.to_string());
722 if let Some(key) = key {
723 if key == "schema" || key == "type" {
724 if let Expr::Lit(lit) = &nv.value {
725 if let Lit::Str(s) = &lit.lit {
726 param_schema = Some(s.value());
727 }
728 }
729 } else if param_name.is_none() {
730 param_name = Some(key);
732 if let Expr::Lit(lit) = &nv.value {
733 if let Lit::Str(s) = &lit.lit {
734 param_schema = Some(s.value());
735 }
736 }
737 }
738 }
739 }
740 _ => {}
741 }
742 }
743
744 if let (Some(pname), Some(pschema)) = (param_name, param_schema) {
745 chained_calls = quote! { #chained_calls .param(#pname, #pschema) };
746 }
747 }
748 } else if ident_str == "errors" {
749 if let Ok(error_args) = attr.parse_args_with(
751 syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
752 ) {
753 for meta in error_args {
754 if let Meta::NameValue(nv) = &meta {
755 let status_str = nv.path.get_ident().map(|i| i.to_string());
758 if let Some(status_key) = status_str {
759 if let Expr::Lit(lit) = &nv.value {
761 if let Lit::Str(s) = &lit.lit {
762 let desc = s.value();
763 chained_calls = quote! {
764 #chained_calls .error_response(#status_key, #desc)
765 };
766 }
767 }
768 }
769 } else if let Meta::List(list) = &meta {
770 let _ = list;
773 }
774 }
775 }
776 if let Ok(ts) = attr.parse_args::<proc_macro2::TokenStream>() {
780 let tokens: Vec<proc_macro2::TokenTree> = ts.into_iter().collect();
781 let mut i = 0;
782 while i < tokens.len() {
783 if let proc_macro2::TokenTree::Literal(lit) = &tokens[i] {
785 let lit_str = lit.to_string();
786 if let Ok(status_code) = lit_str.parse::<u16>() {
787 if i + 2 < tokens.len() {
789 if let proc_macro2::TokenTree::Punct(p) = &tokens[i + 1] {
790 if p.as_char() == '=' {
791 if let proc_macro2::TokenTree::Literal(desc_lit) =
792 &tokens[i + 2]
793 {
794 let desc_str = desc_lit.to_string();
795 let desc = desc_str.trim_matches('"').to_string();
797 chained_calls = quote! {
798 #chained_calls .error_response(#status_code, #desc)
799 };
800 i += 3;
801 if i < tokens.len() {
803 if let proc_macro2::TokenTree::Punct(p) =
804 &tokens[i]
805 {
806 if p.as_char() == ',' {
807 i += 1;
808 }
809 }
810 }
811 continue;
812 }
813 }
814 }
815 }
816 }
817 }
818 i += 1;
819 }
820 }
821 }
822 }
823 }
824
825 let expanded = quote! {
826 #(#fn_attrs)*
828 #fn_vis #fn_async fn #fn_name #fn_generics (#fn_inputs) #fn_output #fn_block
829
830 #[doc(hidden)]
832 #fn_vis fn #route_fn_name() -> #rustapi_path::Route {
833 #route_helper(#path_value, #fn_name)
834 #chained_calls
835 }
836
837 #[doc(hidden)]
841 #[allow(non_upper_case_globals)]
842 #[#rustapi_path::__private::linkme::distributed_slice(#rustapi_path::__private::AUTO_ROUTES)]
843 #[linkme(crate = #rustapi_path::__private::linkme)]
844 static #auto_route_name: fn() -> #rustapi_path::Route = #route_fn_name;
845
846 #[doc(hidden)]
848 #[allow(non_snake_case)]
849 fn #schema_reg_fn_name(spec: &mut #rustapi_path::__private::openapi::OpenApiSpec) {
850 #( spec.register_in_place::<#schema_types>(); )*
851 }
852
853 #[doc(hidden)]
857 #[allow(non_upper_case_globals)]
858 #[#rustapi_path::__private::linkme::distributed_slice(#rustapi_path::__private::AUTO_SCHEMAS)]
860 #[linkme(crate = #rustapi_path::__private::linkme)]
861 static #auto_schema_name: fn(&mut #rustapi_path::__private::openapi::OpenApiSpec) = #schema_reg_fn_name;
862 };
863
864 debug_output(&format!("{} {}", method, path_value), &expanded);
865
866 TokenStream::from(expanded)
867}
868
869#[proc_macro_attribute]
885pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream {
886 generate_route_handler("GET", attr, item)
887}
888
889#[proc_macro_attribute]
891pub fn post(attr: TokenStream, item: TokenStream) -> TokenStream {
892 generate_route_handler("POST", attr, item)
893}
894
895#[proc_macro_attribute]
897pub fn put(attr: TokenStream, item: TokenStream) -> TokenStream {
898 generate_route_handler("PUT", attr, item)
899}
900
901#[proc_macro_attribute]
903pub fn patch(attr: TokenStream, item: TokenStream) -> TokenStream {
904 generate_route_handler("PATCH", attr, item)
905}
906
907#[proc_macro_attribute]
909pub fn delete(attr: TokenStream, item: TokenStream) -> TokenStream {
910 generate_route_handler("DELETE", attr, item)
911}
912
913#[proc_macro_attribute]
929pub fn tag(attr: TokenStream, item: TokenStream) -> TokenStream {
930 let tag = parse_macro_input!(attr as LitStr);
931 let input = parse_macro_input!(item as ItemFn);
932
933 let attrs = &input.attrs;
934 let vis = &input.vis;
935 let sig = &input.sig;
936 let block = &input.block;
937 let tag_value = tag.value();
938
939 let expanded = quote! {
941 #[doc = concat!("**Tag:** ", #tag_value)]
942 #(#attrs)*
943 #vis #sig #block
944 };
945
946 TokenStream::from(expanded)
947}
948
949#[proc_macro_attribute]
961pub fn summary(attr: TokenStream, item: TokenStream) -> TokenStream {
962 let summary = parse_macro_input!(attr as LitStr);
963 let input = parse_macro_input!(item as ItemFn);
964
965 let attrs = &input.attrs;
966 let vis = &input.vis;
967 let sig = &input.sig;
968 let block = &input.block;
969 let summary_value = summary.value();
970
971 let expanded = quote! {
973 #[doc = #summary_value]
974 #(#attrs)*
975 #vis #sig #block
976 };
977
978 TokenStream::from(expanded)
979}
980
981#[proc_macro_attribute]
993pub fn description(attr: TokenStream, item: TokenStream) -> TokenStream {
994 let desc = parse_macro_input!(attr as LitStr);
995 let input = parse_macro_input!(item as ItemFn);
996
997 let attrs = &input.attrs;
998 let vis = &input.vis;
999 let sig = &input.sig;
1000 let block = &input.block;
1001 let desc_value = desc.value();
1002
1003 let expanded = quote! {
1005 #[doc = ""]
1006 #[doc = #desc_value]
1007 #(#attrs)*
1008 #vis #sig #block
1009 };
1010
1011 TokenStream::from(expanded)
1012}
1013
1014#[proc_macro_attribute]
1034pub fn mcp(_attr: TokenStream, item: TokenStream) -> TokenStream {
1035 item
1038}
1039
1040#[proc_macro_attribute]
1072pub fn param(_attr: TokenStream, item: TokenStream) -> TokenStream {
1073 item
1076}
1077
1078#[proc_macro_attribute]
1102pub fn errors(_attr: TokenStream, item: TokenStream) -> TokenStream {
1103 item
1106}
1107
1108#[derive(Debug)]
1114struct ValidationRuleInfo {
1115 rule_type: String,
1116 params: Vec<(String, String)>,
1117 message: Option<String>,
1118 groups: Vec<String>,
1119}
1120
1121fn parse_validate_attrs(attrs: &[Attribute]) -> Vec<ValidationRuleInfo> {
1123 let mut rules = Vec::new();
1124
1125 for attr in attrs {
1126 if !attr.path().is_ident("validate") {
1127 continue;
1128 }
1129
1130 if let Ok(meta) = attr.parse_args::<Meta>() {
1132 if let Some(rule) = parse_validate_meta(&meta) {
1133 rules.push(rule);
1134 }
1135 } else if let Ok(nested) = attr
1136 .parse_args_with(syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated)
1137 {
1138 for meta in nested {
1139 if let Some(rule) = parse_validate_meta(&meta) {
1140 rules.push(rule);
1141 }
1142 }
1143 }
1144 }
1145
1146 rules
1147}
1148
1149fn parse_validate_meta(meta: &Meta) -> Option<ValidationRuleInfo> {
1151 match meta {
1152 Meta::Path(path) => {
1153 let ident = path.get_ident()?.to_string();
1155 Some(ValidationRuleInfo {
1156 rule_type: ident,
1157 params: Vec::new(),
1158 message: None,
1159 groups: Vec::new(),
1160 })
1161 }
1162 Meta::List(list) => {
1163 let rule_type = list.path.get_ident()?.to_string();
1165 let mut params = Vec::new();
1166 let mut message = None;
1167 let mut groups = Vec::new();
1168
1169 if let Ok(nested) = list.parse_args_with(
1171 syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
1172 ) {
1173 for nested_meta in nested {
1174 if let Meta::NameValue(nv) = &nested_meta {
1175 let key = nv.path.get_ident()?.to_string();
1176
1177 if key == "groups" {
1178 let vec = expr_to_string_vec(&nv.value);
1179 groups.extend(vec);
1180 } else if let Some(value) = expr_to_string(&nv.value) {
1181 if key == "message" {
1182 message = Some(value);
1183 } else if key == "group" {
1184 groups.push(value);
1185 } else {
1186 params.push((key, value));
1187 }
1188 }
1189 } else if let Meta::Path(path) = &nested_meta {
1190 if let Some(ident) = path.get_ident() {
1192 params.push((ident.to_string(), "true".to_string()));
1193 }
1194 }
1195 }
1196 }
1197
1198 Some(ValidationRuleInfo {
1199 rule_type,
1200 params,
1201 message,
1202 groups,
1203 })
1204 }
1205 Meta::NameValue(nv) => {
1206 let rule_type = nv.path.get_ident()?.to_string();
1208 let value = expr_to_string(&nv.value)?;
1209
1210 Some(ValidationRuleInfo {
1211 rule_type: rule_type.clone(),
1212 params: vec![(rule_type.clone(), value)],
1213 message: None,
1214 groups: Vec::new(),
1215 })
1216 }
1217 }
1218}
1219
1220fn expr_to_string(expr: &Expr) -> Option<String> {
1222 match expr {
1223 Expr::Lit(lit) => match &lit.lit {
1224 Lit::Str(s) => Some(s.value()),
1225 Lit::Int(i) => Some(i.base10_digits().to_string()),
1226 Lit::Float(f) => Some(f.base10_digits().to_string()),
1227 Lit::Bool(b) => Some(b.value.to_string()),
1228 _ => None,
1229 },
1230 _ => None,
1231 }
1232}
1233
1234fn expr_to_string_vec(expr: &Expr) -> Vec<String> {
1236 match expr {
1237 Expr::Array(arr) => {
1238 let mut result = Vec::new();
1239 for elem in &arr.elems {
1240 if let Some(s) = expr_to_string(elem) {
1241 result.push(s);
1242 }
1243 }
1244 result
1245 }
1246 _ => {
1247 if let Some(s) = expr_to_string(expr) {
1248 vec![s]
1249 } else {
1250 Vec::new()
1251 }
1252 }
1253 }
1254}
1255
1256fn get_validate_path() -> proc_macro2::TokenStream {
1266 let rustapi_rs_found = crate_name("rustapi-rs").or_else(|_| crate_name("rustapi_rs"));
1267
1268 if let Ok(found) = rustapi_rs_found {
1269 match found {
1270 FoundCrate::Itself => {
1271 quote! { ::rustapi_rs::__private::validate }
1272 }
1273 FoundCrate::Name(name) => {
1274 let normalized = name.replace('-', "_");
1275 let ident = syn::Ident::new(&normalized, proc_macro2::Span::call_site());
1276 quote! { ::#ident::__private::validate }
1277 }
1278 }
1279 } else if let Ok(found) =
1280 crate_name("rustapi-validate").or_else(|_| crate_name("rustapi_validate"))
1281 {
1282 match found {
1283 FoundCrate::Itself => quote! { crate },
1284 FoundCrate::Name(name) => {
1285 let normalized = name.replace('-', "_");
1286 let ident = syn::Ident::new(&normalized, proc_macro2::Span::call_site());
1287 quote! { ::#ident }
1288 }
1289 }
1290 } else {
1291 quote! { ::rustapi_validate }
1293 }
1294}
1295
1296fn get_core_path() -> proc_macro2::TokenStream {
1302 let rustapi_rs_found = crate_name("rustapi-rs").or_else(|_| crate_name("rustapi_rs"));
1303
1304 if let Ok(found) = rustapi_rs_found {
1305 match found {
1306 FoundCrate::Itself => quote! { ::rustapi_rs::__private::core },
1307 FoundCrate::Name(name) => {
1308 let normalized = name.replace('-', "_");
1309 let ident = syn::Ident::new(&normalized, proc_macro2::Span::call_site());
1310 quote! { ::#ident::__private::core }
1311 }
1312 }
1313 } else if let Ok(found) = crate_name("rustapi-core").or_else(|_| crate_name("rustapi_core")) {
1314 match found {
1315 FoundCrate::Itself => quote! { crate },
1316 FoundCrate::Name(name) => {
1317 let normalized = name.replace('-', "_");
1318 let ident = syn::Ident::new(&normalized, proc_macro2::Span::call_site());
1319 quote! { ::#ident }
1320 }
1321 }
1322 } else {
1323 quote! { ::rustapi_core }
1324 }
1325}
1326
1327fn get_async_trait_path() -> proc_macro2::TokenStream {
1333 let rustapi_rs_found = crate_name("rustapi-rs").or_else(|_| crate_name("rustapi_rs"));
1334
1335 if let Ok(found) = rustapi_rs_found {
1336 match found {
1337 FoundCrate::Itself => {
1338 quote! { ::rustapi_rs::__private::async_trait }
1339 }
1340 FoundCrate::Name(name) => {
1341 let normalized = name.replace('-', "_");
1342 let ident = syn::Ident::new(&normalized, proc_macro2::Span::call_site());
1343 quote! { ::#ident::__private::async_trait }
1344 }
1345 }
1346 } else if let Ok(found) = crate_name("async-trait").or_else(|_| crate_name("async_trait")) {
1347 match found {
1348 FoundCrate::Itself => quote! { crate },
1349 FoundCrate::Name(name) => {
1350 let normalized = name.replace('-', "_");
1351 let ident = syn::Ident::new(&normalized, proc_macro2::Span::call_site());
1352 quote! { ::#ident }
1353 }
1354 }
1355 } else {
1356 quote! { ::async_trait }
1357 }
1358}
1359
1360fn generate_rule_validation(
1361 field_name: &str,
1362 _field_type: &Type,
1363 rule: &ValidationRuleInfo,
1364 validate_path: &proc_macro2::TokenStream,
1365) -> proc_macro2::TokenStream {
1366 let field_ident = syn::Ident::new(field_name, proc_macro2::Span::call_site());
1367 let field_name_str = field_name;
1368
1369 let group_check = if rule.groups.is_empty() {
1371 quote! { true }
1372 } else {
1373 let group_names = rule.groups.iter().map(|g| g.as_str());
1374 quote! {
1375 {
1376 let rule_groups = [#(#validate_path::v2::ValidationGroup::from(#group_names)),*];
1377 rule_groups.iter().any(|g| g.matches(&group))
1378 }
1379 }
1380 };
1381
1382 let validation_logic = match rule.rule_type.as_str() {
1383 "email" => {
1384 let message = rule
1385 .message
1386 .as_ref()
1387 .map(|m| quote! { .with_message(#m) })
1388 .unwrap_or_default();
1389 quote! {
1390 {
1391 let rule = #validate_path::v2::EmailRule::new() #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 "length" => {
1399 let min = rule
1400 .params
1401 .iter()
1402 .find(|(k, _)| k == "min")
1403 .and_then(|(_, v)| v.parse::<usize>().ok());
1404 let max = rule
1405 .params
1406 .iter()
1407 .find(|(k, _)| k == "max")
1408 .and_then(|(_, v)| v.parse::<usize>().ok());
1409 let message = rule
1410 .message
1411 .as_ref()
1412 .map(|m| quote! { .with_message(#m) })
1413 .unwrap_or_default();
1414
1415 let rule_creation = match (min, max) {
1416 (Some(min), Some(max)) => {
1417 quote! { #validate_path::v2::LengthRule::new(#min, #max) }
1418 }
1419 (Some(min), None) => quote! { #validate_path::v2::LengthRule::min(#min) },
1420 (None, Some(max)) => quote! { #validate_path::v2::LengthRule::max(#max) },
1421 (None, None) => quote! { #validate_path::v2::LengthRule::new(0, usize::MAX) },
1422 };
1423
1424 quote! {
1425 {
1426 let rule = #rule_creation #message;
1427 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1428 errors.add(#field_name_str, e);
1429 }
1430 }
1431 }
1432 }
1433 "range" => {
1434 let min = rule
1435 .params
1436 .iter()
1437 .find(|(k, _)| k == "min")
1438 .map(|(_, v)| v.clone());
1439 let max = rule
1440 .params
1441 .iter()
1442 .find(|(k, _)| k == "max")
1443 .map(|(_, v)| v.clone());
1444 let message = rule
1445 .message
1446 .as_ref()
1447 .map(|m| quote! { .with_message(#m) })
1448 .unwrap_or_default();
1449
1450 let rule_creation = match (min, max) {
1452 (Some(min), Some(max)) => {
1453 let min_lit: proc_macro2::TokenStream = min.parse().unwrap();
1454 let max_lit: proc_macro2::TokenStream = max.parse().unwrap();
1455 quote! { #validate_path::v2::RangeRule::new(#min_lit, #max_lit) }
1456 }
1457 (Some(min), None) => {
1458 let min_lit: proc_macro2::TokenStream = min.parse().unwrap();
1459 quote! { #validate_path::v2::RangeRule::min(#min_lit) }
1460 }
1461 (None, Some(max)) => {
1462 let max_lit: proc_macro2::TokenStream = max.parse().unwrap();
1463 quote! { #validate_path::v2::RangeRule::max(#max_lit) }
1464 }
1465 (None, None) => {
1466 return quote! {};
1467 }
1468 };
1469
1470 quote! {
1471 {
1472 let rule = #rule_creation #message;
1473 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1474 errors.add(#field_name_str, e);
1475 }
1476 }
1477 }
1478 }
1479 "regex" => {
1480 let pattern = rule
1481 .params
1482 .iter()
1483 .find(|(k, _)| k == "regex" || k == "pattern")
1484 .map(|(_, v)| v.clone())
1485 .unwrap_or_default();
1486 let message = rule
1487 .message
1488 .as_ref()
1489 .map(|m| quote! { .with_message(#m) })
1490 .unwrap_or_default();
1491
1492 quote! {
1493 {
1494 let rule = #validate_path::v2::RegexRule::new(#pattern) #message;
1495 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1496 errors.add(#field_name_str, e);
1497 }
1498 }
1499 }
1500 }
1501 "url" => {
1502 let message = rule
1503 .message
1504 .as_ref()
1505 .map(|m| quote! { .with_message(#m) })
1506 .unwrap_or_default();
1507 quote! {
1508 {
1509 let rule = #validate_path::v2::UrlRule::new() #message;
1510 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1511 errors.add(#field_name_str, e);
1512 }
1513 }
1514 }
1515 }
1516 "required" => {
1517 let message = rule
1518 .message
1519 .as_ref()
1520 .map(|m| quote! { .with_message(#m) })
1521 .unwrap_or_default();
1522 quote! {
1523 {
1524 let rule = #validate_path::v2::RequiredRule::new() #message;
1525 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1526 errors.add(#field_name_str, e);
1527 }
1528 }
1529 }
1530 }
1531 "credit_card" => {
1532 let message = rule
1533 .message
1534 .as_ref()
1535 .map(|m| quote! { .with_message(#m) })
1536 .unwrap_or_default();
1537 quote! {
1538 {
1539 let rule = #validate_path::v2::CreditCardRule::new() #message;
1540 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1541 errors.add(#field_name_str, e);
1542 }
1543 }
1544 }
1545 }
1546 "ip" => {
1547 let v4 = rule.params.iter().any(|(k, _)| k == "v4");
1548 let v6 = rule.params.iter().any(|(k, _)| k == "v6");
1549
1550 let rule_creation = if v4 && !v6 {
1551 quote! { #validate_path::v2::IpRule::v4() }
1552 } else if !v4 && v6 {
1553 quote! { #validate_path::v2::IpRule::v6() }
1554 } else {
1555 quote! { #validate_path::v2::IpRule::new() }
1556 };
1557
1558 let message = rule
1559 .message
1560 .as_ref()
1561 .map(|m| quote! { .with_message(#m) })
1562 .unwrap_or_default();
1563
1564 quote! {
1565 {
1566 let rule = #rule_creation #message;
1567 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1568 errors.add(#field_name_str, e);
1569 }
1570 }
1571 }
1572 }
1573 "phone" => {
1574 let message = rule
1575 .message
1576 .as_ref()
1577 .map(|m| quote! { .with_message(#m) })
1578 .unwrap_or_default();
1579 quote! {
1580 {
1581 let rule = #validate_path::v2::PhoneRule::new() #message;
1582 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1583 errors.add(#field_name_str, e);
1584 }
1585 }
1586 }
1587 }
1588 "contains" => {
1589 let needle = rule
1590 .params
1591 .iter()
1592 .find(|(k, _)| k == "needle")
1593 .map(|(_, v)| v.clone())
1594 .unwrap_or_default();
1595
1596 let message = rule
1597 .message
1598 .as_ref()
1599 .map(|m| quote! { .with_message(#m) })
1600 .unwrap_or_default();
1601
1602 quote! {
1603 {
1604 let rule = #validate_path::v2::ContainsRule::new(#needle) #message;
1605 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1606 errors.add(#field_name_str, e);
1607 }
1608 }
1609 }
1610 }
1611 _ => {
1612 quote! {}
1614 }
1615 };
1616
1617 quote! {
1618 if #group_check {
1619 #validation_logic
1620 }
1621 }
1622}
1623
1624fn generate_async_rule_validation(
1626 field_name: &str,
1627 rule: &ValidationRuleInfo,
1628 validate_path: &proc_macro2::TokenStream,
1629) -> proc_macro2::TokenStream {
1630 let field_ident = syn::Ident::new(field_name, proc_macro2::Span::call_site());
1631 let field_name_str = field_name;
1632
1633 let group_check = if rule.groups.is_empty() {
1635 quote! { true }
1636 } else {
1637 let group_names = rule.groups.iter().map(|g| g.as_str());
1638 quote! {
1639 {
1640 let rule_groups = [#(#validate_path::v2::ValidationGroup::from(#group_names)),*];
1641 rule_groups.iter().any(|g| g.matches(&group))
1642 }
1643 }
1644 };
1645
1646 let validation_logic = match rule.rule_type.as_str() {
1647 "async_unique" => {
1648 let table = rule
1649 .params
1650 .iter()
1651 .find(|(k, _)| k == "table")
1652 .map(|(_, v)| v.clone())
1653 .unwrap_or_default();
1654 let column = rule
1655 .params
1656 .iter()
1657 .find(|(k, _)| k == "column")
1658 .map(|(_, v)| v.clone())
1659 .unwrap_or_default();
1660 let message = rule
1661 .message
1662 .as_ref()
1663 .map(|m| quote! { .with_message(#m) })
1664 .unwrap_or_default();
1665
1666 quote! {
1667 {
1668 let rule = #validate_path::v2::AsyncUniqueRule::new(#table, #column) #message;
1669 if let Err(e) = #validate_path::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
1670 errors.add(#field_name_str, e);
1671 }
1672 }
1673 }
1674 }
1675 "async_exists" => {
1676 let table = rule
1677 .params
1678 .iter()
1679 .find(|(k, _)| k == "table")
1680 .map(|(_, v)| v.clone())
1681 .unwrap_or_default();
1682 let column = rule
1683 .params
1684 .iter()
1685 .find(|(k, _)| k == "column")
1686 .map(|(_, v)| v.clone())
1687 .unwrap_or_default();
1688 let message = rule
1689 .message
1690 .as_ref()
1691 .map(|m| quote! { .with_message(#m) })
1692 .unwrap_or_default();
1693
1694 quote! {
1695 {
1696 let rule = #validate_path::v2::AsyncExistsRule::new(#table, #column) #message;
1697 if let Err(e) = #validate_path::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
1698 errors.add(#field_name_str, e);
1699 }
1700 }
1701 }
1702 }
1703 "async_api" => {
1704 let endpoint = rule
1705 .params
1706 .iter()
1707 .find(|(k, _)| k == "endpoint")
1708 .map(|(_, v)| v.clone())
1709 .unwrap_or_default();
1710 let message = rule
1711 .message
1712 .as_ref()
1713 .map(|m| quote! { .with_message(#m) })
1714 .unwrap_or_default();
1715
1716 quote! {
1717 {
1718 let rule = #validate_path::v2::AsyncApiRule::new(#endpoint) #message;
1719 if let Err(e) = #validate_path::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
1720 errors.add(#field_name_str, e);
1721 }
1722 }
1723 }
1724 }
1725 "custom_async" => {
1726 let function_path = rule
1728 .params
1729 .iter()
1730 .find(|(k, _)| k == "custom_async" || k == "function")
1731 .map(|(_, v)| v.clone())
1732 .unwrap_or_default();
1733
1734 if function_path.is_empty() {
1735 quote! {}
1737 } else {
1738 let func: syn::Path = syn::parse_str(&function_path).unwrap();
1739 let message_handling = if let Some(msg) = &rule.message {
1740 quote! {
1741 let e = #validate_path::v2::RuleError::new("custom_async", #msg);
1742 errors.add(#field_name_str, e);
1743 }
1744 } else {
1745 quote! {
1746 errors.add(#field_name_str, e);
1747 }
1748 };
1749
1750 quote! {
1751 {
1752 if let Err(e) = #func(&self.#field_ident, ctx).await {
1754 #message_handling
1755 }
1756 }
1757 }
1758 }
1759 }
1760 _ => {
1761 quote! {}
1763 }
1764 };
1765
1766 quote! {
1767 if #group_check {
1768 #validation_logic
1769 }
1770 }
1771}
1772
1773fn is_async_rule(rule: &ValidationRuleInfo) -> bool {
1775 matches!(
1776 rule.rule_type.as_str(),
1777 "async_unique" | "async_exists" | "async_api" | "custom_async"
1778 )
1779}
1780
1781#[proc_macro_derive(Validate, attributes(validate))]
1804pub fn derive_validate(input: TokenStream) -> TokenStream {
1805 let input = parse_macro_input!(input as DeriveInput);
1806 let name = &input.ident;
1807 let generics = &input.generics;
1808 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
1809
1810 let fields = match &input.data {
1812 Data::Struct(data) => match &data.fields {
1813 Fields::Named(fields) => &fields.named,
1814 _ => {
1815 return syn::Error::new_spanned(
1816 &input,
1817 "Validate can only be derived for structs with named fields",
1818 )
1819 .to_compile_error()
1820 .into();
1821 }
1822 },
1823 _ => {
1824 return syn::Error::new_spanned(&input, "Validate can only be derived for structs")
1825 .to_compile_error()
1826 .into();
1827 }
1828 };
1829
1830 let validate_path = get_validate_path();
1832 let core_path = get_core_path();
1833 let async_trait_path = get_async_trait_path();
1834
1835 let mut sync_validations = Vec::new();
1837 let mut async_validations = Vec::new();
1838 let mut has_async_rules = false;
1839
1840 for field in fields {
1841 let field_name = field.ident.as_ref().unwrap().to_string();
1842 let field_type = &field.ty;
1843 let rules = parse_validate_attrs(&field.attrs);
1844
1845 for rule in &rules {
1846 if is_async_rule(rule) {
1847 has_async_rules = true;
1848 let validation = generate_async_rule_validation(&field_name, rule, &validate_path);
1849 async_validations.push(validation);
1850 } else {
1851 let validation =
1852 generate_rule_validation(&field_name, field_type, rule, &validate_path);
1853 sync_validations.push(validation);
1854 }
1855 }
1856 }
1857
1858 let validate_impl = quote! {
1860 impl #impl_generics #validate_path::v2::Validate for #name #ty_generics #where_clause {
1861 fn validate_with_group(&self, group: #validate_path::v2::ValidationGroup) -> Result<(), #validate_path::v2::ValidationErrors> {
1862 let mut errors = #validate_path::v2::ValidationErrors::new();
1863
1864 #(#sync_validations)*
1865
1866 errors.into_result()
1867 }
1868 }
1869 };
1870
1871 let async_validate_impl = if has_async_rules {
1873 quote! {
1874 #[#async_trait_path::async_trait]
1875 impl #impl_generics #validate_path::v2::AsyncValidate for #name #ty_generics #where_clause {
1876 async fn validate_async_with_group(&self, ctx: &#validate_path::v2::ValidationContext, group: #validate_path::v2::ValidationGroup) -> Result<(), #validate_path::v2::ValidationErrors> {
1877 let mut errors = #validate_path::v2::ValidationErrors::new();
1878
1879 #(#async_validations)*
1880
1881 errors.into_result()
1882 }
1883 }
1884 }
1885 } else {
1886 quote! {
1888 #[#async_trait_path::async_trait]
1889 impl #impl_generics #validate_path::v2::AsyncValidate for #name #ty_generics #where_clause {
1890 async fn validate_async_with_group(&self, _ctx: &#validate_path::v2::ValidationContext, _group: #validate_path::v2::ValidationGroup) -> Result<(), #validate_path::v2::ValidationErrors> {
1891 Ok(())
1892 }
1893 }
1894 }
1895 };
1896
1897 let validatable_impl = quote! {
1900 impl #impl_generics #core_path::validation::Validatable for #name #ty_generics #where_clause {
1901 fn do_validate(&self) -> Result<(), #core_path::ApiError> {
1902 match #validate_path::v2::Validate::validate(self) {
1903 Ok(_) => Ok(()),
1904 Err(e) => Err(#core_path::validation::convert_v2_errors(e)),
1905 }
1906 }
1907 }
1908 };
1909
1910 let expanded = quote! {
1911 #validate_impl
1912 #async_validate_impl
1913 #validatable_impl
1914 };
1915
1916 debug_output("Validate derive", &expanded);
1917
1918 TokenStream::from(expanded)
1919}
1920
1921#[proc_macro_derive(ApiError, attributes(error))]
1940pub fn derive_api_error(input: TokenStream) -> TokenStream {
1941 api_error::expand_derive_api_error(input)
1942}
1943
1944#[proc_macro_derive(TypedPath, attributes(typed_path))]
1961pub fn derive_typed_path(input: TokenStream) -> TokenStream {
1962 let input = parse_macro_input!(input as DeriveInput);
1963 let name = &input.ident;
1964 let generics = &input.generics;
1965 let rustapi_path = get_rustapi_path();
1966 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
1967
1968 let mut path_str = None;
1970 for attr in &input.attrs {
1971 if attr.path().is_ident("typed_path") {
1972 if let Ok(lit) = attr.parse_args::<LitStr>() {
1973 path_str = Some(lit.value());
1974 }
1975 }
1976 }
1977
1978 let path = match path_str {
1979 Some(p) => p,
1980 None => {
1981 return syn::Error::new_spanned(
1982 &input,
1983 "#[derive(TypedPath)] requires a #[typed_path(\"...\")] attribute",
1984 )
1985 .to_compile_error()
1986 .into();
1987 }
1988 };
1989
1990 if let Err(err) = validate_path_syntax(&path, proc_macro2::Span::call_site()) {
1992 return err.to_compile_error().into();
1993 }
1994
1995 let mut format_string = String::new();
1998 let mut format_args = Vec::new();
1999
2000 let mut chars = path.chars().peekable();
2001 while let Some(ch) = chars.next() {
2002 if ch == '{' {
2003 let mut param_name = String::new();
2004 while let Some(&c) = chars.peek() {
2005 if c == '}' {
2006 chars.next(); break;
2008 }
2009 param_name.push(chars.next().unwrap());
2010 }
2011
2012 if param_name.is_empty() {
2013 return syn::Error::new_spanned(
2014 &input,
2015 "Empty path parameter not allowed in typed_path",
2016 )
2017 .to_compile_error()
2018 .into();
2019 }
2020
2021 format_string.push_str("{}");
2022 let ident = syn::Ident::new(¶m_name, proc_macro2::Span::call_site());
2023 format_args.push(quote! { self.#ident });
2024 } else {
2025 format_string.push(ch);
2026 }
2027 }
2028
2029 let expanded = quote! {
2030 impl #impl_generics #rustapi_path::prelude::TypedPath for #name #ty_generics #where_clause {
2031 const PATH: &'static str = #path;
2032
2033 fn to_uri(&self) -> String {
2034 format!(#format_string, #(#format_args),*)
2035 }
2036 }
2037 };
2038
2039 debug_output("TypedPath derive", &expanded);
2040 TokenStream::from(expanded)
2041}