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