1use proc_macro::TokenStream;
2use proc_macro2::TokenStream as TokenStream2;
3use quote::quote;
4use syn::{
5 bracketed,
6 parse::{Parse, ParseStream},
7 parse_macro_input, parse_quote,
8 punctuated::Punctuated,
9 spanned::Spanned,
10 Attribute, Data, DeriveInput, Expr, Field, Fields, Ident, ImplItem, ImplItemFn, ItemImpl,
11 ItemStruct, LitStr, Meta, Token, Type,
12};
13
14#[proc_macro_attribute]
19pub fn controller(attr: TokenStream, item: TokenStream) -> TokenStream {
20 let base_path = parse_macro_input!(attr as LitStr);
21 let input = parse_macro_input!(item as ItemStruct);
22
23 let name = &input.ident;
24 let path = base_path.value();
25
26 let expanded = quote! {
27 #input
28
29 impl nestforge::ControllerBasePath for #name {
30 fn base_path() -> &'static str {
31 #path
32 }
33 }
34 };
35
36 TokenStream::from(expanded)
37}
38
39#[proc_macro_attribute]
45pub fn routes(_attr: TokenStream, item: TokenStream) -> TokenStream {
46 let mut input = parse_macro_input!(item as ItemImpl);
47
48 let self_ty = input.self_ty.clone();
49 let controller_meta = extract_controller_route_meta(&mut input);
50
51 let mut route_calls = Vec::new();
52 let mut route_docs = Vec::new();
53
54 for impl_item in &mut input.items {
55 let ImplItem::Fn(method) = impl_item else {
56 continue;
57 };
58 let (guards, interceptors, exception_filters) = extract_pipeline_meta(method);
59 let version = extract_version_meta(method);
60 let mut doc_meta = extract_route_doc_meta(method);
61 doc_meta.tags = merge_string_lists(controller_meta.tags.clone(), doc_meta.tags);
62 doc_meta.required_roles =
63 merge_string_lists(controller_meta.required_roles.clone(), doc_meta.required_roles);
64 doc_meta.requires_auth =
65 controller_meta.requires_auth || doc_meta.requires_auth || !doc_meta.required_roles.is_empty();
66 let guards = merge_type_lists(controller_meta.guards.clone(), guards);
67 let interceptors = merge_type_lists(controller_meta.interceptors.clone(), interceptors);
68 let exception_filters =
69 merge_type_lists(controller_meta.exception_filters.clone(), exception_filters);
70
71 if let Some((http_method, path)) = extract_route_meta(method) {
72 let method_name = &method.sig.ident;
73 let path_lit = LitStr::new(&path, method.sig.ident.span());
74 let guard_inits = guards.iter().map(|ty| {
75 quote! { std::sync::Arc::new(<#ty as std::default::Default>::default()) as std::sync::Arc<dyn nestforge::Guard> }
76 });
77 let auth_guard_init = if doc_meta.requires_auth && doc_meta.required_roles.is_empty() {
78 quote! {
79 std::sync::Arc::new(nestforge::RequireAuthenticationGuard::default())
80 as std::sync::Arc<dyn nestforge::Guard>
81 }
82 } else {
83 quote! {}
84 };
85 let role_guard_init = if doc_meta.required_roles.is_empty() {
86 quote! {}
87 } else {
88 let roles = doc_meta
89 .required_roles
90 .iter()
91 .map(|role| LitStr::new(role, method.sig.ident.span()));
92 quote! {
93 std::sync::Arc::new(nestforge::RoleRequirementsGuard::new([#(#roles),*]))
94 as std::sync::Arc<dyn nestforge::Guard>
95 }
96 };
97 let interceptor_inits = interceptors.iter().map(|ty| {
98 quote! { std::sync::Arc::new(<#ty as std::default::Default>::default()) as std::sync::Arc<dyn nestforge::Interceptor> }
99 });
100 let exception_filter_inits = exception_filters.iter().map(|ty| {
101 quote! { std::sync::Arc::new(<#ty as std::default::Default>::default()) as std::sync::Arc<dyn nestforge::ExceptionFilter> }
102 });
103 let guard_tokens = if doc_meta.requires_auth || !doc_meta.required_roles.is_empty() {
104 quote! { vec![#(#guard_inits,)* #auth_guard_init #role_guard_init] }
105 } else {
106 quote! { vec![#(#guard_inits),*] }
107 };
108 let version_tokens = if let Some(version) = &version {
109 let lit = LitStr::new(version, method.sig.ident.span());
110 quote! { Some(#lit) }
111 } else {
112 quote! { None }
113 };
114
115 let call = match http_method.as_str() {
116 "get" => quote! {
117 builder = builder.get_with_pipeline(
118 #path_lit,
119 Self::#method_name,
120 #guard_tokens,
121 vec![#(#interceptor_inits),*],
122 vec![#(#exception_filter_inits),*],
123 #version_tokens
124 );
125 },
126 "post" => quote! {
127 builder = builder.post_with_pipeline(
128 #path_lit,
129 Self::#method_name,
130 #guard_tokens,
131 vec![#(#interceptor_inits),*],
132 vec![#(#exception_filter_inits),*],
133 #version_tokens
134 );
135 },
136 "put" => quote! {
137 builder = builder.put_with_pipeline(
138 #path_lit,
139 Self::#method_name,
140 #guard_tokens,
141 vec![#(#interceptor_inits),*],
142 vec![#(#exception_filter_inits),*],
143 #version_tokens
144 );
145 },
146 "delete" => quote! {
147 builder = builder.delete_with_pipeline(
148 #path_lit,
149 Self::#method_name,
150 #guard_tokens,
151 vec![#(#interceptor_inits),*],
152 vec![#(#exception_filter_inits),*],
153 #version_tokens
154 );
155 },
156 _ => continue,
157 };
158
159 route_calls.push(call);
160
161 let method_lit = LitStr::new(&http_method.to_uppercase(), method.sig.ident.span());
162 let response_docs = if doc_meta.responses.is_empty() {
163 quote! {
164 vec![nestforge::RouteResponseDocumentation {
165 status: 200,
166 description: "OK".to_string(),
167 }]
168 }
169 } else {
170 let responses = doc_meta.responses.iter().map(|response| {
171 let description = LitStr::new(&response.description, method.sig.ident.span());
172 let status = response.status;
173 quote! {
174 nestforge::RouteResponseDocumentation {
175 status: #status,
176 description: #description.to_string(),
177 }
178 }
179 });
180 quote! { vec![#(#responses),*] }
181 };
182 let summary_tokens = if let Some(summary) = &doc_meta.summary {
183 let summary_lit = LitStr::new(summary, method.sig.ident.span());
184 quote! { doc = doc.with_summary(#summary_lit); }
185 } else {
186 quote! {}
187 };
188 let description_tokens = if let Some(description) = &doc_meta.description {
189 let description_lit = LitStr::new(description, method.sig.ident.span());
190 quote! { doc = doc.with_description(#description_lit); }
191 } else {
192 quote! {}
193 };
194 let tag_tokens = if doc_meta.tags.is_empty() {
195 quote! {}
196 } else {
197 let tags = doc_meta.tags.iter().map(|tag| LitStr::new(tag, method.sig.ident.span()));
198 quote! { doc = doc.with_tags([#(#tags),*]); }
199 };
200 let auth_tokens = if doc_meta.requires_auth {
201 quote! { doc = doc.requires_auth(); }
202 } else {
203 quote! {}
204 };
205 let role_tokens = if doc_meta.required_roles.is_empty() {
206 quote! {}
207 } else {
208 let roles = doc_meta
209 .required_roles
210 .iter()
211 .map(|role| LitStr::new(role, method.sig.ident.span()));
212 quote! { doc = doc.with_required_roles([#(#roles),*]); }
213 };
214
215 route_docs.push(quote! {
216 {
217 let mut doc = nestforge::RouteDocumentation::new(
218 #method_lit,
219 nestforge::RouteBuilder::<#self_ty>::full_path(#path_lit, #version_tokens),
220 )
221 .with_responses(#response_docs);
222 #summary_tokens
223 #description_tokens
224 #tag_tokens
225 #auth_tokens
226 #role_tokens
227 doc
228 }
229 });
230 }
231 }
232
233 let expanded = quote! {
234 #input
235
236 impl nestforge::ControllerDefinition for #self_ty {
237 fn router() -> axum::Router<nestforge::Container> {
238 nestforge::framework_log_event(
239 "controller_register",
240 &[("controller", std::any::type_name::<#self_ty>().to_string())],
241 );
242 let mut builder = nestforge::RouteBuilder::<#self_ty>::new();
243 #(#route_calls)*
244 builder.build()
245 }
246 }
247
248 impl nestforge::DocumentedController for #self_ty {
249 fn route_docs() -> Vec<nestforge::RouteDocumentation> {
250 vec![#(#route_docs),*]
251 }
252 }
253 };
254
255 TokenStream::from(expanded)
256}
257
258#[proc_macro_attribute]
268pub fn module(attr: TokenStream, item: TokenStream) -> TokenStream {
269 let args = parse_macro_input!(attr as ModuleArgs);
270 let input = parse_macro_input!(item as ItemStruct);
271
272 let name = &input.ident;
273
274 let controller_calls = args.controllers.iter().map(|ty| {
275 quote! { <#ty as nestforge::ControllerDefinition>::router() }
276 });
277 let controller_doc_calls = args.controllers.iter().map(|ty| {
278 quote! { docs.extend(<#ty as nestforge::DocumentedController>::route_docs()); }
279 });
280
281 let provider_regs = args.providers.iter().map(build_provider_registration);
282
283 let import_refs = args.imports.iter().map(|ty| {
284 quote! { nestforge::ModuleRef::of::<#ty>() }
285 });
286 let module_init_hooks = args.on_module_init.iter().map(|expr| {
287 quote! { #expr as nestforge::LifecycleHook }
288 });
289 let module_destroy_hooks = args.on_module_destroy.iter().map(|expr| {
290 quote! { #expr as nestforge::LifecycleHook }
291 });
292 let application_bootstrap_hooks = args.on_application_bootstrap.iter().map(|expr| {
293 quote! { #expr as nestforge::LifecycleHook }
294 });
295 let application_shutdown_hooks = args.on_application_shutdown.iter().map(|expr| {
296 quote! { #expr as nestforge::LifecycleHook }
297 });
298
299 let exported_types = args.exports.iter().map(|ty| {
300 quote! { std::any::type_name::<#ty>() }
301 });
302 let global_flag = args.global;
303
304 let expanded = quote! {
305 #input
306
307 impl nestforge::ModuleDefinition for #name {
308 fn register(container: &nestforge::Container) -> anyhow::Result<()> {
309 #(#provider_regs)*
310 Ok(())
311 }
312
313 fn imports() -> Vec<nestforge::ModuleRef> {
314 vec![
315 #(#import_refs),*
316 ]
317 }
318
319 fn exports() -> Vec<&'static str> {
320 vec![
321 #(#exported_types),*
322 ]
323 }
324
325 fn is_global() -> bool {
326 #global_flag
327 }
328
329 fn controllers() -> Vec<axum::Router<nestforge::Container>> {
330 vec![
331 #(#controller_calls),*
332 ]
333 }
334
335 fn route_docs() -> Vec<nestforge::RouteDocumentation> {
336 let mut docs = Vec::new();
337 #(#controller_doc_calls)*
338 docs
339 }
340
341 fn on_module_init() -> Vec<nestforge::LifecycleHook> {
342 vec![#(#module_init_hooks),*]
343 }
344
345 fn on_module_destroy() -> Vec<nestforge::LifecycleHook> {
346 vec![#(#module_destroy_hooks),*]
347 }
348
349 fn on_application_bootstrap() -> Vec<nestforge::LifecycleHook> {
350 vec![#(#application_bootstrap_hooks),*]
351 }
352
353 fn on_application_shutdown() -> Vec<nestforge::LifecycleHook> {
354 vec![#(#application_shutdown_hooks),*]
355 }
356 }
357 };
358
359 TokenStream::from(expanded)
360}
361
362#[proc_macro_attribute]
366pub fn get(_attr: TokenStream, item: TokenStream) -> TokenStream {
367 item
368}
369
370#[proc_macro_attribute]
371pub fn post(_attr: TokenStream, item: TokenStream) -> TokenStream {
372 item
373}
374
375#[proc_macro_attribute]
376pub fn put(_attr: TokenStream, item: TokenStream) -> TokenStream {
377 item
378}
379
380#[proc_macro_attribute]
381pub fn delete(_attr: TokenStream, item: TokenStream) -> TokenStream {
382 item
383}
384
385#[proc_macro_attribute]
386pub fn version(_attr: TokenStream, item: TokenStream) -> TokenStream {
387 item
388}
389
390#[proc_macro_attribute]
391pub fn use_guard(_attr: TokenStream, item: TokenStream) -> TokenStream {
392 item
393}
394
395#[proc_macro_attribute]
396pub fn use_interceptor(_attr: TokenStream, item: TokenStream) -> TokenStream {
397 item
398}
399
400#[proc_macro_attribute]
401pub fn use_exception_filter(_attr: TokenStream, item: TokenStream) -> TokenStream {
402 item
403}
404
405#[proc_macro_attribute]
406pub fn summary(_attr: TokenStream, item: TokenStream) -> TokenStream {
407 item
408}
409
410#[proc_macro_attribute]
411pub fn description(_attr: TokenStream, item: TokenStream) -> TokenStream {
412 item
413}
414
415#[proc_macro_attribute]
416pub fn tag(_attr: TokenStream, item: TokenStream) -> TokenStream {
417 item
418}
419
420#[proc_macro_attribute]
421pub fn response(_attr: TokenStream, item: TokenStream) -> TokenStream {
422 item
423}
424
425#[proc_macro_attribute]
426pub fn authenticated(_attr: TokenStream, item: TokenStream) -> TokenStream {
427 item
428}
429
430#[proc_macro_attribute]
431pub fn roles(_attr: TokenStream, item: TokenStream) -> TokenStream {
432 item
433}
434
435#[proc_macro_attribute]
436pub fn dto(_attr: TokenStream, item: TokenStream) -> TokenStream {
437 let mut input = parse_macro_input!(item as ItemStruct);
438
439 input.attrs.push(parse_quote!(
440 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, nestforge::Validate)]
441 ));
442
443 TokenStream::from(quote! { #input })
444}
445
446#[proc_macro_attribute]
447pub fn identifiable(_attr: TokenStream, item: TokenStream) -> TokenStream {
448 let input = parse_macro_input!(item as ItemStruct);
449 let name = &input.ident;
450
451 let Some((id_field_name, id_field_ty)) = find_id_field(&input.fields) else {
452 return syn::Error::new(
453 input.ident.span(),
454 "identifiable requires an `id: u64` field or a field marked with #[id]",
455 )
456 .to_compile_error()
457 .into();
458 };
459 let ty_ok = matches!(id_field_ty, Type::Path(ref tp) if tp.path.is_ident("u64"));
460 if !ty_ok {
461 return syn::Error::new(
462 id_field_ty.span(),
463 "identifiable id field must be of type `u64`",
464 )
465 .to_compile_error()
466 .into();
467 }
468
469 TokenStream::from(quote! {
470 #input
471
472 impl nestforge::Identifiable for #name {
473 fn id(&self) -> u64 {
474 self.#id_field_name
475 }
476
477 fn set_id(&mut self, id: u64) {
478 self.#id_field_name = id;
479 }
480 }
481 })
482}
483
484#[proc_macro_attribute]
485pub fn response_dto(_attr: TokenStream, item: TokenStream) -> TokenStream {
486 let mut input = parse_macro_input!(item as ItemStruct);
487
488 input
489 .attrs
490 .push(parse_quote!(#[derive(Debug, Clone, serde::Serialize)]));
491
492 TokenStream::from(quote! { #input })
493}
494
495#[proc_macro_attribute]
496pub fn entity_dto(_attr: TokenStream, item: TokenStream) -> TokenStream {
497 let mut input = parse_macro_input!(item as ItemStruct);
498
499 let Some((id_field_name, id_field_ty)) = find_id_field(&input.fields) else {
500 return syn::Error::new(
501 input.ident.span(),
502 "entity_dto requires an `id: u64` field or a field marked with #[id]",
503 )
504 .to_compile_error()
505 .into();
506 };
507 let ty_ok = matches!(id_field_ty, Type::Path(ref tp) if tp.path.is_ident("u64"));
508 if !ty_ok {
509 return syn::Error::new(
510 id_field_ty.span(),
511 "entity_dto id field must be of type `u64`",
512 )
513 .to_compile_error()
514 .into();
515 }
516
517 input.attrs.push(parse_quote!(
518 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, nestforge::Validate)]
519 ));
520
521 let name = &input.ident;
522
523 TokenStream::from(quote! {
524 #input
525
526 impl nestforge::Identifiable for #name {
527 fn id(&self) -> u64 {
528 self.#id_field_name
529 }
530
531 fn set_id(&mut self, id: u64) {
532 self.#id_field_name = id;
533 }
534 }
535 })
536}
537
538#[proc_macro_attribute]
539pub fn entity(attr: TokenStream, item: TokenStream) -> TokenStream {
540 let args = parse_macro_input!(attr as EntityArgs);
541 let mut input = parse_macro_input!(item as ItemStruct);
542
543 let name = &input.ident;
544 let Some((id_field_name, id_field_ty)) = extract_id_field(&mut input.fields) else {
545 return syn::Error::new(
546 input.ident.span(),
547 "#[entity(...)] requires exactly one field annotated with #[id]",
548 )
549 .to_compile_error()
550 .into();
551 };
552
553 let table_name = args.table.value();
554 let id_column = id_field_name.to_string();
555
556 let expanded = quote! {
557 #input
558
559 impl nestforge::EntityMeta for #name {
560 type Id = #id_field_ty;
561
562 fn table_name() -> &'static str {
563 #table_name
564 }
565
566 fn id_column() -> &'static str {
567 #id_column
568 }
569
570 fn id_value(&self) -> &Self::Id {
571 &self.#id_field_name
572 }
573 }
574 };
575
576 TokenStream::from(expanded)
577}
578
579#[proc_macro_attribute]
580pub fn id(_attr: TokenStream, item: TokenStream) -> TokenStream {
581 item
582}
583
584#[proc_macro_derive(Identifiable, attributes(id))]
585pub fn derive_identifiable(item: TokenStream) -> TokenStream {
586 let input = parse_macro_input!(item as DeriveInput);
587 let name = &input.ident;
588
589 let Data::Struct(data) = &input.data else {
590 return syn::Error::new(
591 input.ident.span(),
592 "Identifiable can only be derived on structs",
593 )
594 .to_compile_error()
595 .into();
596 };
597
598 let Some((id_field_name, id_field_ty)) = find_id_field(&data.fields) else {
599 return syn::Error::new(
600 input.ident.span(),
601 "Identifiable derive requires an `id: u64` field or a field marked with #[id]",
602 )
603 .to_compile_error()
604 .into();
605 };
606
607 let ty_ok = matches!(id_field_ty, Type::Path(ref tp) if tp.path.is_ident("u64"));
608 if !ty_ok {
609 return syn::Error::new(
610 id_field_ty.span(),
611 "Identifiable id field must be of type `u64`",
612 )
613 .to_compile_error()
614 .into();
615 }
616
617 let expanded = quote! {
618 impl nestforge::Identifiable for #name {
619 fn id(&self) -> u64 {
620 self.#id_field_name
621 }
622
623 fn set_id(&mut self, id: u64) {
624 self.#id_field_name = id;
625 }
626 }
627 };
628
629 TokenStream::from(expanded)
630}
631
632#[proc_macro_derive(Validate, attributes(validate))]
633pub fn derive_validate(item: TokenStream) -> TokenStream {
634 let input = parse_macro_input!(item as DeriveInput);
635 let name = &input.ident;
636
637 let Data::Struct(data) = &input.data else {
638 return syn::Error::new(
639 input.ident.span(),
640 "Validate can only be derived on structs",
641 )
642 .to_compile_error()
643 .into();
644 };
645
646 let Fields::Named(fields) = &data.fields else {
647 return syn::Error::new(input.ident.span(), "Validate derive requires named fields")
648 .to_compile_error()
649 .into();
650 };
651
652 let mut checks = Vec::new();
653 for field in &fields.named {
654 let Some(field_ident) = &field.ident else {
655 continue;
656 };
657 let field_name_lit = field_ident.to_string();
658 let rules = parse_validate_rules(&field.attrs);
659 if !rules.has_rules() {
660 continue;
661 }
662
663 let is_string = is_type_named(&field.ty, "String");
664 let is_option_string = is_option_of(&field.ty, "String");
665 let is_option_any = is_option_any(&field.ty);
666 let is_numeric = is_numeric_type(&field.ty);
667 let is_option_numeric = is_option_numeric_type(&field.ty);
668
669 if rules.required {
670 if is_string {
671 checks.push(quote! {
672 if self.#field_ident.trim().is_empty() {
673 errors.push(nestforge::ValidationIssue {
674 field: #field_name_lit,
675 message: format!("{} is required", #field_name_lit),
676 });
677 }
678 });
679 } else if is_option_string {
680 checks.push(quote! {
681 match &self.#field_ident {
682 Some(v) if !v.trim().is_empty() => {}
683 _ => {
684 errors.push(nestforge::ValidationIssue {
685 field: #field_name_lit,
686 message: format!("{} is required", #field_name_lit),
687 });
688 }
689 }
690 });
691 } else if is_option_any {
692 checks.push(quote! {
693 if self.#field_ident.is_none() {
694 errors.push(nestforge::ValidationIssue {
695 field: #field_name_lit,
696 message: format!("{} is required", #field_name_lit),
697 });
698 }
699 });
700 }
701 }
702
703 if rules.email {
704 if is_string {
705 checks.push(quote! {
706 if !self.#field_ident.trim().is_empty() && !self.#field_ident.contains('@') {
707 errors.push(nestforge::ValidationIssue {
708 field: #field_name_lit,
709 message: format!("{} must be a valid email", #field_name_lit),
710 });
711 }
712 });
713 } else if is_option_string {
714 checks.push(quote! {
715 if let Some(v) = &self.#field_ident {
716 if !v.trim().is_empty() && !v.contains('@') {
717 errors.push(nestforge::ValidationIssue {
718 field: #field_name_lit,
719 message: format!("{} must be a valid email", #field_name_lit),
720 });
721 }
722 }
723 });
724 }
725 }
726
727 if let Some(min_length) = rules.min_length {
728 if is_string {
729 checks.push(quote! {
730 if self.#field_ident.len() < #min_length {
731 errors.push(nestforge::ValidationIssue {
732 field: #field_name_lit,
733 message: format!("{} must be at least {} characters", #field_name_lit, #min_length),
734 });
735 }
736 });
737 } else if is_option_string {
738 checks.push(quote! {
739 if let Some(v) = &self.#field_ident {
740 if v.len() < #min_length {
741 errors.push(nestforge::ValidationIssue {
742 field: #field_name_lit,
743 message: format!("{} must be at least {} characters", #field_name_lit, #min_length),
744 });
745 }
746 }
747 });
748 }
749 }
750
751 if let Some(max_length) = rules.max_length {
752 if is_string {
753 checks.push(quote! {
754 if self.#field_ident.len() > #max_length {
755 errors.push(nestforge::ValidationIssue {
756 field: #field_name_lit,
757 message: format!("{} must be at most {} characters", #field_name_lit, #max_length),
758 });
759 }
760 });
761 } else if is_option_string {
762 checks.push(quote! {
763 if let Some(v) = &self.#field_ident {
764 if v.len() > #max_length {
765 errors.push(nestforge::ValidationIssue {
766 field: #field_name_lit,
767 message: format!("{} must be at most {} characters", #field_name_lit, #max_length),
768 });
769 }
770 }
771 });
772 }
773 }
774
775 if let Some(min) = &rules.min {
776 if is_numeric {
777 checks.push(quote! {
778 if self.#field_ident < #min {
779 errors.push(nestforge::ValidationIssue {
780 field: #field_name_lit,
781 message: format!("{} must be at least {}", #field_name_lit, #min),
782 });
783 }
784 });
785 } else if is_option_numeric {
786 checks.push(quote! {
787 if let Some(v) = self.#field_ident {
788 if v < #min {
789 errors.push(nestforge::ValidationIssue {
790 field: #field_name_lit,
791 message: format!("{} must be at least {}", #field_name_lit, #min),
792 });
793 }
794 }
795 });
796 }
797 }
798
799 if let Some(max) = &rules.max {
800 if is_numeric {
801 checks.push(quote! {
802 if self.#field_ident > #max {
803 errors.push(nestforge::ValidationIssue {
804 field: #field_name_lit,
805 message: format!("{} must be at most {}", #field_name_lit, #max),
806 });
807 }
808 });
809 } else if is_option_numeric {
810 checks.push(quote! {
811 if let Some(v) = self.#field_ident {
812 if v > #max {
813 errors.push(nestforge::ValidationIssue {
814 field: #field_name_lit,
815 message: format!("{} must be at most {}", #field_name_lit, #max),
816 });
817 }
818 }
819 });
820 }
821 }
822 }
823
824 let expanded = quote! {
825 impl nestforge::Validate for #name {
826 fn validate(&self) -> Result<(), nestforge::ValidationErrors> {
827 let mut errors = Vec::new();
828 #(#checks)*
829 if errors.is_empty() {
830 Ok(())
831 } else {
832 Err(nestforge::ValidationErrors::new(errors))
833 }
834 }
835 }
836 };
837
838 TokenStream::from(expanded)
839}
840
841fn extract_route_meta(method: &mut ImplItemFn) -> Option<(String, String)> {
844 let mut found: Option<(String, String)> = None;
845 let mut kept_attrs: Vec<Attribute> = Vec::new();
846
847 for attr in method.attrs.drain(..) {
848 let Some((verb, path)) = parse_route_attr(&attr) else {
849 kept_attrs.push(attr);
850 continue;
851 };
852
853 if found.is_none() {
854 found = Some((verb, path));
855 }
856 }
857
858 method.attrs = kept_attrs;
859 found
860}
861
862fn extract_pipeline_meta(method: &mut ImplItemFn) -> (Vec<Type>, Vec<Type>, Vec<Type>) {
863 let mut guards = Vec::new();
864 let mut interceptors = Vec::new();
865 let mut exception_filters = Vec::new();
866 let mut kept_attrs: Vec<Attribute> = Vec::new();
867
868 for attr in method.attrs.drain(..) {
869 let ident = attr
870 .path()
871 .segments
872 .last()
873 .map(|seg| seg.ident.to_string())
874 .unwrap_or_default();
875
876 if ident == "use_guard" {
877 if let Ok(ty) = attr.parse_args::<Type>() {
878 guards.push(ty);
879 }
880 continue;
881 }
882
883 if ident == "use_interceptor" {
884 if let Ok(ty) = attr.parse_args::<Type>() {
885 interceptors.push(ty);
886 }
887 continue;
888 }
889
890 if ident == "use_exception_filter" {
891 if let Ok(ty) = attr.parse_args::<Type>() {
892 exception_filters.push(ty);
893 }
894 continue;
895 }
896
897 kept_attrs.push(attr);
898 }
899
900 method.attrs = kept_attrs;
901 (guards, interceptors, exception_filters)
902}
903
904#[derive(Default)]
905struct ControllerRouteMeta {
906 guards: Vec<Type>,
907 interceptors: Vec<Type>,
908 exception_filters: Vec<Type>,
909 tags: Vec<String>,
910 requires_auth: bool,
911 required_roles: Vec<String>,
912}
913
914fn extract_controller_route_meta(input: &mut ItemImpl) -> ControllerRouteMeta {
915 let mut meta = ControllerRouteMeta::default();
916 let mut kept_attrs: Vec<Attribute> = Vec::new();
917
918 for attr in input.attrs.drain(..) {
919 let ident = attr
920 .path()
921 .segments
922 .last()
923 .map(|seg| seg.ident.to_string())
924 .unwrap_or_default();
925
926 match ident.as_str() {
927 "use_guard" => {
928 if let Ok(ty) = attr.parse_args::<Type>() {
929 meta.guards.push(ty);
930 }
931 }
932 "use_interceptor" => {
933 if let Ok(ty) = attr.parse_args::<Type>() {
934 meta.interceptors.push(ty);
935 }
936 }
937 "use_exception_filter" => {
938 if let Ok(ty) = attr.parse_args::<Type>() {
939 meta.exception_filters.push(ty);
940 }
941 }
942 "tag" => {
943 if let Ok(lit) = attr.parse_args::<LitStr>() {
944 meta.tags.push(lit.value());
945 }
946 }
947 "authenticated" => {
948 meta.requires_auth = true;
949 }
950 "roles" => {
951 if let Ok(values) =
952 attr.parse_args_with(Punctuated::<LitStr, Token![,]>::parse_terminated)
953 {
954 meta.required_roles
955 .extend(values.into_iter().map(|value| value.value()));
956 meta.requires_auth = true;
957 }
958 }
959 _ => kept_attrs.push(attr),
960 }
961 }
962
963 input.attrs = kept_attrs;
964 meta
965}
966
967fn extract_version_meta(method: &mut ImplItemFn) -> Option<String> {
968 let mut version: Option<String> = None;
969 let mut kept_attrs: Vec<Attribute> = Vec::new();
970
971 for attr in method.attrs.drain(..) {
972 let ident = attr
973 .path()
974 .segments
975 .last()
976 .map(|seg| seg.ident.to_string())
977 .unwrap_or_default();
978
979 if ident == "version" {
980 if let Ok(lit) = attr.parse_args::<LitStr>() {
981 version = Some(lit.value());
982 }
983 continue;
984 }
985
986 kept_attrs.push(attr);
987 }
988
989 method.attrs = kept_attrs;
990 version
991}
992
993#[derive(Default)]
994struct RouteDocMeta {
995 summary: Option<String>,
996 description: Option<String>,
997 tags: Vec<String>,
998 responses: Vec<RouteResponseMeta>,
999 requires_auth: bool,
1000 required_roles: Vec<String>,
1001}
1002
1003struct RouteResponseMeta {
1004 status: u16,
1005 description: String,
1006}
1007
1008fn extract_route_doc_meta(method: &mut ImplItemFn) -> RouteDocMeta {
1009 let mut meta = RouteDocMeta::default();
1010 let mut kept_attrs: Vec<Attribute> = Vec::new();
1011
1012 for attr in method.attrs.drain(..) {
1013 let ident = attr
1014 .path()
1015 .segments
1016 .last()
1017 .map(|seg| seg.ident.to_string())
1018 .unwrap_or_default();
1019
1020 match ident.as_str() {
1021 "summary" => {
1022 if let Ok(lit) = attr.parse_args::<LitStr>() {
1023 meta.summary = Some(lit.value());
1024 }
1025 }
1026 "description" => {
1027 if let Ok(lit) = attr.parse_args::<LitStr>() {
1028 meta.description = Some(lit.value());
1029 }
1030 }
1031 "tag" => {
1032 if let Ok(lit) = attr.parse_args::<LitStr>() {
1033 meta.tags.push(lit.value());
1034 }
1035 }
1036 "response" => {
1037 if let Ok(response) = attr.parse_args::<RouteResponseArgs>() {
1038 meta.responses.push(RouteResponseMeta {
1039 status: response.status,
1040 description: response.description.value(),
1041 });
1042 }
1043 }
1044 "authenticated" => {
1045 meta.requires_auth = true;
1046 }
1047 "roles" => {
1048 if let Ok(values) = attr.parse_args_with(Punctuated::<LitStr, Token![,]>::parse_terminated) {
1049 meta.required_roles.extend(values.into_iter().map(|value| value.value()));
1050 meta.requires_auth = true;
1051 }
1052 }
1053 _ => kept_attrs.push(attr),
1054 }
1055 }
1056
1057 method.attrs = kept_attrs;
1058 meta
1059}
1060
1061fn merge_string_lists(primary: Vec<String>, secondary: Vec<String>) -> Vec<String> {
1062 let mut merged = primary;
1063 for value in secondary {
1064 if !merged.contains(&value) {
1065 merged.push(value);
1066 }
1067 }
1068 merged
1069}
1070
1071fn merge_type_lists(primary: Vec<Type>, secondary: Vec<Type>) -> Vec<Type> {
1072 let mut merged = primary;
1073 for ty in secondary {
1074 if !merged
1075 .iter()
1076 .any(|existing| quote!(#existing).to_string() == quote!(#ty).to_string())
1077 {
1078 merged.push(ty);
1079 }
1080 }
1081 merged
1082}
1083
1084fn parse_route_attr(attr: &Attribute) -> Option<(String, String)> {
1085 let ident = attr.path().segments.last()?.ident.to_string();
1091
1092 if ident != "get" && ident != "post" && ident != "put" && ident != "delete" {
1093 return None;
1094 }
1095
1096 let path = match &attr.meta {
1097 Meta::List(_) => attr.parse_args::<LitStr>().ok()?.value(),
1098 _ => return None,
1099 };
1100
1101 Some((ident, path))
1102}
1103
1104struct ModuleArgs {
1107 imports: Vec<Type>,
1108 controllers: Vec<Type>,
1109 providers: Vec<Expr>,
1110 exports: Vec<Type>,
1111 on_module_init: Vec<Expr>,
1112 on_module_destroy: Vec<Expr>,
1113 on_application_bootstrap: Vec<Expr>,
1114 on_application_shutdown: Vec<Expr>,
1115 global: bool,
1116}
1117
1118struct RouteResponseArgs {
1119 status: u16,
1120 description: LitStr,
1121}
1122
1123struct EntityArgs {
1124 table: LitStr,
1125}
1126
1127impl Parse for EntityArgs {
1128 fn parse(input: ParseStream) -> syn::Result<Self> {
1129 let key: Ident = input.parse()?;
1130 if key != "table" {
1131 return Err(syn::Error::new(
1132 key.span(),
1133 "Unsupported entity key. Use `table = \"...\"`.",
1134 ));
1135 }
1136 input.parse::<Token![=]>()?;
1137 let table = input.parse::<LitStr>()?;
1138 Ok(Self { table })
1139 }
1140}
1141
1142impl Parse for RouteResponseArgs {
1143 fn parse(input: ParseStream) -> syn::Result<Self> {
1144 let mut status = None;
1145 let mut description = None;
1146
1147 while !input.is_empty() {
1148 let key: Ident = input.parse()?;
1149 input.parse::<Token![=]>()?;
1150
1151 if key == "status" {
1152 let value = input.parse::<syn::LitInt>()?;
1153 status = Some(value.base10_parse()?);
1154 } else if key == "description" {
1155 description = Some(input.parse::<LitStr>()?);
1156 } else {
1157 return Err(syn::Error::new(
1158 key.span(),
1159 "Unsupported response key. Use `status = ...` and `description = \"...\"`.",
1160 ));
1161 }
1162
1163 if input.peek(Token![,]) {
1164 input.parse::<Token![,]>()?;
1165 }
1166 }
1167
1168 Ok(Self {
1169 status: status.ok_or_else(|| {
1170 syn::Error::new(input.span(), "response metadata requires `status = ...`")
1171 })?,
1172 description: description.ok_or_else(|| {
1173 syn::Error::new(
1174 input.span(),
1175 "response metadata requires `description = \"...\"`",
1176 )
1177 })?,
1178 })
1179 }
1180}
1181
1182impl Parse for ModuleArgs {
1183 fn parse(input: ParseStream) -> syn::Result<Self> {
1184 let mut imports: Vec<Type> = Vec::new();
1185 let mut controllers: Vec<Type> = Vec::new();
1186 let mut providers: Vec<Expr> = Vec::new();
1187 let mut exports: Vec<Type> = Vec::new();
1188 let mut on_module_init: Vec<Expr> = Vec::new();
1189 let mut on_module_destroy: Vec<Expr> = Vec::new();
1190 let mut on_application_bootstrap: Vec<Expr> = Vec::new();
1191 let mut on_application_shutdown: Vec<Expr> = Vec::new();
1192 let mut global = false;
1193
1194 while !input.is_empty() {
1195 let key: Ident = input.parse()?;
1196 input.parse::<Token![=]>()?;
1197
1198 if key == "imports" {
1199 imports = parse_bracket_list::<Type>(input)?;
1200 } else if key == "controllers" {
1201 controllers = parse_bracket_list::<Type>(input)?;
1202 } else if key == "providers" {
1203 providers = parse_bracket_list::<Expr>(input)?;
1204 } else if key == "exports" {
1205 exports = parse_bracket_list::<Type>(input)?;
1206 } else if key == "on_module_init" {
1207 on_module_init = parse_bracket_list::<Expr>(input)?;
1208 } else if key == "on_module_destroy" {
1209 on_module_destroy = parse_bracket_list::<Expr>(input)?;
1210 } else if key == "on_application_bootstrap" {
1211 on_application_bootstrap = parse_bracket_list::<Expr>(input)?;
1212 } else if key == "on_application_shutdown" {
1213 on_application_shutdown = parse_bracket_list::<Expr>(input)?;
1214 } else if key == "global" {
1215 let lit: syn::LitBool = input.parse()?;
1216 global = lit.value;
1217 } else {
1218 return Err(syn::Error::new(
1219 key.span(),
1220 "Unsupported module key. Use `imports`, `controllers`, `providers`, `exports`, lifecycle hook lists, or `global`.",
1221 ));
1222 }
1223
1224 if input.peek(Token![,]) {
1225 input.parse::<Token![,]>()?;
1226 }
1227 }
1228
1229 Ok(Self {
1230 imports,
1231 controllers,
1232 providers,
1233 exports,
1234 on_module_init,
1235 on_module_destroy,
1236 on_application_bootstrap,
1237 on_application_shutdown,
1238 global,
1239 })
1240 }
1241}
1242
1243fn parse_bracket_list<T>(input: ParseStream) -> syn::Result<Vec<T>>
1244where
1245 T: Parse,
1246{
1247 let content;
1248 bracketed!(content in input);
1249
1250 let items: Punctuated<T, Token![,]> = content.parse_terminated(T::parse, Token![,])?;
1251 Ok(items.into_iter().collect())
1252}
1253
1254fn build_provider_registration(expr: &Expr) -> TokenStream2 {
1255 if is_provider_builder_expr(expr) {
1256 quote! { nestforge::register_provider(container, #expr)?; }
1257 } else {
1258 quote! { nestforge::register_provider(container, nestforge::Provider::value(#expr))?; }
1259 }
1260}
1261
1262fn is_provider_builder_expr(expr: &Expr) -> bool {
1263 let Expr::Call(call) = expr else {
1264 return false;
1265 };
1266 let Expr::Path(path_expr) = call.func.as_ref() else {
1267 return false;
1268 };
1269
1270 let mut segments = path_expr.path.segments.iter().rev();
1271 let Some(method) = segments.next() else {
1272 return false;
1273 };
1274
1275 if method.ident != "value" && method.ident != "factory" {
1276 return false;
1277 }
1278
1279 let Some(provider) = segments.next() else {
1280 return false;
1281 };
1282
1283 provider.ident == "Provider"
1284}
1285
1286fn extract_id_field(fields: &mut Fields) -> Option<(Ident, Type)> {
1287 let Fields::Named(named_fields) = fields else {
1288 return None;
1289 };
1290
1291 let mut found: Option<(Ident, Type)> = None;
1292
1293 for field in &mut named_fields.named {
1294 let has_id_attr = remove_id_attr(field);
1295 if !has_id_attr {
1296 continue;
1297 }
1298
1299 let field_name = field.ident.clone()?;
1300 let field_ty = field.ty.clone();
1301
1302 if found.is_some() {
1303 return None;
1304 }
1305
1306 found = Some((field_name, field_ty));
1307 }
1308
1309 found
1310}
1311
1312fn remove_id_attr(field: &mut Field) -> bool {
1313 let mut kept = Vec::new();
1314 let mut has_id = false;
1315
1316 for attr in field.attrs.drain(..) {
1317 let is_id = attr
1318 .path()
1319 .segments
1320 .last()
1321 .map(|seg| seg.ident == "id")
1322 .unwrap_or(false);
1323 if is_id {
1324 has_id = true;
1325 } else {
1326 kept.push(attr);
1327 }
1328 }
1329
1330 field.attrs = kept;
1331 has_id
1332}
1333
1334fn find_id_field(fields: &Fields) -> Option<(Ident, Type)> {
1335 let Fields::Named(named_fields) = fields else {
1336 return None;
1337 };
1338
1339 let mut by_attr: Option<(Ident, Type)> = None;
1340 let mut by_name: Option<(Ident, Type)> = None;
1341
1342 for field in &named_fields.named {
1343 let field_ident = field.ident.clone()?;
1344 if field_ident == "id" {
1345 by_name = Some((field_ident.clone(), field.ty.clone()));
1346 }
1347 let has_id_attr = field.attrs.iter().any(|attr| {
1348 attr.path()
1349 .segments
1350 .last()
1351 .map(|s| s.ident == "id")
1352 .unwrap_or(false)
1353 });
1354 if has_id_attr {
1355 by_attr = Some((field_ident, field.ty.clone()));
1356 }
1357 }
1358
1359 by_attr.or(by_name)
1360}
1361
1362#[derive(Default)]
1363struct ValidateRules {
1364 required: bool,
1365 email: bool,
1366 min_length: Option<usize>,
1367 max_length: Option<usize>,
1368 min: Option<syn::Lit>,
1369 max: Option<syn::Lit>,
1370}
1371
1372impl ValidateRules {
1373 fn has_rules(&self) -> bool {
1374 self.required
1375 || self.email
1376 || self.min_length.is_some()
1377 || self.max_length.is_some()
1378 || self.min.is_some()
1379 || self.max.is_some()
1380 }
1381}
1382
1383fn parse_validate_rules(attrs: &[Attribute]) -> ValidateRules {
1384 let mut rules = ValidateRules::default();
1385
1386 for attr in attrs {
1387 let is_validate = attr
1388 .path()
1389 .segments
1390 .last()
1391 .map(|seg| seg.ident == "validate")
1392 .unwrap_or(false);
1393 if !is_validate {
1394 continue;
1395 }
1396
1397 let _ = attr.parse_nested_meta(|meta| {
1398 if meta.path.is_ident("required") {
1399 rules.required = true;
1400 } else if meta.path.is_ident("email") {
1401 rules.email = true;
1402 } else if meta.path.is_ident("min_length") {
1403 let value = meta.value()?.parse::<syn::LitInt>()?;
1404 rules.min_length = Some(value.base10_parse()?);
1405 } else if meta.path.is_ident("max_length") {
1406 let value = meta.value()?.parse::<syn::LitInt>()?;
1407 rules.max_length = Some(value.base10_parse()?);
1408 } else if meta.path.is_ident("min") {
1409 rules.min = Some(meta.value()?.parse::<syn::Lit>()?);
1410 } else if meta.path.is_ident("max") {
1411 rules.max = Some(meta.value()?.parse::<syn::Lit>()?);
1412 }
1413 Ok(())
1414 });
1415 }
1416
1417 rules
1418}
1419
1420fn is_type_named(ty: &Type, name: &str) -> bool {
1421 matches!(ty, Type::Path(tp) if tp.path.is_ident(name))
1422}
1423
1424fn is_option_any(ty: &Type) -> bool {
1425 match ty {
1426 Type::Path(tp) => tp
1427 .path
1428 .segments
1429 .last()
1430 .map(|seg| seg.ident == "Option")
1431 .unwrap_or(false),
1432 _ => false,
1433 }
1434}
1435
1436fn is_option_of(ty: &Type, inner_name: &str) -> bool {
1437 let Type::Path(tp) = ty else {
1438 return false;
1439 };
1440 let Some(seg) = tp.path.segments.last() else {
1441 return false;
1442 };
1443 if seg.ident != "Option" {
1444 return false;
1445 }
1446 let syn::PathArguments::AngleBracketed(args) = &seg.arguments else {
1447 return false;
1448 };
1449 let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() else {
1450 return false;
1451 };
1452 is_type_named(inner_ty, inner_name)
1453}
1454
1455fn is_numeric_type(ty: &Type) -> bool {
1456 matches!(ty, Type::Path(tp) if tp.path.is_ident("u8")
1457 || tp.path.is_ident("u16")
1458 || tp.path.is_ident("u32")
1459 || tp.path.is_ident("u64")
1460 || tp.path.is_ident("usize")
1461 || tp.path.is_ident("i8")
1462 || tp.path.is_ident("i16")
1463 || tp.path.is_ident("i32")
1464 || tp.path.is_ident("i64")
1465 || tp.path.is_ident("isize")
1466 || tp.path.is_ident("f32")
1467 || tp.path.is_ident("f64"))
1468}
1469
1470fn is_option_numeric_type(ty: &Type) -> bool {
1471 let Type::Path(tp) = ty else {
1472 return false;
1473 };
1474 let Some(seg) = tp.path.segments.last() else {
1475 return false;
1476 };
1477 if seg.ident != "Option" {
1478 return false;
1479 }
1480 let syn::PathArguments::AngleBracketed(args) = &seg.arguments else {
1481 return false;
1482 };
1483 let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() else {
1484 return false;
1485 };
1486 is_numeric_type(inner_ty)
1487}