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 on a covered lane 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        ("bool", Some(d)) => {
913            quote! {
914                extracted.get_bool(#name_str).unwrap_or(#d)
915            }
916        }
917        ("bool", None) => {
918            quote! { extracted.get_bool(#name_str)? }
919        }
920        ("Vec<String>", _) => {
921            quote! { extracted.get_string_array(#name_str)? }
922        }
923        ("JsonValue", _) => {
924            quote! { extracted.get_json_body()? }
925        }
926        // Raw request-body bytes. Use for binary uploads (e.g. `.uwx`
927        // packages). The framework discriminates JSON/form/binary at
928        // ingest by `Content-Type`; declaring `body: Bytes` is the
929        // signal that this handler wants the unparsed payload.
930        ("Bytes", _) => {
931            quote! { extracted.get_body_bytes() }
932        }
933        _ => {
934            quote! { compile_error!(concat!("Unsupported type: ", #ty_str)) }
935        }
936    }
937}
938
939// =========================
940// app_routes! — application-level route map with deps + per-route service injection
941// =========================
942//
943// Grammar:
944//
945//     app_routes! {
946//         // Optional. The `deps(...)` parens declare *inputs* — values
947//         // constructed by the caller (typically in `main()`) and passed
948//         // into the generated `init()` function. The brace block is the
949//         // `let`-block of dependencies built inside `init()`.
950//         deps(store: Arc<Store>) {
951//             cache = Cache::redis(...).await?,
952//         }
953//         routes {
954//             "api/entities" => EntityController { store },
955//             "api/cache"    => CacheController { cache },
956//             "health"       => HealthController,
957//             "*"            => SpaController,
958//         }
959//     }
960//
961// Generates `pub async fn init(<inputs>) -> actus::InitResult<actus::Router>`,
962// where `InitResult<T> = Result<T, anyhow::Error>` — `?` on any error type
963// implementing `std::error::Error + Send + Sync + 'static` works inside.
964// The `deps` block is optional; the `(<inputs>)` clause inside it is
965// optional too. All four shapes are valid:
966//
967//     deps { ... }                            // only let-bindings
968//     deps(a: T, b: U) { ... }                // both inputs and let-bindings
969//     deps(a: T, b: U) {}                     // only inputs
970//     // (no deps block at all)               // neither
971//
972// In each route's controller construction, struct-literal shorthand
973// (`{ store, cache }`) and rest-spread (`..base`) are auto-cloned, since
974// deps and inputs are typically `Arc`-wrapped and shared across multiple
975// controllers. Non-struct-literal expressions pass through unchanged.
976
977struct AppRoutesInput {
978    inputs: Vec<InputParam>,
979    deps: Vec<DepBinding>,
980    routes: Vec<RouteBinding>,
981    /// `families { "api", "hooks" => ["signature"], … }` — mount prefixes whose
982    /// controllers must declare `expects`, each optionally with the floors the
983    /// family accepts. Empty when the block is absent.
984    families: Vec<FamilyDecl>,
985}
986
987struct FamilyDecl {
988    prefix: LitStr,
989    /// `None` = presence only; `Some(list)` = presence + membership.
990    accepts: Option<Vec<LitStr>>,
991}
992
993struct InputParam {
994    name: Ident,
995    ty: syn::Type,
996}
997
998struct DepBinding {
999    name: Ident,
1000    value: Expr,
1001}
1002
1003struct RouteBinding {
1004    path: LitStr,
1005    construction: Expr,
1006}
1007
1008impl Parse for AppRoutesInput {
1009    fn parse(input: ParseStream) -> syn::Result<Self> {
1010        let mut inputs: Vec<InputParam> = Vec::new();
1011        let mut deps: Vec<DepBinding> = Vec::new();
1012        let mut routes: Option<Vec<RouteBinding>> = None;
1013        let mut families: Vec<FamilyDecl> = Vec::new();
1014
1015        while !input.is_empty() {
1016            let kw: Ident = input.parse()?;
1017            let kw_str = kw.to_string();
1018
1019            match kw_str.as_str() {
1020                "deps" => {
1021                    // Optional `(name: Type, ...)` declaring init() inputs.
1022                    if input.peek(syn::token::Paren) {
1023                        let paren_content;
1024                        syn::parenthesized!(paren_content in input);
1025                        while !paren_content.is_empty() {
1026                            let name: Ident = paren_content.parse()?;
1027                            paren_content.parse::<Token![:]>()?;
1028                            let ty: syn::Type = paren_content.parse()?;
1029                            inputs.push(InputParam { name, ty });
1030                            if !paren_content.is_empty() {
1031                                paren_content.parse::<Token![,]>()?;
1032                            }
1033                        }
1034                    }
1035                    // Then the `{ name = expr, ... }` block of let bindings
1036                    // (may be empty).
1037                    let content;
1038                    syn::braced!(content in input);
1039                    while !content.is_empty() {
1040                        let name: Ident = content.parse()?;
1041                        content.parse::<Token![=]>()?;
1042                        let value: Expr = content.parse()?;
1043                        deps.push(DepBinding { name, value });
1044                        if !content.is_empty() {
1045                            content.parse::<Token![,]>()?;
1046                        }
1047                    }
1048                }
1049                "families" => {
1050                    // `families { "api", "public" => ["anonymous"], … }`
1051                    let content;
1052                    syn::braced!(content in input);
1053                    while !content.is_empty() {
1054                        let prefix: LitStr = content.parse()?;
1055                        let accepts = if content.peek(Token![=>]) {
1056                            content.parse::<Token![=>]>()?;
1057                            let list;
1058                            syn::bracketed!(list in content);
1059                            let floors: Punctuated<LitStr, Token![,]> =
1060                                Punctuated::parse_terminated(&list)?;
1061                            if floors.is_empty() {
1062                                return Err(syn::Error::new(
1063                                    prefix.span(),
1064                                    "a family's accepted-floor list must name at least one floor \
1065                                     (or omit `=> [...]` to require only that a floor be declared)",
1066                                ));
1067                            }
1068                            Some(floors.into_iter().collect())
1069                        } else {
1070                            None
1071                        };
1072                        families.push(FamilyDecl { prefix, accepts });
1073                        if !content.is_empty() {
1074                            content.parse::<Token![,]>()?;
1075                        }
1076                    }
1077                }
1078                "routes" => {
1079                    let content;
1080                    syn::braced!(content in input);
1081                    let mut rs = Vec::new();
1082                    while !content.is_empty() {
1083                        let path: LitStr = content.parse()?;
1084                        content.parse::<Token![=>]>()?;
1085                        let construction: Expr = content.parse()?;
1086                        rs.push(RouteBinding { path, construction });
1087                        if !content.is_empty() {
1088                            content.parse::<Token![,]>()?;
1089                        }
1090                    }
1091                    routes = Some(rs);
1092                }
1093                other => {
1094                    return Err(syn::Error::new(
1095                        kw.span(),
1096                        format!("expected 'deps', 'families' or 'routes', got '{}'", other),
1097                    ));
1098                }
1099            }
1100        }
1101
1102        let routes = routes.ok_or_else(|| {
1103            syn::Error::new(
1104                proc_macro2::Span::call_site(),
1105                "app_routes! requires a 'routes { ... }' block",
1106            )
1107        })?;
1108
1109        Ok(Self {
1110            inputs,
1111            deps,
1112            routes,
1113            families,
1114        })
1115    }
1116}
1117
1118/// A mount path's segments, normalised the way `RouterBuilder::add_route`
1119/// normalises them: surrounding slashes trimmed, a trailing `*` (the
1120/// catch-all sugar) dropped — so `"api/*"` and `"api"` cover the same tree.
1121fn mount_segments(path: &str) -> Vec<String> {
1122    let mut segs: Vec<String> = path
1123        .trim_matches('/')
1124        .split('/')
1125        .filter(|s| !s.is_empty())
1126        .map(String::from)
1127        .collect();
1128    if segs.last().is_some_and(|s| s == "*") {
1129        segs.pop();
1130    }
1131    segs
1132}
1133
1134/// Declares the application's URL blueprint and generates its `init()`.
1135///
1136/// Takes an optional `deps( … ) { … }` block — constructor-injected services
1137/// and `let`-bindings shared across controllers — and a `routes { mount =>
1138/// Controller … }` map. Expands to an async `init(…)` returning the built
1139/// `Router`: it constructs every controller, wires its dependencies, and
1140/// registers each mount. See the `actus` crate's top-level docs for a worked
1141/// example.
1142#[proc_macro]
1143pub fn app_routes(input: TokenStream) -> TokenStream {
1144    let parsed = parse_macro_input!(input as AppRoutesInput);
1145    generate_app_routes(parsed).into()
1146}
1147
1148fn generate_app_routes(parsed: AppRoutesInput) -> proc_macro2::TokenStream {
1149    let init_params = parsed.inputs.iter().map(|p| {
1150        let name = &p.name;
1151        let ty = &p.ty;
1152        quote! { #name: #ty }
1153    });
1154
1155    let dep_lets = parsed.deps.iter().map(|d| {
1156        let name = &d.name;
1157        let value = &d.value;
1158        quote! { let #name = #value; }
1159    });
1160
1161    // Route families: which family (if any) covers each mount. Longest
1162    // covering prefix wins, mirroring longest-prefix routing, so a deeper
1163    // family entry can carve a subtree out of a shallower one. A family that
1164    // covers no mount is a compile error at its literal — a typo there would
1165    // otherwise constrain nothing, which is the failure the block exists to
1166    // prevent, one level up.
1167    let family_segs: Vec<Vec<String>> = parsed
1168        .families
1169        .iter()
1170        .map(|f| mount_segments(&f.prefix.value()))
1171        .collect();
1172    let mut family_used = vec![false; parsed.families.len()];
1173    let mut covering: Vec<Option<usize>> = Vec::with_capacity(parsed.routes.len());
1174    for r in &parsed.routes {
1175        let segs = mount_segments(&r.path.value());
1176        let best = family_segs
1177            .iter()
1178            .enumerate()
1179            .filter(|(_, fs)| {
1180                segs.len() >= fs.len() && segs.iter().zip(fs.iter()).all(|(a, b)| a == b)
1181            })
1182            .max_by_key(|(_, fs)| fs.len())
1183            .map(|(i, _)| i);
1184        if let Some(i) = best {
1185            family_used[i] = true;
1186        }
1187        covering.push(best);
1188    }
1189    for (i, f) in parsed.families.iter().enumerate() {
1190        if !family_used[i] {
1191            return syn::Error::new(
1192                f.prefix.span(),
1193                format!(
1194                    "route family `{}` covers no mount in this `routes` block — a family that \
1195                     constrains nothing is usually a typo; fix the prefix, or remove the entry \
1196                     until its first controller is mounted",
1197                    f.prefix.value()
1198                ),
1199            )
1200            .to_compile_error();
1201        }
1202    }
1203
1204    // One zero-sized `Family` type per entry that names accepted floors, so the
1205    // membership check can run in a `const` inside the generic pass-through.
1206    let family_types: Vec<proc_macro2::TokenStream> = parsed
1207        .families
1208        .iter()
1209        .enumerate()
1210        .filter_map(|(i, f)| {
1211            let accepts = f.accepts.as_ref()?;
1212            let ident = quote::format_ident!("__ActusFamily{}", i);
1213            Some(quote! {
1214                struct #ident;
1215                impl ::actus::__internal::Family for #ident {
1216                    const ACCEPTS: &'static [&'static str] = &[ #(#accepts),* ];
1217                }
1218            })
1219        })
1220        .collect();
1221
1222    let route_calls = parsed.routes.iter().zip(covering.iter()).map(|(r, cover)| {
1223        let path = &r.path;
1224        let construction = rewrite_construction(&r.construction);
1225        let construction = match cover {
1226            None => construction,
1227            Some(i) if parsed.families[*i].accepts.is_some() => {
1228                let ident = quote::format_ident!("__ActusFamily{}", i);
1229                quote! { ::actus::__internal::declares_expectation_in::<#ident, _>(#construction) }
1230            }
1231            Some(_) => quote! { ::actus::__internal::declares_expectation(#construction) },
1232        };
1233        quote! {
1234            .add_route(#path, ::std::sync::Arc::new(#construction))
1235        }
1236    });
1237
1238    quote! {
1239        pub async fn init(#(#init_params),*) -> ::actus::InitResult<::actus::Router> {
1240            #(#family_types)*
1241            #(#dep_lets)*
1242
1243            let router = ::actus::RouterBuilder::new()
1244                #(#route_calls)*
1245                .build();
1246
1247            ::std::result::Result::Ok(router)
1248        }
1249    }
1250}
1251
1252/// In a struct-literal controller construction, auto-clone simple references
1253/// to bound names so the same value can be threaded into multiple
1254/// controllers without each call site spelling `.clone()`.
1255///
1256/// Three cases get auto-cloned, all gated on the right-hand side being a
1257/// bare unqualified identifier (no path segments, no generic args, no
1258/// `qself`). The escape hatch in every case is the same: write any
1259/// non-ident expression — method call, function call, qualified path, an
1260/// already-`.clone()`d value — and it passes through unchanged.
1261///
1262/// * **Shorthand** — `Foo { db }` → `Foo { db: db.clone() }`.
1263/// * **Bare-ident explicit form** — `Foo { svc: store }` →
1264///   `Foo { svc: store.clone() }`.
1265/// * **Bare-ident rest spread** — `Foo { ..base }` → `Foo { ..(base).clone() }`.
1266///   Non-ident rest expressions (`..base.clone()`, `..self.template()`)
1267///   pass through verbatim — no double-cloning.
1268fn rewrite_construction(expr: &Expr) -> proc_macro2::TokenStream {
1269    let Expr::Struct(s) = expr else {
1270        return expr.to_token_stream();
1271    };
1272
1273    let path = &s.path;
1274    let mut inner = proc_macro2::TokenStream::new();
1275    let mut wrote_field = false;
1276
1277    for f in s.fields.iter() {
1278        if wrote_field {
1279            inner.extend(quote! { , });
1280        }
1281        wrote_field = true;
1282
1283        let member = &f.member;
1284        if f.colon_token.is_none() {
1285            // Shorthand: `name` → `name: name.clone()`
1286            inner.extend(quote! { #member: #member.clone() });
1287        } else if is_bare_ident(&f.expr) {
1288            // Explicit `target: source` where `source` is a simple ident:
1289            // treat like shorthand and auto-clone. Any non-ident expression
1290            // (method call, function call, qualified path, …) passes
1291            // through unchanged so callers retain a clean escape hatch.
1292            let value = &f.expr;
1293            inner.extend(quote! { #member: #value.clone() });
1294        } else {
1295            let value = &f.expr;
1296            inner.extend(quote! { #member: #value });
1297        }
1298    }
1299
1300    if let Some(rest) = &s.rest {
1301        if wrote_field {
1302            inner.extend(quote! { , });
1303        }
1304        if is_bare_ident(rest) {
1305            inner.extend(quote! { ..(#rest).clone() });
1306        } else {
1307            inner.extend(quote! { ..#rest });
1308        }
1309    }
1310
1311    quote! { #path { #inner } }
1312}
1313
1314/// Whether `expr` is a single, unqualified identifier path (no qself, no
1315/// leading `::`, exactly one segment, no generic args). The criterion the
1316/// auto-clone rule uses to decide that an explicit field assignment looks
1317/// "shorthand-like."
1318fn is_bare_ident(expr: &Expr) -> bool {
1319    let Expr::Path(p) = expr else { return false };
1320    p.qself.is_none()
1321        && p.path.leading_colon.is_none()
1322        && p.path.segments.len() == 1
1323        && p.path.segments[0].arguments.is_none()
1324}