1#![deny(missing_docs)]
20
21use proc_macro::TokenStream;
22use proc_macro2::{Span, TokenStream as TokenStream2};
23use quote::{format_ident, quote};
24use syn::parse::{Parse, ParseStream};
25use syn::punctuated::Punctuated;
26use syn::spanned::Spanned;
27use syn::{
28 parse_macro_input, parse_quote, Attribute, Block, Expr, ExprLit, Fields, FnArg,
29 GenericArgument, Ident, ImplItem, ItemFn, ItemImpl, ItemStruct, Lit, LitInt, LitStr, Meta,
30 PatType, Path, PathArguments, ReturnType, Token, Type,
31};
32
33#[proc_macro_attribute]
41#[allow(non_snake_case)]
42pub fn Injectable(_attr: TokenStream, item: TokenStream) -> TokenStream {
43 let st = parse_macro_input!(item as ItemStruct);
44 let name = st.ident.clone();
45 let name_str = name.to_string();
46
47 let mut deps_tys: Vec<Type> = Vec::new();
51 let mut ctor_field_inits: Vec<TokenStream2> = Vec::new();
52
53 match &st.fields {
54 Fields::Named(named) => {
55 for f in &named.named {
56 let fname = f.ident.as_ref().unwrap();
57 if let Some(inner) = inject_inner_ty(&f.ty) {
58 deps_tys.push(inner.clone());
59 ctor_field_inits.push(quote! {
60 #fname: ::arcly_http::__macro_support::Inject::__from_static(
61 __r.get::<#inner>(),
62 )
63 });
64 } else {
65 let fty = &f.ty;
66 ctor_field_inits.push(quote! {
67 #fname: <#fty as ::core::default::Default>::default()
68 });
69 }
70 }
71 }
72 Fields::Unit => {
73 }
75 Fields::Unnamed(_) => {
76 return syn::Error::new(
77 st.fields.span(),
78 "#[Injectable] does not yet support tuple structs — use a named-field struct",
79 )
80 .to_compile_error()
81 .into();
82 }
83 }
84
85 let deps_ty_paths: Vec<TokenStream2> = deps_tys.iter().map(|t| quote!(#t)).collect();
86 let desc_name = format_ident!("__ARCLY_PROVIDER_{}", name_str.to_uppercase());
87
88 let ctor_body = match &st.fields {
89 Fields::Named(_) => quote! { #name { #( #ctor_field_inits ),* } },
90 Fields::Unit => quote! { #name },
91 Fields::Unnamed(_) => unreachable!(),
92 };
93
94 quote! {
95 #st
96
97 impl #name {
98 #[doc(hidden)]
99 pub fn __arcly_build(__r: &::arcly_http::__macro_support::Resolver<'_>) -> Self {
100 #ctor_body
101 }
102 }
103
104 #[allow(non_upper_case_globals)]
105 static #desc_name: ::arcly_http::__macro_support::ProviderDescriptor =
106 ::arcly_http::__macro_support::ProviderDescriptor {
107 name: #name_str,
108 type_id_fn: || ::core::any::TypeId::of::<#name>(),
109 deps_fn: || ::std::vec![
110 #( ::core::any::TypeId::of::<#deps_ty_paths>() ),*
111 ],
112 build: |__r| ::std::sync::Arc::new(#name::__arcly_build(__r)),
113 };
114
115 impl #name {
116 #[doc(hidden)]
117 pub const fn __arcly_descriptor() -> &'static ::arcly_http::__macro_support::ProviderDescriptor {
118 &#desc_name
119 }
120 }
121 }
122 .into()
123}
124
125fn inject_inner_ty(ty: &Type) -> Option<&Type> {
126 let Type::Path(tp) = ty else { return None };
127 let seg = tp.path.segments.last()?;
128 if seg.ident != "Inject" {
129 return None;
130 }
131 first_generic(&seg.arguments)
132}
133
134struct ModuleArgs {
138 providers: Vec<Path>,
139 controllers: Vec<Path>,
140 imports: Vec<Path>,
141 gateways: Vec<Path>,
142}
143
144impl Parse for ModuleArgs {
145 fn parse(input: ParseStream) -> syn::Result<Self> {
146 let mut out = ModuleArgs {
147 providers: vec![],
148 controllers: vec![],
149 imports: vec![],
150 gateways: vec![],
151 };
152 while !input.is_empty() {
153 let key: Ident = input.parse()?;
154 let content;
155 syn::parenthesized!(content in input);
156 let list: Punctuated<Path, Token![,]> =
157 content.parse_terminated(Path::parse, Token![,])?;
158 match key.to_string().as_str() {
159 "providers" => out.providers.extend(list),
160 "controllers" => out.controllers.extend(list),
161 "imports" => out.imports.extend(list),
162 "gateways" => out.gateways.extend(list),
163 other => return Err(syn::Error::new(
164 key.span(),
165 format!("unknown Module key `{other}` (expected providers/controllers/imports/gateways)"),
166 )),
167 }
168 let _ = input.parse::<Token![,]>();
169 }
170 Ok(out)
171 }
172}
173
174#[proc_macro_attribute]
181#[allow(non_snake_case)]
182pub fn Module(attr: TokenStream, item: TokenStream) -> TokenStream {
183 let args = parse_macro_input!(attr as ModuleArgs);
184 let st = parse_macro_input!(item as ItemStruct);
185 let st_name = &st.ident;
186 let mod_name_str = st_name.to_string();
187
188 let provider_refs: Vec<TokenStream2> = args
189 .providers
190 .iter()
191 .map(|p| {
192 quote! { <#p>::__arcly_descriptor() }
193 })
194 .collect();
195
196 let controller_names: Vec<TokenStream2> = args
200 .controllers
201 .iter()
202 .map(|p| {
203 let n = p
204 .segments
205 .last()
206 .map(|s| s.ident.to_string())
207 .unwrap_or_default();
208 quote! { #n }
209 })
210 .collect();
211
212 let gateway_names: Vec<TokenStream2> = args
215 .gateways
216 .iter()
217 .map(|p| {
218 let n = p
219 .segments
220 .last()
221 .map(|s| s.ident.to_string())
222 .unwrap_or_default();
223 quote! { #n }
224 })
225 .collect();
226
227 let import_fns: Vec<TokenStream2> = args
231 .imports
232 .iter()
233 .map(|p| {
234 quote! { (<#p as ::arcly_http::__macro_support::Module>::descriptor) }
235 })
236 .collect();
237
238 let static_name = format_ident!("__ARCLY_MODULE_{}", st_name.to_string().to_uppercase());
239
240 quote! {
241 #st
242
243 #[allow(non_upper_case_globals)]
244 static #static_name: ::arcly_http::__macro_support::ModuleDescriptor =
245 ::arcly_http::__macro_support::ModuleDescriptor {
246 name: #mod_name_str,
247 providers: &[ #( #provider_refs ),* ],
248 controllers: &[ #( #controller_names ),* ],
249 imports: &[ #( #import_fns ),* ],
250 gateways: &[ #( #gateway_names ),* ],
251 };
252
253 impl ::arcly_http::__macro_support::Module for #st_name {
254 fn descriptor() -> &'static ::arcly_http::__macro_support::ModuleDescriptor {
255 &#static_name
256 }
257 }
258
259 ::arcly_http::inventory::submit! {
260 &#static_name
261 }
262 }
263 .into()
264}
265
266struct RouteArgs {
270 path: LitStr,
271 guards: Vec<Expr>,
272 tags: Vec<LitStr>,
273 security: Vec<LitStr>,
274 summary: Option<LitStr>,
275 description: Option<LitStr>,
276 operation_id: Option<LitStr>,
277 status: Option<LitInt>,
278 deprecated: bool,
279}
280
281impl Parse for RouteArgs {
282 fn parse(input: ParseStream) -> syn::Result<Self> {
283 let path: LitStr = input.parse()?;
284 let mut out = RouteArgs {
285 path,
286 guards: vec![],
287 tags: vec![],
288 security: vec![],
289 summary: None,
290 description: None,
291 operation_id: None,
292 status: None,
293 deprecated: false,
294 };
295 while input.peek(Token![,]) {
296 let _: Token![,] = input.parse()?;
297 if input.is_empty() {
298 break;
299 }
300 let key: Ident = input.parse()?;
301 let k = key.to_string();
302 if k == "deprecated" {
303 out.deprecated = true;
304 continue;
305 }
306 let content;
307 syn::parenthesized!(content in input);
308 match k.as_str() {
309 "guards" => {
310 let list: Punctuated<Expr, Token![,]> = content.parse_terminated(Expr::parse, Token![,])?;
311 out.guards.extend(list);
312 }
313 "tags" => {
314 let list: Punctuated<LitStr, Token![,]> = content.parse_terminated(|s| s.parse::<LitStr>(), Token![,])?;
315 out.tags.extend(list);
316 }
317 "security" => {
318 let list: Punctuated<LitStr, Token![,]> = content.parse_terminated(|s| s.parse::<LitStr>(), Token![,])?;
319 out.security.extend(list);
320 }
321 "summary" => out.summary = Some(content.parse()?),
322 "description" => out.description = Some(content.parse()?),
323 "operation_id" => out.operation_id = Some(content.parse()?),
324 "status" => out.status = Some(content.parse()?),
325 other => return Err(syn::Error::new(
326 key.span(),
327 format!("unknown route key `{other}` (expected guards/tags/security/summary/description/operation_id/status/deprecated)"),
328 )),
329 }
330 }
331 Ok(out)
332 }
333}
334
335struct ControllerArgs {
339 prefix: LitStr,
340 tags: Vec<LitStr>,
341}
342impl Parse for ControllerArgs {
343 fn parse(input: ParseStream) -> syn::Result<Self> {
344 let prefix: LitStr = input.parse()?;
345 let mut tags: Vec<LitStr> = vec![];
346 if input.peek(Token![,]) {
347 let _: Token![,] = input.parse()?;
348 if !input.is_empty() {
349 let key: Ident = input.parse()?;
350 if key != "tags" {
351 return Err(syn::Error::new(key.span(), "expected `tags(...)`"));
352 }
353 let content;
354 syn::parenthesized!(content in input);
355 let list: Punctuated<LitStr, Token![,]> =
356 content.parse_terminated(|s| s.parse::<LitStr>(), Token![,])?;
357 tags.extend(list);
358 }
359 }
360 Ok(Self { prefix, tags })
361 }
362}
363
364#[proc_macro_attribute]
369#[allow(non_snake_case)]
370pub fn Controller(attr: TokenStream, item: TokenStream) -> TokenStream {
371 if let Ok(imp) = syn::parse::<ItemImpl>(item.clone()) {
375 return controller_on_impl(attr, imp);
376 }
377 item
378}
379
380fn controller_on_impl(attr: TokenStream, mut imp: ItemImpl) -> TokenStream {
381 let ControllerArgs {
382 prefix,
383 tags: ctrl_tags,
384 } = parse_macro_input!(attr as ControllerArgs);
385
386 let mut api_version = String::new();
389 let mut sunset = String::new();
390 {
391 let mut keep: Vec<Attribute> = Vec::with_capacity(imp.attrs.len());
392 for a in imp.attrs.drain(..) {
393 let id = a
394 .path()
395 .get_ident()
396 .map(|i| i.to_string())
397 .unwrap_or_default();
398 match id.as_str() {
399 "Version" => {
400 if let Ok(v) = a.parse_args::<LitStr>() {
401 api_version = v.value().trim_matches('/').to_owned();
402 }
403 }
404 "Deprecated" => {
405 let _ = a.parse_nested_meta(|meta| {
406 if meta.path.is_ident("sunset") {
407 let v: LitStr = meta.value()?.parse()?;
408 sunset = v.value();
409 }
410 Ok(())
411 });
412 }
413 _ => keep.push(a),
414 }
415 }
416 imp.attrs = keep;
417 }
418
419 let raw_prefix = prefix.value();
421 let prefix_str = if api_version.is_empty() {
422 raw_prefix
423 } else {
424 format!("/{api_version}{raw_prefix}")
425 };
426 let self_ty = (*imp.self_ty).clone();
427
428 let controller_name = match &self_ty {
431 Type::Path(tp) => tp
432 .path
433 .segments
434 .last()
435 .map(|s| s.ident.to_string())
436 .unwrap_or_default(),
437 _ => String::new(),
438 };
439
440 let mut route_registrations: Vec<TokenStream2> = Vec::new();
441 let mut errors: Vec<syn::Error> = Vec::new();
442
443 for item in imp.items.iter_mut() {
444 let ImplItem::Fn(m) = item else { continue };
445
446 let mut route_attr_idx: Option<usize> = None;
448 let mut route_method: Option<&'static str> = None;
449 let mut interceptor_attr_idxs: Vec<usize> = Vec::new();
450
451 for (i, a) in m.attrs.iter().enumerate() {
452 let ident = a
453 .path()
454 .get_ident()
455 .map(|i| i.to_string())
456 .unwrap_or_default();
457 match ident.as_str() {
458 "Get" | "Post" | "Put" | "Delete" | "Patch" => {
459 route_attr_idx = Some(i);
460 route_method = Some(match ident.as_str() {
461 "Get" => "GET",
462 "Post" => "POST",
463 "Put" => "PUT",
464 "Delete" => "DELETE",
465 "Patch" => "PATCH",
466 _ => unreachable!(),
467 });
468 }
469 "UseInterceptors" => interceptor_attr_idxs.push(i),
470 _ => {}
471 }
472 }
473
474 let Some(idx) = route_attr_idx else { continue };
475 let route_method = route_method.unwrap();
476
477 let route_attr = m.attrs[idx].clone();
479 let route_args: RouteArgs = match route_attr.parse_args() {
480 Ok(a) => a,
481 Err(e) => {
482 errors.push(e);
483 continue;
484 }
485 };
486
487 let mut interceptor_paths: Vec<Path> = Vec::new();
489 for i in &interceptor_attr_idxs {
490 let a = &m.attrs[*i];
491 match a.parse_args_with(Punctuated::<Path, Token![,]>::parse_terminated) {
492 Ok(list) => interceptor_paths.extend(list),
493 Err(e) => errors.push(e),
494 }
495 }
496
497 let local_path = route_args.path.value();
499 let full = join_paths(&prefix_str, &local_path);
500
501 let merged_tags: Vec<LitStr> = ctrl_tags
503 .iter()
504 .cloned()
505 .chain(route_args.tags.iter().cloned())
506 .collect();
507
508 let mut keep: Vec<Attribute> = Vec::with_capacity(m.attrs.len());
511 for (i, a) in m.attrs.iter().enumerate() {
512 if i == idx || interceptor_attr_idxs.contains(&i) {
513 continue;
514 }
515 keep.push(a.clone());
516 }
517 m.attrs = keep;
518
519 let (cache_ttl_secs, cache_key) = match harvest_cache_attrs(&mut m.attrs) {
520 Ok(p) => p,
521 Err(e) => {
522 errors.push(e);
523 continue;
524 }
525 };
526
527 let audit = match harvest_audit_attr(&mut m.attrs) {
528 Ok(a) => a,
529 Err(e) => {
530 errors.push(e);
531 continue;
532 }
533 };
534
535 let timeout_ms = match harvest_timeout_attr(&mut m.attrs) {
536 Ok(t) => t,
537 Err(e) => {
538 errors.push(e);
539 continue;
540 }
541 };
542
543 let transactional = harvest_transactional_attr(&mut m.attrs);
544
545 let idempotent_ttl = match harvest_idempotent_attr(&mut m.attrs) {
546 Ok(t) => t,
547 Err(e) => {
548 errors.push(e);
549 continue;
550 }
551 };
552
553 let policies = match harvest_policies_attr(&mut m.attrs) {
554 Ok(p) => p,
555 Err(e) => {
556 errors.push(e);
557 continue;
558 }
559 };
560
561 let mask_fields = match harvest_mask_attr(&mut m.attrs) {
562 Ok(f) => f,
563 Err(e) => {
564 errors.push(e);
565 continue;
566 }
567 };
568
569 let reg = match build_method_route_registration(
571 &self_ty,
572 m,
573 route_method,
574 full,
575 &route_args,
576 &merged_tags,
577 &interceptor_paths,
578 cache_ttl_secs,
579 &cache_key,
580 &controller_name,
581 &audit,
582 timeout_ms,
583 &api_version,
584 &sunset,
585 transactional,
586 idempotent_ttl,
587 &policies,
588 &mask_fields,
589 ) {
590 Ok(ts) => ts,
591 Err(e) => {
592 errors.push(e);
593 continue;
594 }
595 };
596 route_registrations.push(reg);
597
598 for input in m.sig.inputs.iter_mut() {
600 if let FnArg::Typed(pt) = input {
601 pt.attrs.retain(|a| {
602 let id = a
603 .path()
604 .get_ident()
605 .map(|i| i.to_string())
606 .unwrap_or_default();
607 !matches!(id.as_str(), "Param" | "Query" | "Body" | "Header")
608 });
609 }
610 }
611 }
612
613 if let Some(err) = errors.into_iter().reduce(|mut a, b| {
614 a.combine(b);
615 a
616 }) {
617 return err.to_compile_error().into();
618 }
619
620 quote! {
621 #imp
622 #( #route_registrations )*
623 }
624 .into()
625}
626
627fn harvest_cache_attrs(attrs: &mut Vec<Attribute>) -> syn::Result<(u64, String)> {
631 let mut ttl_secs: u64 = 0;
632 let mut key: String = String::new();
633 let mut keep: Vec<Attribute> = Vec::with_capacity(attrs.len());
634 for a in attrs.drain(..) {
635 let id = a
636 .path()
637 .get_ident()
638 .map(|i| i.to_string())
639 .unwrap_or_default();
640 match id.as_str() {
641 "CacheTTL" => {
642 let n: LitInt = a.parse_args()?;
643 ttl_secs = n.base10_parse()?;
644 }
645 "CacheKey" => {
646 let s: LitStr = a.parse_args()?;
647 key = s.value();
648 }
649 _ => {
650 keep.push(a);
651 }
652 }
653 }
654 *attrs = keep;
655 Ok((ttl_secs, key))
656}
657
658fn harvest_audit_attr(attrs: &mut Vec<Attribute>) -> syn::Result<Option<(String, String)>> {
661 let mut found: Option<(String, String)> = None;
662 let mut keep: Vec<Attribute> = Vec::with_capacity(attrs.len());
663 for a in attrs.drain(..) {
664 let id = a
665 .path()
666 .get_ident()
667 .map(|i| i.to_string())
668 .unwrap_or_default();
669 if id == "AuditLog" {
670 let mut action = String::new();
671 let mut resource = String::new();
672 a.parse_nested_meta(|meta| {
673 if meta.path.is_ident("action") {
674 let v: LitStr = meta.value()?.parse()?;
675 action = v.value();
676 } else if meta.path.is_ident("resource") {
677 let v: LitStr = meta.value()?.parse()?;
678 resource = v.value();
679 } else {
680 return Err(meta.error("expected `action = \"…\"` or `resource = \"…\"`"));
681 }
682 Ok(())
683 })?;
684 if action.is_empty() {
685 return Err(syn::Error::new_spanned(
686 &a,
687 "#[AuditLog] requires action = \"…\"",
688 ));
689 }
690 found = Some((action, resource));
691 } else {
692 keep.push(a);
693 }
694 }
695 *attrs = keep;
696 Ok(found)
697}
698
699fn harvest_timeout_attr(attrs: &mut Vec<Attribute>) -> syn::Result<Option<u64>> {
701 let mut found: Option<u64> = None;
702 let mut keep: Vec<Attribute> = Vec::with_capacity(attrs.len());
703 for a in attrs.drain(..) {
704 let id = a
705 .path()
706 .get_ident()
707 .map(|i| i.to_string())
708 .unwrap_or_default();
709 if id == "Timeout" {
710 let lit: LitStr = a.parse_args()?;
711 let raw = lit.value();
712 let ms = parse_duration_str_ms(&raw).ok_or_else(|| {
713 syn::Error::new_spanned(&a, "expected a duration like \"250ms\", \"2s\", or \"1m\"")
714 })?;
715 found = Some(ms);
716 } else {
717 keep.push(a);
718 }
719 }
720 *attrs = keep;
721 Ok(found)
722}
723
724fn parse_duration_str_ms(s: &str) -> Option<u64> {
725 let s = s.trim();
726 if let Some(v) = s.strip_suffix("ms") {
727 return v.trim().parse().ok();
728 }
729 if let Some(v) = s.strip_suffix('s') {
730 return v.trim().parse::<u64>().ok().map(|n| n * 1_000);
731 }
732 if let Some(v) = s.strip_suffix('h') {
733 return v.trim().parse::<u64>().ok().map(|n| n * 3_600_000);
734 }
735 if let Some(v) = s.strip_suffix('m') {
736 return v.trim().parse::<u64>().ok().map(|n| n * 60_000);
737 }
738 s.parse().ok()
739}
740
741fn harvest_transactional_attr(attrs: &mut Vec<Attribute>) -> bool {
743 let before = attrs.len();
744 attrs.retain(|a| {
745 a.path()
746 .get_ident()
747 .map(|i| i.to_string())
748 .unwrap_or_default()
749 != "Transactional"
750 });
751 attrs.len() != before
752}
753
754fn harvest_idempotent_attr(attrs: &mut Vec<Attribute>) -> syn::Result<Option<u64>> {
756 let mut found: Option<u64> = None;
757 let mut keep: Vec<Attribute> = Vec::with_capacity(attrs.len());
758 for a in attrs.drain(..) {
759 let id = a
760 .path()
761 .get_ident()
762 .map(|i| i.to_string())
763 .unwrap_or_default();
764 if id == "Idempotent" {
765 let mut ttl_secs: u64 = 24 * 3600;
766 a.parse_nested_meta(|meta| {
767 if meta.path.is_ident("ttl") {
768 let v: LitStr = meta.value()?.parse()?;
769 let ms = parse_duration_str_ms(&v.value())
770 .ok_or_else(|| meta.error("expected a duration like \"30m\", \"24h\""))?;
771 ttl_secs = (ms / 1000).max(1);
772 }
773 Ok(())
774 })?;
775 found = Some(ttl_secs);
776 } else {
777 keep.push(a);
778 }
779 }
780 *attrs = keep;
781 Ok(found)
782}
783
784fn harvest_policies_attr(attrs: &mut Vec<Attribute>) -> syn::Result<Vec<LitStr>> {
786 let mut found: Vec<LitStr> = Vec::new();
787 let mut keep: Vec<Attribute> = Vec::with_capacity(attrs.len());
788 for a in attrs.drain(..) {
789 let id = a
790 .path()
791 .get_ident()
792 .map(|i| i.to_string())
793 .unwrap_or_default();
794 if id == "RequirePolicies" {
795 let list = a.parse_args_with(Punctuated::<LitStr, Token![,]>::parse_terminated)?;
796 found.extend(list);
797 } else {
798 keep.push(a);
799 }
800 }
801 *attrs = keep;
802 Ok(found)
803}
804
805fn harvest_mask_attr(attrs: &mut Vec<Attribute>) -> syn::Result<Vec<LitStr>> {
807 let mut found: Vec<LitStr> = Vec::new();
808 let mut keep: Vec<Attribute> = Vec::with_capacity(attrs.len());
809 for a in attrs.drain(..) {
810 let id = a
811 .path()
812 .get_ident()
813 .map(|i| i.to_string())
814 .unwrap_or_default();
815 if id == "MaskFields" {
816 let list = a.parse_args_with(Punctuated::<LitStr, Token![,]>::parse_terminated)?;
817 found.extend(list);
818 } else {
819 keep.push(a);
820 }
821 }
822 *attrs = keep;
823 Ok(found)
824}
825
826fn join_paths(prefix: &str, local: &str) -> String {
827 let p = prefix.trim_end_matches('/');
828 let l = if local.starts_with('/') {
829 local
830 } else {
831 return format!("{p}/{local}");
832 };
833 if p.is_empty() {
834 l.to_owned()
835 } else {
836 format!("{p}{l}")
837 }
838}
839
840fn route_idents(controller: &str, fn_name: &str) -> (Ident, Ident, Ident) {
855 let (thunk, desc, spec) = if controller.is_empty() {
856 (
857 format!("__arcly_thunk_{fn_name}"),
858 format!("__ARCLY_ROUTE_{}", fn_name.to_uppercase()),
859 format!("__ARCLY_SPEC_{}", fn_name.to_uppercase()),
860 )
861 } else {
862 (
863 format!("__arcly_thunk_{controller}_{fn_name}"),
864 format!(
865 "__ARCLY_ROUTE_{}_{}",
866 controller.to_uppercase(),
867 fn_name.to_uppercase()
868 ),
869 format!(
870 "__ARCLY_SPEC_{}_{}",
871 controller.to_uppercase(),
872 fn_name.to_uppercase()
873 ),
874 )
875 };
876 (
877 format_ident!("{}", thunk),
878 format_ident!("{}", desc),
879 format_ident!("{}", spec),
880 )
881}
882
883#[allow(clippy::too_many_arguments)]
887fn build_method_route_registration(
888 self_ty: &Type,
889 m: &syn::ImplItemFn,
890 method: &'static str,
891 full_path: String,
892 args: &RouteArgs,
893 tags: &[LitStr],
894 interceptors: &[Path],
895 cache_ttl_secs: u64,
896 cache_key: &str,
897 controller_name: &str,
898 audit: &Option<(String, String)>,
899 timeout_ms: Option<u64>,
900 api_version: &str,
901 sunset: &str,
902 transactional: bool,
903 idempotent_ttl: Option<u64>,
904 policies: &[LitStr],
905 mask_fields: &[LitStr],
906) -> syn::Result<TokenStream2> {
907 let fn_name = m.sig.ident.clone();
908 let (thunk_name, desc_name, spec_name) = route_idents(controller_name, &fn_name.to_string());
911 let method_ident = Ident::new(method, Span::call_site());
912
913 let doc = collect_doc_comments(&m.attrs);
914
915 let mut extract_stmts: Vec<TokenStream2> = Vec::new();
916 let mut call_args: Vec<TokenStream2> = Vec::new();
917 let mut spec_params: Vec<TokenStream2> = Vec::new();
918 let mut has_body = false;
919 let mut body_ty: Option<Type> = None;
920 let mut query_ty: Option<Type> = None;
921
922 for (i, input) in m.sig.inputs.iter().enumerate() {
923 let FnArg::Typed(pt) = input else {
924 return Err(syn::Error::new(
925 input.span(),
926 "controller methods must not take `self` — use Inject<T> fields instead",
927 ));
928 };
929 let var = format_ident!("__arg_{i}");
930 let (kind, ty) = classify_arg(pt)?;
931 let stmt = emit_extractor(
932 &kind,
933 &ty,
934 &var,
935 &mut spec_params,
936 &mut has_body,
937 &mut body_ty,
938 &mut query_ty,
939 );
940 extract_stmts.push(stmt);
941 call_args.push(quote! { #var });
942 }
943
944 let mut guard_stmts: Vec<TokenStream2> = args
945 .guards
946 .iter()
947 .map(|g| {
948 quote! {
949 <_ as ::arcly_http::__macro_support::Guard>::check(&#g, &ctx)?;
950 }
951 })
952 .collect();
953
954 if !policies.is_empty() {
958 let action_lits: Vec<TokenStream2> = policies.iter().map(|p| quote!(#p)).collect();
959 guard_stmts.push(quote! {
960 ::arcly_http::__macro_support::check_policies(
961 &ctx, &[ #( #action_lits ),* ], ::arcly_http::serde_json::Value::Null,
962 )?;
963 });
964 }
965
966 let run_body = if transactional {
971 quote! {
976 let __tx_ctx = ctx.clone();
977 ::core::result::Result::<_, ::arcly_http::__macro_support::Error>::Ok(
978 ::arcly_http::__macro_support::run_transactional(&__tx_ctx, async move {
979 #( #guard_stmts )*
980 #( #extract_stmts )*
981 <#self_ty>::#fn_name( #( #call_args ),* ).await
982 }).await
983 )
984 }
985 } else {
986 quote! {
987 #( #guard_stmts )*
988 #( #extract_stmts )*
989 ::core::result::Result::<_, ::arcly_http::__macro_support::Error>::Ok(
990 <#self_ty>::#fn_name( #( #call_args ),* ).await
991 )
992 }
993 };
994
995 let inner = quote! {
996 let __run = async move {
997 #run_body
998 };
999 match __run.await {
1000 ::core::result::Result::Ok(v) => {
1001 ::arcly_http::__axum::response::IntoResponse::into_response(v)
1002 }
1003 ::core::result::Result::Err(e) => {
1004 ::arcly_http::__axum::response::IntoResponse::into_response(e)
1005 }
1006 }
1007 };
1008
1009 let inner = match timeout_ms {
1013 Some(ms) => {
1014 let route_lit = LitStr::new(&full_path, Span::call_site());
1015 quote! {
1016 ::arcly_http::__macro_support::run_with_timeout(
1017 #ms, #route_lit, async move { #inner },
1018 ).await
1019 }
1020 }
1021 None => inner,
1022 };
1023
1024 let inner = match audit {
1028 Some((action, resource)) => {
1029 let action_lit = LitStr::new(action, Span::call_site());
1030 let resource_lit = LitStr::new(resource, Span::call_site());
1031 quote! {
1032 let __audit_ctx = ctx.clone();
1033 let __resp: ::arcly_http::__axum::response::Response = { #inner };
1034 ::arcly_http::__macro_support::emit_route_audit(
1035 &__audit_ctx, #action_lit, #resource_lit, __resp.status().as_u16(),
1036 );
1037 __resp
1038 }
1039 }
1040 None => inner,
1041 };
1042
1043 let inner = if mask_fields.is_empty() {
1046 inner
1047 } else {
1048 let mask_lits: Vec<TokenStream2> = mask_fields.iter().map(|f| quote!(#f)).collect();
1049 quote! {
1050 let __mask_ctx = ctx.clone();
1051 let __resp: ::arcly_http::__axum::response::Response = { #inner };
1052 ::arcly_http::__macro_support::mask_response(
1053 &__mask_ctx, &[ #( #mask_lits ),* ], __resp,
1054 ).await
1055 }
1056 };
1057
1058 let inner = match idempotent_ttl {
1061 Some(ttl) => {
1062 let route_lit = LitStr::new(&full_path, Span::call_site());
1063 quote! {
1064 let __idem_ctx = ctx.clone();
1065 ::arcly_http::__macro_support::run_idempotent(
1066 &__idem_ctx, #ttl, #route_lit, async move { #inner },
1067 ).await
1068 }
1069 }
1070 None => inner,
1071 };
1072
1073 let thunk_body = wrap_interceptors(inner, interceptors);
1074
1075 let fn_str = fn_name.to_string();
1076 let summary_str = args
1077 .summary
1078 .as_ref()
1079 .map(|s| s.value())
1080 .unwrap_or_else(|| fn_str.clone());
1081 let operation_id = args
1082 .operation_id
1083 .as_ref()
1084 .map(|s| s.value())
1085 .unwrap_or_else(|| fn_str.clone());
1086 let description_str = args.description.as_ref().map(|s| s.value()).unwrap_or(doc);
1087 let deprecated = args.deprecated;
1088 let tag_lits: Vec<TokenStream2> = tags.iter().map(|t| quote!(#t)).collect();
1089 let sec_lits: Vec<TokenStream2> = args.security.iter().map(|s| quote!(#s)).collect();
1090 let status_expr = match &args.status {
1091 Some(n) => quote! { ::core::option::Option::Some(#n as u16) },
1092 None => quote! { ::core::option::Option::None },
1093 };
1094 let body_schema_expr = schema_expr(&body_ty);
1095 let query_schema_expr = schema_expr(&query_ty);
1096 let response_schema_expr = schema_expr(&extract_response_ty(&m.sig.output));
1097 let full_path_lit = LitStr::new(&full_path, Span::call_site());
1098
1099 let spec_idem_ttl = idempotent_ttl.unwrap_or(0);
1101 let spec_policy_lits: Vec<TokenStream2> = policies.iter().map(|p| quote!(#p)).collect();
1102 let (spec_audit_action, spec_audit_resource) = match audit {
1103 Some((a, r)) => (a.clone(), r.clone()),
1104 None => (String::new(), String::new()),
1105 };
1106 let spec_timeout_ms = timeout_ms.unwrap_or(0);
1107 let spec_mask_lits: Vec<TokenStream2> = mask_fields.iter().map(|f| quote!(#f)).collect();
1108
1109 Ok(quote! {
1110 fn #thunk_name(ctx: ::arcly_http::__macro_support::RequestContext)
1111 -> ::arcly_http::futures::future::BoxFuture<'static, ::arcly_http::__axum::response::Response>
1112 {
1113 ::arcly_http::futures::FutureExt::boxed(async move { #thunk_body })
1114 }
1115
1116 #[allow(non_upper_case_globals)]
1117 static #spec_name: ::arcly_http::__macro_support::RouteSpec =
1118 ::arcly_http::__macro_support::RouteSpec {
1119 summary: #summary_str,
1120 description: #description_str,
1121 operation_id: #operation_id,
1122 tags: &[ #( #tag_lits ),* ],
1123 security: &[ #( #sec_lits ),* ],
1124 status_code: #status_expr,
1125 deprecated: #deprecated,
1126 params: &[ #( #spec_params ),* ],
1127 has_body: #has_body,
1128 body_schema: #body_schema_expr,
1129 query_schema: #query_schema_expr,
1130 response_schema: #response_schema_expr,
1131 cache_ttl_secs: #cache_ttl_secs,
1132 cache_key: #cache_key,
1133 api_version: #api_version,
1134 sunset: #sunset,
1135 idempotent_ttl_secs: #spec_idem_ttl,
1136 policies: &[ #( #spec_policy_lits ),* ],
1137 audit_action: #spec_audit_action,
1138 audit_resource: #spec_audit_resource,
1139 timeout_ms: #spec_timeout_ms,
1140 transactional: #transactional,
1141 mask_fields: &[ #( #spec_mask_lits ),* ],
1142 };
1143
1144 #[allow(non_upper_case_globals)]
1145 static #desc_name: ::arcly_http::__macro_support::RouteDescriptor =
1146 ::arcly_http::__macro_support::RouteDescriptor {
1147 method: ::arcly_http::__macro_support::HttpMethod::#method_ident,
1148 path: #full_path_lit,
1149 handler: #thunk_name,
1150 spec: &#spec_name,
1151 controller: #controller_name,
1152 };
1153
1154 ::arcly_http::inventory::submit! {
1155 &#desc_name
1156 }
1157 })
1158}
1159
1160fn schema_expr(ty: &Option<Type>) -> TokenStream2 {
1161 match ty {
1162 Some(t) => quote! { ::core::option::Option::Some(|| ::arcly_http::__schema_for::<#t>()) },
1163 None => quote! { ::core::option::Option::None },
1164 }
1165}
1166
1167fn wrap_interceptors(inner: TokenStream2, interceptors: &[Path]) -> TokenStream2 {
1179 match interceptors.len() {
1180 0 => return inner,
1181 1 => {
1182 let icp = &interceptors[0];
1183 return quote! {
1184 {
1185 static __ICP: #icp = #icp;
1186 let __inner = ::arcly_http::__macro_support::NextHandler::new(
1187 move |ctx: ::arcly_http::__macro_support::RequestContext|
1188 ::arcly_http::futures::FutureExt::boxed(async move {
1189 let ctx = ctx;
1190 #inner
1191 })
1192 );
1193 <#icp as ::arcly_http::__macro_support::Interceptor>::around(&__ICP, ctx, __inner).await
1194 }
1195 };
1196 }
1197 _ => {}
1198 }
1199
1200 let mut current = quote! {
1202 ::arcly_http::__macro_support::NextHandler::new(
1203 move |ctx: ::arcly_http::__macro_support::RequestContext|
1204 ::arcly_http::futures::FutureExt::boxed(async move {
1205 let ctx = ctx;
1206 #inner
1207 })
1208 )
1209 };
1210 for icp in interceptors.iter().rev() {
1211 current = quote! {
1212 {
1213 static __ICP: #icp = #icp;
1214 let __inner = #current;
1215 ::arcly_http::__macro_support::NextHandler::new(
1216 move |ctx: ::arcly_http::__macro_support::RequestContext| {
1217 <#icp as ::arcly_http::__macro_support::Interceptor>::around(&__ICP, ctx, __inner.__clone_for_chain())
1218 },
1219 )
1220 }
1221 };
1222 }
1223 quote! { #current.run(ctx).await }
1224}
1225
1226#[proc_macro_attribute]
1232#[allow(non_snake_case)]
1233pub fn Get(a: TokenStream, i: TokenStream) -> TokenStream {
1234 route_free_fn(a, i, "GET")
1235}
1236#[proc_macro_attribute]
1238#[allow(non_snake_case)]
1239pub fn Post(a: TokenStream, i: TokenStream) -> TokenStream {
1240 route_free_fn(a, i, "POST")
1241}
1242#[proc_macro_attribute]
1244#[allow(non_snake_case)]
1245pub fn Put(a: TokenStream, i: TokenStream) -> TokenStream {
1246 route_free_fn(a, i, "PUT")
1247}
1248#[proc_macro_attribute]
1250#[allow(non_snake_case)]
1251pub fn Delete(a: TokenStream, i: TokenStream) -> TokenStream {
1252 route_free_fn(a, i, "DELETE")
1253}
1254#[proc_macro_attribute]
1256#[allow(non_snake_case)]
1257pub fn Patch(a: TokenStream, i: TokenStream) -> TokenStream {
1258 route_free_fn(a, i, "PATCH")
1259}
1260
1261#[proc_macro_attribute]
1265#[allow(non_snake_case)]
1266pub fn CacheTTL(_attr: TokenStream, item: TokenStream) -> TokenStream {
1267 item
1268}
1269
1270#[proc_macro_attribute]
1274#[allow(non_snake_case)]
1275pub fn AuditLog(_attr: TokenStream, item: TokenStream) -> TokenStream {
1276 item
1277}
1278
1279#[proc_macro_attribute]
1282#[allow(non_snake_case)]
1283pub fn Timeout(_attr: TokenStream, item: TokenStream) -> TokenStream {
1284 item
1285}
1286
1287#[proc_macro_attribute]
1291#[allow(non_snake_case)]
1292pub fn Version(_attr: TokenStream, item: TokenStream) -> TokenStream {
1293 item
1294}
1295
1296#[proc_macro_attribute]
1300#[allow(non_snake_case)]
1301pub fn Deprecated(_attr: TokenStream, item: TokenStream) -> TokenStream {
1302 item
1303}
1304
1305#[proc_macro_attribute]
1309#[allow(non_snake_case)]
1310pub fn Transactional(_attr: TokenStream, item: TokenStream) -> TokenStream {
1311 item
1312}
1313
1314#[proc_macro_attribute]
1318#[allow(non_snake_case)]
1319pub fn Idempotent(_attr: TokenStream, item: TokenStream) -> TokenStream {
1320 item
1321}
1322
1323#[proc_macro_attribute]
1327#[allow(non_snake_case)]
1328pub fn RequirePolicies(_attr: TokenStream, item: TokenStream) -> TokenStream {
1329 item
1330}
1331
1332#[proc_macro_attribute]
1335#[allow(non_snake_case)]
1336pub fn EventPattern(_attr: TokenStream, item: TokenStream) -> TokenStream {
1337 item
1338}
1339
1340#[proc_macro_attribute]
1345#[allow(non_snake_case)]
1346pub fn EventConsumer(_attr: TokenStream, item: TokenStream) -> TokenStream {
1347 let mut imp = parse_macro_input!(item as ItemImpl);
1348 let self_ty = (*imp.self_ty).clone();
1349 let consumer_name = match &self_ty {
1350 Type::Path(tp) => tp
1351 .path
1352 .segments
1353 .last()
1354 .map(|s| s.ident.to_string())
1355 .unwrap_or_default(),
1356 _ => String::new(),
1357 };
1358
1359 let mut registrations: Vec<TokenStream2> = Vec::new();
1360 let mut errors: Vec<syn::Error> = Vec::new();
1361
1362 for item in imp.items.iter_mut() {
1363 let ImplItem::Fn(m) = item else { continue };
1364 let mut topic: Option<LitStr> = None;
1365 let mut keep: Vec<Attribute> = Vec::with_capacity(m.attrs.len());
1366 for a in m.attrs.drain(..) {
1367 let id = a
1368 .path()
1369 .get_ident()
1370 .map(|i| i.to_string())
1371 .unwrap_or_default();
1372 if id == "EventPattern" {
1373 match a.parse_args::<LitStr>() {
1374 Ok(t) => topic = Some(t),
1375 Err(e) => errors.push(e),
1376 }
1377 } else {
1378 keep.push(a);
1379 }
1380 }
1381 m.attrs = keep;
1382 let Some(topic) = topic else { continue };
1383
1384 let fn_name = m.sig.ident.clone();
1385 let thunk = format_ident!("__arcly_event_{}_{}", consumer_name, fn_name);
1386 let desc = format_ident!("__ARCLY_EVENT_DESC_{}_{}", consumer_name, fn_name);
1387 let consumer_lit = LitStr::new(&consumer_name, Span::call_site());
1388
1389 registrations.push(quote! {
1390 #[allow(non_snake_case)]
1391 fn #thunk(ctx: ::arcly_http::__macro_support::EventContext)
1392 -> ::arcly_http::futures::future::BoxFuture<'static, ::core::result::Result<(), ::arcly_http::__macro_support::EventError>>
1393 {
1394 ::arcly_http::futures::FutureExt::boxed(async move {
1397 <#self_ty>::#fn_name(ctx).await.map_err(::core::convert::Into::into)
1398 })
1399 }
1400
1401 #[allow(non_upper_case_globals)]
1402 static #desc: ::arcly_http::__macro_support::EventHandlerDescriptor =
1403 ::arcly_http::__macro_support::EventHandlerDescriptor {
1404 topic: #topic,
1405 consumer: #consumer_lit,
1406 handler: #thunk,
1407 };
1408
1409 ::arcly_http::inventory::submit! { &#desc }
1410 });
1411 }
1412
1413 if let Some(err) = errors.into_iter().reduce(|mut a, b| {
1414 a.combine(b);
1415 a
1416 }) {
1417 return err.to_compile_error().into();
1418 }
1419
1420 quote! {
1421 #imp
1422 #( #registrations )*
1423 }
1424 .into()
1425}
1426
1427#[proc_macro_attribute]
1431#[allow(non_snake_case)]
1432pub fn MaskFields(_attr: TokenStream, item: TokenStream) -> TokenStream {
1433 item
1434}
1435
1436#[proc_macro_attribute]
1438#[allow(non_snake_case)]
1439pub fn CacheKey(_attr: TokenStream, item: TokenStream) -> TokenStream {
1440 item
1441}
1442
1443#[proc_macro_attribute]
1445#[allow(non_snake_case)]
1446pub fn UseInterceptors(_attr: TokenStream, item: TokenStream) -> TokenStream {
1447 item
1453}
1454
1455fn route_free_fn(attr: TokenStream, item: TokenStream, method: &'static str) -> TokenStream {
1456 let args = parse_macro_input!(attr as RouteArgs);
1457 let mut f = parse_macro_input!(item as ItemFn);
1458
1459 let mut interceptors: Vec<Path> = Vec::new();
1462 let mut keep_attrs: Vec<Attribute> = Vec::with_capacity(f.attrs.len());
1463 for a in f.attrs.drain(..) {
1464 let id = a
1465 .path()
1466 .get_ident()
1467 .map(|i| i.to_string())
1468 .unwrap_or_default();
1469 if id == "UseInterceptors" {
1470 match a.parse_args_with(Punctuated::<Path, Token![,]>::parse_terminated) {
1471 Ok(list) => interceptors.extend(list),
1472 Err(e) => return e.to_compile_error().into(),
1473 }
1474 } else {
1475 keep_attrs.push(a);
1476 }
1477 }
1478 f.attrs = keep_attrs;
1479
1480 let (cache_ttl_secs, cache_key): (u64, String) = match harvest_cache_attrs(&mut f.attrs) {
1481 Ok(p) => p,
1482 Err(e) => return e.to_compile_error().into(),
1483 };
1484
1485 let path_lit = args.path.clone();
1486 let full_path = path_lit.value();
1487
1488 let fn_name = f.sig.ident.clone();
1489 let (thunk_name, desc_name, spec_name) = route_idents("", &fn_name.to_string());
1492 let method_ident = Ident::new(method, Span::call_site());
1493
1494 let doc = collect_doc_comments(&f.attrs);
1495
1496 let mut extract_stmts: Vec<TokenStream2> = Vec::new();
1497 let mut call_args: Vec<TokenStream2> = Vec::new();
1498 let mut errors: Vec<syn::Error> = Vec::new();
1499 let mut spec_params: Vec<TokenStream2> = Vec::new();
1500 let mut has_body = false;
1501 let mut body_ty: Option<Type> = None;
1502 let mut query_ty: Option<Type> = None;
1503
1504 for (i, input) in f.sig.inputs.iter_mut().enumerate() {
1505 let FnArg::Typed(pt) = input else {
1506 errors.push(syn::Error::new(input.span(), "handler must not take self"));
1507 continue;
1508 };
1509 let var = format_ident!("__arg_{i}");
1510 match classify_arg(pt) {
1511 Ok((kind, ty)) => {
1512 let stmt = emit_extractor(
1513 &kind,
1514 &ty,
1515 &var,
1516 &mut spec_params,
1517 &mut has_body,
1518 &mut body_ty,
1519 &mut query_ty,
1520 );
1521 extract_stmts.push(stmt);
1522 call_args.push(quote! { #var });
1523 }
1524 Err(e) => errors.push(e),
1525 }
1526 pt.attrs.retain(|a| {
1527 let id = a
1528 .path()
1529 .get_ident()
1530 .map(|i| i.to_string())
1531 .unwrap_or_default();
1532 !matches!(id.as_str(), "Param" | "Query" | "Body" | "Header")
1533 });
1534 }
1535
1536 if let Some(err) = errors.into_iter().reduce(|mut a, b| {
1537 a.combine(b);
1538 a
1539 }) {
1540 return err.to_compile_error().into();
1541 }
1542
1543 let guard_stmts: Vec<TokenStream2> = args
1544 .guards
1545 .iter()
1546 .map(|g| {
1547 quote! {
1548 <_ as ::arcly_http::__macro_support::Guard>::check(&#g, &ctx)?;
1549 }
1550 })
1551 .collect();
1552
1553 let inner = quote! {
1554 let __run = async move {
1555 #( #guard_stmts )*
1556 #( #extract_stmts )*
1557 ::core::result::Result::<_, ::arcly_http::__macro_support::Error>::Ok(
1558 #fn_name( #( #call_args ),* ).await
1559 )
1560 };
1561 match __run.await {
1562 ::core::result::Result::Ok(v) => {
1563 ::arcly_http::__axum::response::IntoResponse::into_response(v)
1564 }
1565 ::core::result::Result::Err(e) => {
1566 ::arcly_http::__axum::response::IntoResponse::into_response(e)
1567 }
1568 }
1569 };
1570
1571 let thunk_body = wrap_interceptors(inner, &interceptors);
1572
1573 let fn_str = fn_name.to_string();
1574 let summary_str = args
1575 .summary
1576 .as_ref()
1577 .map(|s| s.value())
1578 .unwrap_or_else(|| fn_str.clone());
1579 let operation_id = args
1580 .operation_id
1581 .as_ref()
1582 .map(|s| s.value())
1583 .unwrap_or_else(|| fn_str.clone());
1584 let description_str = args.description.as_ref().map(|s| s.value()).unwrap_or(doc);
1585 let deprecated = args.deprecated;
1586 let tag_lits: Vec<TokenStream2> = args.tags.iter().map(|t| quote!(#t)).collect();
1587 let sec_lits: Vec<TokenStream2> = args.security.iter().map(|s| quote!(#s)).collect();
1588 let status_expr = match &args.status {
1589 Some(n) => quote! { ::core::option::Option::Some(#n as u16) },
1590 None => quote! { ::core::option::Option::None },
1591 };
1592 let body_schema_expr = schema_expr(&body_ty);
1593 let query_schema_expr = schema_expr(&query_ty);
1594 let response_schema_expr = schema_expr(&extract_response_ty(&f.sig.output));
1595 let full_path_lit = LitStr::new(&full_path, Span::call_site());
1596
1597 quote! {
1598 #f
1599
1600 fn #thunk_name(ctx: ::arcly_http::__macro_support::RequestContext)
1601 -> ::arcly_http::futures::future::BoxFuture<'static, ::arcly_http::__axum::response::Response>
1602 {
1603 ::arcly_http::futures::FutureExt::boxed(async move { #thunk_body })
1604 }
1605
1606 #[allow(non_upper_case_globals)]
1607 static #spec_name: ::arcly_http::__macro_support::RouteSpec =
1608 ::arcly_http::__macro_support::RouteSpec {
1609 summary: #summary_str,
1610 description: #description_str,
1611 operation_id: #operation_id,
1612 tags: &[ #( #tag_lits ),* ],
1613 security: &[ #( #sec_lits ),* ],
1614 status_code: #status_expr,
1615 deprecated: #deprecated,
1616 params: &[ #( #spec_params ),* ],
1617 has_body: #has_body,
1618 body_schema: #body_schema_expr,
1619 query_schema: #query_schema_expr,
1620 response_schema: #response_schema_expr,
1621 cache_ttl_secs: #cache_ttl_secs,
1622 cache_key: #cache_key,
1623 api_version: "",
1624 sunset: "",
1625 idempotent_ttl_secs: 0,
1626 policies: &[],
1627 audit_action: "",
1628 audit_resource: "",
1629 timeout_ms: 0,
1630 transactional: false,
1631 mask_fields: &[],
1632 };
1633
1634 #[allow(non_upper_case_globals)]
1635 static #desc_name: ::arcly_http::__macro_support::RouteDescriptor =
1636 ::arcly_http::__macro_support::RouteDescriptor {
1637 method: ::arcly_http::__macro_support::HttpMethod::#method_ident,
1638 path: #full_path_lit,
1639 handler: #thunk_name,
1640 spec: &#spec_name,
1641 controller: "",
1642 };
1643
1644 ::arcly_http::inventory::submit! {
1645 &#desc_name
1646 }
1647 }
1648 .into()
1649}
1650
1651enum ParamKind {
1655 Param(LitStr),
1656 Query,
1657 Body,
1658 Header(LitStr),
1659 Ctx,
1660 FromContext,
1661}
1662
1663fn classify_arg(arg: &PatType) -> syn::Result<(ParamKind, Type)> {
1664 for attr in &arg.attrs {
1665 let ident = attr
1666 .path()
1667 .get_ident()
1668 .map(|i| i.to_string())
1669 .unwrap_or_default();
1670 match ident.as_str() {
1671 "Param" => {
1672 let name: LitStr = attr.parse_args()?;
1673 return Ok((ParamKind::Param(name), (*arg.ty).clone()));
1674 }
1675 "Query" => return Ok((ParamKind::Query, (*arg.ty).clone())),
1676 "Body" => return Ok((ParamKind::Body, (*arg.ty).clone())),
1677 "Header" => {
1678 let name: LitStr = attr.parse_args()?;
1679 return Ok((ParamKind::Header(name), (*arg.ty).clone()));
1680 }
1681 _ => {}
1682 }
1683 }
1684 let ty_ref = &*arg.ty;
1685 let ty_str = quote!(#ty_ref).to_string();
1686 let ty = (*arg.ty).clone();
1687 let kind = if ty_str.contains("RequestContext") {
1688 ParamKind::Ctx
1689 } else {
1690 ParamKind::FromContext
1691 };
1692 Ok((kind, ty))
1693}
1694
1695fn emit_extractor(
1696 kind: &ParamKind,
1697 ty: &Type,
1698 var: &Ident,
1699 spec_params: &mut Vec<TokenStream2>,
1700 has_body: &mut bool,
1701 body_ty: &mut Option<Type>,
1702 query_ty: &mut Option<Type>,
1703) -> TokenStream2 {
1704 match kind {
1705 ParamKind::Param(name) => {
1706 spec_params.push(quote! {
1707 ::arcly_http::__macro_support::ParamSpec {
1708 name: #name,
1709 loc: ::arcly_http::__macro_support::ParamLoc::Path,
1710 required: true,
1711 schema: || ::arcly_http::__schema_for::<#ty>(),
1712 }
1713 });
1714 quote! { let #var: #ty = ::arcly_http::__macro_support::extract_param(&ctx, #name)?; }
1715 }
1716 ParamKind::Query => {
1717 *query_ty = Some(ty.clone());
1718 quote! { let #var: #ty = ::arcly_http::__macro_support::extract_query_validated(&ctx)?; }
1719 }
1720 ParamKind::Body => {
1721 *has_body = true;
1722 *body_ty = Some(ty.clone());
1723 quote! { let #var: #ty = ::arcly_http::__macro_support::extract_body_validated(&ctx)?; }
1724 }
1725 ParamKind::Header(name) => {
1726 spec_params.push(quote! {
1727 ::arcly_http::__macro_support::ParamSpec {
1728 name: #name,
1729 loc: ::arcly_http::__macro_support::ParamLoc::Header,
1730 required: true,
1731 schema: || ::arcly_http::__schema_for::<#ty>(),
1732 }
1733 });
1734 quote! { let #var: #ty = ::arcly_http::__macro_support::extract_header(&ctx, #name)?.to_owned(); }
1735 }
1736 ParamKind::Ctx => quote! { let #var: #ty = ctx.clone(); },
1737 ParamKind::FromContext => quote! { let #var: #ty = <#ty>::from_ctx(&ctx); },
1738 }
1739}
1740
1741fn extract_response_ty(ret: &ReturnType) -> Option<Type> {
1745 let ty = match ret {
1746 ReturnType::Default => return None,
1747 ReturnType::Type(_, ty) => &**ty,
1748 };
1749 inner_payload_ty(ty).cloned()
1750}
1751
1752fn inner_payload_ty(ty: &Type) -> Option<&Type> {
1753 let Type::Path(tp) = ty else { return None };
1754 let seg = tp.path.segments.last()?;
1755 match seg.ident.to_string().as_str() {
1756 "Json" | "Created" | "Accepted" => first_generic(&seg.arguments),
1757 "Result" => {
1758 let ok = first_generic(&seg.arguments)?;
1759 inner_payload_ty(ok)
1760 }
1761 "NoContent" => None,
1762 _ => None,
1763 }
1764}
1765
1766fn first_generic(args: &PathArguments) -> Option<&Type> {
1767 let PathArguments::AngleBracketed(ab) = args else {
1768 return None;
1769 };
1770 ab.args.iter().find_map(|a| match a {
1771 GenericArgument::Type(t) => Some(t),
1772 _ => None,
1773 })
1774}
1775
1776fn collect_doc_comments(attrs: &[Attribute]) -> String {
1777 let mut out = String::new();
1778 for a in attrs {
1779 if !a.path().is_ident("doc") {
1780 continue;
1781 }
1782 if let Meta::NameValue(nv) = &a.meta {
1783 if let Expr::Lit(ExprLit {
1784 lit: Lit::Str(s), ..
1785 }) = &nv.value
1786 {
1787 let line = s.value();
1788 if !out.is_empty() {
1789 out.push('\n');
1790 }
1791 out.push_str(line.trim_start());
1792 }
1793 }
1794 }
1795 out
1796}
1797
1798struct GatewayArgs {
1809 path: LitStr,
1810 #[allow(dead_code)]
1811 tags: Vec<LitStr>,
1812}
1813
1814impl Parse for GatewayArgs {
1815 fn parse(input: ParseStream) -> syn::Result<Self> {
1816 let path: LitStr = input.parse()?;
1817 let mut tags: Vec<LitStr> = vec![];
1818 if input.peek(Token![,]) {
1819 let _: Token![,] = input.parse()?;
1820 if !input.is_empty() {
1821 let key: Ident = input.parse()?;
1822 if key != "tags" {
1823 return Err(syn::Error::new(key.span(), "expected `tags(...)`"));
1824 }
1825 let content;
1826 syn::parenthesized!(content in input);
1827 let list: Punctuated<LitStr, Token![,]> =
1828 content.parse_terminated(|s| s.parse::<LitStr>(), Token![,])?;
1829 tags.extend(list);
1830 }
1831 }
1832 Ok(Self { path, tags })
1833 }
1834}
1835
1836#[proc_macro_attribute]
1842#[allow(non_snake_case)]
1843pub fn Gateway(attr: TokenStream, item: TokenStream) -> TokenStream {
1844 match syn::parse::<ItemImpl>(item) {
1845 Ok(imp) => gateway_on_impl(attr, imp),
1846 Err(_) => syn::Error::new(
1847 Span::call_site(),
1848 "#[Gateway(\"/path\")] must be placed on the gateway's `impl` block \
1849 (the struct uses #[Injectable]; lifecycle goes in `impl ArclyGateway`)",
1850 )
1851 .to_compile_error()
1852 .into(),
1853 }
1854}
1855
1856#[proc_macro_attribute]
1859#[allow(non_snake_case)]
1860pub fn Subscribe(_attr: TokenStream, item: TokenStream) -> TokenStream {
1861 item
1862}
1863
1864fn gateway_on_impl(attr: TokenStream, mut imp: ItemImpl) -> TokenStream {
1865 let GatewayArgs { path, .. } = parse_macro_input!(attr as GatewayArgs);
1866 let self_ty = (*imp.self_ty).clone();
1867 let name = match &self_ty {
1868 Type::Path(tp) => tp
1869 .path
1870 .segments
1871 .last()
1872 .map(|s| s.ident.to_string())
1873 .unwrap_or_default(),
1874 _ => String::new(),
1875 };
1876
1877 let mut dispatch_inserts: Vec<TokenStream2> = Vec::new();
1878 let mut errors: Vec<syn::Error> = Vec::new();
1879
1880 for item in imp.items.iter_mut() {
1881 let ImplItem::Fn(m) = item else { continue };
1882
1883 let sub_idx = m.attrs.iter().position(|a| {
1885 a.path()
1886 .get_ident()
1887 .map(|i| i.to_string())
1888 .unwrap_or_default()
1889 == "Subscribe"
1890 });
1891 let Some(idx) = sub_idx else { continue };
1892
1893 let event: LitStr = match m.attrs[idx].parse_args() {
1894 Ok(e) => e,
1895 Err(e) => {
1896 errors.push(e);
1897 continue;
1898 }
1899 };
1900 m.attrs.remove(idx); let fn_name = m.sig.ident.clone();
1903 match build_subscribe_insert(&self_ty, m, &event, &fn_name) {
1904 Ok(ts) => dispatch_inserts.push(ts),
1905 Err(e) => errors.push(e),
1906 }
1907 }
1908
1909 if let Some(err) = errors.into_iter().reduce(|mut a, b| {
1910 a.combine(b);
1911 a
1912 }) {
1913 return err.to_compile_error().into();
1914 }
1915
1916 let path_lit = LitStr::new(&path.value(), Span::call_site());
1917 let name_lit = LitStr::new(&name, Span::call_site());
1918 let build_ident = format_ident!("__arcly_build_gateway_{}", name.to_uppercase());
1919 let desc_ident = format_ident!("__ARCLY_GATEWAY_{}", name.to_uppercase());
1920
1921 quote! {
1922 #imp
1923
1924 #[doc(hidden)]
1925 #[allow(non_snake_case)]
1926 fn #build_ident(__container: &'static ::arcly_http::__macro_support::FrozenDiContainer)
1927 -> &'static ::arcly_http::__macro_support::GatewayRuntime
1928 {
1929 let __gw: &'static #self_ty = ::std::boxed::Box::leak(::std::boxed::Box::new(
1932 <#self_ty>::__arcly_build(&__container.resolver())
1933 ));
1934
1935 let mut __dispatch: ::std::collections::HashMap<
1936 &'static str,
1937 ::arcly_http::__macro_support::MessageHandler,
1938 > = ::std::collections::HashMap::new();
1939 #( #dispatch_inserts )*
1940
1941 ::std::boxed::Box::leak(::std::boxed::Box::new(
1942 ::arcly_http::__macro_support::GatewayRuntime {
1943 path: #path_lit,
1944 on_connect: ::std::boxed::Box::new(move |__c: ::arcly_http::__macro_support::WsClient| {
1945 let __gw = __gw;
1946 ::arcly_http::futures::FutureExt::boxed(async move {
1947 <#self_ty as ::arcly_http::__macro_support::ArclyGateway>::on_connect(__gw, __c).await
1948 })
1949 }),
1950 on_disconnect: ::std::boxed::Box::new(move |__c: ::arcly_http::__macro_support::WsClient| {
1951 let __gw = __gw;
1952 ::arcly_http::futures::FutureExt::boxed(async move {
1953 <#self_ty as ::arcly_http::__macro_support::ArclyGateway>::on_disconnect(__gw, __c).await
1954 })
1955 }),
1956 dispatch: __dispatch,
1957 }
1958 ))
1959 }
1960
1961 #[allow(non_upper_case_globals)]
1962 static #desc_ident: ::arcly_http::__macro_support::GatewayDescriptor =
1963 ::arcly_http::__macro_support::GatewayDescriptor {
1964 name: #name_lit,
1965 path: #path_lit,
1966 build: #build_ident,
1967 };
1968
1969 ::arcly_http::inventory::submit! { &#desc_ident }
1970 }
1971 .into()
1972}
1973
1974fn build_subscribe_insert(
1979 self_ty: &Type,
1980 m: &syn::ImplItemFn,
1981 event: &LitStr,
1982 fn_name: &Ident,
1983) -> syn::Result<TokenStream2> {
1984 let mut extract_stmts: Vec<TokenStream2> = Vec::new();
1985 let mut call_args: Vec<TokenStream2> = Vec::new();
1986
1987 for (i, input) in m.sig.inputs.iter().enumerate() {
1988 let pt = match input {
1989 FnArg::Receiver(_) => continue, FnArg::Typed(pt) => pt,
1991 };
1992 let ty = (*pt.ty).clone();
1993 let var = format_ident!("__sub_arg_{i}");
1994
1995 if type_last_ident_is(&ty, "WsClient") {
1996 extract_stmts.push(
1997 quote! { let #var: ::arcly_http::__macro_support::WsClient = __client.clone(); },
1998 );
1999 call_args.push(quote! { #var });
2000 } else if let Some(inner) = json_inner_ty(&ty) {
2001 extract_stmts.push(quote! {
2002 let #var: ::arcly_http::__macro_support::Json<#inner> = ::arcly_http::__macro_support::Json(
2003 ::arcly_http::serde_json::from_str::<#inner>(&__data)
2004 .map_err(|_| ::arcly_http::__macro_support::Error::BadRequest("invalid websocket payload"))?
2005 );
2006 });
2007 call_args.push(quote! { #var });
2008 } else {
2009 return Err(syn::Error::new(
2010 pt.span(),
2011 "#[Subscribe] handler params must be `WsClient` or `Json<T>`",
2012 ));
2013 }
2014 }
2015
2016 Ok(quote! {
2017 {
2018 let __gw = __gw;
2019 let __handler: ::arcly_http::__macro_support::MessageHandler = ::std::sync::Arc::new(
2020 move |__client: ::arcly_http::__macro_support::WsClient, __data: ::std::sync::Arc<str>| {
2021 let __gw = __gw;
2022 ::arcly_http::futures::FutureExt::boxed(async move {
2023 #( #extract_stmts )*
2024 <#self_ty>::#fn_name(__gw, #( #call_args ),*).await
2025 })
2026 }
2027 );
2028 __dispatch.insert(#event, __handler);
2029 }
2030 })
2031}
2032
2033fn type_last_ident_is(ty: &Type, name: &str) -> bool {
2034 matches!(ty, Type::Path(tp)
2035 if tp.path.segments.last().map(|s| s.ident == name).unwrap_or(false))
2036}
2037
2038fn json_inner_ty(ty: &Type) -> Option<Type> {
2039 let Type::Path(tp) = ty else { return None };
2040 let seg = tp.path.segments.last()?;
2041 if seg.ident != "Json" {
2042 return None;
2043 }
2044 first_generic(&seg.arguments).cloned()
2045}
2046
2047struct BreakerArgs {
2051 threshold: u32,
2052 cooldown_millis: u64,
2053}
2054
2055impl Parse for BreakerArgs {
2056 fn parse(input: ParseStream) -> syn::Result<Self> {
2057 let mut threshold: Option<u32> = None;
2058 let mut cooldown_millis: Option<u64> = None;
2059 while !input.is_empty() {
2060 let key: Ident = input.parse()?;
2061 let _: Token![=] = input.parse()?;
2062 match key.to_string().as_str() {
2063 "threshold" => {
2064 let n: LitInt = input.parse()?;
2065 threshold = Some(n.base10_parse()?);
2066 }
2067 "cooldown" => {
2068 let s: LitStr = input.parse()?;
2069 cooldown_millis = Some(parse_duration_ms(&s)?);
2070 }
2071 other => {
2072 return Err(syn::Error::new(
2073 key.span(),
2074 format!("unknown circuit_breaker key `{other}`"),
2075 ))
2076 }
2077 }
2078 let _ = input.parse::<Token![,]>();
2079 }
2080 Ok(Self {
2081 threshold: threshold
2082 .ok_or_else(|| syn::Error::new(input.span(), "missing `threshold = N`"))?,
2083 cooldown_millis: cooldown_millis
2084 .ok_or_else(|| syn::Error::new(input.span(), "missing `cooldown = \"…\"`"))?,
2085 })
2086 }
2087}
2088
2089fn parse_duration_ms(s: &LitStr) -> syn::Result<u64> {
2090 let raw = s.value();
2091 let r = raw.trim();
2092 let (num_s, unit) = match r.rfind(|c: char| c.is_ascii_digit()) {
2093 Some(i) => (&r[..=i], &r[i + 1..]),
2094 None => return Err(syn::Error::new(s.span(), "invalid duration")),
2095 };
2096 let n: u64 = num_s
2097 .parse()
2098 .map_err(|_| syn::Error::new(s.span(), "invalid duration number"))?;
2099 let mult = match unit.trim() {
2100 "ms" => 1,
2101 "s" | "" => 1_000,
2102 "m" => 60_000,
2103 "h" => 3_600_000,
2104 other => {
2105 return Err(syn::Error::new(
2106 s.span(),
2107 format!("unknown duration unit `{other}`"),
2108 ))
2109 }
2110 };
2111 Ok(n * mult)
2112}
2113
2114#[proc_macro_attribute]
2119pub fn circuit_breaker(attr: TokenStream, item: TokenStream) -> TokenStream {
2120 let args = parse_macro_input!(attr as BreakerArgs);
2121 let mut f = parse_macro_input!(item as syn::ImplItemFn);
2122
2123 let threshold = args.threshold;
2124 let cooldown_ms = args.cooldown_millis;
2125
2126 let breaker_name = format_ident!("__BREAKER_{}", f.sig.ident.to_string().to_uppercase());
2127 let breaker_label = f.sig.ident.to_string();
2128
2129 let original_body = f.block.clone();
2131 let new_block: Block = parse_quote! {{
2132 static #breaker_name: ::arcly_http::__macro_support::CircuitBreaker =
2133 ::arcly_http::__macro_support::CircuitBreaker::const_named(
2134 #breaker_label, #threshold, #cooldown_ms,
2135 );
2136 match #breaker_name.execute(|| async move #original_body).await {
2137 ::core::result::Result::Ok(inner) => inner,
2138 ::core::result::Result::Err(_open) => ::core::result::Result::Err(
2139 <_ as ::core::convert::From<::arcly_http::__macro_support::BreakerOpen>>::from(_open),
2140 ),
2141 }
2142 }};
2143 f.block = new_block;
2144
2145 quote! { #f }.into()
2146}
2147
2148struct EncryptFieldsArgs {
2154 key: LitStr,
2155 fields: Vec<LitStr>,
2156}
2157
2158impl Parse for EncryptFieldsArgs {
2159 fn parse(input: ParseStream) -> syn::Result<Self> {
2160 let mut key: Option<LitStr> = None;
2161 let mut fields: Vec<LitStr> = Vec::new();
2162 while !input.is_empty() {
2163 let ident: Ident = input.parse()?;
2164 match ident.to_string().as_str() {
2165 "key" => {
2166 input.parse::<Token![=]>()?;
2167 key = Some(input.parse()?);
2168 }
2169 "fields" => {
2170 let inner;
2171 syn::parenthesized!(inner in input);
2172 let lits: Punctuated<LitStr, Token![,]> =
2173 inner.parse_terminated(|p: ParseStream| p.parse::<LitStr>(), Token![,])?;
2174 fields.extend(lits);
2175 }
2176 other => {
2177 return Err(syn::Error::new(
2178 ident.span(),
2179 format!(
2180 "unknown EncryptFields argument `{other}` (expected `key` or `fields`)"
2181 ),
2182 ))
2183 }
2184 }
2185 if input.peek(Token![,]) {
2186 input.parse::<Token![,]>()?;
2187 }
2188 }
2189 let key = key.ok_or_else(|| {
2190 syn::Error::new(Span::call_site(), "EncryptFields requires `key = \"...\"`")
2191 })?;
2192 if fields.is_empty() {
2193 return Err(syn::Error::new(
2194 Span::call_site(),
2195 "EncryptFields requires at least one entry in `fields(...)`",
2196 ));
2197 }
2198 Ok(Self { key, fields })
2199 }
2200}
2201
2202#[proc_macro_attribute]
2209#[allow(non_snake_case)]
2210pub fn EncryptFields(attr: TokenStream, item: TokenStream) -> TokenStream {
2211 let args = parse_macro_input!(attr as EncryptFieldsArgs);
2212 let st = parse_macro_input!(item as ItemStruct);
2213 let name = &st.ident;
2214 let (impl_g, ty_g, where_c) = st.generics.split_for_impl();
2215 let key = &args.key;
2216 let fields = &args.fields;
2217
2218 quote! {
2219 #st
2220
2221 impl #impl_g ::arcly_http::__macro_support::EncryptRecord for #name #ty_g #where_c {
2222 const ENCRYPT_FIELDS: &'static [&'static str] = &[ #( #fields ),* ];
2223 const KEY_ID: &'static str = #key;
2224 }
2225 }
2226 .into()
2227}