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 extract_path_params_from_route(path: &str) -> Vec<String> {
161 let mut params = Vec::new();
162 let mut in_brace = false;
163 let mut current = String::new();
164
165 for ch in path.chars() {
166 match ch {
167 '{' => {
168 in_brace = true;
169 current.clear();
170 }
171 '}' => {
172 if in_brace && !current.is_empty() {
173 params.push(current.clone());
174 }
175 in_brace = false;
176 current.clear();
177 }
178 _ if in_brace => {
179 current.push(ch);
180 }
181 _ => {}
182 }
183 }
184 params
185}
186
187fn extract_param_names_from_pattern(pat: &syn::Pat) -> Vec<String> {
190 let mut names = Vec::new();
191
192 match pat {
193 syn::Pat::TupleStruct(ts) => {
195 for elem in &ts.elems {
196 match elem {
197 syn::Pat::Ident(pi) => {
199 names.push(pi.ident.to_string());
200 }
201 syn::Pat::Tuple(tuple) => {
203 for inner in &tuple.elems {
204 if let syn::Pat::Ident(pi) = inner {
205 names.push(pi.ident.to_string());
206 }
207 }
208 }
209 _ => {}
210 }
211 }
212 }
213 syn::Pat::Ident(pi) => {
215 names.push(pi.ident.to_string());
216 }
217 _ => {}
218 }
219 names
220}
221
222fn extract_inner_types_from_path(ty: &Type) -> Vec<String> {
225 let mut types = Vec::new();
226
227 if let Type::Path(type_path) = ty {
228 if let Some(seg) = type_path.path.segments.last() {
229 if seg.ident == "Path" {
230 if let PathArguments::AngleBracketed(args) = &seg.arguments {
231 if let Some(GenericArgument::Type(inner)) = args.args.first() {
232 if let Type::Tuple(tuple) = inner {
234 for elem in &tuple.elems {
235 types.push(quote!(#elem).to_string().replace(' ', ""));
236 }
237 } else {
238 types.push(quote!(#inner).to_string().replace(' ', ""));
240 }
241 }
242 }
243 }
244 }
245 }
246 types
247}
248
249fn extract_path_param_types(input: &ItemFn, route_path: &str) -> Vec<(String, String)> {
253 let route_params = extract_path_params_from_route(route_path);
254 let mut result = Vec::new();
255
256 for arg in &input.sig.inputs {
257 if let FnArg::Typed(pat_ty) = arg {
258 if let Type::Path(type_path) = &*pat_ty.ty {
260 if let Some(seg) = type_path.path.segments.last() {
261 if seg.ident == "Path" {
262 let inner_types = extract_inner_types_from_path(&pat_ty.ty);
263 let param_names = extract_param_names_from_pattern(&pat_ty.pat);
264
265 if inner_types.len() > 1 {
267 for (i, param_name) in route_params.iter().enumerate() {
269 if let Some(type_str) = inner_types.get(i) {
270 result.push((param_name.clone(), type_str.clone()));
271 }
272 }
273 } else if let Some(type_str) = inner_types.first() {
274 let name = param_names
276 .first()
277 .cloned()
278 .or_else(|| route_params.first().cloned())
279 .unwrap_or_else(|| "id".to_string());
280 result.push((name, type_str.clone()));
281 }
282 }
283 }
284 }
285 }
286 }
287 result
288}
289
290fn rust_type_to_schema_type(type_str: &str) -> &'static str {
292 let normalized = type_str
294 .replace(' ', "")
295 .replace("::", "")
296 .to_lowercase();
297
298 if normalized.contains("uuid") {
300 return "uuid";
301 }
302
303 match normalized.as_str() {
304 "i64" | "u64" | "isize" | "usize" => "int64",
305 "i32" | "u32" => "int32",
306 "i16" | "u16" | "i8" | "u8" => "int32",
307 "f64" | "f32" => "number",
308 "bool" => "boolean",
309 "string" | "str" | "&str" => "string",
310 _ => "string", }
312}
313
314fn is_debug_enabled() -> bool {
316 std::env::var("RUSTAPI_DEBUG")
317 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
318 .unwrap_or(false)
319}
320
321fn debug_output(name: &str, tokens: &proc_macro2::TokenStream) {
323 if is_debug_enabled() {
324 eprintln!("\n=== RUSTAPI_DEBUG: {} ===", name);
325 eprintln!("{}", tokens);
326 eprintln!("=== END {} ===\n", name);
327 }
328}
329
330fn validate_path_syntax(path: &str, span: proc_macro2::Span) -> Result<(), syn::Error> {
334 if !path.starts_with('/') {
336 return Err(syn::Error::new(
337 span,
338 format!("route path must start with '/', got: \"{}\"", path),
339 ));
340 }
341
342 if path.contains("//") {
344 return Err(syn::Error::new(
345 span,
346 format!(
347 "route path contains empty segment (double slash): \"{}\"",
348 path
349 ),
350 ));
351 }
352
353 let mut brace_depth = 0;
355 let mut param_start = None;
356
357 for (i, ch) in path.char_indices() {
358 match ch {
359 '{' => {
360 if brace_depth > 0 {
361 return Err(syn::Error::new(
362 span,
363 format!(
364 "nested braces are not allowed in route path at position {}: \"{}\"",
365 i, path
366 ),
367 ));
368 }
369 brace_depth += 1;
370 param_start = Some(i);
371 }
372 '}' => {
373 if brace_depth == 0 {
374 return Err(syn::Error::new(
375 span,
376 format!(
377 "unmatched closing brace '}}' at position {} in route path: \"{}\"",
378 i, path
379 ),
380 ));
381 }
382 brace_depth -= 1;
383
384 if let Some(start) = param_start {
386 let param_name = &path[start + 1..i];
387 if param_name.is_empty() {
388 return Err(syn::Error::new(
389 span,
390 format!(
391 "empty parameter name '{{}}' at position {} in route path: \"{}\"",
392 start, path
393 ),
394 ));
395 }
396 if !param_name.chars().all(|c| c.is_alphanumeric() || c == '_') {
398 return Err(syn::Error::new(
399 span,
400 format!(
401 "invalid parameter name '{{{}}}' at position {} - parameter names must contain only alphanumeric characters and underscores: \"{}\"",
402 param_name, start, path
403 ),
404 ));
405 }
406 if param_name
408 .chars()
409 .next()
410 .map(|c| c.is_ascii_digit())
411 .unwrap_or(false)
412 {
413 return Err(syn::Error::new(
414 span,
415 format!(
416 "parameter name '{{{}}}' cannot start with a digit at position {}: \"{}\"",
417 param_name, start, path
418 ),
419 ));
420 }
421 }
422 param_start = None;
423 }
424 _ if brace_depth == 0 => {
426 if !ch.is_alphanumeric() && !"-_./*".contains(ch) {
428 return Err(syn::Error::new(
429 span,
430 format!(
431 "invalid character '{}' at position {} in route path: \"{}\"",
432 ch, i, path
433 ),
434 ));
435 }
436 }
437 _ => {}
438 }
439 }
440
441 if brace_depth > 0 {
443 return Err(syn::Error::new(
444 span,
445 format!(
446 "unclosed brace '{{' in route path (missing closing '}}'): \"{}\"",
447 path
448 ),
449 ));
450 }
451
452 Ok(())
453}
454
455#[proc_macro_attribute]
473pub fn main(_attr: TokenStream, item: TokenStream) -> TokenStream {
474 let input = parse_macro_input!(item as ItemFn);
475
476 let attrs = &input.attrs;
477 let vis = &input.vis;
478 let sig = &input.sig;
479 let block = &input.block;
480
481 let expanded = quote! {
482 #(#attrs)*
483 #[::tokio::main]
484 #vis #sig {
485 #block
486 }
487 };
488
489 debug_output("main", &expanded);
490
491 TokenStream::from(expanded)
492}
493
494fn generate_route_handler(method: &str, attr: TokenStream, item: TokenStream) -> TokenStream {
496 let path = parse_macro_input!(attr as LitStr);
497 let input = parse_macro_input!(item as ItemFn);
498
499 let fn_name = &input.sig.ident;
500 let fn_vis = &input.vis;
501 let fn_attrs = &input.attrs;
502 let fn_async = &input.sig.asyncness;
503 let fn_inputs = &input.sig.inputs;
504 let fn_output = &input.sig.output;
505 let fn_block = &input.block;
506 let fn_generics = &input.sig.generics;
507
508 let schema_types = collect_handler_schema_types(&input);
509
510 let path_value = path.value();
511
512 if let Err(err) = validate_path_syntax(&path_value, path.span()) {
514 return err.to_compile_error().into();
515 }
516
517 let route_fn_name = syn::Ident::new(&format!("{}_route", fn_name), fn_name.span());
519 let auto_route_name = syn::Ident::new(&format!("__AUTO_ROUTE_{}", fn_name), fn_name.span());
521
522 let schema_reg_fn_name =
524 syn::Ident::new(&format!("__{}_register_schemas", fn_name), fn_name.span());
525 let auto_schema_name = syn::Ident::new(&format!("__AUTO_SCHEMA_{}", fn_name), fn_name.span());
526
527 let route_helper = match method {
529 "GET" => quote!(::rustapi_rs::get_route),
530 "POST" => quote!(::rustapi_rs::post_route),
531 "PUT" => quote!(::rustapi_rs::put_route),
532 "PATCH" => quote!(::rustapi_rs::patch_route),
533 "DELETE" => quote!(::rustapi_rs::delete_route),
534 _ => quote!(::rustapi_rs::get_route),
535 };
536
537 let mut chained_calls = quote!();
539
540 let mut manual_params = HashSet::<String>::new();
542
543 for attr in fn_attrs {
545 if let Some(ident) = attr.path().segments.last().map(|s| &s.ident) {
546 if ident == "param" {
547 if let Ok(param_args) = attr.parse_args_with(
548 syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
549 ) {
550 for meta in ¶m_args {
551 match meta {
552 Meta::Path(path) => {
553 if let Some(ident) = path.get_ident() {
554 manual_params.insert(ident.to_string());
555 }
556 }
557 Meta::NameValue(nv) => {
558 let key = nv.path.get_ident().map(|i| i.to_string());
559 if let Some(key) = key {
560 if key != "schema" && key != "type" {
561 manual_params.insert(key);
562 }
563 }
564 }
565 _ => {}
566 }
567 }
568 }
569 }
570 }
571 }
572
573 let auto_param_types = extract_path_param_types(&input, &path_value);
576 for (param_name, rust_type) in &auto_param_types {
577 if !manual_params.contains(param_name) {
578 let schema_type = rust_type_to_schema_type(rust_type);
579 chained_calls = quote! { #chained_calls .param(#param_name, #schema_type) };
580 }
581 }
582
583 for attr in fn_attrs {
584 if let Some(ident) = attr.path().segments.last().map(|s| &s.ident) {
587 let ident_str = ident.to_string();
588 if ident_str == "tag" {
589 if let Ok(lit) = attr.parse_args::<LitStr>() {
590 let val = lit.value();
591 chained_calls = quote! { #chained_calls .tag(#val) };
592 }
593 } else if ident_str == "summary" {
594 if let Ok(lit) = attr.parse_args::<LitStr>() {
595 let val = lit.value();
596 chained_calls = quote! { #chained_calls .summary(#val) };
597 }
598 } else if ident_str == "description" {
599 if let Ok(lit) = attr.parse_args::<LitStr>() {
600 let val = lit.value();
601 chained_calls = quote! { #chained_calls .description(#val) };
602 }
603 } else if ident_str == "param" {
604 if let Ok(param_args) = attr.parse_args_with(
606 syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
607 ) {
608 let mut param_name: Option<String> = None;
609 let mut param_schema: Option<String> = None;
610
611 for meta in param_args {
612 match &meta {
613 Meta::Path(path) => {
615 if param_name.is_none() {
616 if let Some(ident) = path.get_ident() {
617 param_name = Some(ident.to_string());
618 }
619 }
620 }
621 Meta::NameValue(nv) => {
623 let key = nv.path.get_ident().map(|i| i.to_string());
624 if let Some(key) = key {
625 if key == "schema" || key == "type" {
626 if let Expr::Lit(lit) = &nv.value {
627 if let Lit::Str(s) = &lit.lit {
628 param_schema = Some(s.value());
629 }
630 }
631 } else if param_name.is_none() {
632 param_name = Some(key);
634 if let Expr::Lit(lit) = &nv.value {
635 if let Lit::Str(s) = &lit.lit {
636 param_schema = Some(s.value());
637 }
638 }
639 }
640 }
641 }
642 _ => {}
643 }
644 }
645
646 if let (Some(pname), Some(pschema)) = (param_name, param_schema) {
647 chained_calls = quote! { #chained_calls .param(#pname, #pschema) };
648 }
649 }
650 }
651 }
652 }
653
654 let expanded = quote! {
655 #(#fn_attrs)*
657 #fn_vis #fn_async fn #fn_name #fn_generics (#fn_inputs) #fn_output #fn_block
658
659 #[doc(hidden)]
661 #fn_vis fn #route_fn_name() -> ::rustapi_rs::Route {
662 #route_helper(#path_value, #fn_name)
663 #chained_calls
664 }
665
666 #[doc(hidden)]
668 #[allow(non_upper_case_globals)]
669 #[::rustapi_rs::__private::linkme::distributed_slice(::rustapi_rs::__private::AUTO_ROUTES)]
670 #[linkme(crate = ::rustapi_rs::__private::linkme)]
671 static #auto_route_name: fn() -> ::rustapi_rs::Route = #route_fn_name;
672
673 #[doc(hidden)]
675 #[allow(non_snake_case)]
676 fn #schema_reg_fn_name(spec: &mut ::rustapi_rs::__private::rustapi_openapi::OpenApiSpec) {
677 #( spec.register_in_place::<#schema_types>(); )*
678 }
679
680 #[doc(hidden)]
681 #[allow(non_upper_case_globals)]
682 #[::rustapi_rs::__private::linkme::distributed_slice(::rustapi_rs::__private::AUTO_SCHEMAS)]
683 #[linkme(crate = ::rustapi_rs::__private::linkme)]
684 static #auto_schema_name: fn(&mut ::rustapi_rs::__private::rustapi_openapi::OpenApiSpec) = #schema_reg_fn_name;
685 };
686
687 debug_output(&format!("{} {}", method, path_value), &expanded);
688
689 TokenStream::from(expanded)
690}
691
692#[proc_macro_attribute]
708pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream {
709 generate_route_handler("GET", attr, item)
710}
711
712#[proc_macro_attribute]
714pub fn post(attr: TokenStream, item: TokenStream) -> TokenStream {
715 generate_route_handler("POST", attr, item)
716}
717
718#[proc_macro_attribute]
720pub fn put(attr: TokenStream, item: TokenStream) -> TokenStream {
721 generate_route_handler("PUT", attr, item)
722}
723
724#[proc_macro_attribute]
726pub fn patch(attr: TokenStream, item: TokenStream) -> TokenStream {
727 generate_route_handler("PATCH", attr, item)
728}
729
730#[proc_macro_attribute]
732pub fn delete(attr: TokenStream, item: TokenStream) -> TokenStream {
733 generate_route_handler("DELETE", attr, item)
734}
735
736#[proc_macro_attribute]
752pub fn tag(attr: TokenStream, item: TokenStream) -> TokenStream {
753 let tag = parse_macro_input!(attr as LitStr);
754 let input = parse_macro_input!(item as ItemFn);
755
756 let attrs = &input.attrs;
757 let vis = &input.vis;
758 let sig = &input.sig;
759 let block = &input.block;
760 let tag_value = tag.value();
761
762 let expanded = quote! {
764 #[doc = concat!("**Tag:** ", #tag_value)]
765 #(#attrs)*
766 #vis #sig #block
767 };
768
769 TokenStream::from(expanded)
770}
771
772#[proc_macro_attribute]
784pub fn summary(attr: TokenStream, item: TokenStream) -> TokenStream {
785 let summary = parse_macro_input!(attr as LitStr);
786 let input = parse_macro_input!(item as ItemFn);
787
788 let attrs = &input.attrs;
789 let vis = &input.vis;
790 let sig = &input.sig;
791 let block = &input.block;
792 let summary_value = summary.value();
793
794 let expanded = quote! {
796 #[doc = #summary_value]
797 #(#attrs)*
798 #vis #sig #block
799 };
800
801 TokenStream::from(expanded)
802}
803
804#[proc_macro_attribute]
816pub fn description(attr: TokenStream, item: TokenStream) -> TokenStream {
817 let desc = parse_macro_input!(attr as LitStr);
818 let input = parse_macro_input!(item as ItemFn);
819
820 let attrs = &input.attrs;
821 let vis = &input.vis;
822 let sig = &input.sig;
823 let block = &input.block;
824 let desc_value = desc.value();
825
826 let expanded = quote! {
828 #[doc = ""]
829 #[doc = #desc_value]
830 #(#attrs)*
831 #vis #sig #block
832 };
833
834 TokenStream::from(expanded)
835}
836
837#[proc_macro_attribute]
869pub fn param(_attr: TokenStream, item: TokenStream) -> TokenStream {
870 item
873}
874
875#[derive(Debug)]
881struct ValidationRuleInfo {
882 rule_type: String,
883 params: Vec<(String, String)>,
884 message: Option<String>,
885 groups: Vec<String>,
886}
887
888fn parse_validate_attrs(attrs: &[Attribute]) -> Vec<ValidationRuleInfo> {
890 let mut rules = Vec::new();
891
892 for attr in attrs {
893 if !attr.path().is_ident("validate") {
894 continue;
895 }
896
897 if let Ok(meta) = attr.parse_args::<Meta>() {
899 if let Some(rule) = parse_validate_meta(&meta) {
900 rules.push(rule);
901 }
902 } else if let Ok(nested) = attr
903 .parse_args_with(syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated)
904 {
905 for meta in nested {
906 if let Some(rule) = parse_validate_meta(&meta) {
907 rules.push(rule);
908 }
909 }
910 }
911 }
912
913 rules
914}
915
916fn parse_validate_meta(meta: &Meta) -> Option<ValidationRuleInfo> {
918 match meta {
919 Meta::Path(path) => {
920 let ident = path.get_ident()?.to_string();
922 Some(ValidationRuleInfo {
923 rule_type: ident,
924 params: Vec::new(),
925 message: None,
926 groups: Vec::new(),
927 })
928 }
929 Meta::List(list) => {
930 let rule_type = list.path.get_ident()?.to_string();
932 let mut params = Vec::new();
933 let mut message = None;
934 let mut groups = Vec::new();
935
936 if let Ok(nested) = list.parse_args_with(
938 syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
939 ) {
940 for nested_meta in nested {
941 if let Meta::NameValue(nv) = &nested_meta {
942 let key = nv.path.get_ident()?.to_string();
943
944 if key == "groups" {
945 let vec = expr_to_string_vec(&nv.value);
946 groups.extend(vec);
947 } else if let Some(value) = expr_to_string(&nv.value) {
948 if key == "message" {
949 message = Some(value);
950 } else if key == "group" {
951 groups.push(value);
952 } else {
953 params.push((key, value));
954 }
955 }
956 } else if let Meta::Path(path) = &nested_meta {
957 if let Some(ident) = path.get_ident() {
959 params.push((ident.to_string(), "true".to_string()));
960 }
961 }
962 }
963 }
964
965 Some(ValidationRuleInfo {
966 rule_type,
967 params,
968 message,
969 groups,
970 })
971 }
972 Meta::NameValue(nv) => {
973 let rule_type = nv.path.get_ident()?.to_string();
975 let value = expr_to_string(&nv.value)?;
976
977 Some(ValidationRuleInfo {
978 rule_type: rule_type.clone(),
979 params: vec![(rule_type.clone(), value)],
980 message: None,
981 groups: Vec::new(),
982 })
983 }
984 }
985}
986
987fn expr_to_string(expr: &Expr) -> Option<String> {
989 match expr {
990 Expr::Lit(lit) => match &lit.lit {
991 Lit::Str(s) => Some(s.value()),
992 Lit::Int(i) => Some(i.base10_digits().to_string()),
993 Lit::Float(f) => Some(f.base10_digits().to_string()),
994 Lit::Bool(b) => Some(b.value.to_string()),
995 _ => None,
996 },
997 _ => None,
998 }
999}
1000
1001fn expr_to_string_vec(expr: &Expr) -> Vec<String> {
1003 match expr {
1004 Expr::Array(arr) => {
1005 let mut result = Vec::new();
1006 for elem in &arr.elems {
1007 if let Some(s) = expr_to_string(elem) {
1008 result.push(s);
1009 }
1010 }
1011 result
1012 }
1013 _ => {
1014 if let Some(s) = expr_to_string(expr) {
1015 vec![s]
1016 } else {
1017 Vec::new()
1018 }
1019 }
1020 }
1021}
1022
1023fn generate_rule_validation(
1024 field_name: &str,
1025 _field_type: &Type,
1026 rule: &ValidationRuleInfo,
1027) -> proc_macro2::TokenStream {
1028 let field_ident = syn::Ident::new(field_name, proc_macro2::Span::call_site());
1029 let field_name_str = field_name;
1030
1031 let group_check = if rule.groups.is_empty() {
1033 quote! { true }
1034 } else {
1035 let group_names = rule.groups.iter().map(|g| g.as_str());
1036 quote! {
1037 {
1038 let rule_groups = [#(::rustapi_validate::v2::ValidationGroup::from(#group_names)),*];
1039 rule_groups.iter().any(|g| g.matches(&group))
1040 }
1041 }
1042 };
1043
1044 let validation_logic = match rule.rule_type.as_str() {
1045 "email" => {
1046 let message = rule
1047 .message
1048 .as_ref()
1049 .map(|m| quote! { .with_message(#m) })
1050 .unwrap_or_default();
1051 quote! {
1052 {
1053 let rule = ::rustapi_validate::v2::EmailRule::new() #message;
1054 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1055 errors.add(#field_name_str, e);
1056 }
1057 }
1058 }
1059 }
1060 "length" => {
1061 let min = rule
1062 .params
1063 .iter()
1064 .find(|(k, _)| k == "min")
1065 .and_then(|(_, v)| v.parse::<usize>().ok());
1066 let max = rule
1067 .params
1068 .iter()
1069 .find(|(k, _)| k == "max")
1070 .and_then(|(_, v)| v.parse::<usize>().ok());
1071 let message = rule
1072 .message
1073 .as_ref()
1074 .map(|m| quote! { .with_message(#m) })
1075 .unwrap_or_default();
1076
1077 let rule_creation = match (min, max) {
1078 (Some(min), Some(max)) => {
1079 quote! { ::rustapi_validate::v2::LengthRule::new(#min, #max) }
1080 }
1081 (Some(min), None) => quote! { ::rustapi_validate::v2::LengthRule::min(#min) },
1082 (None, Some(max)) => quote! { ::rustapi_validate::v2::LengthRule::max(#max) },
1083 (None, None) => quote! { ::rustapi_validate::v2::LengthRule::new(0, usize::MAX) },
1084 };
1085
1086 quote! {
1087 {
1088 let rule = #rule_creation #message;
1089 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1090 errors.add(#field_name_str, e);
1091 }
1092 }
1093 }
1094 }
1095 "range" => {
1096 let min = rule
1097 .params
1098 .iter()
1099 .find(|(k, _)| k == "min")
1100 .map(|(_, v)| v.clone());
1101 let max = rule
1102 .params
1103 .iter()
1104 .find(|(k, _)| k == "max")
1105 .map(|(_, v)| v.clone());
1106 let message = rule
1107 .message
1108 .as_ref()
1109 .map(|m| quote! { .with_message(#m) })
1110 .unwrap_or_default();
1111
1112 let rule_creation = match (min, max) {
1114 (Some(min), Some(max)) => {
1115 let min_lit: proc_macro2::TokenStream = min.parse().unwrap();
1116 let max_lit: proc_macro2::TokenStream = max.parse().unwrap();
1117 quote! { ::rustapi_validate::v2::RangeRule::new(#min_lit, #max_lit) }
1118 }
1119 (Some(min), None) => {
1120 let min_lit: proc_macro2::TokenStream = min.parse().unwrap();
1121 quote! { ::rustapi_validate::v2::RangeRule::min(#min_lit) }
1122 }
1123 (None, Some(max)) => {
1124 let max_lit: proc_macro2::TokenStream = max.parse().unwrap();
1125 quote! { ::rustapi_validate::v2::RangeRule::max(#max_lit) }
1126 }
1127 (None, None) => {
1128 return quote! {};
1129 }
1130 };
1131
1132 quote! {
1133 {
1134 let rule = #rule_creation #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 "regex" => {
1142 let pattern = rule
1143 .params
1144 .iter()
1145 .find(|(k, _)| k == "regex" || k == "pattern")
1146 .map(|(_, v)| v.clone())
1147 .unwrap_or_default();
1148 let message = rule
1149 .message
1150 .as_ref()
1151 .map(|m| quote! { .with_message(#m) })
1152 .unwrap_or_default();
1153
1154 quote! {
1155 {
1156 let rule = ::rustapi_validate::v2::RegexRule::new(#pattern) #message;
1157 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1158 errors.add(#field_name_str, e);
1159 }
1160 }
1161 }
1162 }
1163 "url" => {
1164 let message = rule
1165 .message
1166 .as_ref()
1167 .map(|m| quote! { .with_message(#m) })
1168 .unwrap_or_default();
1169 quote! {
1170 {
1171 let rule = ::rustapi_validate::v2::UrlRule::new() #message;
1172 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1173 errors.add(#field_name_str, e);
1174 }
1175 }
1176 }
1177 }
1178 "required" => {
1179 let message = rule
1180 .message
1181 .as_ref()
1182 .map(|m| quote! { .with_message(#m) })
1183 .unwrap_or_default();
1184 quote! {
1185 {
1186 let rule = ::rustapi_validate::v2::RequiredRule::new() #message;
1187 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1188 errors.add(#field_name_str, e);
1189 }
1190 }
1191 }
1192 }
1193 "credit_card" => {
1194 let message = rule
1195 .message
1196 .as_ref()
1197 .map(|m| quote! { .with_message(#m) })
1198 .unwrap_or_default();
1199 quote! {
1200 {
1201 let rule = ::rustapi_validate::v2::CreditCardRule::new() #message;
1202 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1203 errors.add(#field_name_str, e);
1204 }
1205 }
1206 }
1207 }
1208 "ip" => {
1209 let v4 = rule.params.iter().any(|(k, _)| k == "v4");
1210 let v6 = rule.params.iter().any(|(k, _)| k == "v6");
1211
1212 let rule_creation = if v4 && !v6 {
1213 quote! { ::rustapi_validate::v2::IpRule::v4() }
1214 } else if !v4 && v6 {
1215 quote! { ::rustapi_validate::v2::IpRule::v6() }
1216 } else {
1217 quote! { ::rustapi_validate::v2::IpRule::new() }
1218 };
1219
1220 let message = rule
1221 .message
1222 .as_ref()
1223 .map(|m| quote! { .with_message(#m) })
1224 .unwrap_or_default();
1225
1226 quote! {
1227 {
1228 let rule = #rule_creation #message;
1229 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1230 errors.add(#field_name_str, e);
1231 }
1232 }
1233 }
1234 }
1235 "phone" => {
1236 let message = rule
1237 .message
1238 .as_ref()
1239 .map(|m| quote! { .with_message(#m) })
1240 .unwrap_or_default();
1241 quote! {
1242 {
1243 let rule = ::rustapi_validate::v2::PhoneRule::new() #message;
1244 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1245 errors.add(#field_name_str, e);
1246 }
1247 }
1248 }
1249 }
1250 "contains" => {
1251 let needle = rule
1252 .params
1253 .iter()
1254 .find(|(k, _)| k == "needle")
1255 .map(|(_, v)| v.clone())
1256 .unwrap_or_default();
1257
1258 let message = rule
1259 .message
1260 .as_ref()
1261 .map(|m| quote! { .with_message(#m) })
1262 .unwrap_or_default();
1263
1264 quote! {
1265 {
1266 let rule = ::rustapi_validate::v2::ContainsRule::new(#needle) #message;
1267 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1268 errors.add(#field_name_str, e);
1269 }
1270 }
1271 }
1272 }
1273 _ => {
1274 quote! {}
1276 }
1277 };
1278
1279 quote! {
1280 if #group_check {
1281 #validation_logic
1282 }
1283 }
1284}
1285
1286fn generate_async_rule_validation(
1288 field_name: &str,
1289 rule: &ValidationRuleInfo,
1290) -> proc_macro2::TokenStream {
1291 let field_ident = syn::Ident::new(field_name, proc_macro2::Span::call_site());
1292 let field_name_str = field_name;
1293
1294 let group_check = if rule.groups.is_empty() {
1296 quote! { true }
1297 } else {
1298 let group_names = rule.groups.iter().map(|g| g.as_str());
1299 quote! {
1300 {
1301 let rule_groups = [#(::rustapi_validate::v2::ValidationGroup::from(#group_names)),*];
1302 rule_groups.iter().any(|g| g.matches(&group))
1303 }
1304 }
1305 };
1306
1307 let validation_logic = match rule.rule_type.as_str() {
1308 "async_unique" => {
1309 let table = rule
1310 .params
1311 .iter()
1312 .find(|(k, _)| k == "table")
1313 .map(|(_, v)| v.clone())
1314 .unwrap_or_default();
1315 let column = rule
1316 .params
1317 .iter()
1318 .find(|(k, _)| k == "column")
1319 .map(|(_, v)| v.clone())
1320 .unwrap_or_default();
1321 let message = rule
1322 .message
1323 .as_ref()
1324 .map(|m| quote! { .with_message(#m) })
1325 .unwrap_or_default();
1326
1327 quote! {
1328 {
1329 let rule = ::rustapi_validate::v2::AsyncUniqueRule::new(#table, #column) #message;
1330 if let Err(e) = ::rustapi_validate::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
1331 errors.add(#field_name_str, e);
1332 }
1333 }
1334 }
1335 }
1336 "async_exists" => {
1337 let table = rule
1338 .params
1339 .iter()
1340 .find(|(k, _)| k == "table")
1341 .map(|(_, v)| v.clone())
1342 .unwrap_or_default();
1343 let column = rule
1344 .params
1345 .iter()
1346 .find(|(k, _)| k == "column")
1347 .map(|(_, v)| v.clone())
1348 .unwrap_or_default();
1349 let message = rule
1350 .message
1351 .as_ref()
1352 .map(|m| quote! { .with_message(#m) })
1353 .unwrap_or_default();
1354
1355 quote! {
1356 {
1357 let rule = ::rustapi_validate::v2::AsyncExistsRule::new(#table, #column) #message;
1358 if let Err(e) = ::rustapi_validate::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
1359 errors.add(#field_name_str, e);
1360 }
1361 }
1362 }
1363 }
1364 "async_api" => {
1365 let endpoint = rule
1366 .params
1367 .iter()
1368 .find(|(k, _)| k == "endpoint")
1369 .map(|(_, v)| v.clone())
1370 .unwrap_or_default();
1371 let message = rule
1372 .message
1373 .as_ref()
1374 .map(|m| quote! { .with_message(#m) })
1375 .unwrap_or_default();
1376
1377 quote! {
1378 {
1379 let rule = ::rustapi_validate::v2::AsyncApiRule::new(#endpoint) #message;
1380 if let Err(e) = ::rustapi_validate::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
1381 errors.add(#field_name_str, e);
1382 }
1383 }
1384 }
1385 }
1386 "custom_async" => {
1387 let function_path = rule
1389 .params
1390 .iter()
1391 .find(|(k, _)| k == "custom_async" || k == "function")
1392 .map(|(_, v)| v.clone())
1393 .unwrap_or_default();
1394
1395 if function_path.is_empty() {
1396 quote! {}
1398 } else {
1399 let func: syn::Path = syn::parse_str(&function_path).unwrap();
1400 let message_handling = if let Some(msg) = &rule.message {
1401 quote! {
1402 let e = ::rustapi_validate::v2::RuleError::new("custom_async", #msg);
1403 errors.add(#field_name_str, e);
1404 }
1405 } else {
1406 quote! {
1407 errors.add(#field_name_str, e);
1408 }
1409 };
1410
1411 quote! {
1412 {
1413 if let Err(e) = #func(&self.#field_ident, ctx).await {
1415 #message_handling
1416 }
1417 }
1418 }
1419 }
1420 }
1421 _ => {
1422 quote! {}
1424 }
1425 };
1426
1427 quote! {
1428 if #group_check {
1429 #validation_logic
1430 }
1431 }
1432}
1433
1434fn is_async_rule(rule: &ValidationRuleInfo) -> bool {
1436 matches!(
1437 rule.rule_type.as_str(),
1438 "async_unique" | "async_exists" | "async_api" | "custom_async"
1439 )
1440}
1441
1442#[proc_macro_derive(Validate, attributes(validate))]
1465pub fn derive_validate(input: TokenStream) -> TokenStream {
1466 let input = parse_macro_input!(input as DeriveInput);
1467 let name = &input.ident;
1468 let generics = &input.generics;
1469 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
1470
1471 let fields = match &input.data {
1473 Data::Struct(data) => match &data.fields {
1474 Fields::Named(fields) => &fields.named,
1475 _ => {
1476 return syn::Error::new_spanned(
1477 &input,
1478 "Validate can only be derived for structs with named fields",
1479 )
1480 .to_compile_error()
1481 .into();
1482 }
1483 },
1484 _ => {
1485 return syn::Error::new_spanned(&input, "Validate can only be derived for structs")
1486 .to_compile_error()
1487 .into();
1488 }
1489 };
1490
1491 let mut sync_validations = Vec::new();
1493 let mut async_validations = Vec::new();
1494 let mut has_async_rules = false;
1495
1496 for field in fields {
1497 let field_name = field.ident.as_ref().unwrap().to_string();
1498 let field_type = &field.ty;
1499 let rules = parse_validate_attrs(&field.attrs);
1500
1501 for rule in &rules {
1502 if is_async_rule(rule) {
1503 has_async_rules = true;
1504 let validation = generate_async_rule_validation(&field_name, rule);
1505 async_validations.push(validation);
1506 } else {
1507 let validation = generate_rule_validation(&field_name, field_type, rule);
1508 sync_validations.push(validation);
1509 }
1510 }
1511 }
1512
1513 let validate_impl = quote! {
1515 impl #impl_generics ::rustapi_validate::v2::Validate for #name #ty_generics #where_clause {
1516 fn validate_with_group(&self, group: ::rustapi_validate::v2::ValidationGroup) -> Result<(), ::rustapi_validate::v2::ValidationErrors> {
1517 let mut errors = ::rustapi_validate::v2::ValidationErrors::new();
1518
1519 #(#sync_validations)*
1520
1521 errors.into_result()
1522 }
1523 }
1524 };
1525
1526 let async_validate_impl = if has_async_rules {
1528 quote! {
1529 #[::async_trait::async_trait]
1530 impl #impl_generics ::rustapi_validate::v2::AsyncValidate for #name #ty_generics #where_clause {
1531 async fn validate_async_with_group(&self, ctx: &::rustapi_validate::v2::ValidationContext, group: ::rustapi_validate::v2::ValidationGroup) -> Result<(), ::rustapi_validate::v2::ValidationErrors> {
1532 let mut errors = ::rustapi_validate::v2::ValidationErrors::new();
1533
1534 #(#async_validations)*
1535
1536 errors.into_result()
1537 }
1538 }
1539 }
1540 } else {
1541 quote! {
1543 #[::async_trait::async_trait]
1544 impl #impl_generics ::rustapi_validate::v2::AsyncValidate for #name #ty_generics #where_clause {
1545 async fn validate_async_with_group(&self, _ctx: &::rustapi_validate::v2::ValidationContext, _group: ::rustapi_validate::v2::ValidationGroup) -> Result<(), ::rustapi_validate::v2::ValidationErrors> {
1546 Ok(())
1547 }
1548 }
1549 }
1550 };
1551
1552 let expanded = quote! {
1553 #validate_impl
1554 #async_validate_impl
1555 };
1556
1557 debug_output("Validate derive", &expanded);
1558
1559 TokenStream::from(expanded)
1560}
1561
1562#[proc_macro_derive(ApiError, attributes(error))]
1581pub fn derive_api_error(input: TokenStream) -> TokenStream {
1582 api_error::expand_derive_api_error(input)
1583}
1584
1585#[proc_macro_derive(TypedPath, attributes(typed_path))]
1602pub fn derive_typed_path(input: TokenStream) -> TokenStream {
1603 let input = parse_macro_input!(input as DeriveInput);
1604 let name = &input.ident;
1605 let generics = &input.generics;
1606 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
1607
1608 let mut path_str = None;
1610 for attr in &input.attrs {
1611 if attr.path().is_ident("typed_path") {
1612 if let Ok(lit) = attr.parse_args::<LitStr>() {
1613 path_str = Some(lit.value());
1614 }
1615 }
1616 }
1617
1618 let path = match path_str {
1619 Some(p) => p,
1620 None => {
1621 return syn::Error::new_spanned(
1622 &input,
1623 "#[derive(TypedPath)] requires a #[typed_path(\"...\")] attribute",
1624 )
1625 .to_compile_error()
1626 .into();
1627 }
1628 };
1629
1630 if let Err(err) = validate_path_syntax(&path, proc_macro2::Span::call_site()) {
1632 return err.to_compile_error().into();
1633 }
1634
1635 let mut format_string = String::new();
1638 let mut format_args = Vec::new();
1639
1640 let mut chars = path.chars().peekable();
1641 while let Some(ch) = chars.next() {
1642 if ch == '{' {
1643 let mut param_name = String::new();
1644 while let Some(&c) = chars.peek() {
1645 if c == '}' {
1646 chars.next(); break;
1648 }
1649 param_name.push(chars.next().unwrap());
1650 }
1651
1652 if param_name.is_empty() {
1653 return syn::Error::new_spanned(
1654 &input,
1655 "Empty path parameter not allowed in typed_path",
1656 )
1657 .to_compile_error()
1658 .into();
1659 }
1660
1661 format_string.push_str("{}");
1662 let ident = syn::Ident::new(¶m_name, proc_macro2::Span::call_site());
1663 format_args.push(quote! { self.#ident });
1664 } else {
1665 format_string.push(ch);
1666 }
1667 }
1668
1669 let expanded = quote! {
1670 impl #impl_generics ::rustapi_rs::prelude::TypedPath for #name #ty_generics #where_clause {
1671 const PATH: &'static str = #path;
1672
1673 fn to_uri(&self) -> String {
1674 format!(#format_string, #(#format_args),*)
1675 }
1676 }
1677 };
1678
1679 debug_output("TypedPath derive", &expanded);
1680 TokenStream::from(expanded)
1681}