1use proc_macro::TokenStream;
18use proc_macro_crate::{crate_name, FoundCrate};
19use quote::quote;
20use std::collections::HashSet;
21use syn::{
22 parse_macro_input, Attribute, Data, DeriveInput, Expr, Fields, FnArg, GenericArgument, ItemFn,
23 Lit, LitStr, Meta, PathArguments, ReturnType, Type,
24};
25
26mod api_error;
27mod derive_schema;
28
29fn get_rustapi_path() -> proc_macro2::TokenStream {
34 let rustapi_rs_found = crate_name("rustapi-rs").or_else(|_| crate_name("rustapi_rs"));
35
36 if let Ok(found) = rustapi_rs_found {
37 match found {
38 FoundCrate::Itself => quote! { ::rustapi_rs },
41 FoundCrate::Name(name) => {
42 let normalized = name.replace('-', "_");
43 let ident = syn::Ident::new(&normalized, proc_macro2::Span::call_site());
44 quote! { ::#ident }
45 }
46 }
47 } else {
48 quote! { ::rustapi_rs }
49 }
50}
51
52#[proc_macro_derive(Schema, attributes(schema))]
64pub fn derive_schema(input: TokenStream) -> TokenStream {
65 derive_schema::expand_derive_schema(parse_macro_input!(input as DeriveInput)).into()
66}
67
68#[proc_macro_attribute]
82pub fn schema(_attr: TokenStream, item: TokenStream) -> TokenStream {
83 let input = parse_macro_input!(item as syn::Item);
84 let rustapi_path = get_rustapi_path();
85
86 let (ident, generics) = match &input {
87 syn::Item::Struct(s) => (&s.ident, &s.generics),
88 syn::Item::Enum(e) => (&e.ident, &e.generics),
89 _ => {
90 return syn::Error::new_spanned(
91 &input,
92 "#[rustapi_rs::schema] can only be used on structs or enums",
93 )
94 .to_compile_error()
95 .into();
96 }
97 };
98
99 if !generics.params.is_empty() {
100 return syn::Error::new_spanned(
101 generics,
102 "#[rustapi_rs::schema] does not support generic types",
103 )
104 .to_compile_error()
105 .into();
106 }
107
108 let registrar_ident = syn::Ident::new(
109 &format!("__RUSTAPI_AUTO_SCHEMA_{}", ident),
110 proc_macro2::Span::call_site(),
111 );
112
113 let expanded = quote! {
114 #input
115
116 #[allow(non_upper_case_globals)]
117 #[#rustapi_path::__private::linkme::distributed_slice(#rustapi_path::__private::AUTO_SCHEMAS)]
118 #[linkme(crate = #rustapi_path::__private::linkme)]
119 static #registrar_ident: fn(&mut #rustapi_path::__private::openapi::OpenApiSpec) =
120 |spec: &mut #rustapi_path::__private::openapi::OpenApiSpec| {
121 spec.register_in_place::<#ident>();
122 };
123 };
124
125 debug_output("schema", &expanded);
126 expanded.into()
127}
128
129fn extract_schema_types(ty: &Type, out: &mut Vec<Type>, allow_leaf: bool) {
130 match ty {
131 Type::Reference(r) => extract_schema_types(&r.elem, out, allow_leaf),
132 Type::Path(tp) => {
133 let Some(seg) = tp.path.segments.last() else {
134 return;
135 };
136
137 let ident = seg.ident.to_string();
138
139 let unwrap_first_generic = |out: &mut Vec<Type>| {
140 if let PathArguments::AngleBracketed(args) = &seg.arguments {
141 if let Some(GenericArgument::Type(inner)) = args.args.first() {
142 extract_schema_types(inner, out, true);
143 }
144 }
145 };
146
147 match ident.as_str() {
148 "Json" | "ValidatedJson" | "Created" => {
150 unwrap_first_generic(out);
151 }
152 "WithStatus" => {
154 if let PathArguments::AngleBracketed(args) = &seg.arguments {
155 if let Some(GenericArgument::Type(inner)) = args.args.first() {
156 extract_schema_types(inner, out, true);
157 }
158 }
159 }
160 "Option" | "Result" => {
162 if let PathArguments::AngleBracketed(args) = &seg.arguments {
163 if let Some(GenericArgument::Type(inner)) = args.args.first() {
164 extract_schema_types(inner, out, allow_leaf);
165 }
166 }
167 }
168 _ => {
169 if allow_leaf {
170 out.push(ty.clone());
171 }
172 }
173 }
174 }
175 _ => {}
176 }
177}
178
179fn collect_handler_schema_types(input: &ItemFn) -> Vec<Type> {
180 let mut found: Vec<Type> = Vec::new();
181
182 for arg in &input.sig.inputs {
183 if let FnArg::Typed(pat_ty) = arg {
184 extract_schema_types(&pat_ty.ty, &mut found, false);
185 }
186 }
187
188 if let ReturnType::Type(_, ty) = &input.sig.output {
189 extract_schema_types(ty, &mut found, false);
190 }
191
192 let mut seen = HashSet::<String>::new();
194 found
195 .into_iter()
196 .filter(|t| seen.insert(quote!(#t).to_string()))
197 .collect()
198}
199
200fn collect_path_params(input: &ItemFn) -> Vec<(String, String)> {
204 let mut params = Vec::new();
205
206 for arg in &input.sig.inputs {
207 if let FnArg::Typed(pat_ty) = arg {
208 if let Type::Path(tp) = &*pat_ty.ty {
210 if let Some(seg) = tp.path.segments.last() {
211 if seg.ident == "Path" {
212 if let PathArguments::AngleBracketed(args) = &seg.arguments {
214 if let Some(GenericArgument::Type(inner_ty)) = args.args.first() {
215 if let Some(schema_type) = map_type_to_schema(inner_ty) {
217 if let Some(name) = extract_param_name(&pat_ty.pat) {
225 params.push((name, schema_type));
226 }
227 }
228 }
229 }
230 }
231 }
232 }
233 }
234 }
235
236 params
237}
238
239fn extract_param_name(pat: &syn::Pat) -> Option<String> {
244 match pat {
245 syn::Pat::Ident(ident) => Some(ident.ident.to_string()),
246 syn::Pat::TupleStruct(ts) => {
247 if let Some(first) = ts.elems.first() {
250 extract_param_name(first)
251 } else {
252 None
253 }
254 }
255 _ => None, }
257}
258
259fn map_type_to_schema(ty: &Type) -> Option<String> {
261 match ty {
262 Type::Path(tp) => {
263 if let Some(seg) = tp.path.segments.last() {
264 let ident = seg.ident.to_string();
265 match ident.as_str() {
266 "Uuid" => Some("uuid".to_string()),
267 "String" | "str" => Some("string".to_string()),
268 "bool" => Some("boolean".to_string()),
269 "i8" | "i16" | "i32" | "i64" | "isize" | "u8" | "u16" | "u32" | "u64"
270 | "usize" => Some("integer".to_string()),
271 "f32" | "f64" => Some("number".to_string()),
272 _ => None,
273 }
274 } else {
275 None
276 }
277 }
278 _ => None,
279 }
280}
281
282fn is_debug_enabled() -> bool {
284 std::env::var("RUSTAPI_DEBUG")
285 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
286 .unwrap_or(false)
287}
288
289fn debug_output(name: &str, tokens: &proc_macro2::TokenStream) {
291 if is_debug_enabled() {
292 eprintln!("\n=== RUSTAPI_DEBUG: {} ===", name);
293 eprintln!("{}", tokens);
294 eprintln!("=== END {} ===\n", name);
295 }
296}
297
298fn validate_path_syntax(path: &str, span: proc_macro2::Span) -> Result<(), syn::Error> {
302 if !path.starts_with('/') {
304 return Err(syn::Error::new(
305 span,
306 format!("route path must start with '/', got: \"{}\"", path),
307 ));
308 }
309
310 if path.contains("//") {
312 return Err(syn::Error::new(
313 span,
314 format!(
315 "route path contains empty segment (double slash): \"{}\"",
316 path
317 ),
318 ));
319 }
320
321 let mut brace_depth = 0;
323 let mut param_start = None;
324
325 for (i, ch) in path.char_indices() {
326 match ch {
327 '{' => {
328 if brace_depth > 0 {
329 return Err(syn::Error::new(
330 span,
331 format!(
332 "nested braces are not allowed in route path at position {}: \"{}\"",
333 i, path
334 ),
335 ));
336 }
337 brace_depth += 1;
338 param_start = Some(i);
339 }
340 '}' => {
341 if brace_depth == 0 {
342 return Err(syn::Error::new(
343 span,
344 format!(
345 "unmatched closing brace '}}' at position {} in route path: \"{}\"",
346 i, path
347 ),
348 ));
349 }
350 brace_depth -= 1;
351
352 if let Some(start) = param_start {
354 let param_name = &path[start + 1..i];
355 if param_name.is_empty() {
356 return Err(syn::Error::new(
357 span,
358 format!(
359 "empty parameter name '{{}}' at position {} in route path: \"{}\"",
360 start, path
361 ),
362 ));
363 }
364 if !param_name.chars().all(|c| c.is_alphanumeric() || c == '_') {
366 return Err(syn::Error::new(
367 span,
368 format!(
369 "invalid parameter name '{{{}}}' at position {} - parameter names must contain only alphanumeric characters and underscores: \"{}\"",
370 param_name, start, path
371 ),
372 ));
373 }
374 if param_name
376 .chars()
377 .next()
378 .map(|c| c.is_ascii_digit())
379 .unwrap_or(false)
380 {
381 return Err(syn::Error::new(
382 span,
383 format!(
384 "parameter name '{{{}}}' cannot start with a digit at position {}: \"{}\"",
385 param_name, start, path
386 ),
387 ));
388 }
389 }
390 param_start = None;
391 }
392 _ if brace_depth == 0 => {
394 if !ch.is_alphanumeric() && !"-_./*".contains(ch) {
396 return Err(syn::Error::new(
397 span,
398 format!(
399 "invalid character '{}' at position {} in route path: \"{}\"",
400 ch, i, path
401 ),
402 ));
403 }
404 }
405 _ => {}
406 }
407 }
408
409 if brace_depth > 0 {
411 return Err(syn::Error::new(
412 span,
413 format!(
414 "unclosed brace '{{' in route path (missing closing '}}'): \"{}\"",
415 path
416 ),
417 ));
418 }
419
420 Ok(())
421}
422
423#[proc_macro_attribute]
441pub fn main(_attr: TokenStream, item: TokenStream) -> TokenStream {
442 let input = parse_macro_input!(item as ItemFn);
443
444 let attrs = &input.attrs;
445 let vis = &input.vis;
446 let sig = &input.sig;
447 let block = &input.block;
448
449 let expanded = quote! {
450 #(#attrs)*
451 #[::tokio::main]
452 #vis #sig {
453 #block
454 }
455 };
456
457 debug_output("main", &expanded);
458
459 TokenStream::from(expanded)
460}
461
462fn generate_route_handler(method: &str, attr: TokenStream, item: TokenStream) -> TokenStream {
464 let path = parse_macro_input!(attr as LitStr);
465 let input = parse_macro_input!(item as ItemFn);
466 let rustapi_path = get_rustapi_path();
467
468 let fn_name = &input.sig.ident;
469 let fn_vis = &input.vis;
470 let fn_attrs = &input.attrs;
471 let fn_async = &input.sig.asyncness;
472 let fn_inputs = &input.sig.inputs;
473 let fn_output = &input.sig.output;
474 let fn_block = &input.block;
475 let fn_generics = &input.sig.generics;
476
477 let schema_types = collect_handler_schema_types(&input);
478
479 let path_value = path.value();
480
481 if let Err(err) = validate_path_syntax(&path_value, path.span()) {
483 return err.to_compile_error().into();
484 }
485
486 let route_fn_name = syn::Ident::new(&format!("{}_route", fn_name), fn_name.span());
488 let auto_route_name = syn::Ident::new(&format!("__AUTO_ROUTE_{}", fn_name), fn_name.span());
490
491 let schema_reg_fn_name =
493 syn::Ident::new(&format!("__{}_register_schemas", fn_name), fn_name.span());
494 let auto_schema_name = syn::Ident::new(&format!("__AUTO_SCHEMA_{}", fn_name), fn_name.span());
495
496 let route_helper = match method {
498 "GET" => quote!(#rustapi_path::get_route),
499 "POST" => quote!(#rustapi_path::post_route),
500 "PUT" => quote!(#rustapi_path::put_route),
501 "PATCH" => quote!(#rustapi_path::patch_route),
502 "DELETE" => quote!(#rustapi_path::delete_route),
503 _ => quote!(#rustapi_path::get_route),
504 };
505
506 let auto_params = collect_path_params(&input);
508
509 let mut chained_calls = quote!();
511
512 for (name, schema) in auto_params {
514 chained_calls = quote! { #chained_calls .param(#name, #schema) };
515 }
516
517 for attr in fn_attrs {
518 if let Some(ident) = attr.path().segments.last().map(|s| &s.ident) {
521 let ident_str = ident.to_string();
522 if ident_str == "tag" {
523 if let Ok(lit) = attr.parse_args::<LitStr>() {
524 let val = lit.value();
525 chained_calls = quote! { #chained_calls .tag(#val) };
526 }
527 } else if ident_str == "summary" {
528 if let Ok(lit) = attr.parse_args::<LitStr>() {
529 let val = lit.value();
530 chained_calls = quote! { #chained_calls .summary(#val) };
531 }
532 } else if ident_str == "description" {
533 if let Ok(lit) = attr.parse_args::<LitStr>() {
534 let val = lit.value();
535 chained_calls = quote! { #chained_calls .description(#val) };
536 }
537 } else if ident_str == "param" {
538 if let Ok(param_args) = attr.parse_args_with(
540 syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
541 ) {
542 let mut param_name: Option<String> = None;
543 let mut param_schema: Option<String> = None;
544
545 for meta in param_args {
546 match &meta {
547 Meta::Path(path) => {
549 if param_name.is_none() {
550 if let Some(ident) = path.get_ident() {
551 param_name = Some(ident.to_string());
552 }
553 }
554 }
555 Meta::NameValue(nv) => {
557 let key = nv.path.get_ident().map(|i| i.to_string());
558 if let Some(key) = key {
559 if key == "schema" || key == "type" {
560 if let Expr::Lit(lit) = &nv.value {
561 if let Lit::Str(s) = &lit.lit {
562 param_schema = Some(s.value());
563 }
564 }
565 } else if param_name.is_none() {
566 param_name = Some(key);
568 if let Expr::Lit(lit) = &nv.value {
569 if let Lit::Str(s) = &lit.lit {
570 param_schema = Some(s.value());
571 }
572 }
573 }
574 }
575 }
576 _ => {}
577 }
578 }
579
580 if let (Some(pname), Some(pschema)) = (param_name, param_schema) {
581 chained_calls = quote! { #chained_calls .param(#pname, #pschema) };
582 }
583 }
584 }
585 }
586 }
587
588 let expanded = quote! {
589 #(#fn_attrs)*
591 #fn_vis #fn_async fn #fn_name #fn_generics (#fn_inputs) #fn_output #fn_block
592
593 #[doc(hidden)]
595 #fn_vis fn #route_fn_name() -> #rustapi_path::Route {
596 #route_helper(#path_value, #fn_name)
597 #chained_calls
598 }
599
600 #[doc(hidden)]
602 #[allow(non_upper_case_globals)]
603 #[#rustapi_path::__private::linkme::distributed_slice(#rustapi_path::__private::AUTO_ROUTES)]
604 #[linkme(crate = #rustapi_path::__private::linkme)]
605 static #auto_route_name: fn() -> #rustapi_path::Route = #route_fn_name;
606
607 #[doc(hidden)]
609 #[allow(non_snake_case)]
610 fn #schema_reg_fn_name(spec: &mut #rustapi_path::__private::openapi::OpenApiSpec) {
611 #( spec.register_in_place::<#schema_types>(); )*
612 }
613
614 #[doc(hidden)]
615 #[allow(non_upper_case_globals)]
616 #[#rustapi_path::__private::linkme::distributed_slice(#rustapi_path::__private::AUTO_SCHEMAS)]
617 #[linkme(crate = #rustapi_path::__private::linkme)]
618 static #auto_schema_name: fn(&mut #rustapi_path::__private::openapi::OpenApiSpec) = #schema_reg_fn_name;
619 };
620
621 debug_output(&format!("{} {}", method, path_value), &expanded);
622
623 TokenStream::from(expanded)
624}
625
626#[proc_macro_attribute]
642pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream {
643 generate_route_handler("GET", attr, item)
644}
645
646#[proc_macro_attribute]
648pub fn post(attr: TokenStream, item: TokenStream) -> TokenStream {
649 generate_route_handler("POST", attr, item)
650}
651
652#[proc_macro_attribute]
654pub fn put(attr: TokenStream, item: TokenStream) -> TokenStream {
655 generate_route_handler("PUT", attr, item)
656}
657
658#[proc_macro_attribute]
660pub fn patch(attr: TokenStream, item: TokenStream) -> TokenStream {
661 generate_route_handler("PATCH", attr, item)
662}
663
664#[proc_macro_attribute]
666pub fn delete(attr: TokenStream, item: TokenStream) -> TokenStream {
667 generate_route_handler("DELETE", attr, item)
668}
669
670#[proc_macro_attribute]
686pub fn tag(attr: TokenStream, item: TokenStream) -> TokenStream {
687 let tag = parse_macro_input!(attr as LitStr);
688 let input = parse_macro_input!(item as ItemFn);
689
690 let attrs = &input.attrs;
691 let vis = &input.vis;
692 let sig = &input.sig;
693 let block = &input.block;
694 let tag_value = tag.value();
695
696 let expanded = quote! {
698 #[doc = concat!("**Tag:** ", #tag_value)]
699 #(#attrs)*
700 #vis #sig #block
701 };
702
703 TokenStream::from(expanded)
704}
705
706#[proc_macro_attribute]
718pub fn summary(attr: TokenStream, item: TokenStream) -> TokenStream {
719 let summary = parse_macro_input!(attr as LitStr);
720 let input = parse_macro_input!(item as ItemFn);
721
722 let attrs = &input.attrs;
723 let vis = &input.vis;
724 let sig = &input.sig;
725 let block = &input.block;
726 let summary_value = summary.value();
727
728 let expanded = quote! {
730 #[doc = #summary_value]
731 #(#attrs)*
732 #vis #sig #block
733 };
734
735 TokenStream::from(expanded)
736}
737
738#[proc_macro_attribute]
750pub fn description(attr: TokenStream, item: TokenStream) -> TokenStream {
751 let desc = parse_macro_input!(attr as LitStr);
752 let input = parse_macro_input!(item as ItemFn);
753
754 let attrs = &input.attrs;
755 let vis = &input.vis;
756 let sig = &input.sig;
757 let block = &input.block;
758 let desc_value = desc.value();
759
760 let expanded = quote! {
762 #[doc = ""]
763 #[doc = #desc_value]
764 #(#attrs)*
765 #vis #sig #block
766 };
767
768 TokenStream::from(expanded)
769}
770
771#[proc_macro_attribute]
803pub fn param(_attr: TokenStream, item: TokenStream) -> TokenStream {
804 item
807}
808
809#[derive(Debug)]
815struct ValidationRuleInfo {
816 rule_type: String,
817 params: Vec<(String, String)>,
818 message: Option<String>,
819 groups: Vec<String>,
820}
821
822fn parse_validate_attrs(attrs: &[Attribute]) -> Vec<ValidationRuleInfo> {
824 let mut rules = Vec::new();
825
826 for attr in attrs {
827 if !attr.path().is_ident("validate") {
828 continue;
829 }
830
831 if let Ok(meta) = attr.parse_args::<Meta>() {
833 if let Some(rule) = parse_validate_meta(&meta) {
834 rules.push(rule);
835 }
836 } else if let Ok(nested) = attr
837 .parse_args_with(syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated)
838 {
839 for meta in nested {
840 if let Some(rule) = parse_validate_meta(&meta) {
841 rules.push(rule);
842 }
843 }
844 }
845 }
846
847 rules
848}
849
850fn parse_validate_meta(meta: &Meta) -> Option<ValidationRuleInfo> {
852 match meta {
853 Meta::Path(path) => {
854 let ident = path.get_ident()?.to_string();
856 Some(ValidationRuleInfo {
857 rule_type: ident,
858 params: Vec::new(),
859 message: None,
860 groups: Vec::new(),
861 })
862 }
863 Meta::List(list) => {
864 let rule_type = list.path.get_ident()?.to_string();
866 let mut params = Vec::new();
867 let mut message = None;
868 let mut groups = Vec::new();
869
870 if let Ok(nested) = list.parse_args_with(
872 syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
873 ) {
874 for nested_meta in nested {
875 if let Meta::NameValue(nv) = &nested_meta {
876 let key = nv.path.get_ident()?.to_string();
877
878 if key == "groups" {
879 let vec = expr_to_string_vec(&nv.value);
880 groups.extend(vec);
881 } else if let Some(value) = expr_to_string(&nv.value) {
882 if key == "message" {
883 message = Some(value);
884 } else if key == "group" {
885 groups.push(value);
886 } else {
887 params.push((key, value));
888 }
889 }
890 } else if let Meta::Path(path) = &nested_meta {
891 if let Some(ident) = path.get_ident() {
893 params.push((ident.to_string(), "true".to_string()));
894 }
895 }
896 }
897 }
898
899 Some(ValidationRuleInfo {
900 rule_type,
901 params,
902 message,
903 groups,
904 })
905 }
906 Meta::NameValue(nv) => {
907 let rule_type = nv.path.get_ident()?.to_string();
909 let value = expr_to_string(&nv.value)?;
910
911 Some(ValidationRuleInfo {
912 rule_type: rule_type.clone(),
913 params: vec![(rule_type.clone(), value)],
914 message: None,
915 groups: Vec::new(),
916 })
917 }
918 }
919}
920
921fn expr_to_string(expr: &Expr) -> Option<String> {
923 match expr {
924 Expr::Lit(lit) => match &lit.lit {
925 Lit::Str(s) => Some(s.value()),
926 Lit::Int(i) => Some(i.base10_digits().to_string()),
927 Lit::Float(f) => Some(f.base10_digits().to_string()),
928 Lit::Bool(b) => Some(b.value.to_string()),
929 _ => None,
930 },
931 _ => None,
932 }
933}
934
935fn expr_to_string_vec(expr: &Expr) -> Vec<String> {
937 match expr {
938 Expr::Array(arr) => {
939 let mut result = Vec::new();
940 for elem in &arr.elems {
941 if let Some(s) = expr_to_string(elem) {
942 result.push(s);
943 }
944 }
945 result
946 }
947 _ => {
948 if let Some(s) = expr_to_string(expr) {
949 vec![s]
950 } else {
951 Vec::new()
952 }
953 }
954 }
955}
956
957fn get_validate_path() -> proc_macro2::TokenStream {
967 let rustapi_rs_found = crate_name("rustapi-rs").or_else(|_| crate_name("rustapi_rs"));
968
969 if let Ok(found) = rustapi_rs_found {
970 match found {
971 FoundCrate::Itself => {
972 quote! { ::rustapi_rs::__private::validate }
973 }
974 FoundCrate::Name(name) => {
975 let normalized = name.replace('-', "_");
976 let ident = syn::Ident::new(&normalized, proc_macro2::Span::call_site());
977 quote! { ::#ident::__private::validate }
978 }
979 }
980 } else if let Ok(found) =
981 crate_name("rustapi-validate").or_else(|_| crate_name("rustapi_validate"))
982 {
983 match found {
984 FoundCrate::Itself => quote! { crate },
985 FoundCrate::Name(name) => {
986 let normalized = name.replace('-', "_");
987 let ident = syn::Ident::new(&normalized, proc_macro2::Span::call_site());
988 quote! { ::#ident }
989 }
990 }
991 } else {
992 quote! { ::rustapi_validate }
994 }
995}
996
997fn get_core_path() -> proc_macro2::TokenStream {
1003 let rustapi_rs_found = crate_name("rustapi-rs").or_else(|_| crate_name("rustapi_rs"));
1004
1005 if let Ok(found) = rustapi_rs_found {
1006 match found {
1007 FoundCrate::Itself => quote! { ::rustapi_rs::__private::core },
1008 FoundCrate::Name(name) => {
1009 let normalized = name.replace('-', "_");
1010 let ident = syn::Ident::new(&normalized, proc_macro2::Span::call_site());
1011 quote! { ::#ident::__private::core }
1012 }
1013 }
1014 } else if let Ok(found) = crate_name("rustapi-core").or_else(|_| crate_name("rustapi_core")) {
1015 match found {
1016 FoundCrate::Itself => quote! { crate },
1017 FoundCrate::Name(name) => {
1018 let normalized = name.replace('-', "_");
1019 let ident = syn::Ident::new(&normalized, proc_macro2::Span::call_site());
1020 quote! { ::#ident }
1021 }
1022 }
1023 } else {
1024 quote! { ::rustapi_core }
1025 }
1026}
1027
1028fn get_async_trait_path() -> proc_macro2::TokenStream {
1034 let rustapi_rs_found = crate_name("rustapi-rs").or_else(|_| crate_name("rustapi_rs"));
1035
1036 if let Ok(found) = rustapi_rs_found {
1037 match found {
1038 FoundCrate::Itself => {
1039 quote! { ::rustapi_rs::__private::async_trait }
1040 }
1041 FoundCrate::Name(name) => {
1042 let normalized = name.replace('-', "_");
1043 let ident = syn::Ident::new(&normalized, proc_macro2::Span::call_site());
1044 quote! { ::#ident::__private::async_trait }
1045 }
1046 }
1047 } else if let Ok(found) = crate_name("async-trait").or_else(|_| crate_name("async_trait")) {
1048 match found {
1049 FoundCrate::Itself => quote! { crate },
1050 FoundCrate::Name(name) => {
1051 let normalized = name.replace('-', "_");
1052 let ident = syn::Ident::new(&normalized, proc_macro2::Span::call_site());
1053 quote! { ::#ident }
1054 }
1055 }
1056 } else {
1057 quote! { ::async_trait }
1058 }
1059}
1060
1061fn generate_rule_validation(
1062 field_name: &str,
1063 _field_type: &Type,
1064 rule: &ValidationRuleInfo,
1065 validate_path: &proc_macro2::TokenStream,
1066) -> proc_macro2::TokenStream {
1067 let field_ident = syn::Ident::new(field_name, proc_macro2::Span::call_site());
1068 let field_name_str = field_name;
1069
1070 let group_check = if rule.groups.is_empty() {
1072 quote! { true }
1073 } else {
1074 let group_names = rule.groups.iter().map(|g| g.as_str());
1075 quote! {
1076 {
1077 let rule_groups = [#(#validate_path::v2::ValidationGroup::from(#group_names)),*];
1078 rule_groups.iter().any(|g| g.matches(&group))
1079 }
1080 }
1081 };
1082
1083 let validation_logic = match rule.rule_type.as_str() {
1084 "email" => {
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 = #validate_path::v2::EmailRule::new() #message;
1093 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1094 errors.add(#field_name_str, e);
1095 }
1096 }
1097 }
1098 }
1099 "length" => {
1100 let min = rule
1101 .params
1102 .iter()
1103 .find(|(k, _)| k == "min")
1104 .and_then(|(_, v)| v.parse::<usize>().ok());
1105 let max = rule
1106 .params
1107 .iter()
1108 .find(|(k, _)| k == "max")
1109 .and_then(|(_, v)| v.parse::<usize>().ok());
1110 let message = rule
1111 .message
1112 .as_ref()
1113 .map(|m| quote! { .with_message(#m) })
1114 .unwrap_or_default();
1115
1116 let rule_creation = match (min, max) {
1117 (Some(min), Some(max)) => {
1118 quote! { #validate_path::v2::LengthRule::new(#min, #max) }
1119 }
1120 (Some(min), None) => quote! { #validate_path::v2::LengthRule::min(#min) },
1121 (None, Some(max)) => quote! { #validate_path::v2::LengthRule::max(#max) },
1122 (None, None) => quote! { #validate_path::v2::LengthRule::new(0, usize::MAX) },
1123 };
1124
1125 quote! {
1126 {
1127 let rule = #rule_creation #message;
1128 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1129 errors.add(#field_name_str, e);
1130 }
1131 }
1132 }
1133 }
1134 "range" => {
1135 let min = rule
1136 .params
1137 .iter()
1138 .find(|(k, _)| k == "min")
1139 .map(|(_, v)| v.clone());
1140 let max = rule
1141 .params
1142 .iter()
1143 .find(|(k, _)| k == "max")
1144 .map(|(_, v)| v.clone());
1145 let message = rule
1146 .message
1147 .as_ref()
1148 .map(|m| quote! { .with_message(#m) })
1149 .unwrap_or_default();
1150
1151 let rule_creation = match (min, max) {
1153 (Some(min), Some(max)) => {
1154 let min_lit: proc_macro2::TokenStream = min.parse().unwrap();
1155 let max_lit: proc_macro2::TokenStream = max.parse().unwrap();
1156 quote! { #validate_path::v2::RangeRule::new(#min_lit, #max_lit) }
1157 }
1158 (Some(min), None) => {
1159 let min_lit: proc_macro2::TokenStream = min.parse().unwrap();
1160 quote! { #validate_path::v2::RangeRule::min(#min_lit) }
1161 }
1162 (None, Some(max)) => {
1163 let max_lit: proc_macro2::TokenStream = max.parse().unwrap();
1164 quote! { #validate_path::v2::RangeRule::max(#max_lit) }
1165 }
1166 (None, None) => {
1167 return quote! {};
1168 }
1169 };
1170
1171 quote! {
1172 {
1173 let rule = #rule_creation #message;
1174 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1175 errors.add(#field_name_str, e);
1176 }
1177 }
1178 }
1179 }
1180 "regex" => {
1181 let pattern = rule
1182 .params
1183 .iter()
1184 .find(|(k, _)| k == "regex" || k == "pattern")
1185 .map(|(_, v)| v.clone())
1186 .unwrap_or_default();
1187 let message = rule
1188 .message
1189 .as_ref()
1190 .map(|m| quote! { .with_message(#m) })
1191 .unwrap_or_default();
1192
1193 quote! {
1194 {
1195 let rule = #validate_path::v2::RegexRule::new(#pattern) #message;
1196 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1197 errors.add(#field_name_str, e);
1198 }
1199 }
1200 }
1201 }
1202 "url" => {
1203 let message = rule
1204 .message
1205 .as_ref()
1206 .map(|m| quote! { .with_message(#m) })
1207 .unwrap_or_default();
1208 quote! {
1209 {
1210 let rule = #validate_path::v2::UrlRule::new() #message;
1211 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1212 errors.add(#field_name_str, e);
1213 }
1214 }
1215 }
1216 }
1217 "required" => {
1218 let message = rule
1219 .message
1220 .as_ref()
1221 .map(|m| quote! { .with_message(#m) })
1222 .unwrap_or_default();
1223 quote! {
1224 {
1225 let rule = #validate_path::v2::RequiredRule::new() #message;
1226 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1227 errors.add(#field_name_str, e);
1228 }
1229 }
1230 }
1231 }
1232 "credit_card" => {
1233 let message = rule
1234 .message
1235 .as_ref()
1236 .map(|m| quote! { .with_message(#m) })
1237 .unwrap_or_default();
1238 quote! {
1239 {
1240 let rule = #validate_path::v2::CreditCardRule::new() #message;
1241 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1242 errors.add(#field_name_str, e);
1243 }
1244 }
1245 }
1246 }
1247 "ip" => {
1248 let v4 = rule.params.iter().any(|(k, _)| k == "v4");
1249 let v6 = rule.params.iter().any(|(k, _)| k == "v6");
1250
1251 let rule_creation = if v4 && !v6 {
1252 quote! { #validate_path::v2::IpRule::v4() }
1253 } else if !v4 && v6 {
1254 quote! { #validate_path::v2::IpRule::v6() }
1255 } else {
1256 quote! { #validate_path::v2::IpRule::new() }
1257 };
1258
1259 let message = rule
1260 .message
1261 .as_ref()
1262 .map(|m| quote! { .with_message(#m) })
1263 .unwrap_or_default();
1264
1265 quote! {
1266 {
1267 let rule = #rule_creation #message;
1268 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1269 errors.add(#field_name_str, e);
1270 }
1271 }
1272 }
1273 }
1274 "phone" => {
1275 let message = rule
1276 .message
1277 .as_ref()
1278 .map(|m| quote! { .with_message(#m) })
1279 .unwrap_or_default();
1280 quote! {
1281 {
1282 let rule = #validate_path::v2::PhoneRule::new() #message;
1283 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1284 errors.add(#field_name_str, e);
1285 }
1286 }
1287 }
1288 }
1289 "contains" => {
1290 let needle = rule
1291 .params
1292 .iter()
1293 .find(|(k, _)| k == "needle")
1294 .map(|(_, v)| v.clone())
1295 .unwrap_or_default();
1296
1297 let message = rule
1298 .message
1299 .as_ref()
1300 .map(|m| quote! { .with_message(#m) })
1301 .unwrap_or_default();
1302
1303 quote! {
1304 {
1305 let rule = #validate_path::v2::ContainsRule::new(#needle) #message;
1306 if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1307 errors.add(#field_name_str, e);
1308 }
1309 }
1310 }
1311 }
1312 _ => {
1313 quote! {}
1315 }
1316 };
1317
1318 quote! {
1319 if #group_check {
1320 #validation_logic
1321 }
1322 }
1323}
1324
1325fn generate_async_rule_validation(
1327 field_name: &str,
1328 rule: &ValidationRuleInfo,
1329 validate_path: &proc_macro2::TokenStream,
1330) -> proc_macro2::TokenStream {
1331 let field_ident = syn::Ident::new(field_name, proc_macro2::Span::call_site());
1332 let field_name_str = field_name;
1333
1334 let group_check = if rule.groups.is_empty() {
1336 quote! { true }
1337 } else {
1338 let group_names = rule.groups.iter().map(|g| g.as_str());
1339 quote! {
1340 {
1341 let rule_groups = [#(#validate_path::v2::ValidationGroup::from(#group_names)),*];
1342 rule_groups.iter().any(|g| g.matches(&group))
1343 }
1344 }
1345 };
1346
1347 let validation_logic = match rule.rule_type.as_str() {
1348 "async_unique" => {
1349 let table = rule
1350 .params
1351 .iter()
1352 .find(|(k, _)| k == "table")
1353 .map(|(_, v)| v.clone())
1354 .unwrap_or_default();
1355 let column = rule
1356 .params
1357 .iter()
1358 .find(|(k, _)| k == "column")
1359 .map(|(_, v)| v.clone())
1360 .unwrap_or_default();
1361 let message = rule
1362 .message
1363 .as_ref()
1364 .map(|m| quote! { .with_message(#m) })
1365 .unwrap_or_default();
1366
1367 quote! {
1368 {
1369 let rule = #validate_path::v2::AsyncUniqueRule::new(#table, #column) #message;
1370 if let Err(e) = #validate_path::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
1371 errors.add(#field_name_str, e);
1372 }
1373 }
1374 }
1375 }
1376 "async_exists" => {
1377 let table = rule
1378 .params
1379 .iter()
1380 .find(|(k, _)| k == "table")
1381 .map(|(_, v)| v.clone())
1382 .unwrap_or_default();
1383 let column = rule
1384 .params
1385 .iter()
1386 .find(|(k, _)| k == "column")
1387 .map(|(_, v)| v.clone())
1388 .unwrap_or_default();
1389 let message = rule
1390 .message
1391 .as_ref()
1392 .map(|m| quote! { .with_message(#m) })
1393 .unwrap_or_default();
1394
1395 quote! {
1396 {
1397 let rule = #validate_path::v2::AsyncExistsRule::new(#table, #column) #message;
1398 if let Err(e) = #validate_path::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
1399 errors.add(#field_name_str, e);
1400 }
1401 }
1402 }
1403 }
1404 "async_api" => {
1405 let endpoint = rule
1406 .params
1407 .iter()
1408 .find(|(k, _)| k == "endpoint")
1409 .map(|(_, v)| v.clone())
1410 .unwrap_or_default();
1411 let message = rule
1412 .message
1413 .as_ref()
1414 .map(|m| quote! { .with_message(#m) })
1415 .unwrap_or_default();
1416
1417 quote! {
1418 {
1419 let rule = #validate_path::v2::AsyncApiRule::new(#endpoint) #message;
1420 if let Err(e) = #validate_path::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
1421 errors.add(#field_name_str, e);
1422 }
1423 }
1424 }
1425 }
1426 "custom_async" => {
1427 let function_path = rule
1429 .params
1430 .iter()
1431 .find(|(k, _)| k == "custom_async" || k == "function")
1432 .map(|(_, v)| v.clone())
1433 .unwrap_or_default();
1434
1435 if function_path.is_empty() {
1436 quote! {}
1438 } else {
1439 let func: syn::Path = syn::parse_str(&function_path).unwrap();
1440 let message_handling = if let Some(msg) = &rule.message {
1441 quote! {
1442 let e = #validate_path::v2::RuleError::new("custom_async", #msg);
1443 errors.add(#field_name_str, e);
1444 }
1445 } else {
1446 quote! {
1447 errors.add(#field_name_str, e);
1448 }
1449 };
1450
1451 quote! {
1452 {
1453 if let Err(e) = #func(&self.#field_ident, ctx).await {
1455 #message_handling
1456 }
1457 }
1458 }
1459 }
1460 }
1461 _ => {
1462 quote! {}
1464 }
1465 };
1466
1467 quote! {
1468 if #group_check {
1469 #validation_logic
1470 }
1471 }
1472}
1473
1474fn is_async_rule(rule: &ValidationRuleInfo) -> bool {
1476 matches!(
1477 rule.rule_type.as_str(),
1478 "async_unique" | "async_exists" | "async_api" | "custom_async"
1479 )
1480}
1481
1482#[proc_macro_derive(Validate, attributes(validate))]
1505pub fn derive_validate(input: TokenStream) -> TokenStream {
1506 let input = parse_macro_input!(input as DeriveInput);
1507 let name = &input.ident;
1508 let generics = &input.generics;
1509 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
1510
1511 let fields = match &input.data {
1513 Data::Struct(data) => match &data.fields {
1514 Fields::Named(fields) => &fields.named,
1515 _ => {
1516 return syn::Error::new_spanned(
1517 &input,
1518 "Validate can only be derived for structs with named fields",
1519 )
1520 .to_compile_error()
1521 .into();
1522 }
1523 },
1524 _ => {
1525 return syn::Error::new_spanned(&input, "Validate can only be derived for structs")
1526 .to_compile_error()
1527 .into();
1528 }
1529 };
1530
1531 let validate_path = get_validate_path();
1533 let core_path = get_core_path();
1534 let async_trait_path = get_async_trait_path();
1535
1536 let mut sync_validations = Vec::new();
1538 let mut async_validations = Vec::new();
1539 let mut has_async_rules = false;
1540
1541 for field in fields {
1542 let field_name = field.ident.as_ref().unwrap().to_string();
1543 let field_type = &field.ty;
1544 let rules = parse_validate_attrs(&field.attrs);
1545
1546 for rule in &rules {
1547 if is_async_rule(rule) {
1548 has_async_rules = true;
1549 let validation = generate_async_rule_validation(&field_name, rule, &validate_path);
1550 async_validations.push(validation);
1551 } else {
1552 let validation =
1553 generate_rule_validation(&field_name, field_type, rule, &validate_path);
1554 sync_validations.push(validation);
1555 }
1556 }
1557 }
1558
1559 let validate_impl = quote! {
1561 impl #impl_generics #validate_path::v2::Validate for #name #ty_generics #where_clause {
1562 fn validate_with_group(&self, group: #validate_path::v2::ValidationGroup) -> Result<(), #validate_path::v2::ValidationErrors> {
1563 let mut errors = #validate_path::v2::ValidationErrors::new();
1564
1565 #(#sync_validations)*
1566
1567 errors.into_result()
1568 }
1569 }
1570 };
1571
1572 let async_validate_impl = if has_async_rules {
1574 quote! {
1575 #[#async_trait_path::async_trait]
1576 impl #impl_generics #validate_path::v2::AsyncValidate for #name #ty_generics #where_clause {
1577 async fn validate_async_with_group(&self, ctx: &#validate_path::v2::ValidationContext, group: #validate_path::v2::ValidationGroup) -> Result<(), #validate_path::v2::ValidationErrors> {
1578 let mut errors = #validate_path::v2::ValidationErrors::new();
1579
1580 #(#async_validations)*
1581
1582 errors.into_result()
1583 }
1584 }
1585 }
1586 } else {
1587 quote! {
1589 #[#async_trait_path::async_trait]
1590 impl #impl_generics #validate_path::v2::AsyncValidate for #name #ty_generics #where_clause {
1591 async fn validate_async_with_group(&self, _ctx: &#validate_path::v2::ValidationContext, _group: #validate_path::v2::ValidationGroup) -> Result<(), #validate_path::v2::ValidationErrors> {
1592 Ok(())
1593 }
1594 }
1595 }
1596 };
1597
1598 let validatable_impl = quote! {
1601 impl #impl_generics #core_path::validation::Validatable for #name #ty_generics #where_clause {
1602 fn do_validate(&self) -> Result<(), #core_path::ApiError> {
1603 match #validate_path::v2::Validate::validate(self) {
1604 Ok(_) => Ok(()),
1605 Err(e) => Err(#core_path::validation::convert_v2_errors(e)),
1606 }
1607 }
1608 }
1609 };
1610
1611 let expanded = quote! {
1612 #validate_impl
1613 #async_validate_impl
1614 #validatable_impl
1615 };
1616
1617 debug_output("Validate derive", &expanded);
1618
1619 TokenStream::from(expanded)
1620}
1621
1622#[proc_macro_derive(ApiError, attributes(error))]
1641pub fn derive_api_error(input: TokenStream) -> TokenStream {
1642 api_error::expand_derive_api_error(input)
1643}
1644
1645#[proc_macro_derive(TypedPath, attributes(typed_path))]
1662pub fn derive_typed_path(input: TokenStream) -> TokenStream {
1663 let input = parse_macro_input!(input as DeriveInput);
1664 let name = &input.ident;
1665 let generics = &input.generics;
1666 let rustapi_path = get_rustapi_path();
1667 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
1668
1669 let mut path_str = None;
1671 for attr in &input.attrs {
1672 if attr.path().is_ident("typed_path") {
1673 if let Ok(lit) = attr.parse_args::<LitStr>() {
1674 path_str = Some(lit.value());
1675 }
1676 }
1677 }
1678
1679 let path = match path_str {
1680 Some(p) => p,
1681 None => {
1682 return syn::Error::new_spanned(
1683 &input,
1684 "#[derive(TypedPath)] requires a #[typed_path(\"...\")] attribute",
1685 )
1686 .to_compile_error()
1687 .into();
1688 }
1689 };
1690
1691 if let Err(err) = validate_path_syntax(&path, proc_macro2::Span::call_site()) {
1693 return err.to_compile_error().into();
1694 }
1695
1696 let mut format_string = String::new();
1699 let mut format_args = Vec::new();
1700
1701 let mut chars = path.chars().peekable();
1702 while let Some(ch) = chars.next() {
1703 if ch == '{' {
1704 let mut param_name = String::new();
1705 while let Some(&c) = chars.peek() {
1706 if c == '}' {
1707 chars.next(); break;
1709 }
1710 param_name.push(chars.next().unwrap());
1711 }
1712
1713 if param_name.is_empty() {
1714 return syn::Error::new_spanned(
1715 &input,
1716 "Empty path parameter not allowed in typed_path",
1717 )
1718 .to_compile_error()
1719 .into();
1720 }
1721
1722 format_string.push_str("{}");
1723 let ident = syn::Ident::new(¶m_name, proc_macro2::Span::call_site());
1724 format_args.push(quote! { self.#ident });
1725 } else {
1726 format_string.push(ch);
1727 }
1728 }
1729
1730 let expanded = quote! {
1731 impl #impl_generics #rustapi_path::prelude::TypedPath for #name #ty_generics #where_clause {
1732 const PATH: &'static str = #path;
1733
1734 fn to_uri(&self) -> String {
1735 format!(#format_string, #(#format_args),*)
1736 }
1737 }
1738 };
1739
1740 debug_output("TypedPath derive", &expanded);
1741 TokenStream::from(expanded)
1742}