1use proc_macro::TokenStream;
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
26#[proc_macro_attribute]
40pub fn schema(_attr: TokenStream, item: TokenStream) -> TokenStream {
41 let input = parse_macro_input!(item as syn::Item);
42
43 let (ident, generics) = match &input {
44 syn::Item::Struct(s) => (&s.ident, &s.generics),
45 syn::Item::Enum(e) => (&e.ident, &e.generics),
46 _ => {
47 return syn::Error::new_spanned(
48 &input,
49 "#[rustapi_rs::schema] can only be used on structs or enums",
50 )
51 .to_compile_error()
52 .into();
53 }
54 };
55
56 if !generics.params.is_empty() {
57 return syn::Error::new_spanned(
58 generics,
59 "#[rustapi_rs::schema] does not support generic types",
60 )
61 .to_compile_error()
62 .into();
63 }
64
65 let registrar_ident = syn::Ident::new(
66 &format!("__RUSTAPI_AUTO_SCHEMA_{}", ident),
67 proc_macro2::Span::call_site(),
68 );
69
70 let expanded = quote! {
71 #input
72
73 #[allow(non_upper_case_globals)]
74 #[::rustapi_rs::__private::linkme::distributed_slice(::rustapi_rs::__private::AUTO_SCHEMAS)]
75 #[linkme(crate = ::rustapi_rs::__private::linkme)]
76 static #registrar_ident: fn(&mut ::rustapi_rs::__private::rustapi_openapi::OpenApiSpec) =
77 |spec: &mut ::rustapi_rs::__private::rustapi_openapi::OpenApiSpec| {
78 spec.register_in_place::<#ident>();
79 };
80 };
81
82 debug_output("schema", &expanded);
83 expanded.into()
84}
85
86fn extract_schema_types(ty: &Type, out: &mut Vec<Type>, allow_leaf: bool) {
87 match ty {
88 Type::Reference(r) => extract_schema_types(&r.elem, out, allow_leaf),
89 Type::Path(tp) => {
90 let Some(seg) = tp.path.segments.last() else {
91 return;
92 };
93
94 let ident = seg.ident.to_string();
95
96 let unwrap_first_generic = |out: &mut Vec<Type>| {
97 if let PathArguments::AngleBracketed(args) = &seg.arguments {
98 if let Some(GenericArgument::Type(inner)) = args.args.first() {
99 extract_schema_types(inner, out, true);
100 }
101 }
102 };
103
104 match ident.as_str() {
105 "Json" | "ValidatedJson" | "Created" => {
107 unwrap_first_generic(out);
108 }
109 "WithStatus" => {
111 if let PathArguments::AngleBracketed(args) = &seg.arguments {
112 if let Some(GenericArgument::Type(inner)) = args.args.first() {
113 extract_schema_types(inner, out, true);
114 }
115 }
116 }
117 "Option" | "Result" => {
119 if let PathArguments::AngleBracketed(args) = &seg.arguments {
120 if let Some(GenericArgument::Type(inner)) = args.args.first() {
121 extract_schema_types(inner, out, allow_leaf);
122 }
123 }
124 }
125 _ => {
126 if allow_leaf {
127 out.push(ty.clone());
128 }
129 }
130 }
131 }
132 _ => {}
133 }
134}
135
136fn collect_handler_schema_types(input: &ItemFn) -> Vec<Type> {
137 let mut found: Vec<Type> = Vec::new();
138
139 for arg in &input.sig.inputs {
140 if let FnArg::Typed(pat_ty) = arg {
141 extract_schema_types(&pat_ty.ty, &mut found, false);
142 }
143 }
144
145 if let ReturnType::Type(_, ty) = &input.sig.output {
146 extract_schema_types(ty, &mut found, false);
147 }
148
149 let mut seen = HashSet::<String>::new();
151 found
152 .into_iter()
153 .filter(|t| seen.insert(quote!(#t).to_string()))
154 .collect()
155}
156
157fn is_debug_enabled() -> bool {
159 std::env::var("RUSTAPI_DEBUG")
160 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
161 .unwrap_or(false)
162}
163
164fn debug_output(name: &str, tokens: &proc_macro2::TokenStream) {
166 if is_debug_enabled() {
167 eprintln!("\n=== RUSTAPI_DEBUG: {} ===", name);
168 eprintln!("{}", tokens);
169 eprintln!("=== END {} ===\n", name);
170 }
171}
172
173fn validate_path_syntax(path: &str, span: proc_macro2::Span) -> Result<(), syn::Error> {
177 if !path.starts_with('/') {
179 return Err(syn::Error::new(
180 span,
181 format!("route path must start with '/', got: \"{}\"", path),
182 ));
183 }
184
185 if path.contains("//") {
187 return Err(syn::Error::new(
188 span,
189 format!(
190 "route path contains empty segment (double slash): \"{}\"",
191 path
192 ),
193 ));
194 }
195
196 let mut brace_depth = 0;
198 let mut param_start = None;
199
200 for (i, ch) in path.char_indices() {
201 match ch {
202 '{' => {
203 if brace_depth > 0 {
204 return Err(syn::Error::new(
205 span,
206 format!(
207 "nested braces are not allowed in route path at position {}: \"{}\"",
208 i, path
209 ),
210 ));
211 }
212 brace_depth += 1;
213 param_start = Some(i);
214 }
215 '}' => {
216 if brace_depth == 0 {
217 return Err(syn::Error::new(
218 span,
219 format!(
220 "unmatched closing brace '}}' at position {} in route path: \"{}\"",
221 i, path
222 ),
223 ));
224 }
225 brace_depth -= 1;
226
227 if let Some(start) = param_start {
229 let param_name = &path[start + 1..i];
230 if param_name.is_empty() {
231 return Err(syn::Error::new(
232 span,
233 format!(
234 "empty parameter name '{{}}' at position {} in route path: \"{}\"",
235 start, path
236 ),
237 ));
238 }
239 if !param_name.chars().all(|c| c.is_alphanumeric() || c == '_') {
241 return Err(syn::Error::new(
242 span,
243 format!(
244 "invalid parameter name '{{{}}}' at position {} - parameter names must contain only alphanumeric characters and underscores: \"{}\"",
245 param_name, start, path
246 ),
247 ));
248 }
249 if param_name
251 .chars()
252 .next()
253 .map(|c| c.is_ascii_digit())
254 .unwrap_or(false)
255 {
256 return Err(syn::Error::new(
257 span,
258 format!(
259 "parameter name '{{{}}}' cannot start with a digit at position {}: \"{}\"",
260 param_name, start, path
261 ),
262 ));
263 }
264 }
265 param_start = None;
266 }
267 _ if brace_depth == 0 => {
269 if !ch.is_alphanumeric() && !"-_./*".contains(ch) {
271 return Err(syn::Error::new(
272 span,
273 format!(
274 "invalid character '{}' at position {} in route path: \"{}\"",
275 ch, i, path
276 ),
277 ));
278 }
279 }
280 _ => {}
281 }
282 }
283
284 if brace_depth > 0 {
286 return Err(syn::Error::new(
287 span,
288 format!(
289 "unclosed brace '{{' in route path (missing closing '}}'): \"{}\"",
290 path
291 ),
292 ));
293 }
294
295 Ok(())
296}
297
298#[proc_macro_attribute]
316pub fn main(_attr: TokenStream, item: TokenStream) -> TokenStream {
317 let input = parse_macro_input!(item as ItemFn);
318
319 let attrs = &input.attrs;
320 let vis = &input.vis;
321 let sig = &input.sig;
322 let block = &input.block;
323
324 let expanded = quote! {
325 #(#attrs)*
326 #[::tokio::main]
327 #vis #sig {
328 #block
329 }
330 };
331
332 debug_output("main", &expanded);
333
334 TokenStream::from(expanded)
335}
336
337fn generate_route_handler(method: &str, attr: TokenStream, item: TokenStream) -> TokenStream {
339 let path = parse_macro_input!(attr as LitStr);
340 let input = parse_macro_input!(item as ItemFn);
341
342 let fn_name = &input.sig.ident;
343 let fn_vis = &input.vis;
344 let fn_attrs = &input.attrs;
345 let fn_async = &input.sig.asyncness;
346 let fn_inputs = &input.sig.inputs;
347 let fn_output = &input.sig.output;
348 let fn_block = &input.block;
349 let fn_generics = &input.sig.generics;
350
351 let schema_types = collect_handler_schema_types(&input);
352
353 let path_value = path.value();
354
355 if let Err(err) = validate_path_syntax(&path_value, path.span()) {
357 return err.to_compile_error().into();
358 }
359
360 let route_fn_name = syn::Ident::new(&format!("{}_route", fn_name), fn_name.span());
362 let auto_route_name = syn::Ident::new(&format!("__AUTO_ROUTE_{}", fn_name), fn_name.span());
364
365 let schema_reg_fn_name =
367 syn::Ident::new(&format!("__{}_register_schemas", fn_name), fn_name.span());
368 let auto_schema_name = syn::Ident::new(&format!("__AUTO_SCHEMA_{}", fn_name), fn_name.span());
369
370 let route_helper = match method {
372 "GET" => quote!(::rustapi_rs::get_route),
373 "POST" => quote!(::rustapi_rs::post_route),
374 "PUT" => quote!(::rustapi_rs::put_route),
375 "PATCH" => quote!(::rustapi_rs::patch_route),
376 "DELETE" => quote!(::rustapi_rs::delete_route),
377 _ => quote!(::rustapi_rs::get_route),
378 };
379
380 let mut chained_calls = quote!();
382
383 for attr in fn_attrs {
384 if let Some(ident) = attr.path().segments.last().map(|s| &s.ident) {
387 let ident_str = ident.to_string();
388 if ident_str == "tag" {
389 if let Ok(lit) = attr.parse_args::<LitStr>() {
390 let val = lit.value();
391 chained_calls = quote! { #chained_calls .tag(#val) };
392 }
393 } else if ident_str == "summary" {
394 if let Ok(lit) = attr.parse_args::<LitStr>() {
395 let val = lit.value();
396 chained_calls = quote! { #chained_calls .summary(#val) };
397 }
398 } else if ident_str == "description" {
399 if let Ok(lit) = attr.parse_args::<LitStr>() {
400 let val = lit.value();
401 chained_calls = quote! { #chained_calls .description(#val) };
402 }
403 }
404 }
405 }
406
407 let expanded = quote! {
408 #(#fn_attrs)*
410 #fn_vis #fn_async fn #fn_name #fn_generics (#fn_inputs) #fn_output #fn_block
411
412 #[doc(hidden)]
414 #fn_vis fn #route_fn_name() -> ::rustapi_rs::Route {
415 #route_helper(#path_value, #fn_name)
416 #chained_calls
417 }
418
419 #[doc(hidden)]
421 #[allow(non_upper_case_globals)]
422 #[::rustapi_rs::__private::linkme::distributed_slice(::rustapi_rs::__private::AUTO_ROUTES)]
423 #[linkme(crate = ::rustapi_rs::__private::linkme)]
424 static #auto_route_name: fn() -> ::rustapi_rs::Route = #route_fn_name;
425
426 #[doc(hidden)]
428 #[allow(non_snake_case)]
429 fn #schema_reg_fn_name(spec: &mut ::rustapi_rs::__private::rustapi_openapi::OpenApiSpec) {
430 #( spec.register_in_place::<#schema_types>(); )*
431 }
432
433 #[doc(hidden)]
434 #[allow(non_upper_case_globals)]
435 #[::rustapi_rs::__private::linkme::distributed_slice(::rustapi_rs::__private::AUTO_SCHEMAS)]
436 #[linkme(crate = ::rustapi_rs::__private::linkme)]
437 static #auto_schema_name: fn(&mut ::rustapi_rs::__private::rustapi_openapi::OpenApiSpec) = #schema_reg_fn_name;
438 };
439
440 debug_output(&format!("{} {}", method, path_value), &expanded);
441
442 TokenStream::from(expanded)
443}
444
445#[proc_macro_attribute]
461pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream {
462 generate_route_handler("GET", attr, item)
463}
464
465#[proc_macro_attribute]
467pub fn post(attr: TokenStream, item: TokenStream) -> TokenStream {
468 generate_route_handler("POST", attr, item)
469}
470
471#[proc_macro_attribute]
473pub fn put(attr: TokenStream, item: TokenStream) -> TokenStream {
474 generate_route_handler("PUT", attr, item)
475}
476
477#[proc_macro_attribute]
479pub fn patch(attr: TokenStream, item: TokenStream) -> TokenStream {
480 generate_route_handler("PATCH", attr, item)
481}
482
483#[proc_macro_attribute]
485pub fn delete(attr: TokenStream, item: TokenStream) -> TokenStream {
486 generate_route_handler("DELETE", attr, item)
487}
488
489#[proc_macro_attribute]
505pub fn tag(attr: TokenStream, item: TokenStream) -> TokenStream {
506 let tag = parse_macro_input!(attr as LitStr);
507 let input = parse_macro_input!(item as ItemFn);
508
509 let attrs = &input.attrs;
510 let vis = &input.vis;
511 let sig = &input.sig;
512 let block = &input.block;
513 let tag_value = tag.value();
514
515 let expanded = quote! {
517 #[doc = concat!("**Tag:** ", #tag_value)]
518 #(#attrs)*
519 #vis #sig #block
520 };
521
522 TokenStream::from(expanded)
523}
524
525#[proc_macro_attribute]
537pub fn summary(attr: TokenStream, item: TokenStream) -> TokenStream {
538 let summary = parse_macro_input!(attr as LitStr);
539 let input = parse_macro_input!(item as ItemFn);
540
541 let attrs = &input.attrs;
542 let vis = &input.vis;
543 let sig = &input.sig;
544 let block = &input.block;
545 let summary_value = summary.value();
546
547 let expanded = quote! {
549 #[doc = #summary_value]
550 #(#attrs)*
551 #vis #sig #block
552 };
553
554 TokenStream::from(expanded)
555}
556
557#[proc_macro_attribute]
569pub fn description(attr: TokenStream, item: TokenStream) -> TokenStream {
570 let desc = parse_macro_input!(attr as LitStr);
571 let input = parse_macro_input!(item as ItemFn);
572
573 let attrs = &input.attrs;
574 let vis = &input.vis;
575 let sig = &input.sig;
576 let block = &input.block;
577 let desc_value = desc.value();
578
579 let expanded = quote! {
581 #[doc = ""]
582 #[doc = #desc_value]
583 #(#attrs)*
584 #vis #sig #block
585 };
586
587 TokenStream::from(expanded)
588}
589
590#[derive(Debug)]
596struct ValidationRuleInfo {
597 rule_type: String,
598 params: Vec<(String, String)>,
599 message: Option<String>,
600 #[allow(dead_code)]
601 group: Option<String>,
602}
603
604fn parse_validate_attrs(attrs: &[Attribute]) -> Vec<ValidationRuleInfo> {
606 let mut rules = Vec::new();
607
608 for attr in attrs {
609 if !attr.path().is_ident("validate") {
610 continue;
611 }
612
613 if let Ok(meta) = attr.parse_args::<Meta>() {
615 if let Some(rule) = parse_validate_meta(&meta) {
616 rules.push(rule);
617 }
618 } else if let Ok(nested) = attr
619 .parse_args_with(syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated)
620 {
621 for meta in nested {
622 if let Some(rule) = parse_validate_meta(&meta) {
623 rules.push(rule);
624 }
625 }
626 }
627 }
628
629 rules
630}
631
632fn parse_validate_meta(meta: &Meta) -> Option<ValidationRuleInfo> {
634 match meta {
635 Meta::Path(path) => {
636 let ident = path.get_ident()?.to_string();
638 Some(ValidationRuleInfo {
639 rule_type: ident,
640 params: Vec::new(),
641 message: None,
642 group: None,
643 })
644 }
645 Meta::List(list) => {
646 let rule_type = list.path.get_ident()?.to_string();
648 let mut params = Vec::new();
649 let mut message = None;
650 let mut group = None;
651
652 if let Ok(nested) = list.parse_args_with(
654 syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
655 ) {
656 for nested_meta in nested {
657 if let Meta::NameValue(nv) = &nested_meta {
658 let key = nv.path.get_ident()?.to_string();
659 let value = expr_to_string(&nv.value)?;
660
661 if key == "message" {
662 message = Some(value);
663 } else if key == "group" {
664 group = Some(value);
665 } else {
666 params.push((key, value));
667 }
668 }
669 }
670 }
671
672 Some(ValidationRuleInfo {
673 rule_type,
674 params,
675 message,
676 group,
677 })
678 }
679 Meta::NameValue(nv) => {
680 let rule_type = nv.path.get_ident()?.to_string();
682 let value = expr_to_string(&nv.value)?;
683
684 Some(ValidationRuleInfo {
685 rule_type: rule_type.clone(),
686 params: vec![(rule_type, value)],
687 message: None,
688 group: None,
689 })
690 }
691 }
692}
693
694fn expr_to_string(expr: &Expr) -> Option<String> {
696 match expr {
697 Expr::Lit(lit) => match &lit.lit {
698 Lit::Str(s) => Some(s.value()),
699 Lit::Int(i) => Some(i.base10_digits().to_string()),
700 Lit::Float(f) => Some(f.base10_digits().to_string()),
701 Lit::Bool(b) => Some(b.value.to_string()),
702 _ => None,
703 },
704 _ => None,
705 }
706}
707
708fn generate_rule_validation(
710 field_name: &str,
711 _field_type: &Type,
712 rule: &ValidationRuleInfo,
713) -> proc_macro2::TokenStream {
714 let field_ident = syn::Ident::new(field_name, proc_macro2::Span::call_site());
715 let field_name_str = field_name;
716
717 match rule.rule_type.as_str() {
718 "email" => {
719 let message = rule
720 .message
721 .as_ref()
722 .map(|m| quote! { .with_message(#m) })
723 .unwrap_or_default();
724 quote! {
725 {
726 let rule = ::rustapi_validate::v2::EmailRule::new() #message;
727 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
728 errors.add(#field_name_str, e);
729 }
730 }
731 }
732 }
733 "length" => {
734 let min = rule
735 .params
736 .iter()
737 .find(|(k, _)| k == "min")
738 .and_then(|(_, v)| v.parse::<usize>().ok());
739 let max = rule
740 .params
741 .iter()
742 .find(|(k, _)| k == "max")
743 .and_then(|(_, v)| v.parse::<usize>().ok());
744 let message = rule
745 .message
746 .as_ref()
747 .map(|m| quote! { .with_message(#m) })
748 .unwrap_or_default();
749
750 let rule_creation = match (min, max) {
751 (Some(min), Some(max)) => {
752 quote! { ::rustapi_validate::v2::LengthRule::new(#min, #max) }
753 }
754 (Some(min), None) => quote! { ::rustapi_validate::v2::LengthRule::min(#min) },
755 (None, Some(max)) => quote! { ::rustapi_validate::v2::LengthRule::max(#max) },
756 (None, None) => quote! { ::rustapi_validate::v2::LengthRule::new(0, usize::MAX) },
757 };
758
759 quote! {
760 {
761 let rule = #rule_creation #message;
762 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
763 errors.add(#field_name_str, e);
764 }
765 }
766 }
767 }
768 "range" => {
769 let min = rule
770 .params
771 .iter()
772 .find(|(k, _)| k == "min")
773 .map(|(_, v)| v.clone());
774 let max = rule
775 .params
776 .iter()
777 .find(|(k, _)| k == "max")
778 .map(|(_, v)| v.clone());
779 let message = rule
780 .message
781 .as_ref()
782 .map(|m| quote! { .with_message(#m) })
783 .unwrap_or_default();
784
785 let rule_creation = match (min, max) {
787 (Some(min), Some(max)) => {
788 let min_lit: proc_macro2::TokenStream = min.parse().unwrap();
789 let max_lit: proc_macro2::TokenStream = max.parse().unwrap();
790 quote! { ::rustapi_validate::v2::RangeRule::new(#min_lit, #max_lit) }
791 }
792 (Some(min), None) => {
793 let min_lit: proc_macro2::TokenStream = min.parse().unwrap();
794 quote! { ::rustapi_validate::v2::RangeRule::min(#min_lit) }
795 }
796 (None, Some(max)) => {
797 let max_lit: proc_macro2::TokenStream = max.parse().unwrap();
798 quote! { ::rustapi_validate::v2::RangeRule::max(#max_lit) }
799 }
800 (None, None) => {
801 return quote! {};
802 }
803 };
804
805 quote! {
806 {
807 let rule = #rule_creation #message;
808 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
809 errors.add(#field_name_str, e);
810 }
811 }
812 }
813 }
814 "regex" => {
815 let pattern = rule
816 .params
817 .iter()
818 .find(|(k, _)| k == "regex" || k == "pattern")
819 .map(|(_, v)| v.clone())
820 .unwrap_or_default();
821 let message = rule
822 .message
823 .as_ref()
824 .map(|m| quote! { .with_message(#m) })
825 .unwrap_or_default();
826
827 quote! {
828 {
829 let rule = ::rustapi_validate::v2::RegexRule::new(#pattern) #message;
830 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
831 errors.add(#field_name_str, e);
832 }
833 }
834 }
835 }
836 "url" => {
837 let message = rule
838 .message
839 .as_ref()
840 .map(|m| quote! { .with_message(#m) })
841 .unwrap_or_default();
842 quote! {
843 {
844 let rule = ::rustapi_validate::v2::UrlRule::new() #message;
845 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
846 errors.add(#field_name_str, e);
847 }
848 }
849 }
850 }
851 "required" => {
852 let message = rule
853 .message
854 .as_ref()
855 .map(|m| quote! { .with_message(#m) })
856 .unwrap_or_default();
857 quote! {
858 {
859 let rule = ::rustapi_validate::v2::RequiredRule::new() #message;
860 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
861 errors.add(#field_name_str, e);
862 }
863 }
864 }
865 }
866 _ => {
867 quote! {}
869 }
870 }
871}
872
873fn generate_async_rule_validation(
875 field_name: &str,
876 rule: &ValidationRuleInfo,
877) -> proc_macro2::TokenStream {
878 let field_ident = syn::Ident::new(field_name, proc_macro2::Span::call_site());
879 let field_name_str = field_name;
880
881 match rule.rule_type.as_str() {
882 "async_unique" => {
883 let table = rule
884 .params
885 .iter()
886 .find(|(k, _)| k == "table")
887 .map(|(_, v)| v.clone())
888 .unwrap_or_default();
889 let column = rule
890 .params
891 .iter()
892 .find(|(k, _)| k == "column")
893 .map(|(_, v)| v.clone())
894 .unwrap_or_default();
895 let message = rule
896 .message
897 .as_ref()
898 .map(|m| quote! { .with_message(#m) })
899 .unwrap_or_default();
900
901 quote! {
902 {
903 let rule = ::rustapi_validate::v2::AsyncUniqueRule::new(#table, #column) #message;
904 if let Err(e) = ::rustapi_validate::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
905 errors.add(#field_name_str, e);
906 }
907 }
908 }
909 }
910 "async_exists" => {
911 let table = rule
912 .params
913 .iter()
914 .find(|(k, _)| k == "table")
915 .map(|(_, v)| v.clone())
916 .unwrap_or_default();
917 let column = rule
918 .params
919 .iter()
920 .find(|(k, _)| k == "column")
921 .map(|(_, v)| v.clone())
922 .unwrap_or_default();
923 let message = rule
924 .message
925 .as_ref()
926 .map(|m| quote! { .with_message(#m) })
927 .unwrap_or_default();
928
929 quote! {
930 {
931 let rule = ::rustapi_validate::v2::AsyncExistsRule::new(#table, #column) #message;
932 if let Err(e) = ::rustapi_validate::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
933 errors.add(#field_name_str, e);
934 }
935 }
936 }
937 }
938 "async_api" => {
939 let endpoint = rule
940 .params
941 .iter()
942 .find(|(k, _)| k == "endpoint")
943 .map(|(_, v)| v.clone())
944 .unwrap_or_default();
945 let message = rule
946 .message
947 .as_ref()
948 .map(|m| quote! { .with_message(#m) })
949 .unwrap_or_default();
950
951 quote! {
952 {
953 let rule = ::rustapi_validate::v2::AsyncApiRule::new(#endpoint) #message;
954 if let Err(e) = ::rustapi_validate::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
955 errors.add(#field_name_str, e);
956 }
957 }
958 }
959 }
960 _ => {
961 quote! {}
963 }
964 }
965}
966
967fn is_async_rule(rule: &ValidationRuleInfo) -> bool {
969 matches!(
970 rule.rule_type.as_str(),
971 "async_unique" | "async_exists" | "async_api"
972 )
973}
974
975#[proc_macro_derive(Validate, attributes(validate))]
998pub fn derive_validate(input: TokenStream) -> TokenStream {
999 let input = parse_macro_input!(input as DeriveInput);
1000 let name = &input.ident;
1001 let generics = &input.generics;
1002 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
1003
1004 let fields = match &input.data {
1006 Data::Struct(data) => match &data.fields {
1007 Fields::Named(fields) => &fields.named,
1008 _ => {
1009 return syn::Error::new_spanned(
1010 &input,
1011 "Validate can only be derived for structs with named fields",
1012 )
1013 .to_compile_error()
1014 .into();
1015 }
1016 },
1017 _ => {
1018 return syn::Error::new_spanned(&input, "Validate can only be derived for structs")
1019 .to_compile_error()
1020 .into();
1021 }
1022 };
1023
1024 let mut sync_validations = Vec::new();
1026 let mut async_validations = Vec::new();
1027 let mut has_async_rules = false;
1028
1029 for field in fields {
1030 let field_name = field.ident.as_ref().unwrap().to_string();
1031 let field_type = &field.ty;
1032 let rules = parse_validate_attrs(&field.attrs);
1033
1034 for rule in &rules {
1035 if is_async_rule(rule) {
1036 has_async_rules = true;
1037 let validation = generate_async_rule_validation(&field_name, rule);
1038 async_validations.push(validation);
1039 } else {
1040 let validation = generate_rule_validation(&field_name, field_type, rule);
1041 sync_validations.push(validation);
1042 }
1043 }
1044 }
1045
1046 let validate_impl = quote! {
1048 impl #impl_generics ::rustapi_validate::v2::Validate for #name #ty_generics #where_clause {
1049 fn validate(&self) -> Result<(), ::rustapi_validate::v2::ValidationErrors> {
1050 let mut errors = ::rustapi_validate::v2::ValidationErrors::new();
1051
1052 #(#sync_validations)*
1053
1054 errors.into_result()
1055 }
1056 }
1057 };
1058
1059 let async_validate_impl = if has_async_rules {
1061 quote! {
1062 #[::async_trait::async_trait]
1063 impl #impl_generics ::rustapi_validate::v2::AsyncValidate for #name #ty_generics #where_clause {
1064 async fn validate_async(&self, ctx: &::rustapi_validate::v2::ValidationContext) -> Result<(), ::rustapi_validate::v2::ValidationErrors> {
1065 let mut errors = ::rustapi_validate::v2::ValidationErrors::new();
1066
1067 #(#async_validations)*
1068
1069 errors.into_result()
1070 }
1071 }
1072 }
1073 } else {
1074 quote! {
1076 #[::async_trait::async_trait]
1077 impl #impl_generics ::rustapi_validate::v2::AsyncValidate for #name #ty_generics #where_clause {
1078 async fn validate_async(&self, _ctx: &::rustapi_validate::v2::ValidationContext) -> Result<(), ::rustapi_validate::v2::ValidationErrors> {
1079 Ok(())
1080 }
1081 }
1082 }
1083 };
1084
1085 let expanded = quote! {
1086 #validate_impl
1087 #async_validate_impl
1088 };
1089
1090 debug_output("Validate derive", &expanded);
1091
1092 TokenStream::from(expanded)
1093}