1#![warn(missing_docs)]
15
16use proc_macro::TokenStream;
17use quote::{ToTokens, quote};
18use std::collections::BTreeMap; use syn::{
20 Expr, Ident, ImplItem, ItemImpl, LitStr, Token,
21 ext::IdentExt,
22 parse::{Parse, ParseStream},
23 parse_macro_input,
24 punctuated::Punctuated,
25};
26
27struct AllRoutes {
32 routes: Vec<RouteDefinition>,
33}
34
35struct RouteDefinition {
36 verb: Option<Verb>,
37 pattern: LitStr,
38 handler: Ident,
39 params: Punctuated<Param, Token![,]>,
40}
41
42struct Param {
43 name: Ident,
44 ty: syn::Type,
45 default: Option<Expr>,
46}
47
48#[derive(Debug, Clone)]
49enum Verb {
50 GET,
51 POST,
52 PUT,
53 DELETE,
54 PATCH,
55 HEAD,
56 OPTIONS,
57}
58
59impl Verb {
60 fn from_ident(ident: &Ident) -> Option<Self> {
61 match ident.to_string().as_str() {
62 "GET" => Some(Verb::GET),
63 "POST" => Some(Verb::POST),
64 "PUT" => Some(Verb::PUT),
65 "DELETE" => Some(Verb::DELETE),
66 "PATCH" => Some(Verb::PATCH),
67 "HEAD" => Some(Verb::HEAD),
68 "OPTIONS" => Some(Verb::OPTIONS),
69 _ => None,
70 }
71 }
72
73 fn to_tokens(&self) -> proc_macro2::TokenStream {
74 match self {
75 Verb::GET => quote! { ::actus::__internal::Verb::GET },
76 Verb::POST => quote! { ::actus::__internal::Verb::POST },
77 Verb::PUT => quote! { ::actus::__internal::Verb::PUT },
78 Verb::DELETE => quote! { ::actus::__internal::Verb::DELETE },
79 Verb::PATCH => quote! { ::actus::__internal::Verb::PATCH },
80 Verb::HEAD => quote! { ::actus::__internal::Verb::HEAD },
81 Verb::OPTIONS => quote! { ::actus::__internal::Verb::OPTIONS },
82 }
83 }
84}
85
86#[derive(Debug, Clone, Copy)]
91enum ControllerMode {
92 Strict,
93 Lax,
94}
95
96struct ControllerAttrs {
97 mode: ControllerMode,
98 prepare: Option<syn::ExprPath>,
99 max_body_bytes: Option<syn::Expr>,
102 rate_limit: Option<syn::Expr>,
108}
109
110impl Parse for AllRoutes {
115 fn parse(input: ParseStream) -> syn::Result<Self> {
116 let mut routes = Vec::new();
117
118 while !input.is_empty() {
119 if input.peek(syn::token::Bracket) {
123 let bracket_span = input.fork().parse::<proc_macro2::TokenTree>()?.span();
124 return Err(syn::Error::new(
125 bracket_span,
126 "actus no longer ships an `Access` enum or `[Access::*]` section syntax. \
127 Authorization belongs in your `#[controller(prepare = …)]` hook, where \
128 you can call into your own policy layer (e.g. `services::policy::*`).",
129 ));
130 }
131
132 let verb = if input.peek2(LitStr) {
134 if let Ok(ident) = input.parse::<Ident>() {
135 if let Some(v) = Verb::from_ident(&ident) {
136 Some(v)
137 } else {
138 return Err(syn::Error::new(
139 ident.span(),
140 format!(
141 "Unknown HTTP verb: {}. Expected GET, POST, PUT, DELETE, PATCH, HEAD, or OPTIONS",
142 ident
143 ),
144 ));
145 }
146 } else {
147 None
148 }
149 } else {
150 None
151 };
152
153 let pattern: LitStr = input.parse()?;
155 validate_pattern(&pattern)?;
156 input.parse::<Token![=>]>()?;
157 let handler: Ident = input.parse()?;
158
159 let params_content;
161 syn::parenthesized!(params_content in input);
162 let params = Punctuated::parse_terminated(¶ms_content)?;
163
164 routes.push(RouteDefinition {
165 verb,
166 pattern,
167 handler,
168 params,
169 });
170
171 if input.peek(Token![,]) {
172 input.parse::<Token![,]>()?;
173 }
174 }
175
176 Ok(AllRoutes { routes })
177 }
178}
179
180impl Parse for Param {
181 fn parse(input: ParseStream) -> syn::Result<Self> {
182 let name: Ident = input.parse()?;
183 input.parse::<Token![:]>()?;
184 let ty: syn::Type = input.parse()?;
185
186 let default = if input.peek(Token![=]) {
187 input.parse::<Token![=]>()?;
188 Some(input.parse()?)
189 } else {
190 None
191 };
192
193 Ok(Param { name, ty, default })
194 }
195}
196
197impl Parse for ControllerAttrs {
198 fn parse(input: ParseStream) -> syn::Result<Self> {
199 let mut mode = ControllerMode::Strict;
200 let mut prepare = None;
201 let mut max_body_bytes = None;
202 let mut rate_limit = None;
203
204 while !input.is_empty() {
205 let ident: Ident = input.parse()?;
206 match ident.to_string().as_str() {
207 "strict" => mode = ControllerMode::Strict,
208 "lax" => mode = ControllerMode::Lax,
209 "prepare" => {
210 input.parse::<Token![=]>()?;
211 prepare = Some(input.parse()?);
212 }
213 "max_body_bytes" => {
214 input.parse::<Token![=]>()?;
215 max_body_bytes = Some(input.parse()?);
220 }
221 "rate_limit" => {
222 input.parse::<Token![=]>()?;
223 rate_limit = Some(input.parse()?);
228 }
229 _ => {
230 return Err(syn::Error::new(
231 ident.span(),
232 "Expected 'strict', 'lax', 'prepare = <fn>', 'max_body_bytes = <expr>', \
233 or 'rate_limit = <expr>'",
234 ));
235 }
236 }
237
238 if input.peek(Token![,]) {
239 input.parse::<Token![,]>()?;
240 }
241 }
242
243 Ok(ControllerAttrs {
244 mode,
245 prepare,
246 max_body_bytes,
247 rate_limit,
248 })
249 }
250}
251
252fn type_to_string(ty: &syn::Type) -> String {
257 quote!(#ty).to_string().replace(" ", "")
258}
259
260fn extract_path_params(pattern: &str) -> Vec<String> {
261 let mut params = Vec::new();
262 let mut chars = pattern.chars().peekable();
263
264 while let Some(ch) = chars.next() {
265 if ch == '{' {
266 let mut param = String::new();
267 for ch in chars.by_ref() {
268 if ch == '}' {
269 break;
270 }
271 param.push(ch);
272 }
273 let name = param.strip_prefix("...").unwrap_or(param.as_str());
276 if !name.is_empty() {
277 params.push(name.to_string());
278 }
279 }
280 }
281
282 params
283}
284
285fn rest_param_name(segment: &str) -> Option<&str> {
288 segment
289 .strip_prefix("{...")
290 .and_then(|s| s.strip_suffix('}'))
291 .filter(|name| !name.is_empty())
292}
293
294fn validate_pattern(pattern: &LitStr) -> syn::Result<()> {
300 let value = pattern.value();
301 let segments: Vec<&str> = value.split('/').collect();
302
303 for (i, seg) in segments.iter().enumerate() {
304 let Some(inner) = seg.strip_prefix('{').and_then(|s| s.strip_suffix('}')) else {
306 continue;
307 };
308
309 if !inner.starts_with('.') {
310 continue; }
312
313 if rest_param_name(seg).is_none() {
315 return Err(syn::Error::new(
316 pattern.span(),
317 format!(
318 "malformed rest parameter `{{{inner}}}` in route pattern `{value}`; \
319 write it as `{{...name}}` (three dots, then a non-empty name)"
320 ),
321 ));
322 }
323
324 if i != segments.len() - 1 {
325 return Err(syn::Error::new(
326 pattern.span(),
327 format!(
328 "rest parameter `{{{inner}}}` must be the last segment of route \
329 pattern `{value}` (it captures the entire remaining path)"
330 ),
331 ));
332 }
333
334 let earlier_rest = segments[..i]
340 .iter()
341 .filter(|s| rest_param_name(s).is_some())
342 .count();
343 if earlier_rest > 0 {
344 return Err(syn::Error::new(
345 pattern.span(),
346 format!("route pattern `{value}` has more than one `{{...name}}` rest parameter"),
347 ));
348 }
349 }
350
351 Ok(())
352}
353
354fn collect_method_docs(item_impl: &syn::ItemImpl) -> BTreeMap<String, String> {
356 use syn::{Attribute, ImplItem, Meta};
357
358 fn doc_from_attrs(attrs: &[Attribute]) -> String {
359 attrs
360 .iter()
361 .filter(|a| a.path().is_ident("doc"))
362 .filter_map(|a| {
363 match &a.meta {
364 Meta::NameValue(nv) => {
365 if let syn::Expr::Lit(expr_lit) = &nv.value
367 && let syn::Lit::Str(ls) = &expr_lit.lit
368 {
369 return Some(ls.value());
370 }
371 None
372 }
373 _ => None,
374 }
375 })
376 .collect::<Vec<_>>()
377 .join("\n")
378 }
379
380 let mut map = BTreeMap::new();
381 for it in &item_impl.items {
382 if let ImplItem::Fn(m) = it {
383 let name = m.sig.ident.to_string();
384 let doc = doc_from_attrs(&m.attrs);
385 if !doc.trim().is_empty() {
386 map.insert(name, doc);
387 }
388 }
389 }
390 map
391}
392
393#[proc_macro_attribute]
407pub fn controller(attr: TokenStream, item: TokenStream) -> TokenStream {
408 let attrs = if attr.is_empty() {
410 ControllerAttrs {
411 mode: ControllerMode::Strict,
412 prepare: None,
413 max_body_bytes: None,
414 rate_limit: None,
415 }
416 } else {
417 match syn::parse::<ControllerAttrs>(attr) {
418 Ok(a) => a,
419 Err(e) => return e.to_compile_error().into(),
420 }
421 };
422
423 let item_impl = parse_macro_input!(item as ItemImpl);
424
425 let docs_map = collect_method_docs(&item_impl);
427
428 let routes_macro = item_impl
430 .items
431 .iter()
432 .find_map(|item| {
433 if let ImplItem::Macro(m) = item
434 && m.mac.path.is_ident("routes") {
435 return Some(m);
436 }
437 None
438 })
439 .expect("A `routes!` macro invocation is required inside an `impl` block marked with `#[controller]`");
440
441 let all_routes: AllRoutes = match syn::parse2(routes_macro.mac.tokens.clone()) {
443 Ok(routes) => routes,
444 Err(e) => return e.to_compile_error().into(),
445 };
446
447 let generated = generate_controller_impl(&item_impl, &all_routes, &attrs, &docs_map);
449
450 generated.into()
451}
452
453fn generate_controller_impl(
458 item_impl: &ItemImpl,
459 all_routes: &AllRoutes,
460 attrs: &ControllerAttrs,
461 docs_map: &BTreeMap<String, String>, ) -> proc_macro2::TokenStream {
463 let self_ty = &item_impl.self_ty;
464
465 let mut route_defs = Vec::new();
467 let mut handler_arms = Vec::new();
468
469 for (idx, route) in all_routes.routes.iter().enumerate() {
470 let pattern = &route.pattern;
471 let pattern_str = pattern.value();
472 let handler = &route.handler;
473 let handler_id = format!("handler_{}", idx);
474
475 let path_params = extract_path_params(&pattern_str);
477 let rest_param: Option<String> = pattern_str
479 .split('/')
480 .find_map(|s| rest_param_name(s).map(str::to_string));
481
482 let mut param_defs = Vec::new();
484 let mut param_extractions = Vec::new();
485 let mut param_names = Vec::new();
486
487 for param in &route.params {
488 let name = ¶m.name;
489 let name_str = name.unraw().to_string();
494 let ty_str = type_to_string(¶m.ty);
495
496 param_names.push(name.clone());
498
499 if ty_str == "&Params" {
511 param_extractions.push(quote! { ¶ms });
512 continue;
513 }
514
515 let is_rest = rest_param.as_deref() == Some(name_str.as_str());
516
517 if is_rest && ty_str != "String" {
522 let msg = format!(
523 "rest parameter `{{...{name_str}}}` must be typed `String` (it holds the \
524 joined remaining path); found `{ty_str}`"
525 );
526 param_defs.push(quote! { compile_error!(#msg) });
527 param_extractions.push(quote! { compile_error!(#msg) });
528 continue;
529 }
530
531 let source = if path_params.contains(&name_str) {
533 quote! { ::actus::__internal::ParamSource::Path }
534 } else if ty_str == "JsonValue" || ty_str == "Bytes" {
535 quote! { ::actus::__internal::ParamSource::Body }
536 } else {
537 quote! { ::actus::__internal::ParamSource::Query }
538 };
539
540 let (param_type, default_value) =
542 generate_param_type_and_default(&ty_str, ¶m.default);
543
544 param_defs.push(quote! {
545 ::actus::__internal::ParamDef {
546 name: #name_str,
547 ty: #param_type,
548 source: #source,
549 default: #default_value,
550 }
551 });
552
553 let extraction = generate_param_extraction(&name_str, &ty_str, ¶m.default);
555 param_extractions.push(extraction);
556 }
557
558 let verb_expr = match &route.verb {
562 Some(v) => {
563 let verb_tokens = v.to_tokens();
564 quote! { &[#verb_tokens] }
565 }
566 None => quote! { ::actus::__internal::DEFAULT_VERBS },
567 };
568
569 let handler_name_str = handler.to_string();
571 let doc_val = docs_map.get(&handler_name_str).cloned().unwrap_or_default();
572 let doc_lit = syn::LitStr::new(&doc_val, proc_macro2::Span::call_site());
573
574 route_defs.push(quote! {
575 ::actus::__internal::RouteDef {
576 pattern: #pattern_str,
577 handler_id: #handler_id,
578 handler: #handler_name_str,
579 verb: #verb_expr,
580 params: &[ #(#param_defs),* ],
581 doc: if #doc_lit.is_empty() { None } else { Some(#doc_lit) },
582 }
583 });
584
585 handler_arms.push(quote! {
587 #handler_id => {
588 #(let #param_names = #param_extractions;)*
589 self.#handler(#(#param_names),*).await
590 }
591 });
592 }
593
594 let prepare_call = attrs
609 .prepare
610 .as_ref()
611 .map(|prepare_fn| {
612 quote! {
613 if let ::core::option::Option::Some(__actus_early_reply) =
614 #prepare_fn(self, &matched_route, &mut params).await?
615 {
616 return ::core::result::Result::Ok(__actus_early_reply);
617 }
618 }
619 })
620 .unwrap_or_default();
621
622 let mode_value = match attrs.mode {
624 ControllerMode::Strict => quote! { ::actus::__internal::ControllerMode::Strict },
625 ControllerMode::Lax => quote! { ::actus::__internal::ControllerMode::Lax },
626 };
627
628 let mode_str = match attrs.mode {
629 ControllerMode::Strict => "strict",
630 ControllerMode::Lax => "lax",
631 };
632
633 let _ = mode_str;
634
635 let params_binding = if attrs.prepare.is_some() {
639 quote! { mut params: ::actus::__internal::Params }
640 } else {
641 quote! { params: ::actus::__internal::Params }
642 };
643
644 let max_body_bytes_impl = attrs.max_body_bytes.as_ref().map(|expr| {
648 quote! {
649 fn actus_max_body_bytes(&self) -> ::core::option::Option<usize> {
650 ::core::option::Option::Some(#expr)
651 }
652 }
653 });
654
655 let rate_limit_impl = attrs.rate_limit.as_ref().map(|expr| {
661 quote! {
662 fn actus_rate_limit(&self) -> ::core::option::Option<&'static str> {
663 ::core::option::Option::Some(#expr)
664 }
665 }
666 });
667
668 let controller_impl = quote! {
670 #[::actus::__internal::async_trait]
671 impl ::actus::__internal::Controller for #self_ty {
672 async fn actus_dispatch(&self, action: &str, #params_binding) -> ::actus::__internal::Reply {
673 static ROUTES: &[::actus::__internal::RouteDef] = &[ #(#route_defs),* ];
676
677 let (matched_route, extracted) = ::actus::__internal::routing::resolve(
679 ROUTES,
680 action,
681 ¶ms,
682 #mode_value
683 )?;
684
685 #prepare_call
687
688 match matched_route.handler_id {
694 #(#handler_arms),*
695 other => ::core::unreachable!(
696 "dispatch: no handler for route id {:?}", other
697 ),
698 }
699 }
700
701 fn __name(&self) -> &'static str {
702 stringify!(#self_ty)
703 }
704
705 fn actus_describe_routes(&self) -> Vec<::actus::__internal::RouteDef> {
708 static ROUTES: &[::actus::__internal::RouteDef] = &[ #(#route_defs),* ];
709 ROUTES.to_vec()
710 }
711
712 #max_body_bytes_impl
713 #rate_limit_impl
714 }
715 };
716
717 quote! {
718 #item_impl
720
721 #controller_impl
723 }
724}
725
726fn generate_param_type_and_default(
727 ty_str: &str,
728 default: &Option<Expr>,
729) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) {
730 match (ty_str, default) {
731 ("String", Some(d)) => (
732 quote! { ::actus::__internal::ParamType::String },
733 quote! { Some(::actus::__internal::ParamDefault::String(#d)) },
734 ),
735 ("String", None) => (
736 quote! { ::actus::__internal::ParamType::String },
737 quote! { None },
738 ),
739 ("i64", Some(d)) => (
740 quote! { ::actus::__internal::ParamType::Int },
741 quote! { Some(::actus::__internal::ParamDefault::Int(#d)) },
742 ),
743 ("i64", None) => (
744 quote! { ::actus::__internal::ParamType::Int },
745 quote! { None },
746 ),
747 ("u64", Some(d)) => (
748 quote! { ::actus::__internal::ParamType::U64 },
749 quote! { Some(::actus::__internal::ParamDefault::U64(#d)) },
750 ),
751 ("u64", None) => (
752 quote! { ::actus::__internal::ParamType::U64 },
753 quote! { None },
754 ),
755 ("u32", Some(d)) => (
756 quote! { ::actus::__internal::ParamType::U32 },
757 quote! { Some(::actus::__internal::ParamDefault::U32(#d)) },
758 ),
759 ("u32", None) => (
760 quote! { ::actus::__internal::ParamType::U32 },
761 quote! { None },
762 ),
763 ("f64", Some(d)) => (
764 quote! { ::actus::__internal::ParamType::F64 },
765 quote! { Some(::actus::__internal::ParamDefault::F64(#d)) },
766 ),
767 ("f64", None) => (
768 quote! { ::actus::__internal::ParamType::F64 },
769 quote! { None },
770 ),
771 ("bool", Some(d)) => (
772 quote! { ::actus::__internal::ParamType::Bool },
773 quote! { Some(::actus::__internal::ParamDefault::Bool(#d)) },
774 ),
775 ("bool", None) => (
776 quote! { ::actus::__internal::ParamType::Bool },
777 quote! { None },
778 ),
779 ("Vec<String>", _) => (
780 quote! { ::actus::__internal::ParamType::StringArray },
781 quote! { None },
782 ),
783 ("JsonValue", _) => (
784 quote! { ::actus::__internal::ParamType::Json },
785 quote! { None },
786 ),
787 ("Bytes", _) => (
788 quote! { ::actus::__internal::ParamType::Bytes },
789 quote! { None },
790 ),
791 _ => (
792 quote! { compile_error!(concat!("Unsupported type: ", #ty_str)) },
793 quote! { None },
794 ),
795 }
796}
797
798fn generate_param_extraction(
799 name_str: &str,
800 ty_str: &str,
801 default: &Option<Expr>,
802) -> proc_macro2::TokenStream {
803 match (ty_str, default) {
804 ("String", Some(d)) => {
805 quote! {
806 extracted.get_string(#name_str)
807 .unwrap_or_else(|_| #d.to_string())
808 }
809 }
810 ("String", None) => {
811 quote! { extracted.get_string(#name_str)? }
812 }
813 ("i64", Some(d)) => {
814 quote! {
815 extracted.get_i64(#name_str).unwrap_or(#d)
816 }
817 }
818 ("i64", None) => {
819 quote! { extracted.get_i64(#name_str)? }
820 }
821 ("u64", Some(d)) => {
822 quote! {
823 extracted.get_u64(#name_str).unwrap_or(#d)
824 }
825 }
826 ("u64", None) => {
827 quote! { extracted.get_u64(#name_str)? }
828 }
829 ("u32", Some(d)) => {
830 quote! {
831 extracted.get_u32(#name_str).unwrap_or(#d)
832 }
833 }
834 ("u32", None) => {
835 quote! { extracted.get_u32(#name_str)? }
836 }
837 ("f64", Some(d)) => {
838 quote! {
839 extracted.get_f64(#name_str).unwrap_or(#d)
840 }
841 }
842 ("f64", None) => {
843 quote! { extracted.get_f64(#name_str)? }
844 }
845 ("bool", Some(d)) => {
846 quote! {
847 extracted.get_bool(#name_str).unwrap_or(#d)
848 }
849 }
850 ("bool", None) => {
851 quote! { extracted.get_bool(#name_str)? }
852 }
853 ("Vec<String>", _) => {
854 quote! { extracted.get_string_array(#name_str)? }
855 }
856 ("JsonValue", _) => {
857 quote! { extracted.get_json_body()? }
858 }
859 ("Bytes", _) => {
864 quote! { extracted.get_body_bytes() }
865 }
866 _ => {
867 quote! { compile_error!(concat!("Unsupported type: ", #ty_str)) }
868 }
869 }
870}
871
872struct AppRoutesInput {
911 inputs: Vec<InputParam>,
912 deps: Vec<DepBinding>,
913 routes: Vec<RouteBinding>,
914}
915
916struct InputParam {
917 name: Ident,
918 ty: syn::Type,
919}
920
921struct DepBinding {
922 name: Ident,
923 value: Expr,
924}
925
926struct RouteBinding {
927 path: LitStr,
928 construction: Expr,
929}
930
931impl Parse for AppRoutesInput {
932 fn parse(input: ParseStream) -> syn::Result<Self> {
933 let mut inputs: Vec<InputParam> = Vec::new();
934 let mut deps: Vec<DepBinding> = Vec::new();
935 let mut routes: Option<Vec<RouteBinding>> = None;
936
937 while !input.is_empty() {
938 let kw: Ident = input.parse()?;
939 let kw_str = kw.to_string();
940
941 match kw_str.as_str() {
942 "deps" => {
943 if input.peek(syn::token::Paren) {
945 let paren_content;
946 syn::parenthesized!(paren_content in input);
947 while !paren_content.is_empty() {
948 let name: Ident = paren_content.parse()?;
949 paren_content.parse::<Token![:]>()?;
950 let ty: syn::Type = paren_content.parse()?;
951 inputs.push(InputParam { name, ty });
952 if !paren_content.is_empty() {
953 paren_content.parse::<Token![,]>()?;
954 }
955 }
956 }
957 let content;
960 syn::braced!(content in input);
961 while !content.is_empty() {
962 let name: Ident = content.parse()?;
963 content.parse::<Token![=]>()?;
964 let value: Expr = content.parse()?;
965 deps.push(DepBinding { name, value });
966 if !content.is_empty() {
967 content.parse::<Token![,]>()?;
968 }
969 }
970 }
971 "routes" => {
972 let content;
973 syn::braced!(content in input);
974 let mut rs = Vec::new();
975 while !content.is_empty() {
976 let path: LitStr = content.parse()?;
977 content.parse::<Token![=>]>()?;
978 let construction: Expr = content.parse()?;
979 rs.push(RouteBinding { path, construction });
980 if !content.is_empty() {
981 content.parse::<Token![,]>()?;
982 }
983 }
984 routes = Some(rs);
985 }
986 other => {
987 return Err(syn::Error::new(
988 kw.span(),
989 format!("expected 'deps' or 'routes', got '{}'", other),
990 ));
991 }
992 }
993 }
994
995 let routes = routes.ok_or_else(|| {
996 syn::Error::new(
997 proc_macro2::Span::call_site(),
998 "app_routes! requires a 'routes { ... }' block",
999 )
1000 })?;
1001
1002 Ok(Self {
1003 inputs,
1004 deps,
1005 routes,
1006 })
1007 }
1008}
1009
1010#[proc_macro]
1019pub fn app_routes(input: TokenStream) -> TokenStream {
1020 let parsed = parse_macro_input!(input as AppRoutesInput);
1021 generate_app_routes(parsed).into()
1022}
1023
1024fn generate_app_routes(parsed: AppRoutesInput) -> proc_macro2::TokenStream {
1025 let init_params = parsed.inputs.iter().map(|p| {
1026 let name = &p.name;
1027 let ty = &p.ty;
1028 quote! { #name: #ty }
1029 });
1030
1031 let dep_lets = parsed.deps.iter().map(|d| {
1032 let name = &d.name;
1033 let value = &d.value;
1034 quote! { let #name = #value; }
1035 });
1036
1037 let route_calls = parsed.routes.iter().map(|r| {
1038 let path = &r.path;
1039 let construction = rewrite_construction(&r.construction);
1040 quote! {
1041 .add_route(#path, ::std::sync::Arc::new(#construction))
1042 }
1043 });
1044
1045 quote! {
1046 pub async fn init(#(#init_params),*) -> ::actus::InitResult<::actus::Router> {
1047 #(#dep_lets)*
1048
1049 let router = ::actus::RouterBuilder::new()
1050 #(#route_calls)*
1051 .build();
1052
1053 ::std::result::Result::Ok(router)
1054 }
1055 }
1056}
1057
1058fn rewrite_construction(expr: &Expr) -> proc_macro2::TokenStream {
1075 let Expr::Struct(s) = expr else {
1076 return expr.to_token_stream();
1077 };
1078
1079 let path = &s.path;
1080 let mut inner = proc_macro2::TokenStream::new();
1081 let mut wrote_field = false;
1082
1083 for f in s.fields.iter() {
1084 if wrote_field {
1085 inner.extend(quote! { , });
1086 }
1087 wrote_field = true;
1088
1089 let member = &f.member;
1090 if f.colon_token.is_none() {
1091 inner.extend(quote! { #member: #member.clone() });
1093 } else if is_bare_ident(&f.expr) {
1094 let value = &f.expr;
1099 inner.extend(quote! { #member: #value.clone() });
1100 } else {
1101 let value = &f.expr;
1102 inner.extend(quote! { #member: #value });
1103 }
1104 }
1105
1106 if let Some(rest) = &s.rest {
1107 if wrote_field {
1108 inner.extend(quote! { , });
1109 }
1110 if is_bare_ident(rest) {
1111 inner.extend(quote! { ..(#rest).clone() });
1112 } else {
1113 inner.extend(quote! { ..#rest });
1114 }
1115 }
1116
1117 quote! { #path { #inner } }
1118}
1119
1120fn is_bare_ident(expr: &Expr) -> bool {
1125 let Expr::Path(p) = expr else { return false };
1126 p.qself.is_none()
1127 && p.path.leading_colon.is_none()
1128 && p.path.segments.len() == 1
1129 && p.path.segments[0].arguments.is_none()
1130}