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 collect_path_params(input: &ItemFn) -> Vec<(String, String)> {
162 let mut params = Vec::new();
163
164 for arg in &input.sig.inputs {
165 if let FnArg::Typed(pat_ty) = arg {
166 if let Type::Path(tp) = &*pat_ty.ty {
168 if let Some(seg) = tp.path.segments.last() {
169 if seg.ident == "Path" {
170 if let PathArguments::AngleBracketed(args) = &seg.arguments {
172 if let Some(GenericArgument::Type(inner_ty)) = args.args.first() {
173 if let Some(schema_type) = map_type_to_schema(inner_ty) {
175 if let Some(name) = extract_param_name(&pat_ty.pat) {
183 params.push((name, schema_type));
184 }
185 }
186 }
187 }
188 }
189 }
190 }
191 }
192 }
193
194 params
195}
196
197fn extract_param_name(pat: &syn::Pat) -> Option<String> {
202 match pat {
203 syn::Pat::Ident(ident) => Some(ident.ident.to_string()),
204 syn::Pat::TupleStruct(ts) => {
205 if let Some(first) = ts.elems.first() {
208 extract_param_name(first)
209 } else {
210 None
211 }
212 }
213 _ => None, }
215}
216
217fn map_type_to_schema(ty: &Type) -> Option<String> {
219 match ty {
220 Type::Path(tp) => {
221 if let Some(seg) = tp.path.segments.last() {
222 let ident = seg.ident.to_string();
223 match ident.as_str() {
224 "Uuid" => Some("uuid".to_string()),
225 "String" | "str" => Some("string".to_string()),
226 "bool" => Some("boolean".to_string()),
227 "i8" | "i16" | "i32" | "i64" | "isize" | "u8" | "u16" | "u32" | "u64"
228 | "usize" => Some("integer".to_string()),
229 "f32" | "f64" => Some("number".to_string()),
230 _ => None,
231 }
232 } else {
233 None
234 }
235 }
236 _ => None,
237 }
238}
239
240fn is_debug_enabled() -> bool {
242 std::env::var("RUSTAPI_DEBUG")
243 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
244 .unwrap_or(false)
245}
246
247fn debug_output(name: &str, tokens: &proc_macro2::TokenStream) {
249 if is_debug_enabled() {
250 eprintln!("\n=== RUSTAPI_DEBUG: {} ===", name);
251 eprintln!("{}", tokens);
252 eprintln!("=== END {} ===\n", name);
253 }
254}
255
256fn validate_path_syntax(path: &str, span: proc_macro2::Span) -> Result<(), syn::Error> {
260 if !path.starts_with('/') {
262 return Err(syn::Error::new(
263 span,
264 format!("route path must start with '/', got: \"{}\"", path),
265 ));
266 }
267
268 if path.contains("//") {
270 return Err(syn::Error::new(
271 span,
272 format!(
273 "route path contains empty segment (double slash): \"{}\"",
274 path
275 ),
276 ));
277 }
278
279 let mut brace_depth = 0;
281 let mut param_start = None;
282
283 for (i, ch) in path.char_indices() {
284 match ch {
285 '{' => {
286 if brace_depth > 0 {
287 return Err(syn::Error::new(
288 span,
289 format!(
290 "nested braces are not allowed in route path at position {}: \"{}\"",
291 i, path
292 ),
293 ));
294 }
295 brace_depth += 1;
296 param_start = Some(i);
297 }
298 '}' => {
299 if brace_depth == 0 {
300 return Err(syn::Error::new(
301 span,
302 format!(
303 "unmatched closing brace '}}' at position {} in route path: \"{}\"",
304 i, path
305 ),
306 ));
307 }
308 brace_depth -= 1;
309
310 if let Some(start) = param_start {
312 let param_name = &path[start + 1..i];
313 if param_name.is_empty() {
314 return Err(syn::Error::new(
315 span,
316 format!(
317 "empty parameter name '{{}}' at position {} in route path: \"{}\"",
318 start, path
319 ),
320 ));
321 }
322 if !param_name.chars().all(|c| c.is_alphanumeric() || c == '_') {
324 return Err(syn::Error::new(
325 span,
326 format!(
327 "invalid parameter name '{{{}}}' at position {} - parameter names must contain only alphanumeric characters and underscores: \"{}\"",
328 param_name, start, path
329 ),
330 ));
331 }
332 if param_name
334 .chars()
335 .next()
336 .map(|c| c.is_ascii_digit())
337 .unwrap_or(false)
338 {
339 return Err(syn::Error::new(
340 span,
341 format!(
342 "parameter name '{{{}}}' cannot start with a digit at position {}: \"{}\"",
343 param_name, start, path
344 ),
345 ));
346 }
347 }
348 param_start = None;
349 }
350 _ if brace_depth == 0 => {
352 if !ch.is_alphanumeric() && !"-_./*".contains(ch) {
354 return Err(syn::Error::new(
355 span,
356 format!(
357 "invalid character '{}' at position {} in route path: \"{}\"",
358 ch, i, path
359 ),
360 ));
361 }
362 }
363 _ => {}
364 }
365 }
366
367 if brace_depth > 0 {
369 return Err(syn::Error::new(
370 span,
371 format!(
372 "unclosed brace '{{' in route path (missing closing '}}'): \"{}\"",
373 path
374 ),
375 ));
376 }
377
378 Ok(())
379}
380
381#[proc_macro_attribute]
399pub fn main(_attr: TokenStream, item: TokenStream) -> TokenStream {
400 let input = parse_macro_input!(item as ItemFn);
401
402 let attrs = &input.attrs;
403 let vis = &input.vis;
404 let sig = &input.sig;
405 let block = &input.block;
406
407 let expanded = quote! {
408 #(#attrs)*
409 #[::tokio::main]
410 #vis #sig {
411 #block
412 }
413 };
414
415 debug_output("main", &expanded);
416
417 TokenStream::from(expanded)
418}
419
420fn generate_route_handler(method: &str, attr: TokenStream, item: TokenStream) -> TokenStream {
422 let path = parse_macro_input!(attr as LitStr);
423 let input = parse_macro_input!(item as ItemFn);
424
425 let fn_name = &input.sig.ident;
426 let fn_vis = &input.vis;
427 let fn_attrs = &input.attrs;
428 let fn_async = &input.sig.asyncness;
429 let fn_inputs = &input.sig.inputs;
430 let fn_output = &input.sig.output;
431 let fn_block = &input.block;
432 let fn_generics = &input.sig.generics;
433
434 let schema_types = collect_handler_schema_types(&input);
435
436 let path_value = path.value();
437
438 if let Err(err) = validate_path_syntax(&path_value, path.span()) {
440 return err.to_compile_error().into();
441 }
442
443 let route_fn_name = syn::Ident::new(&format!("{}_route", fn_name), fn_name.span());
445 let auto_route_name = syn::Ident::new(&format!("__AUTO_ROUTE_{}", fn_name), fn_name.span());
447
448 let schema_reg_fn_name =
450 syn::Ident::new(&format!("__{}_register_schemas", fn_name), fn_name.span());
451 let auto_schema_name = syn::Ident::new(&format!("__AUTO_SCHEMA_{}", fn_name), fn_name.span());
452
453 let route_helper = match method {
455 "GET" => quote!(::rustapi_rs::get_route),
456 "POST" => quote!(::rustapi_rs::post_route),
457 "PUT" => quote!(::rustapi_rs::put_route),
458 "PATCH" => quote!(::rustapi_rs::patch_route),
459 "DELETE" => quote!(::rustapi_rs::delete_route),
460 _ => quote!(::rustapi_rs::get_route),
461 };
462
463 let auto_params = collect_path_params(&input);
465
466 let mut chained_calls = quote!();
468
469 for (name, schema) in auto_params {
471 chained_calls = quote! { #chained_calls .param(#name, #schema) };
472 }
473
474 for attr in fn_attrs {
475 if let Some(ident) = attr.path().segments.last().map(|s| &s.ident) {
478 let ident_str = ident.to_string();
479 if ident_str == "tag" {
480 if let Ok(lit) = attr.parse_args::<LitStr>() {
481 let val = lit.value();
482 chained_calls = quote! { #chained_calls .tag(#val) };
483 }
484 } else if ident_str == "summary" {
485 if let Ok(lit) = attr.parse_args::<LitStr>() {
486 let val = lit.value();
487 chained_calls = quote! { #chained_calls .summary(#val) };
488 }
489 } else if ident_str == "description" {
490 if let Ok(lit) = attr.parse_args::<LitStr>() {
491 let val = lit.value();
492 chained_calls = quote! { #chained_calls .description(#val) };
493 }
494 } else if ident_str == "param" {
495 if let Ok(param_args) = attr.parse_args_with(
497 syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
498 ) {
499 let mut param_name: Option<String> = None;
500 let mut param_schema: Option<String> = None;
501
502 for meta in param_args {
503 match &meta {
504 Meta::Path(path) => {
506 if param_name.is_none() {
507 if let Some(ident) = path.get_ident() {
508 param_name = Some(ident.to_string());
509 }
510 }
511 }
512 Meta::NameValue(nv) => {
514 let key = nv.path.get_ident().map(|i| i.to_string());
515 if let Some(key) = key {
516 if key == "schema" || key == "type" {
517 if let Expr::Lit(lit) = &nv.value {
518 if let Lit::Str(s) = &lit.lit {
519 param_schema = Some(s.value());
520 }
521 }
522 } else if param_name.is_none() {
523 param_name = Some(key);
525 if let Expr::Lit(lit) = &nv.value {
526 if let Lit::Str(s) = &lit.lit {
527 param_schema = Some(s.value());
528 }
529 }
530 }
531 }
532 }
533 _ => {}
534 }
535 }
536
537 if let (Some(pname), Some(pschema)) = (param_name, param_schema) {
538 chained_calls = quote! { #chained_calls .param(#pname, #pschema) };
539 }
540 }
541 }
542 }
543 }
544
545 let expanded = quote! {
546 #(#fn_attrs)*
548 #fn_vis #fn_async fn #fn_name #fn_generics (#fn_inputs) #fn_output #fn_block
549
550 #[doc(hidden)]
552 #fn_vis fn #route_fn_name() -> ::rustapi_rs::Route {
553 #route_helper(#path_value, #fn_name)
554 #chained_calls
555 }
556
557 #[doc(hidden)]
559 #[allow(non_upper_case_globals)]
560 #[::rustapi_rs::__private::linkme::distributed_slice(::rustapi_rs::__private::AUTO_ROUTES)]
561 #[linkme(crate = ::rustapi_rs::__private::linkme)]
562 static #auto_route_name: fn() -> ::rustapi_rs::Route = #route_fn_name;
563
564 #[doc(hidden)]
566 #[allow(non_snake_case)]
567 fn #schema_reg_fn_name(spec: &mut ::rustapi_rs::__private::rustapi_openapi::OpenApiSpec) {
568 #( spec.register_in_place::<#schema_types>(); )*
569 }
570
571 #[doc(hidden)]
572 #[allow(non_upper_case_globals)]
573 #[::rustapi_rs::__private::linkme::distributed_slice(::rustapi_rs::__private::AUTO_SCHEMAS)]
574 #[linkme(crate = ::rustapi_rs::__private::linkme)]
575 static #auto_schema_name: fn(&mut ::rustapi_rs::__private::rustapi_openapi::OpenApiSpec) = #schema_reg_fn_name;
576 };
577
578 debug_output(&format!("{} {}", method, path_value), &expanded);
579
580 TokenStream::from(expanded)
581}
582
583#[proc_macro_attribute]
599pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream {
600 generate_route_handler("GET", attr, item)
601}
602
603#[proc_macro_attribute]
605pub fn post(attr: TokenStream, item: TokenStream) -> TokenStream {
606 generate_route_handler("POST", attr, item)
607}
608
609#[proc_macro_attribute]
611pub fn put(attr: TokenStream, item: TokenStream) -> TokenStream {
612 generate_route_handler("PUT", attr, item)
613}
614
615#[proc_macro_attribute]
617pub fn patch(attr: TokenStream, item: TokenStream) -> TokenStream {
618 generate_route_handler("PATCH", attr, item)
619}
620
621#[proc_macro_attribute]
623pub fn delete(attr: TokenStream, item: TokenStream) -> TokenStream {
624 generate_route_handler("DELETE", attr, item)
625}
626
627#[proc_macro_attribute]
643pub fn tag(attr: TokenStream, item: TokenStream) -> TokenStream {
644 let tag = parse_macro_input!(attr as LitStr);
645 let input = parse_macro_input!(item as ItemFn);
646
647 let attrs = &input.attrs;
648 let vis = &input.vis;
649 let sig = &input.sig;
650 let block = &input.block;
651 let tag_value = tag.value();
652
653 let expanded = quote! {
655 #[doc = concat!("**Tag:** ", #tag_value)]
656 #(#attrs)*
657 #vis #sig #block
658 };
659
660 TokenStream::from(expanded)
661}
662
663#[proc_macro_attribute]
675pub fn summary(attr: TokenStream, item: TokenStream) -> TokenStream {
676 let summary = parse_macro_input!(attr as LitStr);
677 let input = parse_macro_input!(item as ItemFn);
678
679 let attrs = &input.attrs;
680 let vis = &input.vis;
681 let sig = &input.sig;
682 let block = &input.block;
683 let summary_value = summary.value();
684
685 let expanded = quote! {
687 #[doc = #summary_value]
688 #(#attrs)*
689 #vis #sig #block
690 };
691
692 TokenStream::from(expanded)
693}
694
695#[proc_macro_attribute]
707pub fn description(attr: TokenStream, item: TokenStream) -> TokenStream {
708 let desc = parse_macro_input!(attr as LitStr);
709 let input = parse_macro_input!(item as ItemFn);
710
711 let attrs = &input.attrs;
712 let vis = &input.vis;
713 let sig = &input.sig;
714 let block = &input.block;
715 let desc_value = desc.value();
716
717 let expanded = quote! {
719 #[doc = ""]
720 #[doc = #desc_value]
721 #(#attrs)*
722 #vis #sig #block
723 };
724
725 TokenStream::from(expanded)
726}
727
728#[proc_macro_attribute]
760pub fn param(_attr: TokenStream, item: TokenStream) -> TokenStream {
761 item
764}
765
766#[derive(Debug)]
772struct ValidationRuleInfo {
773 rule_type: String,
774 params: Vec<(String, String)>,
775 message: Option<String>,
776 groups: Vec<String>,
777}
778
779fn parse_validate_attrs(attrs: &[Attribute]) -> Vec<ValidationRuleInfo> {
781 let mut rules = Vec::new();
782
783 for attr in attrs {
784 if !attr.path().is_ident("validate") {
785 continue;
786 }
787
788 if let Ok(meta) = attr.parse_args::<Meta>() {
790 if let Some(rule) = parse_validate_meta(&meta) {
791 rules.push(rule);
792 }
793 } else if let Ok(nested) = attr
794 .parse_args_with(syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated)
795 {
796 for meta in nested {
797 if let Some(rule) = parse_validate_meta(&meta) {
798 rules.push(rule);
799 }
800 }
801 }
802 }
803
804 rules
805}
806
807fn parse_validate_meta(meta: &Meta) -> Option<ValidationRuleInfo> {
809 match meta {
810 Meta::Path(path) => {
811 let ident = path.get_ident()?.to_string();
813 Some(ValidationRuleInfo {
814 rule_type: ident,
815 params: Vec::new(),
816 message: None,
817 groups: Vec::new(),
818 })
819 }
820 Meta::List(list) => {
821 let rule_type = list.path.get_ident()?.to_string();
823 let mut params = Vec::new();
824 let mut message = None;
825 let mut groups = Vec::new();
826
827 if let Ok(nested) = list.parse_args_with(
829 syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
830 ) {
831 for nested_meta in nested {
832 if let Meta::NameValue(nv) = &nested_meta {
833 let key = nv.path.get_ident()?.to_string();
834
835 if key == "groups" {
836 let vec = expr_to_string_vec(&nv.value);
837 groups.extend(vec);
838 } else if let Some(value) = expr_to_string(&nv.value) {
839 if key == "message" {
840 message = Some(value);
841 } else if key == "group" {
842 groups.push(value);
843 } else {
844 params.push((key, value));
845 }
846 }
847 } else if let Meta::Path(path) = &nested_meta {
848 if let Some(ident) = path.get_ident() {
850 params.push((ident.to_string(), "true".to_string()));
851 }
852 }
853 }
854 }
855
856 Some(ValidationRuleInfo {
857 rule_type,
858 params,
859 message,
860 groups,
861 })
862 }
863 Meta::NameValue(nv) => {
864 let rule_type = nv.path.get_ident()?.to_string();
866 let value = expr_to_string(&nv.value)?;
867
868 Some(ValidationRuleInfo {
869 rule_type: rule_type.clone(),
870 params: vec![(rule_type.clone(), value)],
871 message: None,
872 groups: Vec::new(),
873 })
874 }
875 }
876}
877
878fn expr_to_string(expr: &Expr) -> Option<String> {
880 match expr {
881 Expr::Lit(lit) => match &lit.lit {
882 Lit::Str(s) => Some(s.value()),
883 Lit::Int(i) => Some(i.base10_digits().to_string()),
884 Lit::Float(f) => Some(f.base10_digits().to_string()),
885 Lit::Bool(b) => Some(b.value.to_string()),
886 _ => None,
887 },
888 _ => None,
889 }
890}
891
892fn expr_to_string_vec(expr: &Expr) -> Vec<String> {
894 match expr {
895 Expr::Array(arr) => {
896 let mut result = Vec::new();
897 for elem in &arr.elems {
898 if let Some(s) = expr_to_string(elem) {
899 result.push(s);
900 }
901 }
902 result
903 }
904 _ => {
905 if let Some(s) = expr_to_string(expr) {
906 vec![s]
907 } else {
908 Vec::new()
909 }
910 }
911 }
912}
913
914fn generate_rule_validation(
915 field_name: &str,
916 _field_type: &Type,
917 rule: &ValidationRuleInfo,
918) -> proc_macro2::TokenStream {
919 let field_ident = syn::Ident::new(field_name, proc_macro2::Span::call_site());
920 let field_name_str = field_name;
921
922 let group_check = if rule.groups.is_empty() {
924 quote! { true }
925 } else {
926 let group_names = rule.groups.iter().map(|g| g.as_str());
927 quote! {
928 {
929 let rule_groups = [#(::rustapi_validate::v2::ValidationGroup::from(#group_names)),*];
930 rule_groups.iter().any(|g| g.matches(&group))
931 }
932 }
933 };
934
935 let validation_logic = match rule.rule_type.as_str() {
936 "email" => {
937 let message = rule
938 .message
939 .as_ref()
940 .map(|m| quote! { .with_message(#m) })
941 .unwrap_or_default();
942 quote! {
943 {
944 let rule = ::rustapi_validate::v2::EmailRule::new() #message;
945 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
946 errors.add(#field_name_str, e);
947 }
948 }
949 }
950 }
951 "length" => {
952 let min = rule
953 .params
954 .iter()
955 .find(|(k, _)| k == "min")
956 .and_then(|(_, v)| v.parse::<usize>().ok());
957 let max = rule
958 .params
959 .iter()
960 .find(|(k, _)| k == "max")
961 .and_then(|(_, v)| v.parse::<usize>().ok());
962 let message = rule
963 .message
964 .as_ref()
965 .map(|m| quote! { .with_message(#m) })
966 .unwrap_or_default();
967
968 let rule_creation = match (min, max) {
969 (Some(min), Some(max)) => {
970 quote! { ::rustapi_validate::v2::LengthRule::new(#min, #max) }
971 }
972 (Some(min), None) => quote! { ::rustapi_validate::v2::LengthRule::min(#min) },
973 (None, Some(max)) => quote! { ::rustapi_validate::v2::LengthRule::max(#max) },
974 (None, None) => quote! { ::rustapi_validate::v2::LengthRule::new(0, usize::MAX) },
975 };
976
977 quote! {
978 {
979 let rule = #rule_creation #message;
980 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
981 errors.add(#field_name_str, e);
982 }
983 }
984 }
985 }
986 "range" => {
987 let min = rule
988 .params
989 .iter()
990 .find(|(k, _)| k == "min")
991 .map(|(_, v)| v.clone());
992 let max = rule
993 .params
994 .iter()
995 .find(|(k, _)| k == "max")
996 .map(|(_, v)| v.clone());
997 let message = rule
998 .message
999 .as_ref()
1000 .map(|m| quote! { .with_message(#m) })
1001 .unwrap_or_default();
1002
1003 let rule_creation = match (min, max) {
1005 (Some(min), Some(max)) => {
1006 let min_lit: proc_macro2::TokenStream = min.parse().unwrap();
1007 let max_lit: proc_macro2::TokenStream = max.parse().unwrap();
1008 quote! { ::rustapi_validate::v2::RangeRule::new(#min_lit, #max_lit) }
1009 }
1010 (Some(min), None) => {
1011 let min_lit: proc_macro2::TokenStream = min.parse().unwrap();
1012 quote! { ::rustapi_validate::v2::RangeRule::min(#min_lit) }
1013 }
1014 (None, Some(max)) => {
1015 let max_lit: proc_macro2::TokenStream = max.parse().unwrap();
1016 quote! { ::rustapi_validate::v2::RangeRule::max(#max_lit) }
1017 }
1018 (None, None) => {
1019 return quote! {};
1020 }
1021 };
1022
1023 quote! {
1024 {
1025 let rule = #rule_creation #message;
1026 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1027 errors.add(#field_name_str, e);
1028 }
1029 }
1030 }
1031 }
1032 "regex" => {
1033 let pattern = rule
1034 .params
1035 .iter()
1036 .find(|(k, _)| k == "regex" || k == "pattern")
1037 .map(|(_, v)| v.clone())
1038 .unwrap_or_default();
1039 let message = rule
1040 .message
1041 .as_ref()
1042 .map(|m| quote! { .with_message(#m) })
1043 .unwrap_or_default();
1044
1045 quote! {
1046 {
1047 let rule = ::rustapi_validate::v2::RegexRule::new(#pattern) #message;
1048 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1049 errors.add(#field_name_str, e);
1050 }
1051 }
1052 }
1053 }
1054 "url" => {
1055 let message = rule
1056 .message
1057 .as_ref()
1058 .map(|m| quote! { .with_message(#m) })
1059 .unwrap_or_default();
1060 quote! {
1061 {
1062 let rule = ::rustapi_validate::v2::UrlRule::new() #message;
1063 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1064 errors.add(#field_name_str, e);
1065 }
1066 }
1067 }
1068 }
1069 "required" => {
1070 let message = rule
1071 .message
1072 .as_ref()
1073 .map(|m| quote! { .with_message(#m) })
1074 .unwrap_or_default();
1075 quote! {
1076 {
1077 let rule = ::rustapi_validate::v2::RequiredRule::new() #message;
1078 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1079 errors.add(#field_name_str, e);
1080 }
1081 }
1082 }
1083 }
1084 "credit_card" => {
1085 let message = rule
1086 .message
1087 .as_ref()
1088 .map(|m| quote! { .with_message(#m) })
1089 .unwrap_or_default();
1090 quote! {
1091 {
1092 let rule = ::rustapi_validate::v2::CreditCardRule::new() #message;
1093 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1094 errors.add(#field_name_str, e);
1095 }
1096 }
1097 }
1098 }
1099 "ip" => {
1100 let v4 = rule.params.iter().any(|(k, _)| k == "v4");
1101 let v6 = rule.params.iter().any(|(k, _)| k == "v6");
1102
1103 let rule_creation = if v4 && !v6 {
1104 quote! { ::rustapi_validate::v2::IpRule::v4() }
1105 } else if !v4 && v6 {
1106 quote! { ::rustapi_validate::v2::IpRule::v6() }
1107 } else {
1108 quote! { ::rustapi_validate::v2::IpRule::new() }
1109 };
1110
1111 let message = rule
1112 .message
1113 .as_ref()
1114 .map(|m| quote! { .with_message(#m) })
1115 .unwrap_or_default();
1116
1117 quote! {
1118 {
1119 let rule = #rule_creation #message;
1120 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1121 errors.add(#field_name_str, e);
1122 }
1123 }
1124 }
1125 }
1126 "phone" => {
1127 let message = rule
1128 .message
1129 .as_ref()
1130 .map(|m| quote! { .with_message(#m) })
1131 .unwrap_or_default();
1132 quote! {
1133 {
1134 let rule = ::rustapi_validate::v2::PhoneRule::new() #message;
1135 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1136 errors.add(#field_name_str, e);
1137 }
1138 }
1139 }
1140 }
1141 "contains" => {
1142 let needle = rule
1143 .params
1144 .iter()
1145 .find(|(k, _)| k == "needle")
1146 .map(|(_, v)| v.clone())
1147 .unwrap_or_default();
1148
1149 let message = rule
1150 .message
1151 .as_ref()
1152 .map(|m| quote! { .with_message(#m) })
1153 .unwrap_or_default();
1154
1155 quote! {
1156 {
1157 let rule = ::rustapi_validate::v2::ContainsRule::new(#needle) #message;
1158 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1159 errors.add(#field_name_str, e);
1160 }
1161 }
1162 }
1163 }
1164 _ => {
1165 quote! {}
1167 }
1168 };
1169
1170 quote! {
1171 if #group_check {
1172 #validation_logic
1173 }
1174 }
1175}
1176
1177fn generate_async_rule_validation(
1179 field_name: &str,
1180 rule: &ValidationRuleInfo,
1181) -> proc_macro2::TokenStream {
1182 let field_ident = syn::Ident::new(field_name, proc_macro2::Span::call_site());
1183 let field_name_str = field_name;
1184
1185 let group_check = if rule.groups.is_empty() {
1187 quote! { true }
1188 } else {
1189 let group_names = rule.groups.iter().map(|g| g.as_str());
1190 quote! {
1191 {
1192 let rule_groups = [#(::rustapi_validate::v2::ValidationGroup::from(#group_names)),*];
1193 rule_groups.iter().any(|g| g.matches(&group))
1194 }
1195 }
1196 };
1197
1198 let validation_logic = match rule.rule_type.as_str() {
1199 "async_unique" => {
1200 let table = rule
1201 .params
1202 .iter()
1203 .find(|(k, _)| k == "table")
1204 .map(|(_, v)| v.clone())
1205 .unwrap_or_default();
1206 let column = rule
1207 .params
1208 .iter()
1209 .find(|(k, _)| k == "column")
1210 .map(|(_, v)| v.clone())
1211 .unwrap_or_default();
1212 let message = rule
1213 .message
1214 .as_ref()
1215 .map(|m| quote! { .with_message(#m) })
1216 .unwrap_or_default();
1217
1218 quote! {
1219 {
1220 let rule = ::rustapi_validate::v2::AsyncUniqueRule::new(#table, #column) #message;
1221 if let Err(e) = ::rustapi_validate::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
1222 errors.add(#field_name_str, e);
1223 }
1224 }
1225 }
1226 }
1227 "async_exists" => {
1228 let table = rule
1229 .params
1230 .iter()
1231 .find(|(k, _)| k == "table")
1232 .map(|(_, v)| v.clone())
1233 .unwrap_or_default();
1234 let column = rule
1235 .params
1236 .iter()
1237 .find(|(k, _)| k == "column")
1238 .map(|(_, v)| v.clone())
1239 .unwrap_or_default();
1240 let message = rule
1241 .message
1242 .as_ref()
1243 .map(|m| quote! { .with_message(#m) })
1244 .unwrap_or_default();
1245
1246 quote! {
1247 {
1248 let rule = ::rustapi_validate::v2::AsyncExistsRule::new(#table, #column) #message;
1249 if let Err(e) = ::rustapi_validate::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
1250 errors.add(#field_name_str, e);
1251 }
1252 }
1253 }
1254 }
1255 "async_api" => {
1256 let endpoint = rule
1257 .params
1258 .iter()
1259 .find(|(k, _)| k == "endpoint")
1260 .map(|(_, v)| v.clone())
1261 .unwrap_or_default();
1262 let message = rule
1263 .message
1264 .as_ref()
1265 .map(|m| quote! { .with_message(#m) })
1266 .unwrap_or_default();
1267
1268 quote! {
1269 {
1270 let rule = ::rustapi_validate::v2::AsyncApiRule::new(#endpoint) #message;
1271 if let Err(e) = ::rustapi_validate::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
1272 errors.add(#field_name_str, e);
1273 }
1274 }
1275 }
1276 }
1277 "custom_async" => {
1278 let function_path = rule
1280 .params
1281 .iter()
1282 .find(|(k, _)| k == "custom_async" || k == "function")
1283 .map(|(_, v)| v.clone())
1284 .unwrap_or_default();
1285
1286 if function_path.is_empty() {
1287 quote! {}
1289 } else {
1290 let func: syn::Path = syn::parse_str(&function_path).unwrap();
1291 let message_handling = if let Some(msg) = &rule.message {
1292 quote! {
1293 let e = ::rustapi_validate::v2::RuleError::new("custom_async", #msg);
1294 errors.add(#field_name_str, e);
1295 }
1296 } else {
1297 quote! {
1298 errors.add(#field_name_str, e);
1299 }
1300 };
1301
1302 quote! {
1303 {
1304 if let Err(e) = #func(&self.#field_ident, ctx).await {
1306 #message_handling
1307 }
1308 }
1309 }
1310 }
1311 }
1312 _ => {
1313 quote! {}
1315 }
1316 };
1317
1318 quote! {
1319 if #group_check {
1320 #validation_logic
1321 }
1322 }
1323}
1324
1325fn is_async_rule(rule: &ValidationRuleInfo) -> bool {
1327 matches!(
1328 rule.rule_type.as_str(),
1329 "async_unique" | "async_exists" | "async_api" | "custom_async"
1330 )
1331}
1332
1333#[proc_macro_derive(Validate, attributes(validate))]
1356pub fn derive_validate(input: TokenStream) -> TokenStream {
1357 let input = parse_macro_input!(input as DeriveInput);
1358 let name = &input.ident;
1359 let generics = &input.generics;
1360 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
1361
1362 let fields = match &input.data {
1364 Data::Struct(data) => match &data.fields {
1365 Fields::Named(fields) => &fields.named,
1366 _ => {
1367 return syn::Error::new_spanned(
1368 &input,
1369 "Validate can only be derived for structs with named fields",
1370 )
1371 .to_compile_error()
1372 .into();
1373 }
1374 },
1375 _ => {
1376 return syn::Error::new_spanned(&input, "Validate can only be derived for structs")
1377 .to_compile_error()
1378 .into();
1379 }
1380 };
1381
1382 let mut sync_validations = Vec::new();
1384 let mut async_validations = Vec::new();
1385 let mut has_async_rules = false;
1386
1387 for field in fields {
1388 let field_name = field.ident.as_ref().unwrap().to_string();
1389 let field_type = &field.ty;
1390 let rules = parse_validate_attrs(&field.attrs);
1391
1392 for rule in &rules {
1393 if is_async_rule(rule) {
1394 has_async_rules = true;
1395 let validation = generate_async_rule_validation(&field_name, rule);
1396 async_validations.push(validation);
1397 } else {
1398 let validation = generate_rule_validation(&field_name, field_type, rule);
1399 sync_validations.push(validation);
1400 }
1401 }
1402 }
1403
1404 let validate_impl = quote! {
1406 impl #impl_generics ::rustapi_validate::v2::Validate for #name #ty_generics #where_clause {
1407 fn validate_with_group(&self, group: ::rustapi_validate::v2::ValidationGroup) -> Result<(), ::rustapi_validate::v2::ValidationErrors> {
1408 let mut errors = ::rustapi_validate::v2::ValidationErrors::new();
1409
1410 #(#sync_validations)*
1411
1412 errors.into_result()
1413 }
1414 }
1415 };
1416
1417 let async_validate_impl = if has_async_rules {
1419 quote! {
1420 #[::async_trait::async_trait]
1421 impl #impl_generics ::rustapi_validate::v2::AsyncValidate for #name #ty_generics #where_clause {
1422 async fn validate_async_with_group(&self, ctx: &::rustapi_validate::v2::ValidationContext, group: ::rustapi_validate::v2::ValidationGroup) -> Result<(), ::rustapi_validate::v2::ValidationErrors> {
1423 let mut errors = ::rustapi_validate::v2::ValidationErrors::new();
1424
1425 #(#async_validations)*
1426
1427 errors.into_result()
1428 }
1429 }
1430 }
1431 } else {
1432 quote! {
1434 #[::async_trait::async_trait]
1435 impl #impl_generics ::rustapi_validate::v2::AsyncValidate for #name #ty_generics #where_clause {
1436 async fn validate_async_with_group(&self, _ctx: &::rustapi_validate::v2::ValidationContext, _group: ::rustapi_validate::v2::ValidationGroup) -> Result<(), ::rustapi_validate::v2::ValidationErrors> {
1437 Ok(())
1438 }
1439 }
1440 }
1441 };
1442
1443 let validatable_impl = quote! {
1447 impl #impl_generics ::rustapi_core::validation::Validatable for #name #ty_generics #where_clause {
1448 fn do_validate(&self) -> Result<(), ::rustapi_core::ApiError> {
1449 match ::rustapi_validate::v2::Validate::validate(self) {
1450 Ok(_) => Ok(()),
1451 Err(e) => Err(::rustapi_core::validation::convert_v2_errors(e)),
1452 }
1453 }
1454 }
1455 };
1456
1457 let expanded = quote! {
1458 #validate_impl
1459 #async_validate_impl
1460 #validatable_impl
1461 };
1462
1463 debug_output("Validate derive", &expanded);
1464
1465 TokenStream::from(expanded)
1466}
1467
1468#[proc_macro_derive(ApiError, attributes(error))]
1487pub fn derive_api_error(input: TokenStream) -> TokenStream {
1488 api_error::expand_derive_api_error(input)
1489}
1490
1491#[proc_macro_derive(TypedPath, attributes(typed_path))]
1508pub fn derive_typed_path(input: TokenStream) -> TokenStream {
1509 let input = parse_macro_input!(input as DeriveInput);
1510 let name = &input.ident;
1511 let generics = &input.generics;
1512 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
1513
1514 let mut path_str = None;
1516 for attr in &input.attrs {
1517 if attr.path().is_ident("typed_path") {
1518 if let Ok(lit) = attr.parse_args::<LitStr>() {
1519 path_str = Some(lit.value());
1520 }
1521 }
1522 }
1523
1524 let path = match path_str {
1525 Some(p) => p,
1526 None => {
1527 return syn::Error::new_spanned(
1528 &input,
1529 "#[derive(TypedPath)] requires a #[typed_path(\"...\")] attribute",
1530 )
1531 .to_compile_error()
1532 .into();
1533 }
1534 };
1535
1536 if let Err(err) = validate_path_syntax(&path, proc_macro2::Span::call_site()) {
1538 return err.to_compile_error().into();
1539 }
1540
1541 let mut format_string = String::new();
1544 let mut format_args = Vec::new();
1545
1546 let mut chars = path.chars().peekable();
1547 while let Some(ch) = chars.next() {
1548 if ch == '{' {
1549 let mut param_name = String::new();
1550 while let Some(&c) = chars.peek() {
1551 if c == '}' {
1552 chars.next(); break;
1554 }
1555 param_name.push(chars.next().unwrap());
1556 }
1557
1558 if param_name.is_empty() {
1559 return syn::Error::new_spanned(
1560 &input,
1561 "Empty path parameter not allowed in typed_path",
1562 )
1563 .to_compile_error()
1564 .into();
1565 }
1566
1567 format_string.push_str("{}");
1568 let ident = syn::Ident::new(¶m_name, proc_macro2::Span::call_site());
1569 format_args.push(quote! { self.#ident });
1570 } else {
1571 format_string.push(ch);
1572 }
1573 }
1574
1575 let expanded = quote! {
1576 impl #impl_generics ::rustapi_rs::prelude::TypedPath for #name #ty_generics #where_clause {
1577 const PATH: &'static str = #path;
1578
1579 fn to_uri(&self) -> String {
1580 format!(#format_string, #(#format_args),*)
1581 }
1582 }
1583 };
1584
1585 debug_output("TypedPath derive", &expanded);
1586 TokenStream::from(expanded)
1587}