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, FnArg, GenericArgument, Ident, ImplItem,
11    ImplItemFn, ItemImpl, ItemStruct, LitStr, Meta, PatType, PathArguments, ReturnType, Token,
12    Type,
13};
14
15/*
16#[controller("/users")]
17Adds ControllerBasePath metadata to the struct.
18*/
19#[proc_macro_attribute]
20pub fn controller(attr: TokenStream, item: TokenStream) -> TokenStream {
21    let base_path = parse_macro_input!(attr as LitStr);
22    let input = parse_macro_input!(item as ItemStruct);
23
24    let name = &input.ident;
25    let path = base_path.value();
26
27    let expanded = quote! {
28        #input
29
30        impl nestforge::ControllerBasePath for #name {
31            fn base_path() -> &'static str {
32                #path
33            }
34        }
35    };
36
37    TokenStream::from(expanded)
38}
39
40/*
41#[routes]
42Reads #[get], #[post], #[put] on methods inside the impl block,
43removes those attributes, and generates ControllerDefinition::router().
44*/
45#[proc_macro_attribute]
46pub fn routes(_attr: TokenStream, item: TokenStream) -> TokenStream {
47    let mut input = parse_macro_input!(item as ItemImpl);
48
49    let self_ty = input.self_ty.clone();
50    let controller_meta = extract_controller_route_meta(&mut input);
51
52    let mut route_calls = Vec::new();
53    let mut route_docs = Vec::new();
54
55    for impl_item in &mut input.items {
56        let ImplItem::Fn(ref mut method) = impl_item else {
57            continue;
58        };
59        let (guards, interceptors, exception_filters) = extract_pipeline_meta(method);
60        let version = extract_version_meta(method);
61        let mut doc_meta = extract_route_doc_meta(method);
62        doc_meta.tags = merge_string_lists(controller_meta.tags.clone(), doc_meta.tags);
63        doc_meta.required_roles = merge_string_lists(
64            controller_meta.required_roles.clone(),
65            doc_meta.required_roles,
66        );
67        doc_meta.requires_auth = controller_meta.requires_auth
68            || doc_meta.requires_auth
69            || !doc_meta.required_roles.is_empty();
70        let guards = merge_type_lists(controller_meta.guards.clone(), guards);
71        let interceptors = merge_type_lists(controller_meta.interceptors.clone(), interceptors);
72        let exception_filters =
73            merge_type_lists(controller_meta.exception_filters.clone(), exception_filters);
74
75        if let Some((http_method, path)) = extract_route_meta(method) {
76            let method_name = &method.sig.ident;
77            let path_lit = LitStr::new(&path, method.sig.ident.span());
78            let guard_inits = guards.iter().map(|ty| {
79                quote! { std::sync::Arc::new(<#ty as std::default::Default>::default()) as std::sync::Arc<dyn nestforge::Guard> }
80            });
81            let auth_guard_init = if doc_meta.requires_auth && doc_meta.required_roles.is_empty() {
82                quote! {
83                    std::sync::Arc::new(nestforge::RequireAuthenticationGuard::default())
84                        as std::sync::Arc<dyn nestforge::Guard>
85                }
86            } else {
87                quote! {}
88            };
89            let role_guard_init = if doc_meta.required_roles.is_empty() {
90                quote! {}
91            } else {
92                let roles = doc_meta
93                    .required_roles
94                    .iter()
95                    .map(|role| LitStr::new(role, method.sig.ident.span()));
96                quote! {
97                    std::sync::Arc::new(nestforge::RoleRequirementsGuard::new([#(#roles),*]))
98                        as std::sync::Arc<dyn nestforge::Guard>
99                }
100            };
101            let interceptor_inits = interceptors.iter().map(|ty| {
102                quote! { std::sync::Arc::new(<#ty as std::default::Default>::default()) as std::sync::Arc<dyn nestforge::Interceptor> }
103            });
104            let exception_filter_inits = exception_filters.iter().map(|ty| {
105                quote! { std::sync::Arc::new(<#ty as std::default::Default>::default()) as std::sync::Arc<dyn nestforge::ExceptionFilter> }
106            });
107            let guard_tokens = if doc_meta.requires_auth || !doc_meta.required_roles.is_empty() {
108                quote! { vec![#(#guard_inits,)* #auth_guard_init #role_guard_init] }
109            } else {
110                quote! { vec![#(#guard_inits),*] }
111            };
112            let version_tokens = if let Some(version) = &version {
113                let lit = LitStr::new(version, method.sig.ident.span());
114                quote! { Some(#lit) }
115            } else {
116                quote! { None }
117            };
118
119            let call = match http_method.as_str() {
120                "get" => quote! {
121                    builder = builder.get_with_pipeline(
122                        #path_lit,
123                        Self::#method_name,
124                        #guard_tokens,
125                        vec![#(#interceptor_inits),*],
126                        vec![#(#exception_filter_inits),*],
127                        #version_tokens
128                    );
129                },
130                "post" => quote! {
131                    builder = builder.post_with_pipeline(
132                        #path_lit,
133                        Self::#method_name,
134                        #guard_tokens,
135                        vec![#(#interceptor_inits),*],
136                        vec![#(#exception_filter_inits),*],
137                        #version_tokens
138                    );
139                },
140                "put" => quote! {
141                    builder = builder.put_with_pipeline(
142                        #path_lit,
143                        Self::#method_name,
144                        #guard_tokens,
145                        vec![#(#interceptor_inits),*],
146                        vec![#(#exception_filter_inits),*],
147                        #version_tokens
148                    );
149                },
150                "delete" => quote! {
151                    builder = builder.delete_with_pipeline(
152                        #path_lit,
153                        Self::#method_name,
154                        #guard_tokens,
155                        vec![#(#interceptor_inits),*],
156                        vec![#(#exception_filter_inits),*],
157                        #version_tokens
158                    );
159                },
160                _ => continue,
161            };
162
163            route_calls.push(call);
164
165            let method_lit = LitStr::new(&http_method.to_uppercase(), method.sig.ident.span());
166            let response_docs = if doc_meta.responses.is_empty() {
167                quote! {
168                    vec![nestforge::RouteResponseDocumentation {
169                        status: 200,
170                        description: "OK".to_string(),
171                        schema: None,
172                    }]
173                }
174            } else {
175                let responses = doc_meta.responses.iter().map(|response| {
176                    let description = LitStr::new(&response.description, method.sig.ident.span());
177                    let status = response.status;
178                        quote! {
179                            nestforge::RouteResponseDocumentation {
180                                status: #status,
181                                description: #description.to_string(),
182                                schema: None,
183                            }
184                        }
185                    });
186                    quote! { vec![#(#responses),*] }
187                };
188            let request_schema_tokens = infer_request_body_doc_tokens(method);
189            let response_schema_tokens = infer_response_body_doc_tokens(&method.sig.output);
190            let summary_tokens = if let Some(summary) = &doc_meta.summary {
191                let summary_lit = LitStr::new(summary, method.sig.ident.span());
192                quote! { doc = doc.with_summary(#summary_lit); }
193            } else {
194                quote! {}
195            };
196            let description_tokens = if let Some(description) = &doc_meta.description {
197                let description_lit = LitStr::new(description, method.sig.ident.span());
198                quote! { doc = doc.with_description(#description_lit); }
199            } else {
200                quote! {}
201            };
202            let tag_tokens = if doc_meta.tags.is_empty() {
203                quote! {}
204            } else {
205                let tags = doc_meta
206                    .tags
207                    .iter()
208                    .map(|tag| LitStr::new(tag, method.sig.ident.span()));
209                quote! { doc = doc.with_tags([#(#tags),*]); }
210            };
211            let auth_tokens = if doc_meta.requires_auth {
212                quote! { doc = doc.requires_auth(); }
213            } else {
214                quote! {}
215            };
216            let role_tokens = if doc_meta.required_roles.is_empty() {
217                quote! {}
218            } else {
219                let roles = doc_meta
220                    .required_roles
221                    .iter()
222                    .map(|role| LitStr::new(role, method.sig.ident.span()));
223                quote! { doc = doc.with_required_roles([#(#roles),*]); }
224            };
225
226            route_docs.push(quote! {
227                {
228                    let mut doc = nestforge::RouteDocumentation::new(
229                        #method_lit,
230                        nestforge::RouteBuilder::<#self_ty>::full_path(#path_lit, #version_tokens),
231                    )
232                    .with_responses(#response_docs);
233                    #summary_tokens
234                    #description_tokens
235                      #tag_tokens
236                      #auth_tokens
237                      #role_tokens
238                      #request_schema_tokens
239                      #response_schema_tokens
240                      doc
241                  }
242              });
243          }
244    }
245
246    let expanded = quote! {
247        #input
248
249        impl nestforge::ControllerDefinition for #self_ty {
250            fn router() -> axum::Router<nestforge::Container> {
251                nestforge::framework_log_event(
252                    "controller_register",
253                    &[("controller", std::string::String::from(std::any::type_name::<#self_ty>()))] as &[(&str, std::string::String)],
254                );
255                let mut builder = nestforge::RouteBuilder::<#self_ty>::new();
256                #(#route_calls)*
257                builder.build()
258            }
259        }
260
261        impl nestforge::DocumentedController for #self_ty {
262            fn route_docs() -> Vec<nestforge::RouteDocumentation> {
263                vec![#(#route_docs),*]
264            }
265        }
266    };
267
268    TokenStream::from(expanded)
269}
270
271/*
272#[module(
273    imports = [AuthModule],
274    controllers = [AppController, UsersController],
275    providers = [AppConfig { ... }, UsersService::new()],
276    exports = [UsersService]
277)]
278Generates ModuleDefinition for the struct.
279*/
280#[proc_macro_attribute]
281pub fn module(attr: TokenStream, item: TokenStream) -> TokenStream {
282    let args = parse_macro_input!(attr as ModuleArgs);
283    let input = parse_macro_input!(item as ItemStruct);
284
285    let name = &input.ident;
286
287    let controller_calls = args.controllers.iter().map(|ty| {
288        quote! { <#ty as nestforge::ControllerDefinition>::router() }
289    });
290    let controller_doc_calls = args.controllers.iter().map(|ty| {
291        quote! { docs.extend(<#ty as nestforge::DocumentedController>::route_docs()); }
292    });
293
294    let provider_regs = args.providers.iter().map(build_provider_registration);
295
296    let import_refs = args.imports.iter().map(|ty| {
297        quote! { nestforge::ModuleRef::of::<#ty>() }
298    });
299    let module_init_hooks = args.on_module_init.iter().map(|expr| {
300        quote! { #expr as nestforge::LifecycleHook }
301    });
302    let module_destroy_hooks = args.on_module_destroy.iter().map(|expr| {
303        quote! { #expr as nestforge::LifecycleHook }
304    });
305    let application_bootstrap_hooks = args.on_application_bootstrap.iter().map(|expr| {
306        quote! { #expr as nestforge::LifecycleHook }
307    });
308    let application_shutdown_hooks = args.on_application_shutdown.iter().map(|expr| {
309        quote! { #expr as nestforge::LifecycleHook }
310    });
311
312    let exported_types = args.exports.iter().map(|ty| {
313        quote! { std::any::type_name::<#ty>() }
314    });
315    let global_flag = args.global;
316
317    let expanded = quote! {
318        #input
319
320        impl nestforge::ModuleDefinition for #name {
321            fn register(container: &nestforge::Container) -> anyhow::Result<()> {
322                #(#provider_regs)*
323                Ok(())
324            }
325
326            fn imports() -> Vec<nestforge::ModuleRef> {
327                vec![
328                    #(#import_refs),*
329                ]
330            }
331
332            fn exports() -> Vec<&'static str> {
333                vec![
334                    #(#exported_types),*
335                ]
336            }
337
338            fn is_global() -> bool {
339                #global_flag
340            }
341
342            fn controllers() -> Vec<axum::Router<nestforge::Container>> {
343                vec![
344                    #(#controller_calls),*
345                ]
346            }
347
348            fn route_docs() -> Vec<nestforge::RouteDocumentation> {
349                let mut docs = Vec::new();
350                #(#controller_doc_calls)*
351                docs
352            }
353
354            fn on_module_init() -> Vec<nestforge::LifecycleHook> {
355                vec![#(#module_init_hooks),*]
356            }
357
358            fn on_module_destroy() -> Vec<nestforge::LifecycleHook> {
359                vec![#(#module_destroy_hooks),*]
360            }
361
362            fn on_application_bootstrap() -> Vec<nestforge::LifecycleHook> {
363                vec![#(#application_bootstrap_hooks),*]
364            }
365
366            fn on_application_shutdown() -> Vec<nestforge::LifecycleHook> {
367                vec![#(#application_shutdown_hooks),*]
368            }
369        }
370    };
371
372    TokenStream::from(expanded)
373}
374
375/*
376Method route attributes are markers consumed by #[routes].
377*/
378#[proc_macro_attribute]
379pub fn get(_attr: TokenStream, item: TokenStream) -> TokenStream {
380    item
381}
382
383#[proc_macro_attribute]
384pub fn post(_attr: TokenStream, item: TokenStream) -> TokenStream {
385    item
386}
387
388#[proc_macro_attribute]
389pub fn put(_attr: TokenStream, item: TokenStream) -> TokenStream {
390    item
391}
392
393#[proc_macro_attribute]
394pub fn delete(_attr: TokenStream, item: TokenStream) -> TokenStream {
395    item
396}
397
398#[proc_macro_attribute]
399pub fn version(_attr: TokenStream, item: TokenStream) -> TokenStream {
400    item
401}
402
403#[proc_macro_attribute]
404pub fn use_guard(_attr: TokenStream, item: TokenStream) -> TokenStream {
405    item
406}
407
408#[proc_macro_attribute]
409pub fn use_interceptor(_attr: TokenStream, item: TokenStream) -> TokenStream {
410    item
411}
412
413#[proc_macro_attribute]
414pub fn use_exception_filter(_attr: TokenStream, item: TokenStream) -> TokenStream {
415    item
416}
417
418#[proc_macro_attribute]
419pub fn summary(_attr: TokenStream, item: TokenStream) -> TokenStream {
420    item
421}
422
423#[proc_macro_attribute]
424pub fn description(_attr: TokenStream, item: TokenStream) -> TokenStream {
425    item
426}
427
428#[proc_macro_attribute]
429pub fn tag(_attr: TokenStream, item: TokenStream) -> TokenStream {
430    item
431}
432
433#[proc_macro_attribute]
434pub fn response(_attr: TokenStream, item: TokenStream) -> TokenStream {
435    item
436}
437
438#[proc_macro_attribute]
439pub fn authenticated(_attr: TokenStream, item: TokenStream) -> TokenStream {
440    item
441}
442
443#[proc_macro_attribute]
444pub fn roles(_attr: TokenStream, item: TokenStream) -> TokenStream {
445    item
446}
447
448fn build_openapi_schema_impl(input: &ItemStruct) -> TokenStream2 {
449    let name = &input.ident;
450    let schema_body = build_openapi_schema_body(input);
451
452    quote! {
453        impl nestforge::OpenApiSchema for #name {
454            fn schema_name() -> Option<&'static str> {
455                Some(stringify!(#name))
456            }
457
458            fn schema() -> nestforge::serde_json::Value {
459                #schema_body
460            }
461        }
462    }
463}
464
465fn build_openapi_schema_body(input: &ItemStruct) -> TokenStream2 {
466    let Fields::Named(fields) = &input.fields else {
467        return quote! {
468            nestforge::serde_json::json!({
469                "type": "object",
470                "properties": {},
471                "required": []
472            })
473        };
474    };
475
476    let property_builders = fields.named.iter().filter_map(build_openapi_property_tokens);
477    let required_fields = fields
478        .named
479        .iter()
480        .filter_map(required_field_literal)
481        .collect::<Vec<_>>();
482
483    quote! {{
484        let mut properties = nestforge::serde_json::Map::new();
485        #(#property_builders)*
486        nestforge::serde_json::json!({
487            "type": "object",
488            "properties": properties,
489            "required": [#(#required_fields),*]
490        })
491    }}
492}
493
494fn build_openapi_property_tokens(field: &Field) -> Option<TokenStream2> {
495    let field_ident = field.ident.as_ref()?;
496    let field_name = LitStr::new(&field_ident.to_string(), field_ident.span());
497    let field_ty = &field.ty;
498    let rules = parse_validate_rules(&field.attrs);
499    let schema_expr = schema_expression_for_type(field_ty);
500    let validations = validation_schema_mutations(&rules);
501
502    Some(quote! {
503        {
504            let mut property = #schema_expr;
505            #validations
506            properties.insert(#field_name.to_string(), property);
507        }
508    })
509}
510
511fn required_field_literal(field: &Field) -> Option<LitStr> {
512    let field_ident = field.ident.as_ref()?;
513    let rules = parse_validate_rules(&field.attrs);
514    if is_option_any(&field.ty) && !rules.required {
515        return None;
516    }
517
518    Some(LitStr::new(&field_ident.to_string(), field_ident.span()))
519}
520
521fn validation_schema_mutations(rules: &ValidateRules) -> TokenStream2 {
522    let mut tokens = Vec::new();
523
524    if rules.email {
525        tokens.push(quote! {
526            if let Some(object) = property.as_object_mut() {
527                object.insert(
528                    "format".to_string(),
529                    nestforge::serde_json::Value::String("email".to_string()),
530                );
531            }
532        });
533    }
534
535    if let Some(min_length) = rules.min_length {
536        tokens.push(quote! {
537            if let Some(object) = property.as_object_mut() {
538                object.insert(
539                    "minLength".to_string(),
540                    nestforge::serde_json::json!(#min_length),
541                );
542            }
543        });
544    }
545
546    if let Some(max_length) = rules.max_length {
547        tokens.push(quote! {
548            if let Some(object) = property.as_object_mut() {
549                object.insert(
550                    "maxLength".to_string(),
551                    nestforge::serde_json::json!(#max_length),
552                );
553            }
554        });
555    }
556
557    if let Some(min) = &rules.min {
558        tokens.push(quote! {
559            if let Some(object) = property.as_object_mut() {
560                object.insert(
561                    "minimum".to_string(),
562                    nestforge::serde_json::json!(#min),
563                );
564            }
565        });
566    }
567
568    if let Some(max) = &rules.max {
569        tokens.push(quote! {
570            if let Some(object) = property.as_object_mut() {
571                object.insert(
572                    "maximum".to_string(),
573                    nestforge::serde_json::json!(#max),
574                );
575            }
576        });
577    }
578
579    quote! { #(#tokens)* }
580}
581
582#[proc_macro_attribute]
583pub fn dto(_attr: TokenStream, item: TokenStream) -> TokenStream {
584    let mut input = parse_macro_input!(item as ItemStruct);
585    let schema_impl = build_openapi_schema_impl(&input);
586
587    input.attrs.push(parse_quote!(
588        #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, nestforge::Validate)]
589    ));
590
591    TokenStream::from(quote! {
592        #input
593        #schema_impl
594    })
595}
596
597#[proc_macro_attribute]
598pub fn identifiable(_attr: TokenStream, item: TokenStream) -> TokenStream {
599    let input = parse_macro_input!(item as ItemStruct);
600    let name = &input.ident;
601
602    let Some((id_field_name, id_field_ty)) = find_id_field(&input.fields) else {
603        return syn::Error::new(
604            input.ident.span(),
605            "identifiable requires an `id: u64` field or a field marked with #[id]",
606        )
607        .to_compile_error()
608        .into();
609    };
610    let ty_ok = matches!(id_field_ty, Type::Path(ref tp) if tp.path.is_ident("u64"));
611    if !ty_ok {
612        return syn::Error::new(
613            id_field_ty.span(),
614            "identifiable id field must be of type `u64`",
615        )
616        .to_compile_error()
617        .into();
618    }
619
620    TokenStream::from(quote! {
621        #input
622
623        impl nestforge::Identifiable for #name {
624            fn id(&self) -> u64 {
625                self.#id_field_name
626            }
627
628            fn set_id(&mut self, id: u64) {
629                self.#id_field_name = id;
630            }
631        }
632    })
633}
634
635#[proc_macro_attribute]
636pub fn response_dto(_attr: TokenStream, item: TokenStream) -> TokenStream {
637    let mut input = parse_macro_input!(item as ItemStruct);
638    let schema_impl = build_openapi_schema_impl(&input);
639
640    input
641        .attrs
642        .push(parse_quote!(#[derive(Debug, Clone, serde::Serialize)]));
643
644    TokenStream::from(quote! {
645        #input
646        #schema_impl
647    })
648}
649
650#[proc_macro_attribute]
651pub fn entity_dto(_attr: TokenStream, item: TokenStream) -> TokenStream {
652    let mut input = parse_macro_input!(item as ItemStruct);
653
654    let Some((id_field_name, id_field_ty)) = find_id_field(&input.fields) else {
655        return syn::Error::new(
656            input.ident.span(),
657            "entity_dto requires an `id: u64` field or a field marked with #[id]",
658        )
659        .to_compile_error()
660        .into();
661    };
662    let ty_ok = matches!(id_field_ty, Type::Path(ref tp) if tp.path.is_ident("u64"));
663    if !ty_ok {
664        return syn::Error::new(
665            id_field_ty.span(),
666            "entity_dto id field must be of type `u64`",
667        )
668        .to_compile_error()
669        .into();
670    }
671
672    input.attrs.push(parse_quote!(
673        #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, nestforge::Validate)]
674    ));
675
676    let name = &input.ident;
677    let schema_impl = build_openapi_schema_impl(&input);
678
679    TokenStream::from(quote! {
680        #input
681
682        impl nestforge::Identifiable for #name {
683            fn id(&self) -> u64 {
684                self.#id_field_name
685            }
686
687            fn set_id(&mut self, id: u64) {
688                self.#id_field_name = id;
689            }
690        }
691
692        #schema_impl
693    })
694}
695
696#[proc_macro_attribute]
697pub fn entity(attr: TokenStream, item: TokenStream) -> TokenStream {
698    let args = parse_macro_input!(attr as EntityArgs);
699    let mut input = parse_macro_input!(item as ItemStruct);
700
701    let name = &input.ident;
702    let Some((id_field_name, id_field_ty)) = extract_id_field(&mut input.fields) else {
703        return syn::Error::new(
704            input.ident.span(),
705            "#[entity(...)] requires exactly one field annotated with #[id]",
706        )
707        .to_compile_error()
708        .into();
709    };
710
711    let table_name = args.table.value();
712    let id_column = id_field_name.to_string();
713
714    let expanded = quote! {
715        #input
716
717        impl nestforge::EntityMeta for #name {
718            type Id = #id_field_ty;
719
720            fn table_name() -> &'static str {
721                #table_name
722            }
723
724            fn id_column() -> &'static str {
725                #id_column
726            }
727
728            fn id_value(&self) -> &Self::Id {
729                &self.#id_field_name
730            }
731        }
732    };
733
734    TokenStream::from(expanded)
735}
736
737#[proc_macro_attribute]
738pub fn id(_attr: TokenStream, item: TokenStream) -> TokenStream {
739    item
740}
741
742#[proc_macro_derive(Identifiable, attributes(id))]
743pub fn derive_identifiable(item: TokenStream) -> TokenStream {
744    let input = parse_macro_input!(item as DeriveInput);
745    let name = &input.ident;
746
747    let Data::Struct(data) = &input.data else {
748        return syn::Error::new(
749            input.ident.span(),
750            "Identifiable can only be derived on structs",
751        )
752        .to_compile_error()
753        .into();
754    };
755
756    let Some((id_field_name, id_field_ty)) = find_id_field(&data.fields) else {
757        return syn::Error::new(
758            input.ident.span(),
759            "Identifiable derive requires an `id: u64` field or a field marked with #[id]",
760        )
761        .to_compile_error()
762        .into();
763    };
764
765    let ty_ok = matches!(id_field_ty, Type::Path(ref tp) if tp.path.is_ident("u64"));
766    if !ty_ok {
767        return syn::Error::new(
768            id_field_ty.span(),
769            "Identifiable id field must be of type `u64`",
770        )
771        .to_compile_error()
772        .into();
773    }
774
775    let expanded = quote! {
776        impl nestforge::Identifiable for #name {
777            fn id(&self) -> u64 {
778                self.#id_field_name
779            }
780
781            fn set_id(&mut self, id: u64) {
782                self.#id_field_name = id;
783            }
784        }
785    };
786
787    TokenStream::from(expanded)
788}
789
790#[proc_macro_derive(Validate, attributes(validate))]
791pub fn derive_validate(item: TokenStream) -> TokenStream {
792    let input = parse_macro_input!(item as DeriveInput);
793    let name = &input.ident;
794
795    let Data::Struct(data) = &input.data else {
796        return syn::Error::new(
797            input.ident.span(),
798            "Validate can only be derived on structs",
799        )
800        .to_compile_error()
801        .into();
802    };
803
804    let Fields::Named(fields) = &data.fields else {
805        return syn::Error::new(input.ident.span(), "Validate derive requires named fields")
806            .to_compile_error()
807            .into();
808    };
809
810    let mut checks = Vec::new();
811    for field in &fields.named {
812        let Some(field_ident) = &field.ident else {
813            continue;
814        };
815        let field_name_lit = field_ident.to_string();
816        let rules = parse_validate_rules(&field.attrs);
817        if !rules.has_rules() {
818            continue;
819        }
820
821        let is_string = is_type_named(&field.ty, "String");
822        let is_option_string = is_option_of(&field.ty, "String");
823        let is_option_any = is_option_any(&field.ty);
824        let is_numeric = is_numeric_type(&field.ty);
825        let is_option_numeric = is_option_numeric_type(&field.ty);
826
827        if rules.required {
828            if is_string {
829                checks.push(quote! {
830                    if self.#field_ident.trim().is_empty() {
831                        errors.push(nestforge::ValidationIssue {
832                            field: #field_name_lit,
833                            message: format!("{} is required", #field_name_lit),
834                        });
835                    }
836                });
837            } else if is_option_string {
838                checks.push(quote! {
839                    match &self.#field_ident {
840                        Some(v) if !v.trim().is_empty() => {}
841                        _ => {
842                            errors.push(nestforge::ValidationIssue {
843                                field: #field_name_lit,
844                                message: format!("{} is required", #field_name_lit),
845                            });
846                        }
847                    }
848                });
849            } else if is_option_any {
850                checks.push(quote! {
851                    if self.#field_ident.is_none() {
852                        errors.push(nestforge::ValidationIssue {
853                            field: #field_name_lit,
854                            message: format!("{} is required", #field_name_lit),
855                        });
856                    }
857                });
858            }
859        }
860
861        if rules.email {
862            if is_string {
863                checks.push(quote! {
864                    if !self.#field_ident.trim().is_empty() && !self.#field_ident.contains('@') {
865                        errors.push(nestforge::ValidationIssue {
866                            field: #field_name_lit,
867                            message: format!("{} must be a valid email", #field_name_lit),
868                        });
869                    }
870                });
871            } else if is_option_string {
872                checks.push(quote! {
873                    if let Some(v) = &self.#field_ident {
874                        if !v.trim().is_empty() && !v.contains('@') {
875                            errors.push(nestforge::ValidationIssue {
876                                field: #field_name_lit,
877                                message: format!("{} must be a valid email", #field_name_lit),
878                            });
879                        }
880                    }
881                });
882            }
883        }
884
885        if let Some(min_length) = rules.min_length {
886            if is_string {
887                checks.push(quote! {
888                    if self.#field_ident.len() < #min_length {
889                        errors.push(nestforge::ValidationIssue {
890                            field: #field_name_lit,
891                            message: format!("{} must be at least {} characters", #field_name_lit, #min_length),
892                        });
893                    }
894                });
895            } else if is_option_string {
896                checks.push(quote! {
897                    if let Some(v) = &self.#field_ident {
898                        if v.len() < #min_length {
899                            errors.push(nestforge::ValidationIssue {
900                                field: #field_name_lit,
901                                message: format!("{} must be at least {} characters", #field_name_lit, #min_length),
902                            });
903                        }
904                    }
905                });
906            }
907        }
908
909        if let Some(max_length) = rules.max_length {
910            if is_string {
911                checks.push(quote! {
912                    if self.#field_ident.len() > #max_length {
913                        errors.push(nestforge::ValidationIssue {
914                            field: #field_name_lit,
915                            message: format!("{} must be at most {} characters", #field_name_lit, #max_length),
916                        });
917                    }
918                });
919            } else if is_option_string {
920                checks.push(quote! {
921                    if let Some(v) = &self.#field_ident {
922                        if v.len() > #max_length {
923                            errors.push(nestforge::ValidationIssue {
924                                field: #field_name_lit,
925                                message: format!("{} must be at most {} characters", #field_name_lit, #max_length),
926                            });
927                        }
928                    }
929                });
930            }
931        }
932
933        if let Some(min) = &rules.min {
934            if is_numeric {
935                checks.push(quote! {
936                    if self.#field_ident < #min {
937                        errors.push(nestforge::ValidationIssue {
938                            field: #field_name_lit,
939                            message: format!("{} must be at least {}", #field_name_lit, #min),
940                        });
941                    }
942                });
943            } else if is_option_numeric {
944                checks.push(quote! {
945                    if let Some(v) = self.#field_ident {
946                        if v < #min {
947                            errors.push(nestforge::ValidationIssue {
948                                field: #field_name_lit,
949                                message: format!("{} must be at least {}", #field_name_lit, #min),
950                            });
951                        }
952                    }
953                });
954            }
955        }
956
957        if let Some(max) = &rules.max {
958            if is_numeric {
959                checks.push(quote! {
960                    if self.#field_ident > #max {
961                        errors.push(nestforge::ValidationIssue {
962                            field: #field_name_lit,
963                            message: format!("{} must be at most {}", #field_name_lit, #max),
964                        });
965                    }
966                });
967            } else if is_option_numeric {
968                checks.push(quote! {
969                    if let Some(v) = self.#field_ident {
970                        if v > #max {
971                            errors.push(nestforge::ValidationIssue {
972                                field: #field_name_lit,
973                                message: format!("{} must be at most {}", #field_name_lit, #max),
974                            });
975                        }
976                    }
977                });
978            }
979        }
980    }
981
982    let expanded = quote! {
983        impl nestforge::Validate for #name {
984            fn validate(&self) -> Result<(), nestforge::ValidationErrors> {
985                let mut errors = Vec::new();
986                #(#checks)*
987                if errors.is_empty() {
988                    Ok(())
989                } else {
990                    Err(nestforge::ValidationErrors::new(errors))
991                }
992            }
993        }
994    };
995
996    TokenStream::from(expanded)
997}
998
999/* -------- helpers -------- */
1000
1001fn extract_route_meta(method: &mut ImplItemFn) -> Option<(String, String)> {
1002    let mut found: Option<(String, String)> = None;
1003    let mut kept_attrs: Vec<Attribute> = Vec::new();
1004
1005    for attr in method.attrs.drain(..) {
1006        let Some((verb, path)) = parse_route_attr(&attr) else {
1007            kept_attrs.push(attr);
1008            continue;
1009        };
1010
1011        if found.is_none() {
1012            found = Some((verb, path));
1013        }
1014    }
1015
1016    method.attrs = kept_attrs;
1017    found
1018}
1019
1020fn extract_pipeline_meta(method: &mut ImplItemFn) -> (Vec<Type>, Vec<Type>, Vec<Type>) {
1021    let mut guards = Vec::new();
1022    let mut interceptors = Vec::new();
1023    let mut exception_filters = Vec::new();
1024    let mut kept_attrs: Vec<Attribute> = Vec::new();
1025
1026    for attr in method.attrs.drain(..) {
1027        let ident = attr
1028            .path()
1029            .segments
1030            .last()
1031            .map(|seg| seg.ident.to_string())
1032            .unwrap_or_default();
1033
1034        if ident == "use_guard" {
1035            if let Ok(ty) = attr.parse_args::<Type>() {
1036                guards.push(ty);
1037            }
1038            continue;
1039        }
1040
1041        if ident == "use_interceptor" {
1042            if let Ok(ty) = attr.parse_args::<Type>() {
1043                interceptors.push(ty);
1044            }
1045            continue;
1046        }
1047
1048        if ident == "use_exception_filter" {
1049            if let Ok(ty) = attr.parse_args::<Type>() {
1050                exception_filters.push(ty);
1051            }
1052            continue;
1053        }
1054
1055        kept_attrs.push(attr);
1056    }
1057
1058    method.attrs = kept_attrs;
1059    (guards, interceptors, exception_filters)
1060}
1061
1062#[derive(Default)]
1063struct ControllerRouteMeta {
1064    guards: Vec<Type>,
1065    interceptors: Vec<Type>,
1066    exception_filters: Vec<Type>,
1067    tags: Vec<String>,
1068    requires_auth: bool,
1069    required_roles: Vec<String>,
1070}
1071
1072fn extract_controller_route_meta(input: &mut ItemImpl) -> ControllerRouteMeta {
1073    let mut meta = ControllerRouteMeta::default();
1074    let mut kept_attrs: Vec<Attribute> = Vec::new();
1075
1076    for attr in input.attrs.drain(..) {
1077        let ident = attr
1078            .path()
1079            .segments
1080            .last()
1081            .map(|seg| seg.ident.to_string())
1082            .unwrap_or_default();
1083
1084        match ident.as_str() {
1085            "use_guard" => {
1086                if let Ok(ty) = attr.parse_args::<Type>() {
1087                    meta.guards.push(ty);
1088                }
1089            }
1090            "use_interceptor" => {
1091                if let Ok(ty) = attr.parse_args::<Type>() {
1092                    meta.interceptors.push(ty);
1093                }
1094            }
1095            "use_exception_filter" => {
1096                if let Ok(ty) = attr.parse_args::<Type>() {
1097                    meta.exception_filters.push(ty);
1098                }
1099            }
1100            "tag" => {
1101                if let Ok(lit) = attr.parse_args::<LitStr>() {
1102                    meta.tags.push(lit.value());
1103                }
1104            }
1105            "authenticated" => {
1106                meta.requires_auth = true;
1107            }
1108            "roles" => {
1109                if let Ok(values) =
1110                    attr.parse_args_with(Punctuated::<LitStr, Token![,]>::parse_terminated)
1111                {
1112                    meta.required_roles
1113                        .extend(values.into_iter().map(|value| value.value()));
1114                    meta.requires_auth = true;
1115                }
1116            }
1117            _ => kept_attrs.push(attr),
1118        }
1119    }
1120
1121    input.attrs = kept_attrs;
1122    meta
1123}
1124
1125fn extract_version_meta(method: &mut ImplItemFn) -> Option<String> {
1126    let mut version: Option<String> = None;
1127    let mut kept_attrs: Vec<Attribute> = Vec::new();
1128
1129    for attr in method.attrs.drain(..) {
1130        let ident = attr
1131            .path()
1132            .segments
1133            .last()
1134            .map(|seg| seg.ident.to_string())
1135            .unwrap_or_default();
1136
1137        if ident == "version" {
1138            if let Ok(lit) = attr.parse_args::<LitStr>() {
1139                version = Some(lit.value());
1140            }
1141            continue;
1142        }
1143
1144        kept_attrs.push(attr);
1145    }
1146
1147    method.attrs = kept_attrs;
1148    version
1149}
1150
1151#[derive(Default)]
1152struct RouteDocMeta {
1153    summary: Option<String>,
1154    description: Option<String>,
1155    tags: Vec<String>,
1156    responses: Vec<RouteResponseMeta>,
1157    requires_auth: bool,
1158    required_roles: Vec<String>,
1159}
1160
1161struct RouteResponseMeta {
1162    status: u16,
1163    description: String,
1164}
1165
1166fn extract_route_doc_meta(method: &mut ImplItemFn) -> RouteDocMeta {
1167    let mut meta = RouteDocMeta::default();
1168    let mut kept_attrs: Vec<Attribute> = Vec::new();
1169
1170    for attr in method.attrs.drain(..) {
1171        let ident = attr
1172            .path()
1173            .segments
1174            .last()
1175            .map(|seg| seg.ident.to_string())
1176            .unwrap_or_default();
1177
1178        match ident.as_str() {
1179            "summary" => {
1180                if let Ok(lit) = attr.parse_args::<LitStr>() {
1181                    meta.summary = Some(lit.value());
1182                }
1183            }
1184            "description" => {
1185                if let Ok(lit) = attr.parse_args::<LitStr>() {
1186                    meta.description = Some(lit.value());
1187                }
1188            }
1189            "tag" => {
1190                if let Ok(lit) = attr.parse_args::<LitStr>() {
1191                    meta.tags.push(lit.value());
1192                }
1193            }
1194            "response" => {
1195                if let Ok(response) = attr.parse_args::<RouteResponseArgs>() {
1196                    meta.responses.push(RouteResponseMeta {
1197                        status: response.status,
1198                        description: response.description.value(),
1199                    });
1200                }
1201            }
1202            "authenticated" => {
1203                meta.requires_auth = true;
1204            }
1205            "roles" => {
1206                if let Ok(values) =
1207                    attr.parse_args_with(Punctuated::<LitStr, Token![,]>::parse_terminated)
1208                {
1209                    meta.required_roles
1210                        .extend(values.into_iter().map(|value| value.value()));
1211                    meta.requires_auth = true;
1212                }
1213            }
1214            _ => kept_attrs.push(attr),
1215        }
1216    }
1217
1218    method.attrs = kept_attrs;
1219    meta
1220}
1221
1222fn merge_string_lists(primary: Vec<String>, secondary: Vec<String>) -> Vec<String> {
1223    let mut merged = primary;
1224    for value in secondary {
1225        if !merged.contains(&value) {
1226            merged.push(value);
1227        }
1228    }
1229    merged
1230}
1231
1232fn merge_type_lists(primary: Vec<Type>, secondary: Vec<Type>) -> Vec<Type> {
1233    let mut merged = primary;
1234    for ty in secondary {
1235        if !merged
1236            .iter()
1237            .any(|existing| quote!(#existing).to_string() == quote!(#ty).to_string())
1238        {
1239            merged.push(ty);
1240        }
1241    }
1242    merged
1243}
1244
1245fn parse_route_attr(attr: &Attribute) -> Option<(String, String)> {
1246    /*
1247    Support both:
1248    - #[get("/")]
1249    - #[nestforge::get("/")]
1250    */
1251    let ident = attr.path().segments.last()?.ident.to_string();
1252
1253    if ident != "get" && ident != "post" && ident != "put" && ident != "delete" {
1254        return None;
1255    }
1256
1257    let path = match &attr.meta {
1258        Meta::List(_) => attr.parse_args::<LitStr>().ok()?.value(),
1259        _ => return None,
1260    };
1261
1262    Some((ident, path))
1263}
1264
1265fn infer_request_body_doc_tokens(method: &ImplItemFn) -> TokenStream2 {
1266    let Some(payload_ty) = method
1267        .sig
1268        .inputs
1269        .iter()
1270        .find_map(extract_request_payload_type)
1271    else {
1272        return quote! {};
1273    };
1274
1275    let schema_expr = schema_expression_for_type(&payload_ty);
1276    quote! {
1277        doc = doc.with_request_body_schema(#schema_expr);
1278        doc = doc.with_schema_components(nestforge::openapi_schema_components_for::<#payload_ty>());
1279    }
1280}
1281
1282fn infer_response_body_doc_tokens(output: &ReturnType) -> TokenStream2 {
1283    let Some(schema_doc) = extract_response_payload_doc(output) else {
1284        return quote! {};
1285    };
1286
1287    schema_doc
1288}
1289
1290fn extract_request_payload_type(arg: &FnArg) -> Option<Type> {
1291    let FnArg::Typed(PatType { ty, .. }) = arg else {
1292        return None;
1293    };
1294
1295    extract_inner_type_named(ty, &["ValidatedBody", "Body", "Json"])
1296}
1297
1298fn extract_response_payload_doc(output: &ReturnType) -> Option<TokenStream2> {
1299    let ReturnType::Type(_, ty) = output else {
1300        return None;
1301    };
1302
1303    response_payload_doc_tokens(ty)
1304}
1305
1306fn response_payload_doc_tokens(ty: &Type) -> Option<TokenStream2> {
1307    if let Some((value_ty, serializer_ty)) = extract_two_inner_types_named(ty, &["ApiSerializedResult"]) {
1308        return Some(quote! {
1309            doc = doc.with_success_response_schema(
1310                nestforge::openapi_schema_for::<<#serializer_ty as nestforge::ResponseSerializer<#value_ty>>::Output>()
1311            );
1312            doc = doc.with_schema_components(
1313                nestforge::openapi_schema_components_for::<<#serializer_ty as nestforge::ResponseSerializer<#value_ty>>::Output>()
1314            );
1315        });
1316    }
1317
1318    if let Some(inner) = extract_inner_type_named(ty, &["ApiEnvelopeResult"]) {
1319        let schema_expr = quote! {{
1320            nestforge::serde_json::json!({
1321                "type": "object",
1322                "properties": {
1323                    "success": nestforge::openapi_schema_for::<bool>(),
1324                    "data": nestforge::openapi_schema_for::<#inner>()
1325                },
1326                "required": ["success", "data"]
1327            })
1328        }};
1329        return Some(quote! {
1330            doc = doc.with_success_response_schema(#schema_expr);
1331            doc = doc.with_schema_components(nestforge::openapi_schema_components_for::<#inner>());
1332        });
1333    }
1334
1335    if let Some(inner) = extract_inner_type_named(ty, &["ApiResult", "Json"]) {
1336        return response_payload_doc_tokens(&inner).or_else(|| {
1337            let schema_expr = schema_expression_for_type(&inner);
1338            Some(quote! {
1339                doc = doc.with_success_response_schema(#schema_expr);
1340                doc = doc.with_schema_components(nestforge::openapi_schema_components_for::<#inner>());
1341            })
1342        });
1343    }
1344
1345    if let Some(inner) = extract_inner_type_named(ty, &["Result"]) {
1346        return response_payload_doc_tokens(&inner).or_else(|| {
1347            let schema_expr = schema_expression_for_type(&inner);
1348            Some(quote! {
1349                doc = doc.with_success_response_schema(#schema_expr);
1350                doc = doc.with_schema_components(nestforge::openapi_schema_components_for::<#inner>());
1351            })
1352        });
1353    }
1354
1355    if let Some((value_ty, serializer_ty)) = extract_serialized_types(ty) {
1356        return Some(quote! {
1357            doc = doc.with_success_response_schema(
1358                nestforge::openapi_schema_for::<<#serializer_ty as nestforge::ResponseSerializer<#value_ty>>::Output>()
1359            );
1360            doc = doc.with_schema_components(
1361                nestforge::openapi_schema_components_for::<<#serializer_ty as nestforge::ResponseSerializer<#value_ty>>::Output>()
1362            );
1363        });
1364    }
1365
1366    if let Some(inner) = extract_inner_type_named(ty, &["ResponseEnvelope"]) {
1367        let schema_expr = quote!({
1368            nestforge::serde_json::json!({
1369                "type": "object",
1370                "properties": {
1371                    "success": nestforge::openapi_schema_for::<bool>(),
1372                    "data": nestforge::openapi_schema_for::<#inner>()
1373                },
1374                "required": ["success", "data"]
1375            })
1376        });
1377        return Some(quote! {
1378            doc = doc.with_success_response_schema(#schema_expr);
1379            doc = doc.with_schema_components(nestforge::openapi_schema_components_for::<#inner>());
1380        });
1381    }
1382
1383    let schema_expr = schema_expression_for_type(ty);
1384    Some(quote! {
1385        doc = doc.with_success_response_schema(#schema_expr);
1386        doc = doc.with_schema_components(nestforge::openapi_schema_components_for::<#ty>());
1387    })
1388}
1389
1390fn schema_expression_for_type(ty: &Type) -> TokenStream2 {
1391    if let Some(inner) = extract_inner_type_named(ty, &["Vec", "List"]) {
1392        return quote! { nestforge::openapi_array_schema_for::<#inner>() };
1393    }
1394
1395    if let Some(inner) = extract_inner_type_named(ty, &["Option"]) {
1396        return quote! { nestforge::openapi_nullable_schema_for::<#inner>() };
1397    }
1398
1399    quote! { nestforge::openapi_schema_for::<#ty>() }
1400}
1401
1402fn extract_inner_type_named(ty: &Type, names: &[&str]) -> Option<Type> {
1403    let Type::Path(type_path) = ty else {
1404        return None;
1405    };
1406    let segment = type_path.path.segments.last()?;
1407    if !names.iter().any(|name| segment.ident == *name) {
1408        return None;
1409    }
1410
1411    let PathArguments::AngleBracketed(args) = &segment.arguments else {
1412        return None;
1413    };
1414
1415    args.args.iter().find_map(|arg| match arg {
1416        GenericArgument::Type(inner) => Some(inner.clone()),
1417        _ => None,
1418    })
1419}
1420
1421fn extract_serialized_types(ty: &Type) -> Option<(Type, Type)> {
1422    extract_two_inner_types_named(ty, &["Serialized"])
1423}
1424
1425fn extract_two_inner_types_named(ty: &Type, names: &[&str]) -> Option<(Type, Type)> {
1426    let Type::Path(type_path) = ty else {
1427        return None;
1428    };
1429    let segment = type_path.path.segments.last()?;
1430    if !names.iter().any(|name| segment.ident == *name) {
1431        return None;
1432    }
1433
1434    let PathArguments::AngleBracketed(args) = &segment.arguments else {
1435        return None;
1436    };
1437
1438    let mut types = args.args.iter().filter_map(|arg| match arg {
1439        GenericArgument::Type(inner) => Some(inner.clone()),
1440        _ => None,
1441    });
1442
1443    let value_ty = types.next()?;
1444    let serializer_ty = types.next()?;
1445    Some((value_ty, serializer_ty))
1446}
1447
1448/* -------- module parser -------- */
1449
1450struct ModuleArgs {
1451    imports: Vec<Type>,
1452    controllers: Vec<Type>,
1453    providers: Vec<Expr>,
1454    exports: Vec<Type>,
1455    on_module_init: Vec<Expr>,
1456    on_module_destroy: Vec<Expr>,
1457    on_application_bootstrap: Vec<Expr>,
1458    on_application_shutdown: Vec<Expr>,
1459    global: bool,
1460}
1461
1462struct RouteResponseArgs {
1463    status: u16,
1464    description: LitStr,
1465}
1466
1467struct EntityArgs {
1468    table: LitStr,
1469}
1470
1471impl Parse for EntityArgs {
1472    fn parse(input: ParseStream) -> syn::Result<Self> {
1473        let key: Ident = input.parse()?;
1474        if key != "table" {
1475            return Err(syn::Error::new(
1476                key.span(),
1477                "Unsupported entity key. Use `table = \"...\"`.",
1478            ));
1479        }
1480        input.parse::<Token![=]>()?;
1481        let table = input.parse::<LitStr>()?;
1482        Ok(Self { table })
1483    }
1484}
1485
1486impl Parse for RouteResponseArgs {
1487    fn parse(input: ParseStream) -> syn::Result<Self> {
1488        let mut status = None;
1489        let mut description = None;
1490
1491        while !input.is_empty() {
1492            let key: Ident = input.parse()?;
1493            input.parse::<Token![=]>()?;
1494
1495            if key == "status" {
1496                let value = input.parse::<syn::LitInt>()?;
1497                status = Some(value.base10_parse()?);
1498            } else if key == "description" {
1499                description = Some(input.parse::<LitStr>()?);
1500            } else {
1501                return Err(syn::Error::new(
1502                    key.span(),
1503                    "Unsupported response key. Use `status = ...` and `description = \"...\"`.",
1504                ));
1505            }
1506
1507            if input.peek(Token![,]) {
1508                input.parse::<Token![,]>()?;
1509            }
1510        }
1511
1512        Ok(Self {
1513            status: status.ok_or_else(|| {
1514                syn::Error::new(input.span(), "response metadata requires `status = ...`")
1515            })?,
1516            description: description.ok_or_else(|| {
1517                syn::Error::new(
1518                    input.span(),
1519                    "response metadata requires `description = \"...\"`",
1520                )
1521            })?,
1522        })
1523    }
1524}
1525
1526impl Parse for ModuleArgs {
1527    fn parse(input: ParseStream) -> syn::Result<Self> {
1528        let mut imports: Vec<Type> = Vec::new();
1529        let mut controllers: Vec<Type> = Vec::new();
1530        let mut providers: Vec<Expr> = Vec::new();
1531        let mut exports: Vec<Type> = Vec::new();
1532        let mut on_module_init: Vec<Expr> = Vec::new();
1533        let mut on_module_destroy: Vec<Expr> = Vec::new();
1534        let mut on_application_bootstrap: Vec<Expr> = Vec::new();
1535        let mut on_application_shutdown: Vec<Expr> = Vec::new();
1536        let mut global = false;
1537
1538        while !input.is_empty() {
1539            let key: Ident = input.parse()?;
1540            input.parse::<Token![=]>()?;
1541
1542            if key == "imports" {
1543                imports = parse_bracket_list::<Type>(input)?;
1544            } else if key == "controllers" {
1545                controllers = parse_bracket_list::<Type>(input)?;
1546            } else if key == "providers" {
1547                providers = parse_bracket_list::<Expr>(input)?;
1548            } else if key == "exports" {
1549                exports = parse_bracket_list::<Type>(input)?;
1550            } else if key == "on_module_init" {
1551                on_module_init = parse_bracket_list::<Expr>(input)?;
1552            } else if key == "on_module_destroy" {
1553                on_module_destroy = parse_bracket_list::<Expr>(input)?;
1554            } else if key == "on_application_bootstrap" {
1555                on_application_bootstrap = parse_bracket_list::<Expr>(input)?;
1556            } else if key == "on_application_shutdown" {
1557                on_application_shutdown = parse_bracket_list::<Expr>(input)?;
1558            } else if key == "global" {
1559                let lit: syn::LitBool = input.parse()?;
1560                global = lit.value;
1561            } else {
1562                return Err(syn::Error::new(
1563                    key.span(),
1564                    "Unsupported module key. Use `imports`, `controllers`, `providers`, `exports`, lifecycle hook lists, or `global`.",
1565                ));
1566            }
1567
1568            if input.peek(Token![,]) {
1569                input.parse::<Token![,]>()?;
1570            }
1571        }
1572
1573        Ok(Self {
1574            imports,
1575            controllers,
1576            providers,
1577            exports,
1578            on_module_init,
1579            on_module_destroy,
1580            on_application_bootstrap,
1581            on_application_shutdown,
1582            global,
1583        })
1584    }
1585}
1586
1587fn parse_bracket_list<T>(input: ParseStream) -> syn::Result<Vec<T>>
1588where
1589    T: Parse,
1590{
1591    let content;
1592    bracketed!(content in input);
1593
1594    let items: Punctuated<T, Token![,]> = content.parse_terminated(T::parse, Token![,])?;
1595    Ok(items.into_iter().collect())
1596}
1597
1598fn build_provider_registration(expr: &Expr) -> TokenStream2 {
1599    if is_provider_builder_expr(expr) {
1600        quote! { nestforge::register_provider(container, #expr)?; }
1601    } else {
1602        quote! { nestforge::register_provider(container, nestforge::Provider::value(#expr))?; }
1603    }
1604}
1605
1606fn is_provider_builder_expr(expr: &Expr) -> bool {
1607    let Expr::Call(call) = expr else {
1608        return false;
1609    };
1610    let Expr::Path(path_expr) = call.func.as_ref() else {
1611        return false;
1612    };
1613
1614    let mut segments = path_expr.path.segments.iter().rev();
1615    let Some(method) = segments.next() else {
1616        return false;
1617    };
1618
1619    if method.ident != "value" && method.ident != "factory" {
1620        return false;
1621    }
1622
1623    let Some(provider) = segments.next() else {
1624        return false;
1625    };
1626
1627    provider.ident == "Provider"
1628}
1629
1630fn extract_id_field(fields: &mut Fields) -> Option<(Ident, Type)> {
1631    let Fields::Named(named_fields) = fields else {
1632        return None;
1633    };
1634
1635    let mut found: Option<(Ident, Type)> = None;
1636
1637    for field in &mut named_fields.named {
1638        let has_id_attr = remove_id_attr(field);
1639        if !has_id_attr {
1640            continue;
1641        }
1642
1643        let field_name = field.ident.clone()?;
1644        let field_ty = field.ty.clone();
1645
1646        if found.is_some() {
1647            return None;
1648        }
1649
1650        found = Some((field_name, field_ty));
1651    }
1652
1653    found
1654}
1655
1656fn remove_id_attr(field: &mut Field) -> bool {
1657    let mut kept = Vec::new();
1658    let mut has_id = false;
1659
1660    for attr in field.attrs.drain(..) {
1661        let is_id = attr
1662            .path()
1663            .segments
1664            .last()
1665            .map(|seg| seg.ident == "id")
1666            .unwrap_or(false);
1667        if is_id {
1668            has_id = true;
1669        } else {
1670            kept.push(attr);
1671        }
1672    }
1673
1674    field.attrs = kept;
1675    has_id
1676}
1677
1678fn find_id_field(fields: &Fields) -> Option<(Ident, Type)> {
1679    let Fields::Named(named_fields) = fields else {
1680        return None;
1681    };
1682
1683    let mut by_attr: Option<(Ident, Type)> = None;
1684    let mut by_name: Option<(Ident, Type)> = None;
1685
1686    for field in &named_fields.named {
1687        let field_ident = field.ident.clone()?;
1688        if field_ident == "id" {
1689            by_name = Some((field_ident.clone(), field.ty.clone()));
1690        }
1691        let has_id_attr = field.attrs.iter().any(|attr| {
1692            attr.path()
1693                .segments
1694                .last()
1695                .map(|s| s.ident == "id")
1696                .unwrap_or(false)
1697        });
1698        if has_id_attr {
1699            by_attr = Some((field_ident, field.ty.clone()));
1700        }
1701    }
1702
1703    by_attr.or(by_name)
1704}
1705
1706#[derive(Default)]
1707struct ValidateRules {
1708    required: bool,
1709    email: bool,
1710    min_length: Option<usize>,
1711    max_length: Option<usize>,
1712    min: Option<syn::Lit>,
1713    max: Option<syn::Lit>,
1714}
1715
1716impl ValidateRules {
1717    fn has_rules(&self) -> bool {
1718        self.required
1719            || self.email
1720            || self.min_length.is_some()
1721            || self.max_length.is_some()
1722            || self.min.is_some()
1723            || self.max.is_some()
1724    }
1725}
1726
1727fn parse_validate_rules(attrs: &[Attribute]) -> ValidateRules {
1728    let mut rules = ValidateRules::default();
1729
1730    for attr in attrs {
1731        let is_validate = attr
1732            .path()
1733            .segments
1734            .last()
1735            .map(|seg| seg.ident == "validate")
1736            .unwrap_or(false);
1737        if !is_validate {
1738            continue;
1739        }
1740
1741        let _ = attr.parse_nested_meta(|meta| {
1742            if meta.path.is_ident("required") {
1743                rules.required = true;
1744            } else if meta.path.is_ident("email") {
1745                rules.email = true;
1746            } else if meta.path.is_ident("min_length") {
1747                let value = meta.value()?.parse::<syn::LitInt>()?;
1748                rules.min_length = Some(value.base10_parse()?);
1749            } else if meta.path.is_ident("max_length") {
1750                let value = meta.value()?.parse::<syn::LitInt>()?;
1751                rules.max_length = Some(value.base10_parse()?);
1752            } else if meta.path.is_ident("min") {
1753                rules.min = Some(meta.value()?.parse::<syn::Lit>()?);
1754            } else if meta.path.is_ident("max") {
1755                rules.max = Some(meta.value()?.parse::<syn::Lit>()?);
1756            }
1757            Ok(())
1758        });
1759    }
1760
1761    rules
1762}
1763
1764fn is_type_named(ty: &Type, name: &str) -> bool {
1765    match ty {
1766        Type::Path(tp) => tp.path.is_ident(name),
1767        _ => false,
1768    }
1769}
1770
1771fn is_option_any(ty: &Type) -> bool {
1772    match ty {
1773        Type::Path(tp) => tp
1774            .path
1775            .segments
1776            .last()
1777            .map(|seg| seg.ident == "Option")
1778            .unwrap_or(false),
1779        _ => false,
1780    }
1781}
1782
1783fn is_option_of(ty: &Type, inner_name: &str) -> bool {
1784    let Type::Path(tp) = ty else {
1785        return false;
1786    };
1787    let Some(seg) = tp.path.segments.last() else {
1788        return false;
1789    };
1790    if seg.ident != "Option" {
1791        return false;
1792    }
1793    let syn::PathArguments::AngleBracketed(args) = &seg.arguments else {
1794        return false;
1795    };
1796    let Some(syn::GenericArgument::Type(ref inner_ty)) = args.args.first() else {
1797        return false;
1798    };
1799    is_type_named(inner_ty, inner_name)
1800}
1801
1802fn is_numeric_type(ty: &Type) -> bool {
1803    let Type::Path(tp) = ty else {
1804        return false;
1805    };
1806    tp.path.is_ident("u8")
1807        || tp.path.is_ident("u16")
1808        || tp.path.is_ident("u32")
1809        || tp.path.is_ident("u64")
1810        || tp.path.is_ident("usize")
1811        || tp.path.is_ident("i8")
1812        || tp.path.is_ident("i16")
1813        || tp.path.is_ident("i32")
1814        || tp.path.is_ident("i64")
1815        || tp.path.is_ident("isize")
1816        || tp.path.is_ident("f32")
1817        || tp.path.is_ident("f64")
1818}
1819
1820fn is_option_numeric_type(ty: &Type) -> bool {
1821    let Type::Path(tp) = ty else {
1822        return false;
1823    };
1824    let Some(seg) = tp.path.segments.last() else {
1825        return false;
1826    };
1827    if seg.ident != "Option" {
1828        return false;
1829    }
1830    let syn::PathArguments::AngleBracketed(args) = &seg.arguments else {
1831        return false;
1832    };
1833    let Some(syn::GenericArgument::Type(ref inner_ty)) = args.args.first() else {
1834        return false;
1835    };
1836    is_numeric_type(inner_ty)
1837}