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