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 } else if ident_str == "param" {
405 if let Ok(param_args) = attr.parse_args_with(
407 syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
408 ) {
409 let mut param_name: Option<String> = None;
410 let mut param_schema: Option<String> = None;
411
412 for meta in param_args {
413 match &meta {
414 Meta::Path(path) => {
416 if param_name.is_none() {
417 if let Some(ident) = path.get_ident() {
418 param_name = Some(ident.to_string());
419 }
420 }
421 }
422 Meta::NameValue(nv) => {
424 let key = nv.path.get_ident().map(|i| i.to_string());
425 if let Some(key) = key {
426 if key == "schema" || key == "type" {
427 if let Expr::Lit(lit) = &nv.value {
428 if let Lit::Str(s) = &lit.lit {
429 param_schema = Some(s.value());
430 }
431 }
432 } else if param_name.is_none() {
433 param_name = Some(key);
435 if let Expr::Lit(lit) = &nv.value {
436 if let Lit::Str(s) = &lit.lit {
437 param_schema = Some(s.value());
438 }
439 }
440 }
441 }
442 }
443 _ => {}
444 }
445 }
446
447 if let (Some(pname), Some(pschema)) = (param_name, param_schema) {
448 chained_calls = quote! { #chained_calls .param(#pname, #pschema) };
449 }
450 }
451 }
452 }
453 }
454
455 let expanded = quote! {
456 #(#fn_attrs)*
458 #fn_vis #fn_async fn #fn_name #fn_generics (#fn_inputs) #fn_output #fn_block
459
460 #[doc(hidden)]
462 #fn_vis fn #route_fn_name() -> ::rustapi_rs::Route {
463 #route_helper(#path_value, #fn_name)
464 #chained_calls
465 }
466
467 #[doc(hidden)]
469 #[allow(non_upper_case_globals)]
470 #[::rustapi_rs::__private::linkme::distributed_slice(::rustapi_rs::__private::AUTO_ROUTES)]
471 #[linkme(crate = ::rustapi_rs::__private::linkme)]
472 static #auto_route_name: fn() -> ::rustapi_rs::Route = #route_fn_name;
473
474 #[doc(hidden)]
476 #[allow(non_snake_case)]
477 fn #schema_reg_fn_name(spec: &mut ::rustapi_rs::__private::rustapi_openapi::OpenApiSpec) {
478 #( spec.register_in_place::<#schema_types>(); )*
479 }
480
481 #[doc(hidden)]
482 #[allow(non_upper_case_globals)]
483 #[::rustapi_rs::__private::linkme::distributed_slice(::rustapi_rs::__private::AUTO_SCHEMAS)]
484 #[linkme(crate = ::rustapi_rs::__private::linkme)]
485 static #auto_schema_name: fn(&mut ::rustapi_rs::__private::rustapi_openapi::OpenApiSpec) = #schema_reg_fn_name;
486 };
487
488 debug_output(&format!("{} {}", method, path_value), &expanded);
489
490 TokenStream::from(expanded)
491}
492
493#[proc_macro_attribute]
509pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream {
510 generate_route_handler("GET", attr, item)
511}
512
513#[proc_macro_attribute]
515pub fn post(attr: TokenStream, item: TokenStream) -> TokenStream {
516 generate_route_handler("POST", attr, item)
517}
518
519#[proc_macro_attribute]
521pub fn put(attr: TokenStream, item: TokenStream) -> TokenStream {
522 generate_route_handler("PUT", attr, item)
523}
524
525#[proc_macro_attribute]
527pub fn patch(attr: TokenStream, item: TokenStream) -> TokenStream {
528 generate_route_handler("PATCH", attr, item)
529}
530
531#[proc_macro_attribute]
533pub fn delete(attr: TokenStream, item: TokenStream) -> TokenStream {
534 generate_route_handler("DELETE", attr, item)
535}
536
537#[proc_macro_attribute]
553pub fn tag(attr: TokenStream, item: TokenStream) -> TokenStream {
554 let tag = parse_macro_input!(attr as LitStr);
555 let input = parse_macro_input!(item as ItemFn);
556
557 let attrs = &input.attrs;
558 let vis = &input.vis;
559 let sig = &input.sig;
560 let block = &input.block;
561 let tag_value = tag.value();
562
563 let expanded = quote! {
565 #[doc = concat!("**Tag:** ", #tag_value)]
566 #(#attrs)*
567 #vis #sig #block
568 };
569
570 TokenStream::from(expanded)
571}
572
573#[proc_macro_attribute]
585pub fn summary(attr: TokenStream, item: TokenStream) -> TokenStream {
586 let summary = parse_macro_input!(attr as LitStr);
587 let input = parse_macro_input!(item as ItemFn);
588
589 let attrs = &input.attrs;
590 let vis = &input.vis;
591 let sig = &input.sig;
592 let block = &input.block;
593 let summary_value = summary.value();
594
595 let expanded = quote! {
597 #[doc = #summary_value]
598 #(#attrs)*
599 #vis #sig #block
600 };
601
602 TokenStream::from(expanded)
603}
604
605#[proc_macro_attribute]
617pub fn description(attr: TokenStream, item: TokenStream) -> TokenStream {
618 let desc = parse_macro_input!(attr as LitStr);
619 let input = parse_macro_input!(item as ItemFn);
620
621 let attrs = &input.attrs;
622 let vis = &input.vis;
623 let sig = &input.sig;
624 let block = &input.block;
625 let desc_value = desc.value();
626
627 let expanded = quote! {
629 #[doc = ""]
630 #[doc = #desc_value]
631 #(#attrs)*
632 #vis #sig #block
633 };
634
635 TokenStream::from(expanded)
636}
637
638#[proc_macro_attribute]
670pub fn param(_attr: TokenStream, item: TokenStream) -> TokenStream {
671 item
674}
675
676#[derive(Debug)]
682struct ValidationRuleInfo {
683 rule_type: String,
684 params: Vec<(String, String)>,
685 message: Option<String>,
686 groups: Vec<String>,
687}
688
689fn parse_validate_attrs(attrs: &[Attribute]) -> Vec<ValidationRuleInfo> {
691 let mut rules = Vec::new();
692
693 for attr in attrs {
694 if !attr.path().is_ident("validate") {
695 continue;
696 }
697
698 if let Ok(meta) = attr.parse_args::<Meta>() {
700 if let Some(rule) = parse_validate_meta(&meta) {
701 rules.push(rule);
702 }
703 } else if let Ok(nested) = attr
704 .parse_args_with(syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated)
705 {
706 for meta in nested {
707 if let Some(rule) = parse_validate_meta(&meta) {
708 rules.push(rule);
709 }
710 }
711 }
712 }
713
714 rules
715}
716
717fn parse_validate_meta(meta: &Meta) -> Option<ValidationRuleInfo> {
719 match meta {
720 Meta::Path(path) => {
721 let ident = path.get_ident()?.to_string();
723 Some(ValidationRuleInfo {
724 rule_type: ident,
725 params: Vec::new(),
726 message: None,
727 groups: Vec::new(),
728 })
729 }
730 Meta::List(list) => {
731 let rule_type = list.path.get_ident()?.to_string();
733 let mut params = Vec::new();
734 let mut message = None;
735 let mut groups = Vec::new();
736
737 if let Ok(nested) = list.parse_args_with(
739 syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
740 ) {
741 for nested_meta in nested {
742 if let Meta::NameValue(nv) = &nested_meta {
743 let key = nv.path.get_ident()?.to_string();
744
745 if key == "groups" {
746 let vec = expr_to_string_vec(&nv.value);
747 groups.extend(vec);
748 } else if let Some(value) = expr_to_string(&nv.value) {
749 if key == "message" {
750 message = Some(value);
751 } else if key == "group" {
752 groups.push(value);
753 } else {
754 params.push((key, value));
755 }
756 }
757 } else if let Meta::Path(path) = &nested_meta {
758 if let Some(ident) = path.get_ident() {
760 params.push((ident.to_string(), "true".to_string()));
761 }
762 }
763 }
764 }
765
766 Some(ValidationRuleInfo {
767 rule_type,
768 params,
769 message,
770 groups,
771 })
772 }
773 Meta::NameValue(nv) => {
774 let rule_type = nv.path.get_ident()?.to_string();
776 let value = expr_to_string(&nv.value)?;
777
778 Some(ValidationRuleInfo {
779 rule_type: rule_type.clone(),
780 params: vec![(rule_type.clone(), value)],
781 message: None,
782 groups: Vec::new(),
783 })
784 }
785 }
786}
787
788fn expr_to_string(expr: &Expr) -> Option<String> {
790 match expr {
791 Expr::Lit(lit) => match &lit.lit {
792 Lit::Str(s) => Some(s.value()),
793 Lit::Int(i) => Some(i.base10_digits().to_string()),
794 Lit::Float(f) => Some(f.base10_digits().to_string()),
795 Lit::Bool(b) => Some(b.value.to_string()),
796 _ => None,
797 },
798 _ => None,
799 }
800}
801
802fn expr_to_string_vec(expr: &Expr) -> Vec<String> {
804 match expr {
805 Expr::Array(arr) => {
806 let mut result = Vec::new();
807 for elem in &arr.elems {
808 if let Some(s) = expr_to_string(elem) {
809 result.push(s);
810 }
811 }
812 result
813 }
814 _ => {
815 if let Some(s) = expr_to_string(expr) {
816 vec![s]
817 } else {
818 Vec::new()
819 }
820 }
821 }
822}
823
824fn generate_rule_validation(
825 field_name: &str,
826 _field_type: &Type,
827 rule: &ValidationRuleInfo,
828) -> proc_macro2::TokenStream {
829 let field_ident = syn::Ident::new(field_name, proc_macro2::Span::call_site());
830 let field_name_str = field_name;
831
832 let group_check = if rule.groups.is_empty() {
834 quote! { true }
835 } else {
836 let group_names = rule.groups.iter().map(|g| g.as_str());
837 quote! {
838 {
839 let rule_groups = [#(::rustapi_validate::v2::ValidationGroup::from(#group_names)),*];
840 rule_groups.iter().any(|g| g.matches(&group))
841 }
842 }
843 };
844
845 let validation_logic = match rule.rule_type.as_str() {
846 "email" => {
847 let message = rule
848 .message
849 .as_ref()
850 .map(|m| quote! { .with_message(#m) })
851 .unwrap_or_default();
852 quote! {
853 {
854 let rule = ::rustapi_validate::v2::EmailRule::new() #message;
855 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
856 errors.add(#field_name_str, e);
857 }
858 }
859 }
860 }
861 "length" => {
862 let min = rule
863 .params
864 .iter()
865 .find(|(k, _)| k == "min")
866 .and_then(|(_, v)| v.parse::<usize>().ok());
867 let max = rule
868 .params
869 .iter()
870 .find(|(k, _)| k == "max")
871 .and_then(|(_, v)| v.parse::<usize>().ok());
872 let message = rule
873 .message
874 .as_ref()
875 .map(|m| quote! { .with_message(#m) })
876 .unwrap_or_default();
877
878 let rule_creation = match (min, max) {
879 (Some(min), Some(max)) => {
880 quote! { ::rustapi_validate::v2::LengthRule::new(#min, #max) }
881 }
882 (Some(min), None) => quote! { ::rustapi_validate::v2::LengthRule::min(#min) },
883 (None, Some(max)) => quote! { ::rustapi_validate::v2::LengthRule::max(#max) },
884 (None, None) => quote! { ::rustapi_validate::v2::LengthRule::new(0, usize::MAX) },
885 };
886
887 quote! {
888 {
889 let rule = #rule_creation #message;
890 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
891 errors.add(#field_name_str, e);
892 }
893 }
894 }
895 }
896 "range" => {
897 let min = rule
898 .params
899 .iter()
900 .find(|(k, _)| k == "min")
901 .map(|(_, v)| v.clone());
902 let max = rule
903 .params
904 .iter()
905 .find(|(k, _)| k == "max")
906 .map(|(_, v)| v.clone());
907 let message = rule
908 .message
909 .as_ref()
910 .map(|m| quote! { .with_message(#m) })
911 .unwrap_or_default();
912
913 let rule_creation = match (min, max) {
915 (Some(min), Some(max)) => {
916 let min_lit: proc_macro2::TokenStream = min.parse().unwrap();
917 let max_lit: proc_macro2::TokenStream = max.parse().unwrap();
918 quote! { ::rustapi_validate::v2::RangeRule::new(#min_lit, #max_lit) }
919 }
920 (Some(min), None) => {
921 let min_lit: proc_macro2::TokenStream = min.parse().unwrap();
922 quote! { ::rustapi_validate::v2::RangeRule::min(#min_lit) }
923 }
924 (None, Some(max)) => {
925 let max_lit: proc_macro2::TokenStream = max.parse().unwrap();
926 quote! { ::rustapi_validate::v2::RangeRule::max(#max_lit) }
927 }
928 (None, None) => {
929 return quote! {};
930 }
931 };
932
933 quote! {
934 {
935 let rule = #rule_creation #message;
936 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
937 errors.add(#field_name_str, e);
938 }
939 }
940 }
941 }
942 "regex" => {
943 let pattern = rule
944 .params
945 .iter()
946 .find(|(k, _)| k == "regex" || k == "pattern")
947 .map(|(_, v)| v.clone())
948 .unwrap_or_default();
949 let message = rule
950 .message
951 .as_ref()
952 .map(|m| quote! { .with_message(#m) })
953 .unwrap_or_default();
954
955 quote! {
956 {
957 let rule = ::rustapi_validate::v2::RegexRule::new(#pattern) #message;
958 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
959 errors.add(#field_name_str, e);
960 }
961 }
962 }
963 }
964 "url" => {
965 let message = rule
966 .message
967 .as_ref()
968 .map(|m| quote! { .with_message(#m) })
969 .unwrap_or_default();
970 quote! {
971 {
972 let rule = ::rustapi_validate::v2::UrlRule::new() #message;
973 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
974 errors.add(#field_name_str, e);
975 }
976 }
977 }
978 }
979 "required" => {
980 let message = rule
981 .message
982 .as_ref()
983 .map(|m| quote! { .with_message(#m) })
984 .unwrap_or_default();
985 quote! {
986 {
987 let rule = ::rustapi_validate::v2::RequiredRule::new() #message;
988 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
989 errors.add(#field_name_str, e);
990 }
991 }
992 }
993 }
994 "credit_card" => {
995 let message = rule
996 .message
997 .as_ref()
998 .map(|m| quote! { .with_message(#m) })
999 .unwrap_or_default();
1000 quote! {
1001 {
1002 let rule = ::rustapi_validate::v2::CreditCardRule::new() #message;
1003 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1004 errors.add(#field_name_str, e);
1005 }
1006 }
1007 }
1008 }
1009 "ip" => {
1010 let v4 = rule.params.iter().any(|(k, _)| k == "v4");
1011 let v6 = rule.params.iter().any(|(k, _)| k == "v6");
1012
1013 let rule_creation = if v4 && !v6 {
1014 quote! { ::rustapi_validate::v2::IpRule::v4() }
1015 } else if !v4 && v6 {
1016 quote! { ::rustapi_validate::v2::IpRule::v6() }
1017 } else {
1018 quote! { ::rustapi_validate::v2::IpRule::new() }
1019 };
1020
1021 let message = rule
1022 .message
1023 .as_ref()
1024 .map(|m| quote! { .with_message(#m) })
1025 .unwrap_or_default();
1026
1027 quote! {
1028 {
1029 let rule = #rule_creation #message;
1030 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1031 errors.add(#field_name_str, e);
1032 }
1033 }
1034 }
1035 }
1036 "phone" => {
1037 let message = rule
1038 .message
1039 .as_ref()
1040 .map(|m| quote! { .with_message(#m) })
1041 .unwrap_or_default();
1042 quote! {
1043 {
1044 let rule = ::rustapi_validate::v2::PhoneRule::new() #message;
1045 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1046 errors.add(#field_name_str, e);
1047 }
1048 }
1049 }
1050 }
1051 "contains" => {
1052 let needle = rule
1053 .params
1054 .iter()
1055 .find(|(k, _)| k == "needle")
1056 .map(|(_, v)| v.clone())
1057 .unwrap_or_default();
1058
1059 let message = rule
1060 .message
1061 .as_ref()
1062 .map(|m| quote! { .with_message(#m) })
1063 .unwrap_or_default();
1064
1065 quote! {
1066 {
1067 let rule = ::rustapi_validate::v2::ContainsRule::new(#needle) #message;
1068 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1069 errors.add(#field_name_str, e);
1070 }
1071 }
1072 }
1073 }
1074 _ => {
1075 quote! {}
1077 }
1078 };
1079
1080 quote! {
1081 if #group_check {
1082 #validation_logic
1083 }
1084 }
1085}
1086
1087fn generate_async_rule_validation(
1089 field_name: &str,
1090 rule: &ValidationRuleInfo,
1091) -> proc_macro2::TokenStream {
1092 let field_ident = syn::Ident::new(field_name, proc_macro2::Span::call_site());
1093 let field_name_str = field_name;
1094
1095 let group_check = if rule.groups.is_empty() {
1097 quote! { true }
1098 } else {
1099 let group_names = rule.groups.iter().map(|g| g.as_str());
1100 quote! {
1101 {
1102 let rule_groups = [#(::rustapi_validate::v2::ValidationGroup::from(#group_names)),*];
1103 rule_groups.iter().any(|g| g.matches(&group))
1104 }
1105 }
1106 };
1107
1108 let validation_logic = match rule.rule_type.as_str() {
1109 "async_unique" => {
1110 let table = rule
1111 .params
1112 .iter()
1113 .find(|(k, _)| k == "table")
1114 .map(|(_, v)| v.clone())
1115 .unwrap_or_default();
1116 let column = rule
1117 .params
1118 .iter()
1119 .find(|(k, _)| k == "column")
1120 .map(|(_, v)| v.clone())
1121 .unwrap_or_default();
1122 let message = rule
1123 .message
1124 .as_ref()
1125 .map(|m| quote! { .with_message(#m) })
1126 .unwrap_or_default();
1127
1128 quote! {
1129 {
1130 let rule = ::rustapi_validate::v2::AsyncUniqueRule::new(#table, #column) #message;
1131 if let Err(e) = ::rustapi_validate::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
1132 errors.add(#field_name_str, e);
1133 }
1134 }
1135 }
1136 }
1137 "async_exists" => {
1138 let table = rule
1139 .params
1140 .iter()
1141 .find(|(k, _)| k == "table")
1142 .map(|(_, v)| v.clone())
1143 .unwrap_or_default();
1144 let column = rule
1145 .params
1146 .iter()
1147 .find(|(k, _)| k == "column")
1148 .map(|(_, v)| v.clone())
1149 .unwrap_or_default();
1150 let message = rule
1151 .message
1152 .as_ref()
1153 .map(|m| quote! { .with_message(#m) })
1154 .unwrap_or_default();
1155
1156 quote! {
1157 {
1158 let rule = ::rustapi_validate::v2::AsyncExistsRule::new(#table, #column) #message;
1159 if let Err(e) = ::rustapi_validate::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
1160 errors.add(#field_name_str, e);
1161 }
1162 }
1163 }
1164 }
1165 "async_api" => {
1166 let endpoint = rule
1167 .params
1168 .iter()
1169 .find(|(k, _)| k == "endpoint")
1170 .map(|(_, v)| v.clone())
1171 .unwrap_or_default();
1172 let message = rule
1173 .message
1174 .as_ref()
1175 .map(|m| quote! { .with_message(#m) })
1176 .unwrap_or_default();
1177
1178 quote! {
1179 {
1180 let rule = ::rustapi_validate::v2::AsyncApiRule::new(#endpoint) #message;
1181 if let Err(e) = ::rustapi_validate::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
1182 errors.add(#field_name_str, e);
1183 }
1184 }
1185 }
1186 }
1187 "custom_async" => {
1188 let function_path = rule
1190 .params
1191 .iter()
1192 .find(|(k, _)| k == "custom_async" || k == "function")
1193 .map(|(_, v)| v.clone())
1194 .unwrap_or_default();
1195
1196 if function_path.is_empty() {
1197 quote! {}
1199 } else {
1200 let func: syn::Path = syn::parse_str(&function_path).unwrap();
1201 let message_handling = if let Some(msg) = &rule.message {
1202 quote! {
1203 let e = ::rustapi_validate::v2::RuleError::new("custom_async", #msg);
1204 errors.add(#field_name_str, e);
1205 }
1206 } else {
1207 quote! {
1208 errors.add(#field_name_str, e);
1209 }
1210 };
1211
1212 quote! {
1213 {
1214 if let Err(e) = #func(&self.#field_ident, ctx).await {
1216 #message_handling
1217 }
1218 }
1219 }
1220 }
1221 }
1222 _ => {
1223 quote! {}
1225 }
1226 };
1227
1228 quote! {
1229 if #group_check {
1230 #validation_logic
1231 }
1232 }
1233}
1234
1235fn is_async_rule(rule: &ValidationRuleInfo) -> bool {
1237 matches!(
1238 rule.rule_type.as_str(),
1239 "async_unique" | "async_exists" | "async_api" | "custom_async"
1240 )
1241}
1242
1243#[proc_macro_derive(Validate, attributes(validate))]
1266pub fn derive_validate(input: TokenStream) -> TokenStream {
1267 let input = parse_macro_input!(input as DeriveInput);
1268 let name = &input.ident;
1269 let generics = &input.generics;
1270 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
1271
1272 let fields = match &input.data {
1274 Data::Struct(data) => match &data.fields {
1275 Fields::Named(fields) => &fields.named,
1276 _ => {
1277 return syn::Error::new_spanned(
1278 &input,
1279 "Validate can only be derived for structs with named fields",
1280 )
1281 .to_compile_error()
1282 .into();
1283 }
1284 },
1285 _ => {
1286 return syn::Error::new_spanned(&input, "Validate can only be derived for structs")
1287 .to_compile_error()
1288 .into();
1289 }
1290 };
1291
1292 let mut sync_validations = Vec::new();
1294 let mut async_validations = Vec::new();
1295 let mut has_async_rules = false;
1296
1297 for field in fields {
1298 let field_name = field.ident.as_ref().unwrap().to_string();
1299 let field_type = &field.ty;
1300 let rules = parse_validate_attrs(&field.attrs);
1301
1302 for rule in &rules {
1303 if is_async_rule(rule) {
1304 has_async_rules = true;
1305 let validation = generate_async_rule_validation(&field_name, rule);
1306 async_validations.push(validation);
1307 } else {
1308 let validation = generate_rule_validation(&field_name, field_type, rule);
1309 sync_validations.push(validation);
1310 }
1311 }
1312 }
1313
1314 let validate_impl = quote! {
1316 impl #impl_generics ::rustapi_validate::v2::Validate for #name #ty_generics #where_clause {
1317 fn validate_with_group(&self, group: ::rustapi_validate::v2::ValidationGroup) -> Result<(), ::rustapi_validate::v2::ValidationErrors> {
1318 let mut errors = ::rustapi_validate::v2::ValidationErrors::new();
1319
1320 #(#sync_validations)*
1321
1322 errors.into_result()
1323 }
1324 }
1325 };
1326
1327 let async_validate_impl = if has_async_rules {
1329 quote! {
1330 #[::async_trait::async_trait]
1331 impl #impl_generics ::rustapi_validate::v2::AsyncValidate for #name #ty_generics #where_clause {
1332 async fn validate_async_with_group(&self, ctx: &::rustapi_validate::v2::ValidationContext, group: ::rustapi_validate::v2::ValidationGroup) -> Result<(), ::rustapi_validate::v2::ValidationErrors> {
1333 let mut errors = ::rustapi_validate::v2::ValidationErrors::new();
1334
1335 #(#async_validations)*
1336
1337 errors.into_result()
1338 }
1339 }
1340 }
1341 } else {
1342 quote! {
1344 #[::async_trait::async_trait]
1345 impl #impl_generics ::rustapi_validate::v2::AsyncValidate for #name #ty_generics #where_clause {
1346 async fn validate_async_with_group(&self, _ctx: &::rustapi_validate::v2::ValidationContext, _group: ::rustapi_validate::v2::ValidationGroup) -> Result<(), ::rustapi_validate::v2::ValidationErrors> {
1347 Ok(())
1348 }
1349 }
1350 }
1351 };
1352
1353 let expanded = quote! {
1354 #validate_impl
1355 #async_validate_impl
1356 };
1357
1358 debug_output("Validate derive", &expanded);
1359
1360 TokenStream::from(expanded)
1361}
1362
1363#[proc_macro_derive(ApiError, attributes(error))]
1382pub fn derive_api_error(input: TokenStream) -> TokenStream {
1383 api_error::expand_derive_api_error(input)
1384}
1385
1386#[proc_macro_derive(TypedPath, attributes(typed_path))]
1403pub fn derive_typed_path(input: TokenStream) -> TokenStream {
1404 let input = parse_macro_input!(input as DeriveInput);
1405 let name = &input.ident;
1406 let generics = &input.generics;
1407 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
1408
1409 let mut path_str = None;
1411 for attr in &input.attrs {
1412 if attr.path().is_ident("typed_path") {
1413 if let Ok(lit) = attr.parse_args::<LitStr>() {
1414 path_str = Some(lit.value());
1415 }
1416 }
1417 }
1418
1419 let path = match path_str {
1420 Some(p) => p,
1421 None => {
1422 return syn::Error::new_spanned(
1423 &input,
1424 "#[derive(TypedPath)] requires a #[typed_path(\"...\")] attribute",
1425 )
1426 .to_compile_error()
1427 .into();
1428 }
1429 };
1430
1431 if let Err(err) = validate_path_syntax(&path, proc_macro2::Span::call_site()) {
1433 return err.to_compile_error().into();
1434 }
1435
1436 let mut format_string = String::new();
1439 let mut format_args = Vec::new();
1440
1441 let mut chars = path.chars().peekable();
1442 while let Some(ch) = chars.next() {
1443 if ch == '{' {
1444 let mut param_name = String::new();
1445 while let Some(&c) = chars.peek() {
1446 if c == '}' {
1447 chars.next(); break;
1449 }
1450 param_name.push(chars.next().unwrap());
1451 }
1452
1453 if param_name.is_empty() {
1454 return syn::Error::new_spanned(
1455 &input,
1456 "Empty path parameter not allowed in typed_path",
1457 )
1458 .to_compile_error()
1459 .into();
1460 }
1461
1462 format_string.push_str("{}");
1463 let ident = syn::Ident::new(¶m_name, proc_macro2::Span::call_site());
1464 format_args.push(quote! { self.#ident });
1465 } else {
1466 format_string.push(ch);
1467 }
1468 }
1469
1470 let expanded = quote! {
1471 impl #impl_generics ::rustapi_rs::prelude::TypedPath for #name #ty_generics #where_clause {
1472 const PATH: &'static str = #path;
1473
1474 fn to_uri(&self) -> String {
1475 format!(#format_string, #(#format_args),*)
1476 }
1477 }
1478 };
1479
1480 debug_output("TypedPath derive", &expanded);
1481 TokenStream::from(expanded)
1482}