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<(), ::std::string::String>>
1372 {
1373 ::arcly_http::futures::FutureExt::boxed(<#self_ty>::#fn_name(ctx))
1374 }
1375
1376 #[allow(non_upper_case_globals)]
1377 static #desc: ::arcly_http::__macro_support::EventHandlerDescriptor =
1378 ::arcly_http::__macro_support::EventHandlerDescriptor {
1379 topic: #topic,
1380 consumer: #consumer_lit,
1381 handler: #thunk,
1382 };
1383
1384 ::arcly_http::inventory::submit! { &#desc }
1385 });
1386 }
1387
1388 if let Some(err) = errors.into_iter().reduce(|mut a, b| {
1389 a.combine(b);
1390 a
1391 }) {
1392 return err.to_compile_error().into();
1393 }
1394
1395 quote! {
1396 #imp
1397 #( #registrations )*
1398 }
1399 .into()
1400}
1401
1402#[proc_macro_attribute]
1406#[allow(non_snake_case)]
1407pub fn MaskFields(_attr: TokenStream, item: TokenStream) -> TokenStream {
1408 item
1409}
1410
1411#[proc_macro_attribute]
1413#[allow(non_snake_case)]
1414pub fn CacheKey(_attr: TokenStream, item: TokenStream) -> TokenStream {
1415 item
1416}
1417
1418#[proc_macro_attribute]
1420#[allow(non_snake_case)]
1421pub fn UseInterceptors(_attr: TokenStream, item: TokenStream) -> TokenStream {
1422 item
1428}
1429
1430fn route_free_fn(attr: TokenStream, item: TokenStream, method: &'static str) -> TokenStream {
1431 let args = parse_macro_input!(attr as RouteArgs);
1432 let mut f = parse_macro_input!(item as ItemFn);
1433
1434 let mut interceptors: Vec<Path> = Vec::new();
1437 let mut keep_attrs: Vec<Attribute> = Vec::with_capacity(f.attrs.len());
1438 for a in f.attrs.drain(..) {
1439 let id = a
1440 .path()
1441 .get_ident()
1442 .map(|i| i.to_string())
1443 .unwrap_or_default();
1444 if id == "UseInterceptors" {
1445 match a.parse_args_with(Punctuated::<Path, Token![,]>::parse_terminated) {
1446 Ok(list) => interceptors.extend(list),
1447 Err(e) => return e.to_compile_error().into(),
1448 }
1449 } else {
1450 keep_attrs.push(a);
1451 }
1452 }
1453 f.attrs = keep_attrs;
1454
1455 let (cache_ttl_secs, cache_key): (u64, String) = match harvest_cache_attrs(&mut f.attrs) {
1456 Ok(p) => p,
1457 Err(e) => return e.to_compile_error().into(),
1458 };
1459
1460 let path_lit = args.path.clone();
1461 let full_path = path_lit.value();
1462
1463 let fn_name = f.sig.ident.clone();
1464 let (thunk_name, desc_name, spec_name) = route_idents("", &fn_name.to_string());
1467 let method_ident = Ident::new(method, Span::call_site());
1468
1469 let doc = collect_doc_comments(&f.attrs);
1470
1471 let mut extract_stmts: Vec<TokenStream2> = Vec::new();
1472 let mut call_args: Vec<TokenStream2> = Vec::new();
1473 let mut errors: Vec<syn::Error> = Vec::new();
1474 let mut spec_params: Vec<TokenStream2> = Vec::new();
1475 let mut has_body = false;
1476 let mut body_ty: Option<Type> = None;
1477 let mut query_ty: Option<Type> = None;
1478
1479 for (i, input) in f.sig.inputs.iter_mut().enumerate() {
1480 let FnArg::Typed(pt) = input else {
1481 errors.push(syn::Error::new(input.span(), "handler must not take self"));
1482 continue;
1483 };
1484 let var = format_ident!("__arg_{i}");
1485 match classify_arg(pt) {
1486 Ok((kind, ty)) => {
1487 let stmt = emit_extractor(
1488 &kind,
1489 &ty,
1490 &var,
1491 &mut spec_params,
1492 &mut has_body,
1493 &mut body_ty,
1494 &mut query_ty,
1495 );
1496 extract_stmts.push(stmt);
1497 call_args.push(quote! { #var });
1498 }
1499 Err(e) => errors.push(e),
1500 }
1501 pt.attrs.retain(|a| {
1502 let id = a
1503 .path()
1504 .get_ident()
1505 .map(|i| i.to_string())
1506 .unwrap_or_default();
1507 !matches!(id.as_str(), "Param" | "Query" | "Body" | "Header")
1508 });
1509 }
1510
1511 if let Some(err) = errors.into_iter().reduce(|mut a, b| {
1512 a.combine(b);
1513 a
1514 }) {
1515 return err.to_compile_error().into();
1516 }
1517
1518 let guard_stmts: Vec<TokenStream2> = args
1519 .guards
1520 .iter()
1521 .map(|g| {
1522 quote! {
1523 <_ as ::arcly_http::__macro_support::Guard>::check(&#g, &ctx)?;
1524 }
1525 })
1526 .collect();
1527
1528 let inner = quote! {
1529 let __run = async move {
1530 #( #guard_stmts )*
1531 #( #extract_stmts )*
1532 ::core::result::Result::<_, ::arcly_http::__macro_support::Error>::Ok(
1533 #fn_name( #( #call_args ),* ).await
1534 )
1535 };
1536 match __run.await {
1537 ::core::result::Result::Ok(v) => {
1538 ::arcly_http::__axum::response::IntoResponse::into_response(v)
1539 }
1540 ::core::result::Result::Err(e) => {
1541 ::arcly_http::__axum::response::IntoResponse::into_response(e)
1542 }
1543 }
1544 };
1545
1546 let thunk_body = wrap_interceptors(inner, &interceptors);
1547
1548 let fn_str = fn_name.to_string();
1549 let summary_str = args
1550 .summary
1551 .as_ref()
1552 .map(|s| s.value())
1553 .unwrap_or_else(|| fn_str.clone());
1554 let operation_id = args
1555 .operation_id
1556 .as_ref()
1557 .map(|s| s.value())
1558 .unwrap_or_else(|| fn_str.clone());
1559 let description_str = args.description.as_ref().map(|s| s.value()).unwrap_or(doc);
1560 let deprecated = args.deprecated;
1561 let tag_lits: Vec<TokenStream2> = args.tags.iter().map(|t| quote!(#t)).collect();
1562 let sec_lits: Vec<TokenStream2> = args.security.iter().map(|s| quote!(#s)).collect();
1563 let status_expr = match &args.status {
1564 Some(n) => quote! { ::core::option::Option::Some(#n as u16) },
1565 None => quote! { ::core::option::Option::None },
1566 };
1567 let body_schema_expr = schema_expr(&body_ty);
1568 let query_schema_expr = schema_expr(&query_ty);
1569 let response_schema_expr = schema_expr(&extract_response_ty(&f.sig.output));
1570 let full_path_lit = LitStr::new(&full_path, Span::call_site());
1571
1572 quote! {
1573 #f
1574
1575 fn #thunk_name(ctx: ::arcly_http::__macro_support::RequestContext)
1576 -> ::arcly_http::futures::future::BoxFuture<'static, ::arcly_http::__axum::response::Response>
1577 {
1578 ::arcly_http::futures::FutureExt::boxed(async move { #thunk_body })
1579 }
1580
1581 #[allow(non_upper_case_globals)]
1582 static #spec_name: ::arcly_http::__macro_support::RouteSpec =
1583 ::arcly_http::__macro_support::RouteSpec {
1584 summary: #summary_str,
1585 description: #description_str,
1586 operation_id: #operation_id,
1587 tags: &[ #( #tag_lits ),* ],
1588 security: &[ #( #sec_lits ),* ],
1589 status_code: #status_expr,
1590 deprecated: #deprecated,
1591 params: &[ #( #spec_params ),* ],
1592 has_body: #has_body,
1593 body_schema: #body_schema_expr,
1594 query_schema: #query_schema_expr,
1595 response_schema: #response_schema_expr,
1596 cache_ttl_secs: #cache_ttl_secs,
1597 cache_key: #cache_key,
1598 api_version: "",
1599 sunset: "",
1600 idempotent_ttl_secs: 0,
1601 policies: &[],
1602 audit_action: "",
1603 audit_resource: "",
1604 timeout_ms: 0,
1605 transactional: false,
1606 mask_fields: &[],
1607 };
1608
1609 #[allow(non_upper_case_globals)]
1610 static #desc_name: ::arcly_http::__macro_support::RouteDescriptor =
1611 ::arcly_http::__macro_support::RouteDescriptor {
1612 method: ::arcly_http::__macro_support::HttpMethod::#method_ident,
1613 path: #full_path_lit,
1614 handler: #thunk_name,
1615 spec: &#spec_name,
1616 controller: "",
1617 };
1618
1619 ::arcly_http::inventory::submit! {
1620 &#desc_name
1621 }
1622 }
1623 .into()
1624}
1625
1626enum ParamKind {
1630 Param(LitStr),
1631 Query,
1632 Body,
1633 Header(LitStr),
1634 Ctx,
1635 FromContext,
1636}
1637
1638fn classify_arg(arg: &PatType) -> syn::Result<(ParamKind, Type)> {
1639 for attr in &arg.attrs {
1640 let ident = attr
1641 .path()
1642 .get_ident()
1643 .map(|i| i.to_string())
1644 .unwrap_or_default();
1645 match ident.as_str() {
1646 "Param" => {
1647 let name: LitStr = attr.parse_args()?;
1648 return Ok((ParamKind::Param(name), (*arg.ty).clone()));
1649 }
1650 "Query" => return Ok((ParamKind::Query, (*arg.ty).clone())),
1651 "Body" => return Ok((ParamKind::Body, (*arg.ty).clone())),
1652 "Header" => {
1653 let name: LitStr = attr.parse_args()?;
1654 return Ok((ParamKind::Header(name), (*arg.ty).clone()));
1655 }
1656 _ => {}
1657 }
1658 }
1659 let ty_ref = &*arg.ty;
1660 let ty_str = quote!(#ty_ref).to_string();
1661 let ty = (*arg.ty).clone();
1662 let kind = if ty_str.contains("RequestContext") {
1663 ParamKind::Ctx
1664 } else {
1665 ParamKind::FromContext
1666 };
1667 Ok((kind, ty))
1668}
1669
1670fn emit_extractor(
1671 kind: &ParamKind,
1672 ty: &Type,
1673 var: &Ident,
1674 spec_params: &mut Vec<TokenStream2>,
1675 has_body: &mut bool,
1676 body_ty: &mut Option<Type>,
1677 query_ty: &mut Option<Type>,
1678) -> TokenStream2 {
1679 match kind {
1680 ParamKind::Param(name) => {
1681 spec_params.push(quote! {
1682 ::arcly_http::__macro_support::ParamSpec {
1683 name: #name,
1684 loc: ::arcly_http::__macro_support::ParamLoc::Path,
1685 required: true,
1686 schema: || ::arcly_http::__schema_for::<#ty>(),
1687 }
1688 });
1689 quote! { let #var: #ty = ::arcly_http::__macro_support::extract_param(&ctx, #name)?; }
1690 }
1691 ParamKind::Query => {
1692 *query_ty = Some(ty.clone());
1693 quote! { let #var: #ty = ::arcly_http::__macro_support::extract_query_validated(&ctx)?; }
1694 }
1695 ParamKind::Body => {
1696 *has_body = true;
1697 *body_ty = Some(ty.clone());
1698 quote! { let #var: #ty = ::arcly_http::__macro_support::extract_body_validated(&ctx)?; }
1699 }
1700 ParamKind::Header(name) => {
1701 spec_params.push(quote! {
1702 ::arcly_http::__macro_support::ParamSpec {
1703 name: #name,
1704 loc: ::arcly_http::__macro_support::ParamLoc::Header,
1705 required: true,
1706 schema: || ::arcly_http::__schema_for::<#ty>(),
1707 }
1708 });
1709 quote! { let #var: #ty = ::arcly_http::__macro_support::extract_header(&ctx, #name)?.to_owned(); }
1710 }
1711 ParamKind::Ctx => quote! { let #var: #ty = ctx.clone(); },
1712 ParamKind::FromContext => quote! { let #var: #ty = <#ty>::from_ctx(&ctx); },
1713 }
1714}
1715
1716fn extract_response_ty(ret: &ReturnType) -> Option<Type> {
1720 let ty = match ret {
1721 ReturnType::Default => return None,
1722 ReturnType::Type(_, ty) => &**ty,
1723 };
1724 inner_payload_ty(ty).cloned()
1725}
1726
1727fn inner_payload_ty(ty: &Type) -> Option<&Type> {
1728 let Type::Path(tp) = ty else { return None };
1729 let seg = tp.path.segments.last()?;
1730 match seg.ident.to_string().as_str() {
1731 "Json" | "Created" | "Accepted" => first_generic(&seg.arguments),
1732 "Result" => {
1733 let ok = first_generic(&seg.arguments)?;
1734 inner_payload_ty(ok)
1735 }
1736 "NoContent" => None,
1737 _ => None,
1738 }
1739}
1740
1741fn first_generic(args: &PathArguments) -> Option<&Type> {
1742 let PathArguments::AngleBracketed(ab) = args else {
1743 return None;
1744 };
1745 ab.args.iter().find_map(|a| match a {
1746 GenericArgument::Type(t) => Some(t),
1747 _ => None,
1748 })
1749}
1750
1751fn collect_doc_comments(attrs: &[Attribute]) -> String {
1752 let mut out = String::new();
1753 for a in attrs {
1754 if !a.path().is_ident("doc") {
1755 continue;
1756 }
1757 if let Meta::NameValue(nv) = &a.meta {
1758 if let Expr::Lit(ExprLit {
1759 lit: Lit::Str(s), ..
1760 }) = &nv.value
1761 {
1762 let line = s.value();
1763 if !out.is_empty() {
1764 out.push('\n');
1765 }
1766 out.push_str(line.trim_start());
1767 }
1768 }
1769 }
1770 out
1771}
1772
1773struct GatewayArgs {
1784 path: LitStr,
1785 #[allow(dead_code)]
1786 tags: Vec<LitStr>,
1787}
1788
1789impl Parse for GatewayArgs {
1790 fn parse(input: ParseStream) -> syn::Result<Self> {
1791 let path: LitStr = input.parse()?;
1792 let mut tags: Vec<LitStr> = vec![];
1793 if input.peek(Token![,]) {
1794 let _: Token![,] = input.parse()?;
1795 if !input.is_empty() {
1796 let key: Ident = input.parse()?;
1797 if key != "tags" {
1798 return Err(syn::Error::new(key.span(), "expected `tags(...)`"));
1799 }
1800 let content;
1801 syn::parenthesized!(content in input);
1802 let list: Punctuated<LitStr, Token![,]> =
1803 content.parse_terminated(|s| s.parse::<LitStr>(), Token![,])?;
1804 tags.extend(list);
1805 }
1806 }
1807 Ok(Self { path, tags })
1808 }
1809}
1810
1811#[proc_macro_attribute]
1812#[allow(non_snake_case)]
1813pub fn Gateway(attr: TokenStream, item: TokenStream) -> TokenStream {
1814 match syn::parse::<ItemImpl>(item) {
1815 Ok(imp) => gateway_on_impl(attr, imp),
1816 Err(_) => syn::Error::new(
1817 Span::call_site(),
1818 "#[Gateway(\"/path\")] must be placed on the gateway's `impl` block \
1819 (the struct uses #[Injectable]; lifecycle goes in `impl ArclyGateway`)",
1820 )
1821 .to_compile_error()
1822 .into(),
1823 }
1824}
1825
1826#[proc_macro_attribute]
1829#[allow(non_snake_case)]
1830pub fn Subscribe(_attr: TokenStream, item: TokenStream) -> TokenStream {
1831 item
1832}
1833
1834fn gateway_on_impl(attr: TokenStream, mut imp: ItemImpl) -> TokenStream {
1835 let GatewayArgs { path, .. } = parse_macro_input!(attr as GatewayArgs);
1836 let self_ty = (*imp.self_ty).clone();
1837 let name = match &self_ty {
1838 Type::Path(tp) => tp
1839 .path
1840 .segments
1841 .last()
1842 .map(|s| s.ident.to_string())
1843 .unwrap_or_default(),
1844 _ => String::new(),
1845 };
1846
1847 let mut dispatch_inserts: Vec<TokenStream2> = Vec::new();
1848 let mut errors: Vec<syn::Error> = Vec::new();
1849
1850 for item in imp.items.iter_mut() {
1851 let ImplItem::Fn(m) = item else { continue };
1852
1853 let sub_idx = m.attrs.iter().position(|a| {
1855 a.path()
1856 .get_ident()
1857 .map(|i| i.to_string())
1858 .unwrap_or_default()
1859 == "Subscribe"
1860 });
1861 let Some(idx) = sub_idx else { continue };
1862
1863 let event: LitStr = match m.attrs[idx].parse_args() {
1864 Ok(e) => e,
1865 Err(e) => {
1866 errors.push(e);
1867 continue;
1868 }
1869 };
1870 m.attrs.remove(idx); let fn_name = m.sig.ident.clone();
1873 match build_subscribe_insert(&self_ty, m, &event, &fn_name) {
1874 Ok(ts) => dispatch_inserts.push(ts),
1875 Err(e) => errors.push(e),
1876 }
1877 }
1878
1879 if let Some(err) = errors.into_iter().reduce(|mut a, b| {
1880 a.combine(b);
1881 a
1882 }) {
1883 return err.to_compile_error().into();
1884 }
1885
1886 let path_lit = LitStr::new(&path.value(), Span::call_site());
1887 let name_lit = LitStr::new(&name, Span::call_site());
1888 let build_ident = format_ident!("__arcly_build_gateway_{}", name.to_uppercase());
1889 let desc_ident = format_ident!("__ARCLY_GATEWAY_{}", name.to_uppercase());
1890
1891 quote! {
1892 #imp
1893
1894 #[doc(hidden)]
1895 #[allow(non_snake_case)]
1896 fn #build_ident(__container: &'static ::arcly_http::__macro_support::FrozenDiContainer)
1897 -> &'static ::arcly_http::__macro_support::GatewayRuntime
1898 {
1899 let __gw: &'static #self_ty = ::std::boxed::Box::leak(::std::boxed::Box::new(
1902 <#self_ty>::__arcly_build(&__container.resolver())
1903 ));
1904
1905 let mut __dispatch: ::std::collections::HashMap<
1906 &'static str,
1907 ::arcly_http::__macro_support::MessageHandler,
1908 > = ::std::collections::HashMap::new();
1909 #( #dispatch_inserts )*
1910
1911 ::std::boxed::Box::leak(::std::boxed::Box::new(
1912 ::arcly_http::__macro_support::GatewayRuntime {
1913 path: #path_lit,
1914 on_connect: ::std::boxed::Box::new(move |__c: ::arcly_http::__macro_support::WsClient| {
1915 let __gw = __gw;
1916 ::arcly_http::futures::FutureExt::boxed(async move {
1917 <#self_ty as ::arcly_http::__macro_support::ArclyGateway>::on_connect(__gw, __c).await
1918 })
1919 }),
1920 on_disconnect: ::std::boxed::Box::new(move |__c: ::arcly_http::__macro_support::WsClient| {
1921 let __gw = __gw;
1922 ::arcly_http::futures::FutureExt::boxed(async move {
1923 <#self_ty as ::arcly_http::__macro_support::ArclyGateway>::on_disconnect(__gw, __c).await
1924 })
1925 }),
1926 dispatch: __dispatch,
1927 }
1928 ))
1929 }
1930
1931 #[allow(non_upper_case_globals)]
1932 static #desc_ident: ::arcly_http::__macro_support::GatewayDescriptor =
1933 ::arcly_http::__macro_support::GatewayDescriptor {
1934 name: #name_lit,
1935 path: #path_lit,
1936 build: #build_ident,
1937 };
1938
1939 ::arcly_http::inventory::submit! { &#desc_ident }
1940 }
1941 .into()
1942}
1943
1944fn build_subscribe_insert(
1949 self_ty: &Type,
1950 m: &syn::ImplItemFn,
1951 event: &LitStr,
1952 fn_name: &Ident,
1953) -> syn::Result<TokenStream2> {
1954 let mut extract_stmts: Vec<TokenStream2> = Vec::new();
1955 let mut call_args: Vec<TokenStream2> = Vec::new();
1956
1957 for (i, input) in m.sig.inputs.iter().enumerate() {
1958 let pt = match input {
1959 FnArg::Receiver(_) => continue, FnArg::Typed(pt) => pt,
1961 };
1962 let ty = (*pt.ty).clone();
1963 let var = format_ident!("__sub_arg_{i}");
1964
1965 if type_last_ident_is(&ty, "WsClient") {
1966 extract_stmts.push(
1967 quote! { let #var: ::arcly_http::__macro_support::WsClient = __client.clone(); },
1968 );
1969 call_args.push(quote! { #var });
1970 } else if let Some(inner) = json_inner_ty(&ty) {
1971 extract_stmts.push(quote! {
1972 let #var: ::arcly_http::__macro_support::Json<#inner> = ::arcly_http::__macro_support::Json(
1973 ::arcly_http::serde_json::from_str::<#inner>(&__data)
1974 .map_err(|_| ::arcly_http::__macro_support::Error::BadRequest("invalid websocket payload"))?
1975 );
1976 });
1977 call_args.push(quote! { #var });
1978 } else {
1979 return Err(syn::Error::new(
1980 pt.span(),
1981 "#[Subscribe] handler params must be `WsClient` or `Json<T>`",
1982 ));
1983 }
1984 }
1985
1986 Ok(quote! {
1987 {
1988 let __gw = __gw;
1989 let __handler: ::arcly_http::__macro_support::MessageHandler = ::std::sync::Arc::new(
1990 move |__client: ::arcly_http::__macro_support::WsClient, __data: ::std::sync::Arc<str>| {
1991 let __gw = __gw;
1992 ::arcly_http::futures::FutureExt::boxed(async move {
1993 #( #extract_stmts )*
1994 <#self_ty>::#fn_name(__gw, #( #call_args ),*).await
1995 })
1996 }
1997 );
1998 __dispatch.insert(#event, __handler);
1999 }
2000 })
2001}
2002
2003fn type_last_ident_is(ty: &Type, name: &str) -> bool {
2004 matches!(ty, Type::Path(tp)
2005 if tp.path.segments.last().map(|s| s.ident == name).unwrap_or(false))
2006}
2007
2008fn json_inner_ty(ty: &Type) -> Option<Type> {
2009 let Type::Path(tp) = ty else { return None };
2010 let seg = tp.path.segments.last()?;
2011 if seg.ident != "Json" {
2012 return None;
2013 }
2014 first_generic(&seg.arguments).cloned()
2015}
2016
2017struct BreakerArgs {
2021 threshold: u32,
2022 cooldown_millis: u64,
2023}
2024
2025impl Parse for BreakerArgs {
2026 fn parse(input: ParseStream) -> syn::Result<Self> {
2027 let mut threshold: Option<u32> = None;
2028 let mut cooldown_millis: Option<u64> = None;
2029 while !input.is_empty() {
2030 let key: Ident = input.parse()?;
2031 let _: Token![=] = input.parse()?;
2032 match key.to_string().as_str() {
2033 "threshold" => {
2034 let n: LitInt = input.parse()?;
2035 threshold = Some(n.base10_parse()?);
2036 }
2037 "cooldown" => {
2038 let s: LitStr = input.parse()?;
2039 cooldown_millis = Some(parse_duration_ms(&s)?);
2040 }
2041 other => {
2042 return Err(syn::Error::new(
2043 key.span(),
2044 format!("unknown circuit_breaker key `{other}`"),
2045 ))
2046 }
2047 }
2048 let _ = input.parse::<Token![,]>();
2049 }
2050 Ok(Self {
2051 threshold: threshold
2052 .ok_or_else(|| syn::Error::new(input.span(), "missing `threshold = N`"))?,
2053 cooldown_millis: cooldown_millis
2054 .ok_or_else(|| syn::Error::new(input.span(), "missing `cooldown = \"…\"`"))?,
2055 })
2056 }
2057}
2058
2059fn parse_duration_ms(s: &LitStr) -> syn::Result<u64> {
2060 let raw = s.value();
2061 let r = raw.trim();
2062 let (num_s, unit) = match r.rfind(|c: char| c.is_ascii_digit()) {
2063 Some(i) => (&r[..=i], &r[i + 1..]),
2064 None => return Err(syn::Error::new(s.span(), "invalid duration")),
2065 };
2066 let n: u64 = num_s
2067 .parse()
2068 .map_err(|_| syn::Error::new(s.span(), "invalid duration number"))?;
2069 let mult = match unit.trim() {
2070 "ms" => 1,
2071 "s" | "" => 1_000,
2072 "m" => 60_000,
2073 "h" => 3_600_000,
2074 other => {
2075 return Err(syn::Error::new(
2076 s.span(),
2077 format!("unknown duration unit `{other}`"),
2078 ))
2079 }
2080 };
2081 Ok(n * mult)
2082}
2083
2084#[proc_macro_attribute]
2085pub fn circuit_breaker(attr: TokenStream, item: TokenStream) -> TokenStream {
2086 let args = parse_macro_input!(attr as BreakerArgs);
2087 let mut f = parse_macro_input!(item as syn::ImplItemFn);
2088
2089 let threshold = args.threshold;
2090 let cooldown_ms = args.cooldown_millis;
2091
2092 let breaker_name = format_ident!("__BREAKER_{}", f.sig.ident.to_string().to_uppercase());
2093
2094 let original_body = f.block.clone();
2096 let new_block: Block = parse_quote! {{
2097 static #breaker_name: ::arcly_http::__macro_support::CircuitBreaker =
2098 ::arcly_http::__macro_support::CircuitBreaker::const_new(#threshold, #cooldown_ms);
2099 match #breaker_name.execute(|| async move #original_body).await {
2100 ::core::result::Result::Ok(inner) => inner,
2101 ::core::result::Result::Err(_open) => ::core::result::Result::Err(
2102 <_ as ::core::convert::From<::arcly_http::__macro_support::BreakerOpen>>::from(_open),
2103 ),
2104 }
2105 }};
2106 f.block = new_block;
2107
2108 quote! { #f }.into()
2109}
2110
2111struct EncryptFieldsArgs {
2117 key: LitStr,
2118 fields: Vec<LitStr>,
2119}
2120
2121impl Parse for EncryptFieldsArgs {
2122 fn parse(input: ParseStream) -> syn::Result<Self> {
2123 let mut key: Option<LitStr> = None;
2124 let mut fields: Vec<LitStr> = Vec::new();
2125 while !input.is_empty() {
2126 let ident: Ident = input.parse()?;
2127 match ident.to_string().as_str() {
2128 "key" => {
2129 input.parse::<Token![=]>()?;
2130 key = Some(input.parse()?);
2131 }
2132 "fields" => {
2133 let inner;
2134 syn::parenthesized!(inner in input);
2135 let lits: Punctuated<LitStr, Token![,]> =
2136 inner.parse_terminated(|p: ParseStream| p.parse::<LitStr>(), Token![,])?;
2137 fields.extend(lits);
2138 }
2139 other => {
2140 return Err(syn::Error::new(
2141 ident.span(),
2142 format!(
2143 "unknown EncryptFields argument `{other}` (expected `key` or `fields`)"
2144 ),
2145 ))
2146 }
2147 }
2148 if input.peek(Token![,]) {
2149 input.parse::<Token![,]>()?;
2150 }
2151 }
2152 let key = key.ok_or_else(|| {
2153 syn::Error::new(Span::call_site(), "EncryptFields requires `key = \"...\"`")
2154 })?;
2155 if fields.is_empty() {
2156 return Err(syn::Error::new(
2157 Span::call_site(),
2158 "EncryptFields requires at least one entry in `fields(...)`",
2159 ));
2160 }
2161 Ok(Self { key, fields })
2162 }
2163}
2164
2165#[proc_macro_attribute]
2172#[allow(non_snake_case)]
2173pub fn EncryptFields(attr: TokenStream, item: TokenStream) -> TokenStream {
2174 let args = parse_macro_input!(attr as EncryptFieldsArgs);
2175 let st = parse_macro_input!(item as ItemStruct);
2176 let name = &st.ident;
2177 let (impl_g, ty_g, where_c) = st.generics.split_for_impl();
2178 let key = &args.key;
2179 let fields = &args.fields;
2180
2181 quote! {
2182 #st
2183
2184 impl #impl_g ::arcly_http::__macro_support::EncryptRecord for #name #ty_g #where_c {
2185 const ENCRYPT_FIELDS: &'static [&'static str] = &[ #( #fields ),* ];
2186 const KEY_ID: &'static str = #key;
2187 }
2188 }
2189 .into()
2190}