Skip to main content

actus_controller_macros/
lib.rs

1//! Procedural macros for the Actus controller system: the `#[controller]`
2//! attribute and the `app_routes!` macro. (The `routes!` macro that appears
3//! inside a `#[controller]` impl is a `macro_rules!` in `actus-controller`;
4//! `#[controller]` consumes the block it produces.)
5//!
6//! This crate is an implementation detail — depend on `actus` (or
7//! `actus-controller`) and use the macros through their prelude re-exports
8//! rather than depending on this crate directly.
9//!
10//! Supports HTTP verb constraints, path parameters (including a trailing
11//! `{...rest}`), strict/lax parameter modes, `prepare` hooks, per-controller
12//! `rate_limit` / `max_body_bytes`, and per-route docs sourced from each handler's
13//! `///` comment.
14#![warn(missing_docs)]
15
16use proc_macro::TokenStream;
17use quote::{ToTokens, quote};
18use std::collections::BTreeMap; // NEW: used to gather handler docs
19use syn::{
20    Expr, Ident, ImplItem, ItemImpl, LitStr, Token,
21    ext::IdentExt,
22    parse::{Parse, ParseStream},
23    parse_macro_input,
24    punctuated::Punctuated,
25};
26
27// =========================
28// Core types for route definitions
29// =========================
30
31struct 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// =========================
87// Controller attributes (strict/lax, prepare function)
88// =========================
89
90#[derive(Debug, Clone, Copy)]
91enum ControllerMode {
92    Strict,
93    Lax,
94}
95
96struct ControllerAttrs {
97    mode: ControllerMode,
98    prepare: Option<syn::ExprPath>,
99    /// `#[controller(max_body_bytes = <expr>)]` — per-controller maximum
100    /// buffered body, in bytes. `None` means inherit the server-level cap.
101    max_body_bytes: Option<syn::Expr>,
102    /// `#[controller(rate_limit = <expr>)]` — per-controller rate-limit
103    /// *class* label (an `&'static str`). `None` means the controller
104    /// declares no class. A label, not a policy: the application's
105    /// rate-limit middleware maps class → limits (see the `Controller`
106    /// trait's `actus_rate_limit` docs).
107    rate_limit: Option<syn::Expr>,
108}
109
110// =========================
111// Parser implementations
112// =========================
113
114impl Parse for AllRoutes {
115    fn parse(input: ParseStream) -> syn::Result<Self> {
116        let mut routes = Vec::new();
117
118        while !input.is_empty() {
119            // Reject the legacy `[Access::X]` section syntax with a clear
120            // pointer. Actus is now policy-agnostic; access decisions live
121            // in the application's `prepare` hook (and its policy layer).
122            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            // Check for optional HTTP verb prefix (e.g., GET, POST)
133            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            // Parse the route pattern (e.g., "posts/{id}")
154            let pattern: LitStr = input.parse()?;
155            validate_pattern(&pattern)?;
156            input.parse::<Token![=>]>()?;
157            let handler: Ident = input.parse()?;
158
159            // Parse handler parameters
160            let params_content;
161            syn::parenthesized!(params_content in input);
162            let params = Punctuated::parse_terminated(&params_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                    // Accept any expression — a literal (`4096`), a const
216                    // reference (`MAX_BODY`), or an arithmetic expression
217                    // (`4 * 1024`). Resolved at handler-build time, so
218                    // const-fn / static const are both fine.
219                    max_body_bytes = Some(input.parse()?);
220                }
221                "rate_limit" => {
222                    input.parse::<Token![=]>()?;
223                    // Accept any expression that evaluates to `&'static str` —
224                    // a string literal (`"auth"`) is the common case; a const
225                    // reference (`AUTH_CLASS`) works too. It's a *label*, not a
226                    // policy: the app's rate-limit middleware maps it to limits.
227                    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
252// =========================
253// Helper functions
254// =========================
255
256fn 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            // `{...name}` is a "rest" parameter (captures the path remainder);
274            // its handler-side binding is just `name`. `{name}` is unchanged.
275            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
285/// If `segment` is a well-formed `{...name}` rest token, returns `Some(name)`.
286/// Returns `None` for ordinary `{name}` tokens and for literals.
287fn 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
294/// Validate a route pattern at macro-expansion time. Enforces the rules the
295/// runtime matcher ([`actus_controller::routing::match_pattern`]) relies on:
296/// a `{...name}` rest parameter, if present, must be the *last* `/`-segment,
297/// must appear at most once, and must have a non-empty name. Also rejects the
298/// near-miss `{..name}` / `{...}` shapes with a pointed message.
299fn 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        // Only consider segments that look like a single `{...}` token.
305        let Some(inner) = seg.strip_prefix('{').and_then(|s| s.strip_suffix('}')) else {
306            continue;
307        };
308
309        if !inner.starts_with('.') {
310            continue; // ordinary `{name}` token — nothing to check here.
311        }
312
313        // It starts with a dot, so the author meant a rest parameter.
314        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        // Last segment and well-formed; make sure it's the only one. (Any
335        // earlier rest token would already have errored on the position
336        // check above, so reaching here twice is impossible — but a literal
337        // earlier segment that merely *contains* `{...}` text wouldn't, so
338        // be explicit about "at most one".)
339        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
354// NEW: Collect `///` docs from methods in the impl and join them by newlines.
355fn 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                        // #[doc = "..."]  → nv.value is an Expr
366                        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// =========================
394// Main macro entry point
395// =========================
396
397/// Attribute macro for a controller's `impl` block.
398///
399/// Reads the `routes! { … }` block inside the impl and generates the
400/// controller's `Controller` implementation — its route table, parameter
401/// extraction, and dispatch. Attribute options: `prepare = Self::method` (a
402/// hook run before every handler in the controller), `lax` (relax strict
403/// parameter rejection), `rate_limit = "class"` (stamp a rate-limit class onto
404/// matched requests), and `max_body_bytes = N` (per-controller request-body cap, in
405/// bytes).
406#[proc_macro_attribute]
407pub fn controller(attr: TokenStream, item: TokenStream) -> TokenStream {
408    // Parse attributes (strict/lax mode, prepare function)
409    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    // NEW: collect method docs (by handler name)
426    let docs_map = collect_method_docs(&item_impl);
427
428    // Find the routes! macro inside the impl block
429    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    // Parse the routes
442    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    // Generate code (passing docs_map)
448    let generated = generate_controller_impl(&item_impl, &all_routes, &attrs, &docs_map);
449
450    generated.into()
451}
452
453// =========================
454// Code generation
455// =========================
456
457fn generate_controller_impl(
458    item_impl: &ItemImpl,
459    all_routes: &AllRoutes,
460    attrs: &ControllerAttrs,
461    docs_map: &BTreeMap<String, String>, // NEW
462) -> proc_macro2::TokenStream {
463    let self_ty = &item_impl.self_ty;
464
465    // Generate route definitions and handler dispatch arms
466    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        // Extract path parameters from the pattern
476        let path_params = extract_path_params(&pattern_str);
477        // The (at most one) `{...name}` rest parameter, if the pattern has one.
478        let rest_param: Option<String> = pattern_str
479            .split('/')
480            .find_map(|s| rest_param_name(s).map(str::to_string));
481
482        // Build parameter definitions and extraction code
483        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 = &param.name;
489            // The *wire* name (query key / path-segment name) is the bare
490            // identifier — `r#`-strip raw identifiers so a handler can bind a
491            // keyword-named parameter (`r#type: Vec<String>` reads `?type=`).
492            // The handler-call identifier (`param_names`) keeps the `r#`.
493            let name_str = name.unraw().to_string();
494            let ty_str = type_to_string(&param.ty);
495
496            // Collect parameter names for handler call
497            param_names.push(name.clone());
498
499            // Special pass-through: a handler may declare `_: &Params` to
500            // receive a borrow of the per-request `Params` (typically to
501            // read state stashed by a `prepare` hook via `params.insert`).
502            // No `ParamDef` is emitted for this — it's framework plumbing,
503            // not a request input.
504            //
505            // Naming-collision note: if a route's pattern has a `{name}`
506            // capture *and* the handler also declares `name: &Params`, the
507            // `&Params` short-circuit wins — the path capture is silently
508            // discarded. Don't name a `&Params` binding the same as a path
509            // token. (The reader, not the compiler, has to catch it.)
510            if ty_str == "&Params" {
511                param_extractions.push(quote! { &params });
512                continue;
513            }
514
515            let is_rest = rest_param.as_deref() == Some(name_str.as_str());
516
517            // A `{...name}` rest parameter always carries the joined path
518            // remainder, so it must be typed `String`. (A `{name}` segment
519            // can be `u64`/`u32`/… because it's a single segment; the rest
520            // token can't.)
521            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            // Determine parameter source (path, query, or body)
532            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            // Generate parameter type and default value
541            let (param_type, default_value) =
542                generate_param_type_and_default(&ty_str, &param.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            // Generate extraction code for this parameter
554            let extraction = generate_param_extraction(&name_str, &ty_str, &param.default);
555            param_extractions.push(extraction);
556        }
557
558        // Build route definition. `RouteDef.verb` is `&'static [Verb]`:
559        // a single-element slice for an explicit verb, or
560        // `DEFAULT_VERBS` (= [GET, POST]) for an unmarked route.
561        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        // NEW: look up handler docs and attach to RouteDef
570        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        // Build handler dispatch arm
586        handler_arms.push(quote! {
587            #handler_id => {
588                #(let #param_names = #param_extractions;)*
589                self.#handler(#(#param_names),*).await
590            }
591        });
592    }
593
594    // Generate prepare function call if specified.
595    //
596    // Signature contract:
597    //     async fn prepare(&self, route: &RouteDef, params: &mut Params)
598    //         -> Result<Option<ReplyData>, WebError>;
599    //
600    // - `Ok(None)` continues to the handler.
601    // - `Ok(Some(reply))` short-circuits with that reply (any HTTP status the
602    //   hook chose).
603    // - `Err(WebError::*)` short-circuits with the corresponding error response.
604    //
605    // We pass `&mut params` so the hook can both *read* the request (headers,
606    // body, undeclared query params) and *attach* per-request state via
607    // `params.insert(...)` for handlers to read via a `&Params` parameter.
608    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    // Generate mode configuration
623    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    // The prepare hook needs `&mut params` so it can stash per-request state
636    // for handlers via `params.insert(...)`. When no prepare is configured,
637    // omit `mut` to avoid an "unused_mut" warning in the user's crate.
638    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    // `#[controller(max_body_bytes = …)]` — emit an `actus_max_body_bytes` override
645    // returning `Some(<expr>)`. When not set, the trait's default impl
646    // returns `None` and the server falls back to its own cap.
647    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    // `#[controller(rate_limit = "class")]` — emit an `actus_rate_limit`
656    // override returning `Some("class")`. When not set, the trait's default
657    // impl returns `None` (the controller declares no rate-limit class). The
658    // server stamps this label onto the matched request so an application's
659    // rate-limit middleware can read it; the framework owns no policy.
660    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    // Generate main Controller trait implementation
669    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                // Define routes as static data inside the method
674                // This works with dyn Controller since it's not an associated const
675                static ROUTES: &[::actus::__internal::RouteDef] = &[ #(#route_defs),* ];
676
677                // Use shared routing utilities to resolve the route
678                let (matched_route, extracted) = ::actus::__internal::routing::resolve(
679                    ROUTES,
680                    action,
681                    &params,
682                    #mode_value
683                )?;
684
685                // Call prepare function if configured
686                #prepare_call
687
688                // Type-safe dispatch to handlers. `resolve` only ever returns
689                // a route from `ROUTES`, and every route there has a matching
690                // arm below (both are keyed by the macro-assigned handler id),
691                // so the catch-all is genuinely unreachable — `match` on `&str`
692                // just can't prove it.
693                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            /// Returns the static route definitions for this controller.
706            /// Useful for introspection (e.g., generating API documentation).
707            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        // Original impl block unchanged
719        #item_impl
720
721        // Generated Controller implementation
722        #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        // Raw request-body bytes. Use for binary uploads (e.g. `.uwx`
860        // packages). The framework discriminates JSON/form/binary at
861        // ingest by `Content-Type`; declaring `body: Bytes` is the
862        // signal that this handler wants the unparsed payload.
863        ("Bytes", _) => {
864            quote! { extracted.get_body_bytes() }
865        }
866        _ => {
867            quote! { compile_error!(concat!("Unsupported type: ", #ty_str)) }
868        }
869    }
870}
871
872// =========================
873// app_routes! — application-level route map with deps + per-route service injection
874// =========================
875//
876// Grammar:
877//
878//     app_routes! {
879//         // Optional. The `deps(...)` parens declare *inputs* — values
880//         // constructed by the caller (typically in `main()`) and passed
881//         // into the generated `init()` function. The brace block is the
882//         // `let`-block of dependencies built inside `init()`.
883//         deps(store: Arc<Store>) {
884//             cache = Cache::redis(...).await?,
885//         }
886//         routes {
887//             "api/entities" => EntityController { store },
888//             "api/cache"    => CacheController { cache },
889//             "health"       => HealthController,
890//             "*"            => SpaController,
891//         }
892//     }
893//
894// Generates `pub async fn init(<inputs>) -> actus::InitResult<actus::Router>`,
895// where `InitResult<T> = Result<T, anyhow::Error>` — `?` on any error type
896// implementing `std::error::Error + Send + Sync + 'static` works inside.
897// The `deps` block is optional; the `(<inputs>)` clause inside it is
898// optional too. All four shapes are valid:
899//
900//     deps { ... }                            // only let-bindings
901//     deps(a: T, b: U) { ... }                // both inputs and let-bindings
902//     deps(a: T, b: U) {}                     // only inputs
903//     // (no deps block at all)               // neither
904//
905// In each route's controller construction, struct-literal shorthand
906// (`{ store, cache }`) and rest-spread (`..base`) are auto-cloned, since
907// deps and inputs are typically `Arc`-wrapped and shared across multiple
908// controllers. Non-struct-literal expressions pass through unchanged.
909
910struct 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                    // Optional `(name: Type, ...)` declaring init() inputs.
944                    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                    // Then the `{ name = expr, ... }` block of let bindings
958                    // (may be empty).
959                    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/// Declares the application's URL blueprint and generates its `init()`.
1011///
1012/// Takes an optional `deps( … ) { … }` block — constructor-injected services
1013/// and `let`-bindings shared across controllers — and a `routes { mount =>
1014/// Controller … }` map. Expands to an async `init(…)` returning the built
1015/// `Router`: it constructs every controller, wires its dependencies, and
1016/// registers each mount. See the `actus` crate's top-level docs for a worked
1017/// example.
1018#[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
1058/// In a struct-literal controller construction, auto-clone simple references
1059/// to bound names so the same value can be threaded into multiple
1060/// controllers without each call site spelling `.clone()`.
1061///
1062/// Three cases get auto-cloned, all gated on the right-hand side being a
1063/// bare unqualified identifier (no path segments, no generic args, no
1064/// `qself`). The escape hatch in every case is the same: write any
1065/// non-ident expression — method call, function call, qualified path, an
1066/// already-`.clone()`d value — and it passes through unchanged.
1067///
1068/// * **Shorthand** — `Foo { db }` → `Foo { db: db.clone() }`.
1069/// * **Bare-ident explicit form** — `Foo { svc: store }` →
1070///   `Foo { svc: store.clone() }`.
1071/// * **Bare-ident rest spread** — `Foo { ..base }` → `Foo { ..(base).clone() }`.
1072///   Non-ident rest expressions (`..base.clone()`, `..self.template()`)
1073///   pass through verbatim — no double-cloning.
1074fn 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            // Shorthand: `name` → `name: name.clone()`
1092            inner.extend(quote! { #member: #member.clone() });
1093        } else if is_bare_ident(&f.expr) {
1094            // Explicit `target: source` where `source` is a simple ident:
1095            // treat like shorthand and auto-clone. Any non-ident expression
1096            // (method call, function call, qualified path, …) passes
1097            // through unchanged so callers retain a clean escape hatch.
1098            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
1120/// Whether `expr` is a single, unqualified identifier path (no qself, no
1121/// leading `::`, exactly one segment, no generic args). The criterion the
1122/// auto-clone rule uses to decide that an explicit field assignment looks
1123/// "shorthand-like."
1124fn 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}