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    /// `#[controller(expects = <expr>)]` — the controller's declared caller
109    /// expectation (an `&'static str`), a *floor*: the least-privileged
110    /// caller it is written to accept. `None` means the controller declares
111    /// nothing — which is a visible row in `Router::mounts()`, not a skip.
112    /// A label, not a policy (see the `Controller` trait's `actus_expects`
113    /// docs).
114    expects: Option<syn::Expr>,
115}
116
117// =========================
118// Parser implementations
119// =========================
120
121impl Parse for AllRoutes {
122    fn parse(input: ParseStream) -> syn::Result<Self> {
123        let mut routes = Vec::new();
124
125        while !input.is_empty() {
126            // Reject the legacy `[Access::X]` section syntax with a clear
127            // pointer. Actus is now policy-agnostic; access decisions live
128            // in the application's `prepare` hook (and its policy layer).
129            if input.peek(syn::token::Bracket) {
130                let bracket_span = input.fork().parse::<proc_macro2::TokenTree>()?.span();
131                return Err(syn::Error::new(
132                    bracket_span,
133                    "actus no longer ships an `Access` enum or `[Access::*]` section syntax. \
134                     Authorization belongs in your `#[controller(prepare = …)]` hook, where \
135                     you can call into your own policy layer (e.g. `services::policy::*`).",
136                ));
137            }
138
139            // Check for optional HTTP verb prefix (e.g., GET, POST)
140            let verb = if input.peek2(LitStr) {
141                if let Ok(ident) = input.parse::<Ident>() {
142                    if let Some(v) = Verb::from_ident(&ident) {
143                        Some(v)
144                    } else {
145                        return Err(syn::Error::new(
146                            ident.span(),
147                            format!(
148                                "Unknown HTTP verb: {}. Expected GET, POST, PUT, DELETE, PATCH, HEAD, or OPTIONS",
149                                ident
150                            ),
151                        ));
152                    }
153                } else {
154                    None
155                }
156            } else {
157                None
158            };
159
160            // Parse the route pattern (e.g., "posts/{id}")
161            let pattern: LitStr = input.parse()?;
162            validate_pattern(&pattern)?;
163            input.parse::<Token![=>]>()?;
164            let handler: Ident = input.parse()?;
165
166            // Parse handler parameters
167            let params_content;
168            syn::parenthesized!(params_content in input);
169            let params = Punctuated::parse_terminated(&params_content)?;
170
171            routes.push(RouteDefinition {
172                verb,
173                pattern,
174                handler,
175                params,
176            });
177
178            if input.peek(Token![,]) {
179                input.parse::<Token![,]>()?;
180            }
181        }
182
183        Ok(AllRoutes { routes })
184    }
185}
186
187impl Parse for Param {
188    fn parse(input: ParseStream) -> syn::Result<Self> {
189        let name: Ident = input.parse()?;
190        input.parse::<Token![:]>()?;
191        let ty: syn::Type = input.parse()?;
192
193        let default = if input.peek(Token![=]) {
194            input.parse::<Token![=]>()?;
195            Some(input.parse()?)
196        } else {
197            None
198        };
199
200        Ok(Param { name, ty, default })
201    }
202}
203
204impl Parse for ControllerAttrs {
205    fn parse(input: ParseStream) -> syn::Result<Self> {
206        let mut mode = ControllerMode::Strict;
207        let mut prepare = None;
208        let mut max_body_bytes = None;
209        let mut rate_limit = None;
210        let mut expects = None;
211
212        while !input.is_empty() {
213            let ident: Ident = input.parse()?;
214            match ident.to_string().as_str() {
215                "strict" => mode = ControllerMode::Strict,
216                "lax" => mode = ControllerMode::Lax,
217                "prepare" => {
218                    input.parse::<Token![=]>()?;
219                    prepare = Some(input.parse()?);
220                }
221                "max_body_bytes" => {
222                    input.parse::<Token![=]>()?;
223                    // Accept any expression — a literal (`4096`), a const
224                    // reference (`MAX_BODY`), or an arithmetic expression
225                    // (`4 * 1024`). Resolved at handler-build time, so
226                    // const-fn / static const are both fine.
227                    max_body_bytes = Some(input.parse()?);
228                }
229                "rate_limit" => {
230                    input.parse::<Token![=]>()?;
231                    // Accept any expression that evaluates to `&'static str` —
232                    // a string literal (`"auth"`) is the common case; a const
233                    // reference (`AUTH_CLASS`) works too. It's a *label*, not a
234                    // policy: the app's rate-limit middleware maps it to limits.
235                    rate_limit = Some(input.parse()?);
236                }
237                "expects" => {
238                    input.parse::<Token![=]>()?;
239                    // Same contract as `rate_limit`: any expression evaluating
240                    // to `&'static str`. The value is opaque to the framework —
241                    // a floor the application's coverage check and gate read.
242                    expects = Some(input.parse()?);
243                }
244                _ => {
245                    return Err(syn::Error::new(
246                        ident.span(),
247                        "Expected 'strict', 'lax', 'prepare = <fn>', 'max_body_bytes = <expr>', \
248                         'rate_limit = <expr>', or 'expects = <expr>'",
249                    ));
250                }
251            }
252
253            if input.peek(Token![,]) {
254                input.parse::<Token![,]>()?;
255            }
256        }
257
258        Ok(ControllerAttrs {
259            mode,
260            prepare,
261            max_body_bytes,
262            rate_limit,
263            expects,
264        })
265    }
266}
267
268// =========================
269// Helper functions
270// =========================
271
272fn type_to_string(ty: &syn::Type) -> String {
273    quote!(#ty).to_string().replace(" ", "")
274}
275
276fn extract_path_params(pattern: &str) -> Vec<String> {
277    let mut params = Vec::new();
278    let mut chars = pattern.chars().peekable();
279
280    while let Some(ch) = chars.next() {
281        if ch == '{' {
282            let mut param = String::new();
283            for ch in chars.by_ref() {
284                if ch == '}' {
285                    break;
286                }
287                param.push(ch);
288            }
289            // `{...name}` is a "rest" parameter (captures the path remainder);
290            // its handler-side binding is just `name`. `{name}` is unchanged.
291            let name = param.strip_prefix("...").unwrap_or(param.as_str());
292            if !name.is_empty() {
293                params.push(name.to_string());
294            }
295        }
296    }
297
298    params
299}
300
301/// If `segment` is a well-formed `{...name}` rest token, returns `Some(name)`.
302/// Returns `None` for ordinary `{name}` tokens and for literals.
303fn rest_param_name(segment: &str) -> Option<&str> {
304    segment
305        .strip_prefix("{...")
306        .and_then(|s| s.strip_suffix('}'))
307        .filter(|name| !name.is_empty())
308}
309
310/// Validate a route pattern at macro-expansion time. Enforces the rules the
311/// runtime matcher ([`actus_controller::routing::match_pattern`]) relies on:
312/// a `{...name}` rest parameter, if present, must be the *last* `/`-segment,
313/// must appear at most once, and must have a non-empty name. Also rejects the
314/// near-miss `{..name}` / `{...}` shapes with a pointed message.
315fn validate_pattern(pattern: &LitStr) -> syn::Result<()> {
316    let value = pattern.value();
317    let segments: Vec<&str> = value.split('/').collect();
318
319    for (i, seg) in segments.iter().enumerate() {
320        // Only consider segments that look like a single `{...}` token.
321        let Some(inner) = seg.strip_prefix('{').and_then(|s| s.strip_suffix('}')) else {
322            continue;
323        };
324
325        if !inner.starts_with('.') {
326            continue; // ordinary `{name}` token — nothing to check here.
327        }
328
329        // It starts with a dot, so the author meant a rest parameter.
330        if rest_param_name(seg).is_none() {
331            return Err(syn::Error::new(
332                pattern.span(),
333                format!(
334                    "malformed rest parameter `{{{inner}}}` in route pattern `{value}`; \
335                     write it as `{{...name}}` (three dots, then a non-empty name)"
336                ),
337            ));
338        }
339
340        if i != segments.len() - 1 {
341            return Err(syn::Error::new(
342                pattern.span(),
343                format!(
344                    "rest parameter `{{{inner}}}` must be the last segment of route \
345                     pattern `{value}` (it captures the entire remaining path)"
346                ),
347            ));
348        }
349
350        // Last segment and well-formed; make sure it's the only one. (Any
351        // earlier rest token would already have errored on the position
352        // check above, so reaching here twice is impossible — but a literal
353        // earlier segment that merely *contains* `{...}` text wouldn't, so
354        // be explicit about "at most one".)
355        let earlier_rest = segments[..i]
356            .iter()
357            .filter(|s| rest_param_name(s).is_some())
358            .count();
359        if earlier_rest > 0 {
360            return Err(syn::Error::new(
361                pattern.span(),
362                format!("route pattern `{value}` has more than one `{{...name}}` rest parameter"),
363            ));
364        }
365    }
366
367    Ok(())
368}
369
370// NEW: Collect `///` docs from methods in the impl and join them by newlines.
371fn collect_method_docs(item_impl: &syn::ItemImpl) -> BTreeMap<String, String> {
372    use syn::{Attribute, ImplItem, Meta};
373
374    fn doc_from_attrs(attrs: &[Attribute]) -> String {
375        attrs
376            .iter()
377            .filter(|a| a.path().is_ident("doc"))
378            .filter_map(|a| {
379                match &a.meta {
380                    Meta::NameValue(nv) => {
381                        // #[doc = "..."]  → nv.value is an Expr
382                        if let syn::Expr::Lit(expr_lit) = &nv.value
383                            && let syn::Lit::Str(ls) = &expr_lit.lit
384                        {
385                            return Some(ls.value());
386                        }
387                        None
388                    }
389                    _ => None,
390                }
391            })
392            .collect::<Vec<_>>()
393            .join("\n")
394    }
395
396    let mut map = BTreeMap::new();
397    for it in &item_impl.items {
398        if let ImplItem::Fn(m) = it {
399            let name = m.sig.ident.to_string();
400            let doc = doc_from_attrs(&m.attrs);
401            if !doc.trim().is_empty() {
402                map.insert(name, doc);
403            }
404        }
405    }
406    map
407}
408
409// =========================
410// Main macro entry point
411// =========================
412
413/// Attribute macro for a controller's `impl` block.
414///
415/// Reads the `routes! { … }` block inside the impl and generates the
416/// controller's `Controller` implementation — its route table, parameter
417/// extraction, and dispatch. Attribute options: `prepare = Self::method` (a
418/// hook run before every handler in the controller), `lax` (relax strict
419/// parameter rejection), `rate_limit = "class"` (stamp a rate-limit class onto
420/// matched requests), `max_body_bytes = N` (per-controller request-body cap, in
421/// bytes), and `expects = "floor"` (declare the least-privileged caller this
422/// controller accepts — surfaced by `Router::mounts()` for route-family
423/// coverage checks).
424#[proc_macro_attribute]
425pub fn controller(attr: TokenStream, item: TokenStream) -> TokenStream {
426    // Parse attributes (strict/lax mode, prepare function)
427    let attrs = if attr.is_empty() {
428        ControllerAttrs {
429            mode: ControllerMode::Strict,
430            prepare: None,
431            max_body_bytes: None,
432            rate_limit: None,
433            expects: None,
434        }
435    } else {
436        match syn::parse::<ControllerAttrs>(attr) {
437            Ok(a) => a,
438            Err(e) => return e.to_compile_error().into(),
439        }
440    };
441
442    let item_impl = parse_macro_input!(item as ItemImpl);
443
444    // NEW: collect method docs (by handler name)
445    let docs_map = collect_method_docs(&item_impl);
446
447    // Find the routes! macro inside the impl block
448    let routes_macro = item_impl
449        .items
450        .iter()
451        .find_map(|item| {
452            if let ImplItem::Macro(m) = item
453                && m.mac.path.is_ident("routes") {
454                    return Some(m);
455                }
456            None
457        })
458        .expect("A `routes!` macro invocation is required inside an `impl` block marked with `#[controller]`");
459
460    // Parse the routes
461    let all_routes: AllRoutes = match syn::parse2(routes_macro.mac.tokens.clone()) {
462        Ok(routes) => routes,
463        Err(e) => return e.to_compile_error().into(),
464    };
465
466    // Generate code (passing docs_map)
467    let generated = generate_controller_impl(&item_impl, &all_routes, &attrs, &docs_map);
468
469    generated.into()
470}
471
472// =========================
473// Code generation
474// =========================
475
476fn generate_controller_impl(
477    item_impl: &ItemImpl,
478    all_routes: &AllRoutes,
479    attrs: &ControllerAttrs,
480    docs_map: &BTreeMap<String, String>, // NEW
481) -> proc_macro2::TokenStream {
482    let self_ty = &item_impl.self_ty;
483
484    // Generate route definitions and handler dispatch arms
485    let mut route_defs = Vec::new();
486    let mut handler_arms = Vec::new();
487
488    for (idx, route) in all_routes.routes.iter().enumerate() {
489        let pattern = &route.pattern;
490        let pattern_str = pattern.value();
491        let handler = &route.handler;
492        let handler_id = format!("handler_{}", idx);
493
494        // Extract path parameters from the pattern
495        let path_params = extract_path_params(&pattern_str);
496        // The (at most one) `{...name}` rest parameter, if the pattern has one.
497        let rest_param: Option<String> = pattern_str
498            .split('/')
499            .find_map(|s| rest_param_name(s).map(str::to_string));
500
501        // Build parameter definitions and extraction code
502        let mut param_defs = Vec::new();
503        let mut param_extractions = Vec::new();
504        let mut param_names = Vec::new();
505
506        for param in &route.params {
507            let name = &param.name;
508            // The *wire* name (query key / path-segment name) is the bare
509            // identifier — `r#`-strip raw identifiers so a handler can bind a
510            // keyword-named parameter (`r#type: Vec<String>` reads `?type=`).
511            // The handler-call identifier (`param_names`) keeps the `r#`.
512            let name_str = name.unraw().to_string();
513            let ty_str = type_to_string(&param.ty);
514
515            // Collect parameter names for handler call
516            param_names.push(name.clone());
517
518            // Special pass-through: a handler may declare `_: &Params` to
519            // receive a borrow of the per-request `Params` (typically to
520            // read state stashed by a `prepare` hook via `params.insert`).
521            // No `ParamDef` is emitted for this — it's framework plumbing,
522            // not a request input.
523            //
524            // Naming-collision note: if a route's pattern has a `{name}`
525            // capture *and* the handler also declares `name: &Params`, the
526            // `&Params` short-circuit wins — the path capture is silently
527            // discarded. Don't name a `&Params` binding the same as a path
528            // token. (The reader, not the compiler, has to catch it.)
529            if ty_str == "&Params" {
530                param_extractions.push(quote! { &params });
531                continue;
532            }
533
534            let is_rest = rest_param.as_deref() == Some(name_str.as_str());
535
536            // A `{...name}` rest parameter always carries the joined path
537            // remainder, so it must be typed `String`. (A `{name}` segment
538            // can be `u64`/`u32`/… because it's a single segment; the rest
539            // token can't.)
540            if is_rest && ty_str != "String" {
541                let msg = format!(
542                    "rest parameter `{{...{name_str}}}` must be typed `String` (it holds the \
543                     joined remaining path); found `{ty_str}`"
544                );
545                param_defs.push(quote! { compile_error!(#msg) });
546                param_extractions.push(quote! { compile_error!(#msg) });
547                continue;
548            }
549
550            // Determine parameter source (path, query, or body)
551            let source = if path_params.contains(&name_str) {
552                quote! { ::actus::__internal::ParamSource::Path }
553            } else if ty_str == "JsonValue" || ty_str == "Bytes" {
554                quote! { ::actus::__internal::ParamSource::Body }
555            } else {
556                quote! { ::actus::__internal::ParamSource::Query }
557            };
558
559            // Generate parameter type and default value
560            let (param_type, default_value) =
561                generate_param_type_and_default(&ty_str, &param.default);
562
563            param_defs.push(quote! {
564                ::actus::__internal::ParamDef {
565                    name: #name_str,
566                    ty: #param_type,
567                    source: #source,
568                    default: #default_value,
569                }
570            });
571
572            // Generate extraction code for this parameter
573            let extraction = generate_param_extraction(&name_str, &ty_str, &param.default);
574            param_extractions.push(extraction);
575        }
576
577        // Build route definition. `RouteDef.verb` is `&'static [Verb]`:
578        // a single-element slice for an explicit verb, or
579        // `DEFAULT_VERBS` (= [GET, POST]) for an unmarked route.
580        let verb_expr = match &route.verb {
581            Some(v) => {
582                let verb_tokens = v.to_tokens();
583                quote! { &[#verb_tokens] }
584            }
585            None => quote! { ::actus::__internal::DEFAULT_VERBS },
586        };
587
588        // NEW: look up handler docs and attach to RouteDef
589        let handler_name_str = handler.to_string();
590        let doc_val = docs_map.get(&handler_name_str).cloned().unwrap_or_default();
591        let doc_lit = syn::LitStr::new(&doc_val, proc_macro2::Span::call_site());
592
593        route_defs.push(quote! {
594            ::actus::__internal::RouteDef {
595                pattern: #pattern_str,
596                handler_id: #handler_id,
597                handler: #handler_name_str,
598                verb: #verb_expr,
599                params: &[ #(#param_defs),* ],
600                doc: if #doc_lit.is_empty() { None } else { Some(#doc_lit) },
601            }
602        });
603
604        // Build handler dispatch arm
605        handler_arms.push(quote! {
606            #handler_id => {
607                #(let #param_names = #param_extractions;)*
608                self.#handler(#(#param_names),*).await
609            }
610        });
611    }
612
613    // Generate prepare function call if specified.
614    //
615    // Signature contract:
616    //     async fn prepare(&self, route: &RouteDef, params: &mut Params)
617    //         -> Result<Option<ReplyData>, WebError>;
618    //
619    // - `Ok(None)` continues to the handler.
620    // - `Ok(Some(reply))` short-circuits with that reply (any HTTP status the
621    //   hook chose).
622    // - `Err(WebError::*)` short-circuits with the corresponding error response.
623    //
624    // We pass `&mut params` so the hook can both *read* the request (headers,
625    // body, undeclared query params) and *attach* per-request state via
626    // `params.insert(...)` for handlers to read via a `&Params` parameter.
627    let prepare_call = attrs
628        .prepare
629        .as_ref()
630        .map(|prepare_fn| {
631            quote! {
632                if let ::core::option::Option::Some(__actus_early_reply) =
633                    #prepare_fn(self, &matched_route, &mut params).await?
634                {
635                    return ::core::result::Result::Ok(__actus_early_reply);
636                }
637            }
638        })
639        .unwrap_or_default();
640
641    // Generate mode configuration
642    let mode_value = match attrs.mode {
643        ControllerMode::Strict => quote! { ::actus::__internal::ControllerMode::Strict },
644        ControllerMode::Lax => quote! { ::actus::__internal::ControllerMode::Lax },
645    };
646
647    let mode_str = match attrs.mode {
648        ControllerMode::Strict => "strict",
649        ControllerMode::Lax => "lax",
650    };
651
652    let _ = mode_str;
653
654    // The prepare hook needs `&mut params` so it can stash per-request state
655    // for handlers via `params.insert(...)`. When no prepare is configured,
656    // omit `mut` to avoid an "unused_mut" warning in the user's crate.
657    let params_binding = if attrs.prepare.is_some() {
658        quote! { mut params: ::actus::__internal::Params }
659    } else {
660        quote! { params: ::actus::__internal::Params }
661    };
662
663    // `#[controller(max_body_bytes = …)]` — emit an `actus_max_body_bytes` override
664    // returning `Some(<expr>)`. When not set, the trait's default impl
665    // returns `None` and the server falls back to its own cap.
666    let max_body_bytes_impl = attrs.max_body_bytes.as_ref().map(|expr| {
667        quote! {
668            fn actus_max_body_bytes(&self) -> ::core::option::Option<usize> {
669                ::core::option::Option::Some(#expr)
670            }
671        }
672    });
673
674    // `#[controller(rate_limit = "class")]` — emit an `actus_rate_limit`
675    // override returning `Some("class")`. When not set, the trait's default
676    // impl returns `None` (the controller declares no rate-limit class). The
677    // server stamps this label onto the matched request so an application's
678    // rate-limit middleware can read it; the framework owns no policy.
679    let rate_limit_impl = attrs.rate_limit.as_ref().map(|expr| {
680        quote! {
681            fn actus_rate_limit(&self) -> ::core::option::Option<&'static str> {
682                ::core::option::Option::Some(#expr)
683            }
684        }
685    });
686
687    // `#[controller(expects = "floor")]` — emit an `actus_expects` override
688    // returning `Some("floor")`. When not set, the trait's default impl
689    // returns `None`, and `Router::mounts()` reports the absence as a row —
690    // absence being representable is the point of the route-family design.
691    let expects_impl = attrs.expects.as_ref().map(|expr| {
692        quote! {
693            fn actus_expects(&self) -> ::core::option::Option<&'static str> {
694                ::core::option::Option::Some(#expr)
695            }
696        }
697    });
698
699    // …and the compile-time half: the marker trait `app_routes!`'s `families`
700    // block requires of every controller under a covered prefix. Emitted ONLY
701    // when `expects` is declared — its absence is what makes an undeclared
702    // controller under a covered prefix fail to compile. `expects` must therefore
703    // be a `const` expression (a string literal or a `const` path), which is
704    // also what makes the family's accepted-set check possible in a `const`.
705    let declares_expectation_impl = attrs.expects.as_ref().map(|expr| {
706        quote! {
707            impl ::actus::__internal::DeclaresExpectation for #self_ty {
708                const EXPECTS: &'static str = #expr;
709            }
710        }
711    });
712
713    // `#[controller(prepare = Self::auth)]` — additionally surface the hook's
714    // *presence* (and its written path, for route dumps) via `actus_prepare`,
715    // so a route-family coverage check can enforce rules like "a credential
716    // floor requires a hook". The hook itself is still compiled directly into
717    // `actus_dispatch` above; this is introspection, not an invocation handle.
718    let prepare_impl = attrs.prepare.as_ref().map(|prepare_fn| {
719        // Same tokens-to-text idiom as `type_to_string`: `quote!` needs no
720        // `ToTokens` import, and stripping spaces turns the token-stream
721        // rendering `Self :: auth` into the written form `Self::auth`.
722        let path_str = quote!(#prepare_fn).to_string().replace(' ', "");
723        let path_lit = syn::LitStr::new(&path_str, proc_macro2::Span::call_site());
724        quote! {
725            fn actus_prepare(&self) -> ::core::option::Option<&'static str> {
726                ::core::option::Option::Some(#path_lit)
727            }
728        }
729    });
730
731    // Generate main Controller trait implementation
732    let controller_impl = quote! {
733        #[::actus::__internal::async_trait]
734        impl ::actus::__internal::Controller for #self_ty {
735            async fn actus_dispatch(&self, action: &str, #params_binding) -> ::actus::__internal::Reply {
736                // Define routes as static data inside the method
737                // This works with dyn Controller since it's not an associated const
738                static ROUTES: &[::actus::__internal::RouteDef] = &[ #(#route_defs),* ];
739
740                // Use shared routing utilities to resolve the route
741                let (matched_route, extracted) = ::actus::__internal::routing::resolve(
742                    ROUTES,
743                    action,
744                    &params,
745                    #mode_value
746                )?;
747
748                // Call prepare function if configured
749                #prepare_call
750
751                // Type-safe dispatch to handlers. `resolve` only ever returns
752                // a route from `ROUTES`, and every route there has a matching
753                // arm below (both are keyed by the macro-assigned handler id),
754                // so the catch-all is genuinely unreachable — `match` on `&str`
755                // just can't prove it.
756                match matched_route.handler_id {
757                    #(#handler_arms),*
758                    other => ::core::unreachable!(
759                        "dispatch: no handler for route id {:?}", other
760                    ),
761                }
762            }
763
764            fn __name(&self) -> &'static str {
765                stringify!(#self_ty)
766            }
767
768            /// Returns the static route definitions for this controller.
769            /// Useful for introspection (e.g., generating API documentation).
770            fn actus_describe_routes(&self) -> Vec<::actus::__internal::RouteDef> {
771                static ROUTES: &[::actus::__internal::RouteDef] = &[ #(#route_defs),* ];
772                ROUTES.to_vec()
773            }
774
775            #max_body_bytes_impl
776            #rate_limit_impl
777            #expects_impl
778            #prepare_impl
779        }
780    };
781
782    quote! {
783        // Original impl block unchanged
784        #item_impl
785
786        #declares_expectation_impl
787
788        // Generated Controller implementation
789        #controller_impl
790    }
791}
792
793fn generate_param_type_and_default(
794    ty_str: &str,
795    default: &Option<Expr>,
796) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) {
797    match (ty_str, default) {
798        ("String", Some(d)) => (
799            quote! { ::actus::__internal::ParamType::String },
800            quote! { Some(::actus::__internal::ParamDefault::String(#d)) },
801        ),
802        ("String", None) => (
803            quote! { ::actus::__internal::ParamType::String },
804            quote! { None },
805        ),
806        ("i64", Some(d)) => (
807            quote! { ::actus::__internal::ParamType::Int },
808            quote! { Some(::actus::__internal::ParamDefault::Int(#d)) },
809        ),
810        ("i64", None) => (
811            quote! { ::actus::__internal::ParamType::Int },
812            quote! { None },
813        ),
814        ("u64", Some(d)) => (
815            quote! { ::actus::__internal::ParamType::U64 },
816            quote! { Some(::actus::__internal::ParamDefault::U64(#d)) },
817        ),
818        ("u64", None) => (
819            quote! { ::actus::__internal::ParamType::U64 },
820            quote! { None },
821        ),
822        ("u32", Some(d)) => (
823            quote! { ::actus::__internal::ParamType::U32 },
824            quote! { Some(::actus::__internal::ParamDefault::U32(#d)) },
825        ),
826        ("u32", None) => (
827            quote! { ::actus::__internal::ParamType::U32 },
828            quote! { None },
829        ),
830        ("f64", Some(d)) => (
831            quote! { ::actus::__internal::ParamType::F64 },
832            quote! { Some(::actus::__internal::ParamDefault::F64(#d)) },
833        ),
834        ("f64", None) => (
835            quote! { ::actus::__internal::ParamType::F64 },
836            quote! { None },
837        ),
838        ("bool", Some(d)) => (
839            quote! { ::actus::__internal::ParamType::Bool },
840            quote! { Some(::actus::__internal::ParamDefault::Bool(#d)) },
841        ),
842        ("bool", None) => (
843            quote! { ::actus::__internal::ParamType::Bool },
844            quote! { None },
845        ),
846        ("Vec<String>", _) => (
847            quote! { ::actus::__internal::ParamType::StringArray },
848            quote! { None },
849        ),
850        ("JsonValue", _) => (
851            quote! { ::actus::__internal::ParamType::Json },
852            quote! { None },
853        ),
854        ("Bytes", _) => (
855            quote! { ::actus::__internal::ParamType::Bytes },
856            quote! { None },
857        ),
858        _ => (
859            quote! { compile_error!(concat!("Unsupported type: ", #ty_str)) },
860            quote! { None },
861        ),
862    }
863}
864
865fn generate_param_extraction(
866    name_str: &str,
867    ty_str: &str,
868    default: &Option<Expr>,
869) -> proc_macro2::TokenStream {
870    match (ty_str, default) {
871        ("String", Some(d)) => {
872            quote! {
873                extracted.get_string(#name_str)
874                    .unwrap_or_else(|_| #d.to_string())
875            }
876        }
877        ("String", None) => {
878            quote! { extracted.get_string(#name_str)? }
879        }
880        ("i64", Some(d)) => {
881            quote! {
882                extracted.get_i64(#name_str).unwrap_or(#d)
883            }
884        }
885        ("i64", None) => {
886            quote! { extracted.get_i64(#name_str)? }
887        }
888        ("u64", Some(d)) => {
889            quote! {
890                extracted.get_u64(#name_str).unwrap_or(#d)
891            }
892        }
893        ("u64", None) => {
894            quote! { extracted.get_u64(#name_str)? }
895        }
896        ("u32", Some(d)) => {
897            quote! {
898                extracted.get_u32(#name_str).unwrap_or(#d)
899            }
900        }
901        ("u32", None) => {
902            quote! { extracted.get_u32(#name_str)? }
903        }
904        ("f64", Some(d)) => {
905            quote! {
906                extracted.get_f64(#name_str).unwrap_or(#d)
907            }
908        }
909        ("f64", None) => {
910            quote! { extracted.get_f64(#name_str)? }
911        }
912        // ⛔ `get_bool_optional`, NOT `get_bool`. Absence is `Ok(false)` for a bool — the
913        // one type whose "missing" is a usable value — so `get_bool(..).unwrap_or(d)`
914        // unwraps that `false` and the declared default is DEAD CODE. Every other
915        // type reaches its default because `require_scalar` errors on absence.
916        // Measured 2026-09-04: a cancel route declaring `at_period_end: bool = true`
917        // cancelled immediately when the client omitted the parameter.
918        ("bool", Some(d)) => {
919            quote! {
920                extracted.get_bool_optional(#name_str)?.unwrap_or(#d)
921            }
922        }
923        ("bool", None) => {
924            quote! { extracted.get_bool(#name_str)? }
925        }
926        ("Vec<String>", _) => {
927            quote! { extracted.get_string_array(#name_str)? }
928        }
929        ("JsonValue", _) => {
930            quote! { extracted.get_json_body()? }
931        }
932        // Raw request-body bytes. Use for binary uploads (e.g. `.uwx`
933        // packages). The framework discriminates JSON/form/binary at
934        // ingest by `Content-Type`; declaring `body: Bytes` is the
935        // signal that this handler wants the unparsed payload.
936        ("Bytes", _) => {
937            quote! { extracted.get_body_bytes() }
938        }
939        _ => {
940            quote! { compile_error!(concat!("Unsupported type: ", #ty_str)) }
941        }
942    }
943}
944
945// =========================
946// app_routes! — application-level route map with deps + per-route service injection
947// =========================
948//
949// Grammar:
950//
951//     app_routes! {
952//         // Optional. The `deps(...)` parens declare *inputs* — values
953//         // constructed by the caller (typically in `main()`) and passed
954//         // into the generated `init()` function. The brace block is the
955//         // `let`-block of dependencies built inside `init()`.
956//         deps(store: Arc<Store>) {
957//             cache = Cache::redis(...).await?,
958//         }
959//         routes {
960//             "api/entities" => EntityController { store },
961//             "api/cache"    => CacheController { cache },
962//             "health"       => HealthController,
963//             "*"            => SpaController,
964//         }
965//     }
966//
967// Generates `pub async fn init(<inputs>) -> actus::InitResult<actus::Router>`,
968// where `InitResult<T> = Result<T, anyhow::Error>` — `?` on any error type
969// implementing `std::error::Error + Send + Sync + 'static` works inside.
970// The `deps` block is optional; the `(<inputs>)` clause inside it is
971// optional too. All four shapes are valid:
972//
973//     deps { ... }                            // only let-bindings
974//     deps(a: T, b: U) { ... }                // both inputs and let-bindings
975//     deps(a: T, b: U) {}                     // only inputs
976//     // (no deps block at all)               // neither
977//
978// In each route's controller construction, struct-literal shorthand
979// (`{ store, cache }`) and rest-spread (`..base`) are auto-cloned, since
980// deps and inputs are typically `Arc`-wrapped and shared across multiple
981// controllers. Non-struct-literal expressions pass through unchanged.
982
983struct AppRoutesInput {
984    inputs: Vec<InputParam>,
985    deps: Vec<DepBinding>,
986    routes: Vec<RouteBinding>,
987    /// `families { "api", "hooks" => ["signature"], … }` — mount prefixes whose
988    /// controllers must declare `expects`, each optionally with the floors the
989    /// family accepts. Empty when the block is absent.
990    families: Vec<FamilyDecl>,
991}
992
993struct FamilyDecl {
994    prefix: LitStr,
995    /// `None` = presence only; `Some(list)` = presence + membership.
996    accepts: Option<Vec<LitStr>>,
997}
998
999struct InputParam {
1000    name: Ident,
1001    ty: syn::Type,
1002}
1003
1004struct DepBinding {
1005    name: Ident,
1006    value: Expr,
1007}
1008
1009struct RouteBinding {
1010    path: LitStr,
1011    construction: Expr,
1012}
1013
1014impl Parse for AppRoutesInput {
1015    fn parse(input: ParseStream) -> syn::Result<Self> {
1016        let mut inputs: Vec<InputParam> = Vec::new();
1017        let mut deps: Vec<DepBinding> = Vec::new();
1018        let mut routes: Option<Vec<RouteBinding>> = None;
1019        let mut families: Vec<FamilyDecl> = Vec::new();
1020
1021        while !input.is_empty() {
1022            let kw: Ident = input.parse()?;
1023            let kw_str = kw.to_string();
1024
1025            match kw_str.as_str() {
1026                "deps" => {
1027                    // Optional `(name: Type, ...)` declaring init() inputs.
1028                    if input.peek(syn::token::Paren) {
1029                        let paren_content;
1030                        syn::parenthesized!(paren_content in input);
1031                        while !paren_content.is_empty() {
1032                            let name: Ident = paren_content.parse()?;
1033                            paren_content.parse::<Token![:]>()?;
1034                            let ty: syn::Type = paren_content.parse()?;
1035                            inputs.push(InputParam { name, ty });
1036                            if !paren_content.is_empty() {
1037                                paren_content.parse::<Token![,]>()?;
1038                            }
1039                        }
1040                    }
1041                    // Then the `{ name = expr, ... }` block of let bindings
1042                    // (may be empty).
1043                    let content;
1044                    syn::braced!(content in input);
1045                    while !content.is_empty() {
1046                        let name: Ident = content.parse()?;
1047                        content.parse::<Token![=]>()?;
1048                        let value: Expr = content.parse()?;
1049                        deps.push(DepBinding { name, value });
1050                        if !content.is_empty() {
1051                            content.parse::<Token![,]>()?;
1052                        }
1053                    }
1054                }
1055                "families" => {
1056                    // `families { "api", "public" => ["anonymous"], … }`
1057                    let content;
1058                    syn::braced!(content in input);
1059                    while !content.is_empty() {
1060                        let prefix: LitStr = content.parse()?;
1061                        let accepts = if content.peek(Token![=>]) {
1062                            content.parse::<Token![=>]>()?;
1063                            let list;
1064                            syn::bracketed!(list in content);
1065                            let floors: Punctuated<LitStr, Token![,]> =
1066                                Punctuated::parse_terminated(&list)?;
1067                            if floors.is_empty() {
1068                                return Err(syn::Error::new(
1069                                    prefix.span(),
1070                                    "a family's accepted-floor list must name at least one floor \
1071                                     (or omit `=> [...]` to require only that a floor be declared)",
1072                                ));
1073                            }
1074                            Some(floors.into_iter().collect())
1075                        } else {
1076                            None
1077                        };
1078                        families.push(FamilyDecl { prefix, accepts });
1079                        if !content.is_empty() {
1080                            content.parse::<Token![,]>()?;
1081                        }
1082                    }
1083                }
1084                "routes" => {
1085                    let content;
1086                    syn::braced!(content in input);
1087                    let mut rs = Vec::new();
1088                    while !content.is_empty() {
1089                        let path: LitStr = content.parse()?;
1090                        content.parse::<Token![=>]>()?;
1091                        let construction: Expr = content.parse()?;
1092                        rs.push(RouteBinding { path, construction });
1093                        if !content.is_empty() {
1094                            content.parse::<Token![,]>()?;
1095                        }
1096                    }
1097                    routes = Some(rs);
1098                }
1099                other => {
1100                    return Err(syn::Error::new(
1101                        kw.span(),
1102                        format!("expected 'deps', 'families' or 'routes', got '{}'", other),
1103                    ));
1104                }
1105            }
1106        }
1107
1108        let routes = routes.ok_or_else(|| {
1109            syn::Error::new(
1110                proc_macro2::Span::call_site(),
1111                "app_routes! requires a 'routes { ... }' block",
1112            )
1113        })?;
1114
1115        Ok(Self {
1116            inputs,
1117            deps,
1118            routes,
1119            families,
1120        })
1121    }
1122}
1123
1124/// A mount path's segments, normalised the way `RouterBuilder::add_route`
1125/// normalises them: surrounding slashes trimmed, a trailing `*` (the
1126/// catch-all sugar) dropped — so `"api/*"` and `"api"` cover the same tree.
1127///
1128/// ⚠️ Keep in step with `actus_controller::routing::family_segments` /
1129/// `covering_family`, the runtime twin consumers call from their boot-time
1130/// checks (a proc-macro crate cannot depend on the runtime crate, so the rule
1131/// is written twice; the crate-level nesting doctest pins that they agree).
1132fn mount_segments(path: &str) -> Vec<String> {
1133    let mut segs: Vec<String> = path
1134        .trim_matches('/')
1135        .split('/')
1136        .filter(|s| !s.is_empty())
1137        .map(String::from)
1138        .collect();
1139    if segs.last().is_some_and(|s| s == "*") {
1140        segs.pop();
1141    }
1142    segs
1143}
1144
1145/// Declares the application's URL blueprint and generates its `init()`.
1146///
1147/// Takes an optional `deps( … ) { … }` block — constructor-injected services
1148/// and `let`-bindings shared across controllers — and a `routes { mount =>
1149/// Controller … }` map. Expands to an async `init(…)` returning the built
1150/// `Router`: it constructs every controller, wires its dependencies, and
1151/// registers each mount. See the `actus` crate's top-level docs for a worked
1152/// example.
1153#[proc_macro]
1154pub fn app_routes(input: TokenStream) -> TokenStream {
1155    let parsed = parse_macro_input!(input as AppRoutesInput);
1156    generate_app_routes(parsed).into()
1157}
1158
1159fn generate_app_routes(parsed: AppRoutesInput) -> proc_macro2::TokenStream {
1160    let init_params = parsed.inputs.iter().map(|p| {
1161        let name = &p.name;
1162        let ty = &p.ty;
1163        quote! { #name: #ty }
1164    });
1165
1166    let dep_lets = parsed.deps.iter().map(|d| {
1167        let name = &d.name;
1168        let value = &d.value;
1169        quote! { let #name = #value; }
1170    });
1171
1172    // Route families: which family (if any) covers each mount. Longest
1173    // covering prefix wins, mirroring longest-prefix routing, so a deeper
1174    // family entry can carve a subtree out of a shallower one. A family that
1175    // covers no mount is a compile error at its literal — a typo there would
1176    // otherwise constrain nothing, which is the failure the block exists to
1177    // prevent, one level up.
1178    let family_segs: Vec<Vec<String>> = parsed
1179        .families
1180        .iter()
1181        .map(|f| mount_segments(&f.prefix.value()))
1182        .collect();
1183    let mut family_used = vec![false; parsed.families.len()];
1184    let mut covering: Vec<Option<usize>> = Vec::with_capacity(parsed.routes.len());
1185    for r in &parsed.routes {
1186        let segs = mount_segments(&r.path.value());
1187        let best = family_segs
1188            .iter()
1189            .enumerate()
1190            .filter(|(_, fs)| {
1191                segs.len() >= fs.len() && segs.iter().zip(fs.iter()).all(|(a, b)| a == b)
1192            })
1193            .max_by_key(|(_, fs)| fs.len())
1194            .map(|(i, _)| i);
1195        if let Some(i) = best {
1196            family_used[i] = true;
1197        }
1198        covering.push(best);
1199    }
1200    for (i, f) in parsed.families.iter().enumerate() {
1201        if !family_used[i] {
1202            return syn::Error::new(
1203                f.prefix.span(),
1204                format!(
1205                    "route family `{}` covers no mount in this `routes` block — a family that \
1206                     constrains nothing is usually a typo; fix the prefix, or remove the entry \
1207                     until its first controller is mounted",
1208                    f.prefix.value()
1209                ),
1210            )
1211            .to_compile_error();
1212        }
1213    }
1214
1215    // One zero-sized `Family` type per entry that names accepted floors, so the
1216    // membership check can run in a `const` inside the generic pass-through.
1217    let family_types: Vec<proc_macro2::TokenStream> = parsed
1218        .families
1219        .iter()
1220        .enumerate()
1221        .filter_map(|(i, f)| {
1222            let accepts = f.accepts.as_ref()?;
1223            let ident = quote::format_ident!("__ActusFamily{}", i);
1224            Some(quote! {
1225                struct #ident;
1226                impl ::actus::__internal::Family for #ident {
1227                    const ACCEPTS: &'static [&'static str] = &[ #(#accepts),* ];
1228                }
1229            })
1230        })
1231        .collect();
1232
1233    let route_calls = parsed.routes.iter().zip(covering.iter()).map(|(r, cover)| {
1234        let path = &r.path;
1235        let construction = rewrite_construction(&r.construction);
1236        let construction = match cover {
1237            None => construction,
1238            Some(i) if parsed.families[*i].accepts.is_some() => {
1239                let ident = quote::format_ident!("__ActusFamily{}", i);
1240                quote! { ::actus::__internal::declares_expectation_in::<#ident, _>(#construction) }
1241            }
1242            Some(_) => quote! { ::actus::__internal::declares_expectation(#construction) },
1243        };
1244        quote! {
1245            .add_route(#path, ::std::sync::Arc::new(#construction))
1246        }
1247    });
1248
1249    quote! {
1250        pub async fn init(#(#init_params),*) -> ::actus::InitResult<::actus::Router> {
1251            #(#family_types)*
1252            #(#dep_lets)*
1253
1254            let router = ::actus::RouterBuilder::new()
1255                #(#route_calls)*
1256                .build();
1257
1258            ::std::result::Result::Ok(router)
1259        }
1260    }
1261}
1262
1263/// In a struct-literal controller construction, auto-clone simple references
1264/// to bound names so the same value can be threaded into multiple
1265/// controllers without each call site spelling `.clone()`.
1266///
1267/// Three cases get auto-cloned, all gated on the right-hand side being a
1268/// bare unqualified identifier (no path segments, no generic args, no
1269/// `qself`). The escape hatch in every case is the same: write any
1270/// non-ident expression — method call, function call, qualified path, an
1271/// already-`.clone()`d value — and it passes through unchanged.
1272///
1273/// * **Shorthand** — `Foo { db }` → `Foo { db: db.clone() }`.
1274/// * **Bare-ident explicit form** — `Foo { svc: store }` →
1275///   `Foo { svc: store.clone() }`.
1276/// * **Bare-ident rest spread** — `Foo { ..base }` → `Foo { ..(base).clone() }`.
1277///   Non-ident rest expressions (`..base.clone()`, `..self.template()`)
1278///   pass through verbatim — no double-cloning.
1279fn rewrite_construction(expr: &Expr) -> proc_macro2::TokenStream {
1280    let Expr::Struct(s) = expr else {
1281        return expr.to_token_stream();
1282    };
1283
1284    let path = &s.path;
1285    let mut inner = proc_macro2::TokenStream::new();
1286    let mut wrote_field = false;
1287
1288    for f in s.fields.iter() {
1289        if wrote_field {
1290            inner.extend(quote! { , });
1291        }
1292        wrote_field = true;
1293
1294        let member = &f.member;
1295        if f.colon_token.is_none() {
1296            // Shorthand: `name` → `name: name.clone()`
1297            inner.extend(quote! { #member: #member.clone() });
1298        } else if is_bare_ident(&f.expr) {
1299            // Explicit `target: source` where `source` is a simple ident:
1300            // treat like shorthand and auto-clone. Any non-ident expression
1301            // (method call, function call, qualified path, …) passes
1302            // through unchanged so callers retain a clean escape hatch.
1303            let value = &f.expr;
1304            inner.extend(quote! { #member: #value.clone() });
1305        } else {
1306            let value = &f.expr;
1307            inner.extend(quote! { #member: #value });
1308        }
1309    }
1310
1311    if let Some(rest) = &s.rest {
1312        if wrote_field {
1313            inner.extend(quote! { , });
1314        }
1315        if is_bare_ident(rest) {
1316            inner.extend(quote! { ..(#rest).clone() });
1317        } else {
1318            inner.extend(quote! { ..#rest });
1319        }
1320    }
1321
1322    quote! { #path { #inner } }
1323}
1324
1325/// Whether `expr` is a single, unqualified identifier path (no qself, no
1326/// leading `::`, exactly one segment, no generic args). The criterion the
1327/// auto-clone rule uses to decide that an explicit field assignment looks
1328/// "shorthand-like."
1329fn is_bare_ident(expr: &Expr) -> bool {
1330    let Expr::Path(p) = expr else { return false };
1331    p.qself.is_none()
1332        && p.path.leading_colon.is_none()
1333        && p.path.segments.len() == 1
1334        && p.path.segments[0].arguments.is_none()
1335}