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;
26mod derive_schema;
27
28#[proc_macro_derive(Schema, attributes(schema))]
40pub fn derive_schema(input: TokenStream) -> TokenStream {
41 derive_schema::expand_derive_schema(parse_macro_input!(input as DeriveInput)).into()
42}
43
44#[proc_macro_attribute]
58pub fn schema(_attr: TokenStream, item: TokenStream) -> TokenStream {
59 let input = parse_macro_input!(item as syn::Item);
60
61 let (ident, generics) = match &input {
62 syn::Item::Struct(s) => (&s.ident, &s.generics),
63 syn::Item::Enum(e) => (&e.ident, &e.generics),
64 _ => {
65 return syn::Error::new_spanned(
66 &input,
67 "#[rustapi_rs::schema] can only be used on structs or enums",
68 )
69 .to_compile_error()
70 .into();
71 }
72 };
73
74 if !generics.params.is_empty() {
75 return syn::Error::new_spanned(
76 generics,
77 "#[rustapi_rs::schema] does not support generic types",
78 )
79 .to_compile_error()
80 .into();
81 }
82
83 let registrar_ident = syn::Ident::new(
84 &format!("__RUSTAPI_AUTO_SCHEMA_{}", ident),
85 proc_macro2::Span::call_site(),
86 );
87
88 let expanded = quote! {
89 #input
90
91 #[allow(non_upper_case_globals)]
92 #[::rustapi_rs::__private::linkme::distributed_slice(::rustapi_rs::__private::AUTO_SCHEMAS)]
93 #[linkme(crate = ::rustapi_rs::__private::linkme)]
94 static #registrar_ident: fn(&mut ::rustapi_rs::__private::rustapi_openapi::OpenApiSpec) =
95 |spec: &mut ::rustapi_rs::__private::rustapi_openapi::OpenApiSpec| {
96 spec.register_in_place::<#ident>();
97 };
98 };
99
100 debug_output("schema", &expanded);
101 expanded.into()
102}
103
104fn extract_schema_types(ty: &Type, out: &mut Vec<Type>, allow_leaf: bool) {
105 match ty {
106 Type::Reference(r) => extract_schema_types(&r.elem, out, allow_leaf),
107 Type::Path(tp) => {
108 let Some(seg) = tp.path.segments.last() else {
109 return;
110 };
111
112 let ident = seg.ident.to_string();
113
114 let unwrap_first_generic = |out: &mut Vec<Type>| {
115 if let PathArguments::AngleBracketed(args) = &seg.arguments {
116 if let Some(GenericArgument::Type(inner)) = args.args.first() {
117 extract_schema_types(inner, out, true);
118 }
119 }
120 };
121
122 match ident.as_str() {
123 "Json" | "ValidatedJson" | "Created" => {
125 unwrap_first_generic(out);
126 }
127 "WithStatus" => {
129 if let PathArguments::AngleBracketed(args) = &seg.arguments {
130 if let Some(GenericArgument::Type(inner)) = args.args.first() {
131 extract_schema_types(inner, out, true);
132 }
133 }
134 }
135 "Option" | "Result" => {
137 if let PathArguments::AngleBracketed(args) = &seg.arguments {
138 if let Some(GenericArgument::Type(inner)) = args.args.first() {
139 extract_schema_types(inner, out, allow_leaf);
140 }
141 }
142 }
143 _ => {
144 if allow_leaf {
145 out.push(ty.clone());
146 }
147 }
148 }
149 }
150 _ => {}
151 }
152}
153
154fn collect_handler_schema_types(input: &ItemFn) -> Vec<Type> {
155 let mut found: Vec<Type> = Vec::new();
156
157 for arg in &input.sig.inputs {
158 if let FnArg::Typed(pat_ty) = arg {
159 extract_schema_types(&pat_ty.ty, &mut found, false);
160 }
161 }
162
163 if let ReturnType::Type(_, ty) = &input.sig.output {
164 extract_schema_types(ty, &mut found, false);
165 }
166
167 let mut seen = HashSet::<String>::new();
169 found
170 .into_iter()
171 .filter(|t| seen.insert(quote!(#t).to_string()))
172 .collect()
173}
174
175fn collect_path_params(input: &ItemFn) -> Vec<(String, String)> {
179 let mut params = Vec::new();
180
181 for arg in &input.sig.inputs {
182 if let FnArg::Typed(pat_ty) = arg {
183 if let Type::Path(tp) = &*pat_ty.ty {
185 if let Some(seg) = tp.path.segments.last() {
186 if seg.ident == "Path" {
187 if let PathArguments::AngleBracketed(args) = &seg.arguments {
189 if let Some(GenericArgument::Type(inner_ty)) = args.args.first() {
190 if let Some(schema_type) = map_type_to_schema(inner_ty) {
192 if let Some(name) = extract_param_name(&pat_ty.pat) {
200 params.push((name, schema_type));
201 }
202 }
203 }
204 }
205 }
206 }
207 }
208 }
209 }
210
211 params
212}
213
214fn extract_param_name(pat: &syn::Pat) -> Option<String> {
219 match pat {
220 syn::Pat::Ident(ident) => Some(ident.ident.to_string()),
221 syn::Pat::TupleStruct(ts) => {
222 if let Some(first) = ts.elems.first() {
225 extract_param_name(first)
226 } else {
227 None
228 }
229 }
230 _ => None, }
232}
233
234fn map_type_to_schema(ty: &Type) -> Option<String> {
236 match ty {
237 Type::Path(tp) => {
238 if let Some(seg) = tp.path.segments.last() {
239 let ident = seg.ident.to_string();
240 match ident.as_str() {
241 "Uuid" => Some("uuid".to_string()),
242 "String" | "str" => Some("string".to_string()),
243 "bool" => Some("boolean".to_string()),
244 "i8" | "i16" | "i32" | "i64" | "isize" | "u8" | "u16" | "u32" | "u64"
245 | "usize" => Some("integer".to_string()),
246 "f32" | "f64" => Some("number".to_string()),
247 _ => None,
248 }
249 } else {
250 None
251 }
252 }
253 _ => None,
254 }
255}
256
257fn is_debug_enabled() -> bool {
259 std::env::var("RUSTAPI_DEBUG")
260 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
261 .unwrap_or(false)
262}
263
264fn debug_output(name: &str, tokens: &proc_macro2::TokenStream) {
266 if is_debug_enabled() {
267 eprintln!("\n=== RUSTAPI_DEBUG: {} ===", name);
268 eprintln!("{}", tokens);
269 eprintln!("=== END {} ===\n", name);
270 }
271}
272
273fn validate_path_syntax(path: &str, span: proc_macro2::Span) -> Result<(), syn::Error> {
277 if !path.starts_with('/') {
279 return Err(syn::Error::new(
280 span,
281 format!("route path must start with '/', got: \"{}\"", path),
282 ));
283 }
284
285 if path.contains("//") {
287 return Err(syn::Error::new(
288 span,
289 format!(
290 "route path contains empty segment (double slash): \"{}\"",
291 path
292 ),
293 ));
294 }
295
296 let mut brace_depth = 0;
298 let mut param_start = None;
299
300 for (i, ch) in path.char_indices() {
301 match ch {
302 '{' => {
303 if brace_depth > 0 {
304 return Err(syn::Error::new(
305 span,
306 format!(
307 "nested braces are not allowed in route path at position {}: \"{}\"",
308 i, path
309 ),
310 ));
311 }
312 brace_depth += 1;
313 param_start = Some(i);
314 }
315 '}' => {
316 if brace_depth == 0 {
317 return Err(syn::Error::new(
318 span,
319 format!(
320 "unmatched closing brace '}}' at position {} in route path: \"{}\"",
321 i, path
322 ),
323 ));
324 }
325 brace_depth -= 1;
326
327 if let Some(start) = param_start {
329 let param_name = &path[start + 1..i];
330 if param_name.is_empty() {
331 return Err(syn::Error::new(
332 span,
333 format!(
334 "empty parameter name '{{}}' at position {} in route path: \"{}\"",
335 start, path
336 ),
337 ));
338 }
339 if !param_name.chars().all(|c| c.is_alphanumeric() || c == '_') {
341 return Err(syn::Error::new(
342 span,
343 format!(
344 "invalid parameter name '{{{}}}' at position {} - parameter names must contain only alphanumeric characters and underscores: \"{}\"",
345 param_name, start, path
346 ),
347 ));
348 }
349 if param_name
351 .chars()
352 .next()
353 .map(|c| c.is_ascii_digit())
354 .unwrap_or(false)
355 {
356 return Err(syn::Error::new(
357 span,
358 format!(
359 "parameter name '{{{}}}' cannot start with a digit at position {}: \"{}\"",
360 param_name, start, path
361 ),
362 ));
363 }
364 }
365 param_start = None;
366 }
367 _ if brace_depth == 0 => {
369 if !ch.is_alphanumeric() && !"-_./*".contains(ch) {
371 return Err(syn::Error::new(
372 span,
373 format!(
374 "invalid character '{}' at position {} in route path: \"{}\"",
375 ch, i, path
376 ),
377 ));
378 }
379 }
380 _ => {}
381 }
382 }
383
384 if brace_depth > 0 {
386 return Err(syn::Error::new(
387 span,
388 format!(
389 "unclosed brace '{{' in route path (missing closing '}}'): \"{}\"",
390 path
391 ),
392 ));
393 }
394
395 Ok(())
396}
397
398#[proc_macro_attribute]
416pub fn main(_attr: TokenStream, item: TokenStream) -> TokenStream {
417 let input = parse_macro_input!(item as ItemFn);
418
419 let attrs = &input.attrs;
420 let vis = &input.vis;
421 let sig = &input.sig;
422 let block = &input.block;
423
424 let expanded = quote! {
425 #(#attrs)*
426 #[::tokio::main]
427 #vis #sig {
428 #block
429 }
430 };
431
432 debug_output("main", &expanded);
433
434 TokenStream::from(expanded)
435}
436
437fn generate_route_handler(method: &str, attr: TokenStream, item: TokenStream) -> TokenStream {
439 let path = parse_macro_input!(attr as LitStr);
440 let input = parse_macro_input!(item as ItemFn);
441
442 let fn_name = &input.sig.ident;
443 let fn_vis = &input.vis;
444 let fn_attrs = &input.attrs;
445 let fn_async = &input.sig.asyncness;
446 let fn_inputs = &input.sig.inputs;
447 let fn_output = &input.sig.output;
448 let fn_block = &input.block;
449 let fn_generics = &input.sig.generics;
450
451 let schema_types = collect_handler_schema_types(&input);
452
453 let path_value = path.value();
454
455 if let Err(err) = validate_path_syntax(&path_value, path.span()) {
457 return err.to_compile_error().into();
458 }
459
460 let route_fn_name = syn::Ident::new(&format!("{}_route", fn_name), fn_name.span());
462 let auto_route_name = syn::Ident::new(&format!("__AUTO_ROUTE_{}", fn_name), fn_name.span());
464
465 let schema_reg_fn_name =
467 syn::Ident::new(&format!("__{}_register_schemas", fn_name), fn_name.span());
468 let auto_schema_name = syn::Ident::new(&format!("__AUTO_SCHEMA_{}", fn_name), fn_name.span());
469
470 let route_helper = match method {
472 "GET" => quote!(::rustapi_rs::get_route),
473 "POST" => quote!(::rustapi_rs::post_route),
474 "PUT" => quote!(::rustapi_rs::put_route),
475 "PATCH" => quote!(::rustapi_rs::patch_route),
476 "DELETE" => quote!(::rustapi_rs::delete_route),
477 _ => quote!(::rustapi_rs::get_route),
478 };
479
480 let auto_params = collect_path_params(&input);
482
483 let mut chained_calls = quote!();
485
486 for (name, schema) in auto_params {
488 chained_calls = quote! { #chained_calls .param(#name, #schema) };
489 }
490
491 for attr in fn_attrs {
492 if let Some(ident) = attr.path().segments.last().map(|s| &s.ident) {
495 let ident_str = ident.to_string();
496 if ident_str == "tag" {
497 if let Ok(lit) = attr.parse_args::<LitStr>() {
498 let val = lit.value();
499 chained_calls = quote! { #chained_calls .tag(#val) };
500 }
501 } else if ident_str == "summary" {
502 if let Ok(lit) = attr.parse_args::<LitStr>() {
503 let val = lit.value();
504 chained_calls = quote! { #chained_calls .summary(#val) };
505 }
506 } else if ident_str == "description" {
507 if let Ok(lit) = attr.parse_args::<LitStr>() {
508 let val = lit.value();
509 chained_calls = quote! { #chained_calls .description(#val) };
510 }
511 } else if ident_str == "param" {
512 if let Ok(param_args) = attr.parse_args_with(
514 syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
515 ) {
516 let mut param_name: Option<String> = None;
517 let mut param_schema: Option<String> = None;
518
519 for meta in param_args {
520 match &meta {
521 Meta::Path(path) => {
523 if param_name.is_none() {
524 if let Some(ident) = path.get_ident() {
525 param_name = Some(ident.to_string());
526 }
527 }
528 }
529 Meta::NameValue(nv) => {
531 let key = nv.path.get_ident().map(|i| i.to_string());
532 if let Some(key) = key {
533 if key == "schema" || key == "type" {
534 if let Expr::Lit(lit) = &nv.value {
535 if let Lit::Str(s) = &lit.lit {
536 param_schema = Some(s.value());
537 }
538 }
539 } else if param_name.is_none() {
540 param_name = Some(key);
542 if let Expr::Lit(lit) = &nv.value {
543 if let Lit::Str(s) = &lit.lit {
544 param_schema = Some(s.value());
545 }
546 }
547 }
548 }
549 }
550 _ => {}
551 }
552 }
553
554 if let (Some(pname), Some(pschema)) = (param_name, param_schema) {
555 chained_calls = quote! { #chained_calls .param(#pname, #pschema) };
556 }
557 }
558 }
559 }
560 }
561
562 let expanded = quote! {
563 #(#fn_attrs)*
565 #fn_vis #fn_async fn #fn_name #fn_generics (#fn_inputs) #fn_output #fn_block
566
567 #[doc(hidden)]
569 #fn_vis fn #route_fn_name() -> ::rustapi_rs::Route {
570 #route_helper(#path_value, #fn_name)
571 #chained_calls
572 }
573
574 #[doc(hidden)]
576 #[allow(non_upper_case_globals)]
577 #[::rustapi_rs::__private::linkme::distributed_slice(::rustapi_rs::__private::AUTO_ROUTES)]
578 #[linkme(crate = ::rustapi_rs::__private::linkme)]
579 static #auto_route_name: fn() -> ::rustapi_rs::Route = #route_fn_name;
580
581 #[doc(hidden)]
583 #[allow(non_snake_case)]
584 fn #schema_reg_fn_name(spec: &mut ::rustapi_rs::__private::rustapi_openapi::OpenApiSpec) {
585 #( spec.register_in_place::<#schema_types>(); )*
586 }
587
588 #[doc(hidden)]
589 #[allow(non_upper_case_globals)]
590 #[::rustapi_rs::__private::linkme::distributed_slice(::rustapi_rs::__private::AUTO_SCHEMAS)]
591 #[linkme(crate = ::rustapi_rs::__private::linkme)]
592 static #auto_schema_name: fn(&mut ::rustapi_rs::__private::rustapi_openapi::OpenApiSpec) = #schema_reg_fn_name;
593 };
594
595 debug_output(&format!("{} {}", method, path_value), &expanded);
596
597 TokenStream::from(expanded)
598}
599
600#[proc_macro_attribute]
616pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream {
617 generate_route_handler("GET", attr, item)
618}
619
620#[proc_macro_attribute]
622pub fn post(attr: TokenStream, item: TokenStream) -> TokenStream {
623 generate_route_handler("POST", attr, item)
624}
625
626#[proc_macro_attribute]
628pub fn put(attr: TokenStream, item: TokenStream) -> TokenStream {
629 generate_route_handler("PUT", attr, item)
630}
631
632#[proc_macro_attribute]
634pub fn patch(attr: TokenStream, item: TokenStream) -> TokenStream {
635 generate_route_handler("PATCH", attr, item)
636}
637
638#[proc_macro_attribute]
640pub fn delete(attr: TokenStream, item: TokenStream) -> TokenStream {
641 generate_route_handler("DELETE", attr, item)
642}
643
644#[proc_macro_attribute]
660pub fn tag(attr: TokenStream, item: TokenStream) -> TokenStream {
661 let tag = parse_macro_input!(attr as LitStr);
662 let input = parse_macro_input!(item as ItemFn);
663
664 let attrs = &input.attrs;
665 let vis = &input.vis;
666 let sig = &input.sig;
667 let block = &input.block;
668 let tag_value = tag.value();
669
670 let expanded = quote! {
672 #[doc = concat!("**Tag:** ", #tag_value)]
673 #(#attrs)*
674 #vis #sig #block
675 };
676
677 TokenStream::from(expanded)
678}
679
680#[proc_macro_attribute]
692pub fn summary(attr: TokenStream, item: TokenStream) -> TokenStream {
693 let summary = parse_macro_input!(attr as LitStr);
694 let input = parse_macro_input!(item as ItemFn);
695
696 let attrs = &input.attrs;
697 let vis = &input.vis;
698 let sig = &input.sig;
699 let block = &input.block;
700 let summary_value = summary.value();
701
702 let expanded = quote! {
704 #[doc = #summary_value]
705 #(#attrs)*
706 #vis #sig #block
707 };
708
709 TokenStream::from(expanded)
710}
711
712#[proc_macro_attribute]
724pub fn description(attr: TokenStream, item: TokenStream) -> TokenStream {
725 let desc = parse_macro_input!(attr as LitStr);
726 let input = parse_macro_input!(item as ItemFn);
727
728 let attrs = &input.attrs;
729 let vis = &input.vis;
730 let sig = &input.sig;
731 let block = &input.block;
732 let desc_value = desc.value();
733
734 let expanded = quote! {
736 #[doc = ""]
737 #[doc = #desc_value]
738 #(#attrs)*
739 #vis #sig #block
740 };
741
742 TokenStream::from(expanded)
743}
744
745#[proc_macro_attribute]
777pub fn param(_attr: TokenStream, item: TokenStream) -> TokenStream {
778 item
781}
782
783#[derive(Debug)]
789struct ValidationRuleInfo {
790 rule_type: String,
791 params: Vec<(String, String)>,
792 message: Option<String>,
793 groups: Vec<String>,
794}
795
796fn parse_validate_attrs(attrs: &[Attribute]) -> Vec<ValidationRuleInfo> {
798 let mut rules = Vec::new();
799
800 for attr in attrs {
801 if !attr.path().is_ident("validate") {
802 continue;
803 }
804
805 if let Ok(meta) = attr.parse_args::<Meta>() {
807 if let Some(rule) = parse_validate_meta(&meta) {
808 rules.push(rule);
809 }
810 } else if let Ok(nested) = attr
811 .parse_args_with(syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated)
812 {
813 for meta in nested {
814 if let Some(rule) = parse_validate_meta(&meta) {
815 rules.push(rule);
816 }
817 }
818 }
819 }
820
821 rules
822}
823
824fn parse_validate_meta(meta: &Meta) -> Option<ValidationRuleInfo> {
826 match meta {
827 Meta::Path(path) => {
828 let ident = path.get_ident()?.to_string();
830 Some(ValidationRuleInfo {
831 rule_type: ident,
832 params: Vec::new(),
833 message: None,
834 groups: Vec::new(),
835 })
836 }
837 Meta::List(list) => {
838 let rule_type = list.path.get_ident()?.to_string();
840 let mut params = Vec::new();
841 let mut message = None;
842 let mut groups = Vec::new();
843
844 if let Ok(nested) = list.parse_args_with(
846 syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
847 ) {
848 for nested_meta in nested {
849 if let Meta::NameValue(nv) = &nested_meta {
850 let key = nv.path.get_ident()?.to_string();
851
852 if key == "groups" {
853 let vec = expr_to_string_vec(&nv.value);
854 groups.extend(vec);
855 } else if let Some(value) = expr_to_string(&nv.value) {
856 if key == "message" {
857 message = Some(value);
858 } else if key == "group" {
859 groups.push(value);
860 } else {
861 params.push((key, value));
862 }
863 }
864 } else if let Meta::Path(path) = &nested_meta {
865 if let Some(ident) = path.get_ident() {
867 params.push((ident.to_string(), "true".to_string()));
868 }
869 }
870 }
871 }
872
873 Some(ValidationRuleInfo {
874 rule_type,
875 params,
876 message,
877 groups,
878 })
879 }
880 Meta::NameValue(nv) => {
881 let rule_type = nv.path.get_ident()?.to_string();
883 let value = expr_to_string(&nv.value)?;
884
885 Some(ValidationRuleInfo {
886 rule_type: rule_type.clone(),
887 params: vec![(rule_type.clone(), value)],
888 message: None,
889 groups: Vec::new(),
890 })
891 }
892 }
893}
894
895fn expr_to_string(expr: &Expr) -> Option<String> {
897 match expr {
898 Expr::Lit(lit) => match &lit.lit {
899 Lit::Str(s) => Some(s.value()),
900 Lit::Int(i) => Some(i.base10_digits().to_string()),
901 Lit::Float(f) => Some(f.base10_digits().to_string()),
902 Lit::Bool(b) => Some(b.value.to_string()),
903 _ => None,
904 },
905 _ => None,
906 }
907}
908
909fn expr_to_string_vec(expr: &Expr) -> Vec<String> {
911 match expr {
912 Expr::Array(arr) => {
913 let mut result = Vec::new();
914 for elem in &arr.elems {
915 if let Some(s) = expr_to_string(elem) {
916 result.push(s);
917 }
918 }
919 result
920 }
921 _ => {
922 if let Some(s) = expr_to_string(expr) {
923 vec![s]
924 } else {
925 Vec::new()
926 }
927 }
928 }
929}
930
931fn generate_rule_validation(
932 field_name: &str,
933 _field_type: &Type,
934 rule: &ValidationRuleInfo,
935) -> proc_macro2::TokenStream {
936 let field_ident = syn::Ident::new(field_name, proc_macro2::Span::call_site());
937 let field_name_str = field_name;
938
939 let group_check = if rule.groups.is_empty() {
941 quote! { true }
942 } else {
943 let group_names = rule.groups.iter().map(|g| g.as_str());
944 quote! {
945 {
946 let rule_groups = [#(::rustapi_validate::v2::ValidationGroup::from(#group_names)),*];
947 rule_groups.iter().any(|g| g.matches(&group))
948 }
949 }
950 };
951
952 let validation_logic = match rule.rule_type.as_str() {
953 "email" => {
954 let message = rule
955 .message
956 .as_ref()
957 .map(|m| quote! { .with_message(#m) })
958 .unwrap_or_default();
959 quote! {
960 {
961 let rule = ::rustapi_validate::v2::EmailRule::new() #message;
962 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
963 errors.add(#field_name_str, e);
964 }
965 }
966 }
967 }
968 "length" => {
969 let min = rule
970 .params
971 .iter()
972 .find(|(k, _)| k == "min")
973 .and_then(|(_, v)| v.parse::<usize>().ok());
974 let max = rule
975 .params
976 .iter()
977 .find(|(k, _)| k == "max")
978 .and_then(|(_, v)| v.parse::<usize>().ok());
979 let message = rule
980 .message
981 .as_ref()
982 .map(|m| quote! { .with_message(#m) })
983 .unwrap_or_default();
984
985 let rule_creation = match (min, max) {
986 (Some(min), Some(max)) => {
987 quote! { ::rustapi_validate::v2::LengthRule::new(#min, #max) }
988 }
989 (Some(min), None) => quote! { ::rustapi_validate::v2::LengthRule::min(#min) },
990 (None, Some(max)) => quote! { ::rustapi_validate::v2::LengthRule::max(#max) },
991 (None, None) => quote! { ::rustapi_validate::v2::LengthRule::new(0, usize::MAX) },
992 };
993
994 quote! {
995 {
996 let rule = #rule_creation #message;
997 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
998 errors.add(#field_name_str, e);
999 }
1000 }
1001 }
1002 }
1003 "range" => {
1004 let min = rule
1005 .params
1006 .iter()
1007 .find(|(k, _)| k == "min")
1008 .map(|(_, v)| v.clone());
1009 let max = rule
1010 .params
1011 .iter()
1012 .find(|(k, _)| k == "max")
1013 .map(|(_, v)| v.clone());
1014 let message = rule
1015 .message
1016 .as_ref()
1017 .map(|m| quote! { .with_message(#m) })
1018 .unwrap_or_default();
1019
1020 let rule_creation = match (min, max) {
1022 (Some(min), Some(max)) => {
1023 let min_lit: proc_macro2::TokenStream = min.parse().unwrap();
1024 let max_lit: proc_macro2::TokenStream = max.parse().unwrap();
1025 quote! { ::rustapi_validate::v2::RangeRule::new(#min_lit, #max_lit) }
1026 }
1027 (Some(min), None) => {
1028 let min_lit: proc_macro2::TokenStream = min.parse().unwrap();
1029 quote! { ::rustapi_validate::v2::RangeRule::min(#min_lit) }
1030 }
1031 (None, Some(max)) => {
1032 let max_lit: proc_macro2::TokenStream = max.parse().unwrap();
1033 quote! { ::rustapi_validate::v2::RangeRule::max(#max_lit) }
1034 }
1035 (None, None) => {
1036 return quote! {};
1037 }
1038 };
1039
1040 quote! {
1041 {
1042 let rule = #rule_creation #message;
1043 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1044 errors.add(#field_name_str, e);
1045 }
1046 }
1047 }
1048 }
1049 "regex" => {
1050 let pattern = rule
1051 .params
1052 .iter()
1053 .find(|(k, _)| k == "regex" || k == "pattern")
1054 .map(|(_, v)| v.clone())
1055 .unwrap_or_default();
1056 let message = rule
1057 .message
1058 .as_ref()
1059 .map(|m| quote! { .with_message(#m) })
1060 .unwrap_or_default();
1061
1062 quote! {
1063 {
1064 let rule = ::rustapi_validate::v2::RegexRule::new(#pattern) #message;
1065 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1066 errors.add(#field_name_str, e);
1067 }
1068 }
1069 }
1070 }
1071 "url" => {
1072 let message = rule
1073 .message
1074 .as_ref()
1075 .map(|m| quote! { .with_message(#m) })
1076 .unwrap_or_default();
1077 quote! {
1078 {
1079 let rule = ::rustapi_validate::v2::UrlRule::new() #message;
1080 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1081 errors.add(#field_name_str, e);
1082 }
1083 }
1084 }
1085 }
1086 "required" => {
1087 let message = rule
1088 .message
1089 .as_ref()
1090 .map(|m| quote! { .with_message(#m) })
1091 .unwrap_or_default();
1092 quote! {
1093 {
1094 let rule = ::rustapi_validate::v2::RequiredRule::new() #message;
1095 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1096 errors.add(#field_name_str, e);
1097 }
1098 }
1099 }
1100 }
1101 "credit_card" => {
1102 let message = rule
1103 .message
1104 .as_ref()
1105 .map(|m| quote! { .with_message(#m) })
1106 .unwrap_or_default();
1107 quote! {
1108 {
1109 let rule = ::rustapi_validate::v2::CreditCardRule::new() #message;
1110 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1111 errors.add(#field_name_str, e);
1112 }
1113 }
1114 }
1115 }
1116 "ip" => {
1117 let v4 = rule.params.iter().any(|(k, _)| k == "v4");
1118 let v6 = rule.params.iter().any(|(k, _)| k == "v6");
1119
1120 let rule_creation = if v4 && !v6 {
1121 quote! { ::rustapi_validate::v2::IpRule::v4() }
1122 } else if !v4 && v6 {
1123 quote! { ::rustapi_validate::v2::IpRule::v6() }
1124 } else {
1125 quote! { ::rustapi_validate::v2::IpRule::new() }
1126 };
1127
1128 let message = rule
1129 .message
1130 .as_ref()
1131 .map(|m| quote! { .with_message(#m) })
1132 .unwrap_or_default();
1133
1134 quote! {
1135 {
1136 let rule = #rule_creation #message;
1137 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1138 errors.add(#field_name_str, e);
1139 }
1140 }
1141 }
1142 }
1143 "phone" => {
1144 let message = rule
1145 .message
1146 .as_ref()
1147 .map(|m| quote! { .with_message(#m) })
1148 .unwrap_or_default();
1149 quote! {
1150 {
1151 let rule = ::rustapi_validate::v2::PhoneRule::new() #message;
1152 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1153 errors.add(#field_name_str, e);
1154 }
1155 }
1156 }
1157 }
1158 "contains" => {
1159 let needle = rule
1160 .params
1161 .iter()
1162 .find(|(k, _)| k == "needle")
1163 .map(|(_, v)| v.clone())
1164 .unwrap_or_default();
1165
1166 let message = rule
1167 .message
1168 .as_ref()
1169 .map(|m| quote! { .with_message(#m) })
1170 .unwrap_or_default();
1171
1172 quote! {
1173 {
1174 let rule = ::rustapi_validate::v2::ContainsRule::new(#needle) #message;
1175 if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1176 errors.add(#field_name_str, e);
1177 }
1178 }
1179 }
1180 }
1181 _ => {
1182 quote! {}
1184 }
1185 };
1186
1187 quote! {
1188 if #group_check {
1189 #validation_logic
1190 }
1191 }
1192}
1193
1194fn generate_async_rule_validation(
1196 field_name: &str,
1197 rule: &ValidationRuleInfo,
1198) -> proc_macro2::TokenStream {
1199 let field_ident = syn::Ident::new(field_name, proc_macro2::Span::call_site());
1200 let field_name_str = field_name;
1201
1202 let group_check = if rule.groups.is_empty() {
1204 quote! { true }
1205 } else {
1206 let group_names = rule.groups.iter().map(|g| g.as_str());
1207 quote! {
1208 {
1209 let rule_groups = [#(::rustapi_validate::v2::ValidationGroup::from(#group_names)),*];
1210 rule_groups.iter().any(|g| g.matches(&group))
1211 }
1212 }
1213 };
1214
1215 let validation_logic = match rule.rule_type.as_str() {
1216 "async_unique" => {
1217 let table = rule
1218 .params
1219 .iter()
1220 .find(|(k, _)| k == "table")
1221 .map(|(_, v)| v.clone())
1222 .unwrap_or_default();
1223 let column = rule
1224 .params
1225 .iter()
1226 .find(|(k, _)| k == "column")
1227 .map(|(_, v)| v.clone())
1228 .unwrap_or_default();
1229 let message = rule
1230 .message
1231 .as_ref()
1232 .map(|m| quote! { .with_message(#m) })
1233 .unwrap_or_default();
1234
1235 quote! {
1236 {
1237 let rule = ::rustapi_validate::v2::AsyncUniqueRule::new(#table, #column) #message;
1238 if let Err(e) = ::rustapi_validate::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
1239 errors.add(#field_name_str, e);
1240 }
1241 }
1242 }
1243 }
1244 "async_exists" => {
1245 let table = rule
1246 .params
1247 .iter()
1248 .find(|(k, _)| k == "table")
1249 .map(|(_, v)| v.clone())
1250 .unwrap_or_default();
1251 let column = rule
1252 .params
1253 .iter()
1254 .find(|(k, _)| k == "column")
1255 .map(|(_, v)| v.clone())
1256 .unwrap_or_default();
1257 let message = rule
1258 .message
1259 .as_ref()
1260 .map(|m| quote! { .with_message(#m) })
1261 .unwrap_or_default();
1262
1263 quote! {
1264 {
1265 let rule = ::rustapi_validate::v2::AsyncExistsRule::new(#table, #column) #message;
1266 if let Err(e) = ::rustapi_validate::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
1267 errors.add(#field_name_str, e);
1268 }
1269 }
1270 }
1271 }
1272 "async_api" => {
1273 let endpoint = rule
1274 .params
1275 .iter()
1276 .find(|(k, _)| k == "endpoint")
1277 .map(|(_, v)| v.clone())
1278 .unwrap_or_default();
1279 let message = rule
1280 .message
1281 .as_ref()
1282 .map(|m| quote! { .with_message(#m) })
1283 .unwrap_or_default();
1284
1285 quote! {
1286 {
1287 let rule = ::rustapi_validate::v2::AsyncApiRule::new(#endpoint) #message;
1288 if let Err(e) = ::rustapi_validate::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
1289 errors.add(#field_name_str, e);
1290 }
1291 }
1292 }
1293 }
1294 "custom_async" => {
1295 let function_path = rule
1297 .params
1298 .iter()
1299 .find(|(k, _)| k == "custom_async" || k == "function")
1300 .map(|(_, v)| v.clone())
1301 .unwrap_or_default();
1302
1303 if function_path.is_empty() {
1304 quote! {}
1306 } else {
1307 let func: syn::Path = syn::parse_str(&function_path).unwrap();
1308 let message_handling = if let Some(msg) = &rule.message {
1309 quote! {
1310 let e = ::rustapi_validate::v2::RuleError::new("custom_async", #msg);
1311 errors.add(#field_name_str, e);
1312 }
1313 } else {
1314 quote! {
1315 errors.add(#field_name_str, e);
1316 }
1317 };
1318
1319 quote! {
1320 {
1321 if let Err(e) = #func(&self.#field_ident, ctx).await {
1323 #message_handling
1324 }
1325 }
1326 }
1327 }
1328 }
1329 _ => {
1330 quote! {}
1332 }
1333 };
1334
1335 quote! {
1336 if #group_check {
1337 #validation_logic
1338 }
1339 }
1340}
1341
1342fn is_async_rule(rule: &ValidationRuleInfo) -> bool {
1344 matches!(
1345 rule.rule_type.as_str(),
1346 "async_unique" | "async_exists" | "async_api" | "custom_async"
1347 )
1348}
1349
1350#[proc_macro_derive(Validate, attributes(validate))]
1373pub fn derive_validate(input: TokenStream) -> TokenStream {
1374 let input = parse_macro_input!(input as DeriveInput);
1375 let name = &input.ident;
1376 let generics = &input.generics;
1377 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
1378
1379 let fields = match &input.data {
1381 Data::Struct(data) => match &data.fields {
1382 Fields::Named(fields) => &fields.named,
1383 _ => {
1384 return syn::Error::new_spanned(
1385 &input,
1386 "Validate can only be derived for structs with named fields",
1387 )
1388 .to_compile_error()
1389 .into();
1390 }
1391 },
1392 _ => {
1393 return syn::Error::new_spanned(&input, "Validate can only be derived for structs")
1394 .to_compile_error()
1395 .into();
1396 }
1397 };
1398
1399 let mut sync_validations = Vec::new();
1401 let mut async_validations = Vec::new();
1402 let mut has_async_rules = false;
1403
1404 for field in fields {
1405 let field_name = field.ident.as_ref().unwrap().to_string();
1406 let field_type = &field.ty;
1407 let rules = parse_validate_attrs(&field.attrs);
1408
1409 for rule in &rules {
1410 if is_async_rule(rule) {
1411 has_async_rules = true;
1412 let validation = generate_async_rule_validation(&field_name, rule);
1413 async_validations.push(validation);
1414 } else {
1415 let validation = generate_rule_validation(&field_name, field_type, rule);
1416 sync_validations.push(validation);
1417 }
1418 }
1419 }
1420
1421 let validate_impl = quote! {
1423 impl #impl_generics ::rustapi_validate::v2::Validate for #name #ty_generics #where_clause {
1424 fn validate_with_group(&self, group: ::rustapi_validate::v2::ValidationGroup) -> Result<(), ::rustapi_validate::v2::ValidationErrors> {
1425 let mut errors = ::rustapi_validate::v2::ValidationErrors::new();
1426
1427 #(#sync_validations)*
1428
1429 errors.into_result()
1430 }
1431 }
1432 };
1433
1434 let async_validate_impl = if has_async_rules {
1436 quote! {
1437 #[::async_trait::async_trait]
1438 impl #impl_generics ::rustapi_validate::v2::AsyncValidate for #name #ty_generics #where_clause {
1439 async fn validate_async_with_group(&self, ctx: &::rustapi_validate::v2::ValidationContext, group: ::rustapi_validate::v2::ValidationGroup) -> Result<(), ::rustapi_validate::v2::ValidationErrors> {
1440 let mut errors = ::rustapi_validate::v2::ValidationErrors::new();
1441
1442 #(#async_validations)*
1443
1444 errors.into_result()
1445 }
1446 }
1447 }
1448 } else {
1449 quote! {
1451 #[::async_trait::async_trait]
1452 impl #impl_generics ::rustapi_validate::v2::AsyncValidate for #name #ty_generics #where_clause {
1453 async fn validate_async_with_group(&self, _ctx: &::rustapi_validate::v2::ValidationContext, _group: ::rustapi_validate::v2::ValidationGroup) -> Result<(), ::rustapi_validate::v2::ValidationErrors> {
1454 Ok(())
1455 }
1456 }
1457 }
1458 };
1459
1460 let validatable_impl = quote! {
1464 impl #impl_generics ::rustapi_core::validation::Validatable for #name #ty_generics #where_clause {
1465 fn do_validate(&self) -> Result<(), ::rustapi_core::ApiError> {
1466 match ::rustapi_validate::v2::Validate::validate(self) {
1467 Ok(_) => Ok(()),
1468 Err(e) => Err(::rustapi_core::validation::convert_v2_errors(e)),
1469 }
1470 }
1471 }
1472 };
1473
1474 let expanded = quote! {
1475 #validate_impl
1476 #async_validate_impl
1477 #validatable_impl
1478 };
1479
1480 debug_output("Validate derive", &expanded);
1481
1482 TokenStream::from(expanded)
1483}
1484
1485#[proc_macro_derive(ApiError, attributes(error))]
1504pub fn derive_api_error(input: TokenStream) -> TokenStream {
1505 api_error::expand_derive_api_error(input)
1506}
1507
1508#[proc_macro_derive(TypedPath, attributes(typed_path))]
1525pub fn derive_typed_path(input: TokenStream) -> TokenStream {
1526 let input = parse_macro_input!(input as DeriveInput);
1527 let name = &input.ident;
1528 let generics = &input.generics;
1529 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
1530
1531 let mut path_str = None;
1533 for attr in &input.attrs {
1534 if attr.path().is_ident("typed_path") {
1535 if let Ok(lit) = attr.parse_args::<LitStr>() {
1536 path_str = Some(lit.value());
1537 }
1538 }
1539 }
1540
1541 let path = match path_str {
1542 Some(p) => p,
1543 None => {
1544 return syn::Error::new_spanned(
1545 &input,
1546 "#[derive(TypedPath)] requires a #[typed_path(\"...\")] attribute",
1547 )
1548 .to_compile_error()
1549 .into();
1550 }
1551 };
1552
1553 if let Err(err) = validate_path_syntax(&path, proc_macro2::Span::call_site()) {
1555 return err.to_compile_error().into();
1556 }
1557
1558 let mut format_string = String::new();
1561 let mut format_args = Vec::new();
1562
1563 let mut chars = path.chars().peekable();
1564 while let Some(ch) = chars.next() {
1565 if ch == '{' {
1566 let mut param_name = String::new();
1567 while let Some(&c) = chars.peek() {
1568 if c == '}' {
1569 chars.next(); break;
1571 }
1572 param_name.push(chars.next().unwrap());
1573 }
1574
1575 if param_name.is_empty() {
1576 return syn::Error::new_spanned(
1577 &input,
1578 "Empty path parameter not allowed in typed_path",
1579 )
1580 .to_compile_error()
1581 .into();
1582 }
1583
1584 format_string.push_str("{}");
1585 let ident = syn::Ident::new(¶m_name, proc_macro2::Span::call_site());
1586 format_args.push(quote! { self.#ident });
1587 } else {
1588 format_string.push(ch);
1589 }
1590 }
1591
1592 let expanded = quote! {
1593 impl #impl_generics ::rustapi_rs::prelude::TypedPath for #name #ty_generics #where_clause {
1594 const PATH: &'static str = #path;
1595
1596 fn to_uri(&self) -> String {
1597 format!(#format_string, #(#format_args),*)
1598 }
1599 }
1600 };
1601
1602 debug_output("TypedPath derive", &expanded);
1603 TokenStream::from(expanded)
1604}