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#[proc_macro_attribute]
21pub fn controller(attr: TokenStream, item: TokenStream) -> TokenStream {
22 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 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#[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 ensure_derive_trait(&mut input.attrs, "Clone");
66
67 let name = &input.ident;
68
69 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#[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 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 for impl_item in &mut input.items {
131 let ImplItem::Fn(ref mut method) = impl_item else {
132 continue;
133 };
134
135 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 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 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 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 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 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#[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#[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
1110fn 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 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
1561struct 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}