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