Skip to main content

nestforge_macros/
lib.rs

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/*
15#[controller("/users")]
16Adds ControllerBasePath metadata to the struct.
17*/
18#[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/*
40#[routes]
41Reads #[get], #[post], #[put] on methods inside the impl block,
42removes those attributes, and generates ControllerDefinition::router().
43*/
44#[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/*
259#[module(
260    imports = [AuthModule],
261    controllers = [AppController, UsersController],
262    providers = [AppConfig { ... }, UsersService::new()],
263    exports = [UsersService]
264)]
265Generates ModuleDefinition for the struct.
266*/
267#[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/*
363Method route attributes are markers consumed by #[routes].
364*/
365#[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
841/* -------- helpers -------- */
842
843fn 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    /*
1086    Support both:
1087    - #[get("/")]
1088    - #[nestforge::get("/")]
1089    */
1090    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
1104/* -------- module parser -------- */
1105
1106struct 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}