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