Skip to main content

shared_framework/controller/
mod.rs

1//! HTTP routing and controllers built on Hyper.
2//!
3//! Key types: [`Router`] dispatches requests, [`RouteController`] groups routes
4//! under a base path and version, [`RouteDescription`] declares documentation,
5//! validation, and rate-limit metadata, and [`Middleware`] runs before handlers.
6//!
7//! Request lifecycle: Hyper accepts a connection, the body, query parameters, and
8//! headers are attached to a [`CorrelationContext`](CorrelationContext),
9//! global middlewares run, the matching route's middlewares run, then the handler
10//! receives only the context and returns `Result<ServiceResult<T>, ErrorResult>`.
11//!
12//! Handlers read input off the context (`ctx.body::<T>()`, `ctx.header()`,
13//! `ctx.query_param()`) and return [`ServiceResult`](ServiceResult)
14//! or a boxed [`TypedServiceResult`](TypedServiceResult).
15//! ```ignore
16//! // ctx: CorrelationContext
17//! // let dto = ctx.body::<CreateUser>()?;
18//! // Ok(ServiceResult::ok("Created", dto))
19//! ```
20
21pub mod errors;
22
23use futures::FutureExt;
24use std::collections::HashMap;
25use std::net::SocketAddr;
26use std::sync::{
27    Arc, OnceLock,
28    atomic::{AtomicBool, AtomicUsize, Ordering},
29};
30
31use crate::doc::{DocumentableDTO, DocumentationRegistrant};
32use crate::logging::CorrelationContext;
33use crate::middleware::rate::{RateLimiter, create_from_env};
34use crate::response::{ErrorResult, ResponseType, ServiceResult, TypedServiceResult};
35use crate::validation::Validate;
36use http::{HeaderMap, Method, Request, Response};
37use http_body_util::BodyExt;
38use hyper::body::Incoming;
39use hyper::server::conn::http1;
40use hyper::service::service_fn;
41use hyper_util::rt::TokioIo;
42use schemars::generate::SchemaSettings;
43use serde_json::Value;
44use tokio::net::TcpListener;
45use tracing::{info, warn};
46
47#[derive(Clone)]
48struct Holder {
49    path: String,
50    limit: u32,
51}
52
53/// Documented DTO payload: its JSON Schema, example value, and Rust type name.
54/// Used for request bodies and response examples in generated documentation.
55#[derive(Clone, Debug)]
56pub struct DtoEntity {
57    pub(crate) schema: Value,
58    pub(crate) example: Value,
59    pub(crate) name: &'static str,
60}
61
62/// Declared file-upload kind for a multipart file parameter.
63/// Determines the example extension list reported in the documentation.
64#[derive(Clone, Debug, PartialEq, Eq)]
65pub enum FileParameterType {
66    /// Image files (e.g. `.png, .jpg, .jpeg, .gif, .webp`).
67    Image,
68    /// Document files (e.g. `.pdf, .doc, .docx, .txt`).
69    Document,
70    /// Spreadsheet or CSV files (e.g. `.xls, .xlsx, .csv`).
71    Spreadsheet,
72    /// Video files (e.g. `.mp4, .mov, .avi`).
73    Video,
74    /// Audio files (e.g. `.mp3, .wav, .aac`).
75    Audio,
76    /// Archive files (e.g. `.zip, .tar, .gz`).
77    Archive,
78    /// Any file extension is accepted.
79    Any,
80}
81
82impl FileParameterType {
83    /// Returns the example extension list shown in documentation for this file kind.
84    pub fn allowed_extensions_example(&self) -> &'static str {
85        match self {
86            Self::Image => ".png, .jpg, .jpeg, .gif, .webp",
87            Self::Document => ".pdf, .doc, .docx, .txt",
88            Self::Spreadsheet => ".xls, .xlsx, .csv",
89            Self::Video => ".mp4, .mov, .avi",
90            Self::Audio => ".mp3, .wav, .aac",
91            Self::Archive => ".zip, .tar, .gz",
92            Self::Any => "Any extension",
93        }
94    }
95}
96
97/// File-upload declaration for a multipart route: the part of the name, a description,
98/// the accepted file kind, and optional send caps enforced before the handler runs.
99#[derive(Clone, Debug)]
100pub struct FileParameter {
101    /// Multipart part name this declaration applies to.
102    pub name: String,
103    /// Human-readable description shown in documentation.
104    pub description: String,
105    /// Accepted file kind, used for the documented extension hint.
106    pub file_type: FileParameterType,
107    /// Maximum times this part may be sent; enforced as a 400 rejection when exceeded.
108    pub count: Option<u32>,
109    /// Optional size or count limit recorded for documentation.
110    pub limit: Option<u32>,
111}
112
113/// Declarative metadata for one route: documentation, validation, and policy.
114/// A description is registered for docs generation, declares the expected request
115/// body (required when `has_body` is set on mount), response examples, params,
116/// file parts, rate limit, and auth notes. Build with the `new` constructor and
117/// the `group` / `with_body` / `response` / `header` / `query_param` builders.
118#[derive(Debug, Clone)]
119pub struct RouteDescription {
120    /// Documentation group this route belongs to; also seeds the default tag.
121    pub group: String,
122    /// Route name; kept in sync with `summary` by [`RouteDescription::name`].
123    pub name: String,
124    /// Long-form description shown in documentation.
125    pub description: String,
126    /// Declared request-body DTO schema and example, if the route takes a body.
127    pub request_body: Option<DtoEntity>,
128    /// Response examples keyed by HTTP status code.
129    pub response_examples: HashMap<u16, DtoEntity>,
130    /// Documented request headers as name-to-description pairs.
131    pub headers: HashMap<String, String>,
132    /// Documented path parameters as name-to-description pairs.
133    pub path_parameters: HashMap<String, String>,
134    /// Default values for documented path parameters.
135    pub path_parameter_defaults: HashMap<String, String>,
136    /// Documented query parameters as name-to-description pairs.
137    pub query_parameters: HashMap<String, String>,
138    /// Default values for documented query parameters.
139    pub query_parameter_defaults: HashMap<String, String>,
140    /// Declared multipart file parameters keyed by part name.
141    pub file_parameters: HashMap<String, FileParameter>,
142    /// Per-route request cap; `None` means no route-level limit is installed.
143    pub rate_limit: Option<u32>,
144    /// Whether callers must authenticate; enforced by middleware, not by this struct.
145    pub authentication_required: bool,
146    /// Optional note explaining the authentication requirement.
147    pub authentication_comment: Option<String>,
148    /// Short summary shown in documentation; defaults to the name.
149    pub summary: String,
150    /// Documentation tags used for grouping; defaults to the group name.
151    pub tags: Vec<String>,
152    /// Marks the route as deprecated in documentation.
153    pub is_deprecated: bool,
154}
155
156impl RouteDescription {
157    /// Creates a description with the given summary; group and tags default to `"Default"`.
158    pub fn new(summary: impl Into<String>) -> Self {
159        let s = summary.into();
160        Self {
161            group: "Default".to_string(),
162            name: s.clone(),
163            description: String::new(),
164            request_body: None,
165            response_examples: Default::default(),
166            headers: Default::default(),
167            path_parameters: Default::default(),
168            path_parameter_defaults: Default::default(),
169            query_parameters: Default::default(),
170            query_parameter_defaults: Default::default(),
171            file_parameters: Default::default(),
172            rate_limit: None,
173            authentication_required: false,
174            authentication_comment: None,
175            summary: s.clone(),
176            tags: vec!["Default".to_string()],
177            is_deprecated: false,
178        }
179    }
180    /// Sets the documentation group and resets the tag list to that group.
181    pub fn group(mut self, group: impl Into<String>) -> Self {
182        let g = group.into();
183        self.group = g.clone();
184        self.tags = vec![g];
185        self
186    }
187    /// Sets the route name and copies it into the summary.
188    pub fn name(mut self, name: impl Into<String>) -> Self {
189        let n = name.into();
190        self.name = n.clone();
191        self.summary = n;
192        self
193    }
194    /// Sets the long-form description shown in documentation.
195    pub fn description(mut self, desc: impl Into<String>) -> Self {
196        self.description = desc.into();
197        self
198    }
199    /// Sets the per-route request cap (alias for [`RouteDescription::with_rate_limit`]).
200    pub fn rate_limit(mut self, limit: u32) -> Self {
201        self.rate_limit = Some(limit);
202        self
203    }
204    /// Declares the request-body DTO type `T`: records its JSON Schema and example.
205    /// Required when the route is mounted with `has_body`; `T` must also be validatable.
206    pub fn with_body<T>(mut self) -> Self
207    where
208        T: DocumentableDTO + Validate,
209    {
210        let settings = SchemaSettings::openapi3();
211        let generator = settings.into_generator();
212        let schema = generator.into_root_schema_for::<T>();
213        let example = T::make_example();
214        self.request_body = Some(DtoEntity {
215            schema: schema.to_value(),
216            example: example.unwrap(),
217            name: std::any::type_name::<T>(),
218        });
219        self
220    }
221    /// Records whether callers must authenticate (informational; enforced by middleware).
222    pub fn authentication(mut self, required: bool) -> Self {
223        self.authentication_required = required;
224        self
225    }
226
227    /// Records a response example for `code` using DTO type `T`'s schema and example.
228    pub fn response<T>(mut self, code: u16) -> Self
229    where
230        T: DocumentableDTO,
231    {
232        let settings = SchemaSettings::openapi3();
233        let generator = settings.into_generator();
234        let schema = generator.into_root_schema_for::<T>();
235        let example = T::make_example();
236        let response = DtoEntity {
237            schema: schema.to_value(),
238            example: example.unwrap(),
239            name: std::any::type_name::<T>(),
240        };
241        self.response_examples.insert(code, response);
242        self
243    }
244    /// Attaches an explanatory note about the authentication requirement.
245    pub fn authentication_comment(mut self, comment: impl Into<String>) -> Self {
246        self.authentication_comment = Some(comment.into());
247        self
248    }
249    /// Documents one expected request header as a name-to-description entry.
250    pub fn header(mut self, k: impl Into<String>, v: impl Into<String>) -> Self {
251        self.headers.insert(k.into(), v.into());
252        self
253    }
254    /// Documents one path parameter as a name-to-description entry.
255    pub fn path_param(mut self, k: impl Into<String>, v: impl Into<String>) -> Self {
256        self.path_parameters.insert(k.into(), v.into());
257        self
258    }
259    /// Documents one path parameter together with its default value.
260    pub fn path_param_with_default(
261        mut self,
262        k: impl Into<String>,
263        v: impl Into<String>,
264        default: impl Into<String>,
265    ) -> Self {
266        let k = k.into();
267        self.path_parameters.insert(k.clone(), v.into());
268        self.path_parameter_defaults.insert(k, default.into());
269        self
270    }
271    /// Documents one query parameter as a name-to-description entry.
272    pub fn query_param(mut self, k: impl Into<String>, v: impl Into<String>) -> Self {
273        self.query_parameters.insert(k.into(), v.into());
274        self
275    }
276    /// Documents one query parameter together with its default value.
277    pub fn query_param_with_default(
278        mut self,
279        k: impl Into<String>,
280        v: impl Into<String>,
281        default: impl Into<String>,
282    ) -> Self {
283        let k = k.into();
284        self.query_parameters.insert(k.clone(), v.into());
285        self.query_parameter_defaults.insert(k, default.into());
286        self
287    }
288    /// Declares an accepted multipart file part without total send caps.
289    pub fn file_param(
290        mut self,
291        name: impl Into<String>,
292        description: impl Into<String>,
293        file_type: FileParameterType,
294    ) -> Self {
295        let name = name.into();
296        self.file_parameters.insert(
297            name.clone(),
298            FileParameter {
299                name,
300                description: description.into(),
301                file_type,
302                count: None,
303                limit: None,
304            },
305        );
306        self
307    }
308    /// Declares an accepted multipart file part with sending limits.
309    /// Panics if `limit` or `count` is zero; a `count` overrun rejects the request with a 400.
310    pub fn file_param_with_limit(
311        mut self,
312        name: impl Into<String>,
313        description: impl Into<String>,
314        file_type: FileParameterType,
315        limit: u32,
316        count: u32,
317    ) -> Self {
318        let name = name.into();
319        assert!(limit >= 1, "File parameter limit must be greater than 0");
320        assert!(count >= 1, "File parameter count must be greater than 0");
321        self.file_parameters.insert(
322            name.clone(),
323            FileParameter {
324                name,
325                description: description.into(),
326                file_type,
327                count: Some(count),
328                limit: Some(limit),
329            },
330        );
331        self
332    }
333    /// Marks the route as deprecated (`true`) or not (`false`) in documentation.
334    pub fn deprecated(mut self, deprecated: bool) -> Self {
335        self.is_deprecated = deprecated;
336        self
337    }
338    /// Adds a documentation tag unless it is already present.
339    pub fn tag(mut self, tag: impl Into<String>) -> Self {
340        let t = tag.into();
341        if !self.tags.contains(&t) {
342            self.tags.push(t);
343        }
344        self
345    }
346    /// Returns the per-route limit when set, otherwise the given global limit.
347    pub fn effective_rate_limit(&self, global: Option<u32>) -> Option<u32> {
348        self.rate_limit.or(global)
349    }
350}
351
352impl Default for RouteDescription {
353    fn default() -> Self {
354        Self::new("No description")
355    }
356}
357
358// ── Global state ───────────────────────────────────────────────────────────
359
360static TOTAL_COUNT: AtomicUsize = AtomicUsize::new(0);
361static LIMITER: OnceLock<Arc<dyn RateLimiter>> = OnceLock::new();
362static MOUNTED_HANDLERS: AtomicBool = AtomicBool::new(false);
363
364// ── Middleware / Handler types ───────────────────────────────────────────────
365/// Pre-handler hook: inspects or mutates the context and headers, returning `Ok(())`
366/// to continue or `Err(ErrorResult)` to short-circuit with an error response.
367pub type Middleware = Arc<
368    dyn for<'a> Fn(
369            &'a mut CorrelationContext,
370        ) -> futures::future::BoxFuture<'a, Result<(), ErrorResult>>
371        + Send
372        + Sync,
373>;
374
375/// Low-level route handler used by the dispatcher: receives the prepared context
376/// plus raw headers, method, path, and body bytes, and returns a complete HTTP response.
377pub type BoxHandler = Arc<
378    dyn Fn(
379            CorrelationContext,
380            HeaderMap,
381            Method,
382            String,
383            Vec<u8>,
384        ) -> futures::future::BoxFuture<'static, Response<String>>
385        + Send
386        + Sync,
387>;
388
389// ── RouteEntry / Router ──────────────────────────────────────────────────────
390pub(crate) struct RouteEntry {
391    method: Method,
392    path: String,
393    handler: BoxHandler,
394    middlewares: Vec<Middleware>,
395}
396
397#[derive(Default)]
398struct TrieNode {
399    static_children: HashMap<String, TrieNode>,
400    param_child: Option<(String, Box<TrieNode>)>, // (param name, subtree)
401    wildcard_routes: HashMap<Method, RouteEntry>, // method -> route, for a "/*" mounted here
402    exact_routes: HashMap<Method, RouteEntry>,    // method -> route, for a path ending here
403}
404
405/// Hyper-backed route registry: stores routes with per-route middlewares,
406/// runs global middlewares for every request, and dispatches by method and path.
407/// A trailing `/*` on a route path acts as a prefix wildcard when matching.
408pub struct Router {
409    root: TrieNode,
410    global_middlewares: Vec<Middleware>,
411
412    /// Reserved test-execution hook for documentation-driven test clients.
413    /// When installed and the request carries the test-client header, this handler
414    /// runs instead of the matched route.
415    test_client_handler: Option<BoxHandler>,
416}
417
418impl Router {
419    /// Creates an empty router with no routes and no global middlewares.
420    pub fn new() -> Self {
421        Self {
422            root: TrieNode::default(),
423            global_middlewares: Vec::new(),
424            test_client_handler: None,
425        }
426    }
427
428    /// Registers a route in the trie, indexed by its path segments and method.
429    ///
430    /// The path is split on `/` (leading/trailing slashes and empty segments from
431    /// repeated `/` are ignored). Each segment is classified as one of:
432    ///
433    /// - **Static** (`users`, `settings`) — matched literally.
434    /// - **Param** (`:id`, `:slug`) — matches any single non-empty segment; the
435    ///   name keys the captured value after the colon.
436    /// - **Wildcard** (`*`) — must be the final segment; matches the remainder of
437    ///   the path, including zero remaining segments.
438    ///
439    /// Method keys routes at the node where their path terminates, so
440    /// registering the same path with different methods (e.g. `GET /users/:id`
441    /// and `POST /users/:id`) is expected and does not conflict.
442    ///
443    /// # Precedence
444    ///
445    /// Insertion order does not affect resolution order. At lookup time, static
446    /// segments are always preferred over a param segment at the same position,
447    /// which is preferred over a wildcard — this is enforced by the trie
448    /// structure itself, not by the order routes are inserted here.
449    ///
450    /// # Panics
451    ///
452    /// In debug builds, panics if a param segment is inserted at a trie position
453    /// that already holds a param child with a *different* name (e.g., inserting
454    /// both `/users/:id` and `/users/:userId`). This check is compiled out in
455    /// release builds — conflicting names will silently take the first
456    /// registered name instead.
457    ///
458    /// # Examples
459    ///
460    /// ```
461    /// use http::Method;
462    /// router.insert(crate::shared_framework::controller::RouteEntry { method: Method::GET, path: "/users/:id".into(), .. });
463    /// router.insert(crate::shared_framework::controller::RouteEntry { method: Method::GET, path: "/users/active".into(), .. });
464    /// router.insert(crate::shared_framework::controller::RouteEntry { method: Method::GET, path: "/admin/*".into(), .. });
465    /// ```
466    pub(crate) fn insert(&mut self, route: RouteEntry) {
467        let segments: Vec<String> = route
468            .path
469            .trim_matches('/')
470            .split('/')
471            .filter(|s| !s.is_empty())
472            .map(String::from)
473            .collect();
474        Self::insert_at(&mut self.root, &segments, route);
475    }
476
477    fn insert_at(node: &mut TrieNode, segments: &[String], route: RouteEntry) {
478        match segments.split_first() {
479            None => {
480                node.exact_routes.insert(route.method.clone(), route);
481            }
482            Some((seg, _rest)) if seg == "*" => {
483                node.wildcard_routes.insert(route.method.clone(), route);
484            }
485            Some((seg, rest)) if seg.starts_with(':') => {
486                let name = seg[1..].to_string();
487                let (existing_name, child) = node
488                    .param_child
489                    .get_or_insert_with(|| (name.clone(), Box::new(TrieNode::default())));
490                debug_assert_eq!(
491                    existing_name, &name,
492                    "conflicting param names at same position: {existing_name} vs {name}"
493                );
494                Self::insert_at(child, rest, route);
495            }
496            Some((seg, rest)) => {
497                let child = node.static_children.entry(seg.clone()).or_default();
498                Self::insert_at(child, rest, route);
499            }
500        }
501    }
502
503    /// Resolves an incoming request to a registered route, extracting any path
504    /// parameters along the way.
505    ///
506    /// The path is normalized (see [`normalize_path`]) and split into segments
507    /// before matching. Matching descends the trie one segment at a time,
508    /// preferring branches in this order at each level:
509    ///
510    /// 1. **Static** children — exact segment match.
511    /// 2. **Param** child — matches any segment, capturing its value.
512    /// 3. **Wildcard** — matches all remaining segments (including none).
513    ///
514    /// If a static branch matches part of the path but fails to produce a full
515    /// match further down, resolution backtracks and retries via the param
516    /// branch at that level before falling back to the wildcard. This ensures a
517    /// route like `/users/:id/settings` is still reachable even when a sibling
518    /// static route `/users/active` exists but doesn't match past the first
519    /// segment.
520    ///
521    /// The method is checked only at the point a path fully matches (an exact
522    /// route or a wildcard boundary) — a path that matches some route's shape
523    /// but not under the requested method resolves to `None`, indistinguishable
524    /// from no matching path at all.
525    ///
526    /// # Returns
527    ///
528    /// `Some((route, params))` on a match, where `params` maps each `:name`
529    /// segment in the matched route's path to the corresponding literal value
530    /// from the request path. Returns `None` if no route matches the given
531    /// method and path.
532    ///
533    /// # Examples
534    ///
535    /// ```
536    /// let (route, params) = router.resolve("GET", "/users/42").unwrap();
537    /// assert_eq!(params["id"], "42");
538    /// ```
539    ///
540    /// # Warnings
541    /// - Path segments are not percent-decoded; encoded characters (e.g. `%20`)
542    ///   are returned as-is in extracted params.
543    /// - Does not distinguish a path that exists under a different method from a
544    ///   path that doesn't exist at all — both return `None`.
545    pub(crate) fn resolve(
546        &self,
547        method: &Method,
548        path: &str,
549    ) -> Option<(&RouteEntry, HashMap<String, String>)> {
550        let normal_path = normalize_path(path);
551        let segments: Vec<&str> = normal_path
552            .trim_matches('/')
553            .split('/')
554            .filter(|s| !s.is_empty())
555            .collect();
556
557        let mut params = Vec::new();
558        let route = Self::find(&self.root, &segments, method, &mut params)?;
559        Some((route, params.into_iter().collect()))
560    }
561
562    /// Installs the reserved test-client interceptor used for documentation-driven test runs.
563    pub fn set_test_client_handler(&mut self, handler: BoxHandler) {
564        self.test_client_handler = Some(handler);
565    }
566
567    fn ensure_global_middleware(&mut self) {
568        if MOUNTED_HANDLERS
569            .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
570            .is_ok()
571        {
572            self.global_middlewares.push(Arc::new(|ctx| {
573                async move {
574                    if ctx.request_id().is_empty() {
575                        let rid = hex::encode(rand::random::<[u8; 8]>());
576                        ctx.set_request_id(&rid);
577                    }
578                    Ok(())
579                }
580                .boxed()
581            }));
582            self.global_middlewares.push(Arc::new(|ctx| {
583                async move {
584                    let headers = ctx.headers();
585                    if let Some(v) = headers
586                        .get("x-correlation-id")
587                        .and_then(|h| h.to_str().ok())
588                    {
589                        if !v.is_empty() {
590                            ctx.set_correlation_id(v);
591                        }
592                    }
593                    if let Some(v) = headers
594                        .get("x-correlation-flow")
595                        .and_then(|h| h.to_str().ok())
596                    {
597                        if let Ok(flow) = v.parse::<crate::logging::CorrelationFlow>() {
598                            ctx.set_flow(flow);
599                        }
600                    }
601                    Ok(())
602                }
603                .boxed()
604            }));
605        }
606    }
607
608    /// Registers a typed handler at `base_path` + version + `path` for `method`.
609    ///
610    /// Type parameter `T` is the `ServiceResult` payload; `F`/`Fut` describes the
611    /// `Fn(CorrelationContext)` async handler returning `Result<ServiceResult<T>, ErrorResult>`.
612    /// The handler receives only the context: body, headers, and query parameters are
613    /// attached at dispatch, so use `ctx.body::<T>()`, `ctx.header()`, or `ctx.query_param()`.
614    /// Registers the description for documentation, installs per-route rate-limit
615    /// middleware when `description.rate_limit` is set, and enforces declared file-part
616    /// `count` caps before the handler runs.
617    ///
618    /// Panics if `has_body` is set without a declared request body or file parameter.
619    /// ```ignore
620    /// router.mount("/users", 1, "/:id", false, Method::GET, desc, handler, vec![]);
621    /// ```
622    pub fn mount<T, F, Fut>(
623        &mut self,
624        base_path: &str,
625        version: u32,
626        path: &str,
627        has_body: bool,
628        method: Method,
629        description: RouteDescription,
630        handler: F,
631        middlewares: Vec<Middleware>,
632    ) where
633        T: serde::Serialize + Send + Sync + 'static,
634        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
635        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
636    {
637        self.ensure_global_middleware();
638        let full_path = calculate_full_path(base_path, version, path);
639        let meta_path = Holder {
640            path: full_path.to_string(),
641            limit: description.rate_limit.unwrap_or(0),
642        };
643
644        if let Ok(mut reg) = DocumentationRegistrant::global().write() {
645            reg.register_route(
646                &full_path,
647                method.as_str(),
648                "Controller",
649                description.clone(),
650            );
651        }
652
653        if has_body {
654            let expected = description.request_body.clone();
655            // File-only routes satisfy the body contract through file_parameters.
656            if expected.is_none() && description.file_parameters.is_empty() {
657                panic!(
658                    "Request body class must be specified in the route description for {}",
659                    full_path
660                );
661            }
662        }
663
664        let mut all_middlewares = Vec::new();
665        if let Some(limit) = description.rate_limit {
666            if limit > 0 {
667                let path_clone = meta_path.clone();
668
669                let m: Middleware = Arc::new(move |ctx: &mut CorrelationContext| {
670                    let headers = ctx.headers();
671                    let path_inner = path_clone.clone();
672                    let fut = async move {
673                        let limiter = LIMITER.get_or_init(|| create_from_env()).clone();
674                        let key = ctx.user_id().unwrap_or_else(|| {
675                            headers
676                                .get("x-forwarded-for")
677                                .and_then(|v| v.to_str().ok())
678                                .unwrap_or("anonymous")
679                                .to_string()
680                        }) + ":"
681                            + &path_inner.path;
682                        if limiter.is_allowed(&key, path_inner.limit).await {
683                            return Ok(());
684                        }
685                        Err(ErrorResult::from_error(
686                            "Too many requests. Please try again later.",
687                            429,
688                        ))
689                    };
690
691                    // Explicitly force the BoxFuture type to drop any inferred lifetime relationships
692                    // linked to the arguments ctx or headers
693                    FutureExt::boxed(fut)
694                });
695                all_middlewares.push(m);
696            }
697        }
698        all_middlewares.extend(middlewares);
699
700        // Declared per-parameter send caps (`FileParameter.count`), enforced in
701        // place before the handler runs just like body validation.
702        let file_counts: Vec<(String, u32)> = description
703            .file_parameters
704            .iter()
705            .filter_map(|(name, p)| p.count.map(|c| (name.clone(), c)))
706            .collect();
707        let handler = Arc::new(handler);
708
709        let boxed: BoxHandler = Arc::new(move |ctx, _headers, _method, _path, _body| {
710            let file_counts = file_counts.clone();
711            let handler = handler.clone();
712            Box::pin(async move {
713                let ctx = Arc::new(ctx);
714                if let Some(err) = file_count_violation(ctx.clone(), &file_counts) {
715                    return crate::response::error_response(&err, ctx.clone());
716                }
717                match handler((*ctx).clone()).await {
718                    Ok(typed) => {
719                        let body = match typed.serialize() {
720                            Ok(b) => b,
721                            Err(e) => {
722                                let err =
723                                    ErrorResult::from_error(&anyhow::anyhow!(e.to_string()), 500);
724                                return crate::response::error_response(&err, ctx.clone());
725                            }
726                        };
727                        if body.is_empty() {
728                            let err = ErrorResult::new("Response body is null", None, 503);
729                            return crate::response::error_response(&err, ctx.clone());
730                        }
731                        let mut builder = Response::builder()
732                            .status(typed.code())
733                            .header("X-Request-ID", ctx.request_id())
734                            .header("X-Correlation-ID", ctx.correlation_id())
735                            .header("X-Correlation-Flow", ctx.flow().to_string());
736                        let ct = match typed.response_type() {
737                            ResponseType::Json => "application/json",
738                            ResponseType::File => {
739                                let filename = body.rsplit('/').next().unwrap_or("file");
740                                builder = builder.header(
741                                    "Content-Disposition",
742                                    format!("attachment; filename=\"{}\"", filename),
743                                );
744                                "application/octet-stream"
745                            }
746                            ResponseType::Xml => "application/xml",
747                            ResponseType::Javascript => "application/javascript",
748                            ResponseType::Html => "text/html",
749                            ResponseType::Text => "text/plain",
750                        };
751                        builder = builder.header("Content-Type", ct);
752                        builder.body(body).unwrap()
753                    }
754                    Err(e) => crate::response::error_response(&e, ctx.clone()),
755                }
756            })
757        });
758
759        self.insert(RouteEntry {
760            method: method.clone(),
761            path: full_path.clone(),
762            handler: boxed,
763            middlewares: all_middlewares,
764        });
765        info!(
766            target: "routing",
767            handler = "Controller",
768            method = method.as_str(),
769            path = %full_path,
770            "Mounted a '{}' handler which listens on  '{}'",
771            method.as_str(),
772            full_path
773        );
774        TOTAL_COUNT.fetch_add(1, Ordering::SeqCst);
775    }
776
777    /// Registers a handler returning heterogeneous payloads as `Box<dyn TypedServiceResult>`.
778    /// Use this when one handler returns different `ServiceResult<T>` shapes; prefer
779    /// [`Router::mount`] when the result type is statically known.
780    /// Like [`Router::mount`], the handler takes only the [`CorrelationContext`].
781    pub fn mount_typed<F, Fut>(
782        &mut self,
783        base_path: &str,
784        version: u32,
785        path: &str,
786        method: Method,
787        description: RouteDescription,
788        handler: F,
789        middlewares: Vec<Middleware>,
790    ) where
791        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
792        Fut: Future<Output = Result<Box<dyn TypedServiceResult>, ErrorResult>> + Send + 'static,
793    {
794        self.ensure_global_middleware();
795        let full_path = calculate_full_path(base_path, version, path);
796        let meta_path = Holder {
797            path: full_path.to_string(),
798            limit: description.rate_limit.unwrap_or(0),
799        };
800
801        if let Ok(mut reg) = DocumentationRegistrant::global().write() {
802            reg.register_route(
803                &full_path,
804                method.as_str(),
805                "Controller",
806                description.clone(),
807            );
808        }
809
810        let mut all_middlewares = Vec::new();
811        if let Some(limit) = description.rate_limit {
812            if limit > 0 {
813                let path_clone = meta_path.clone();
814                let m: Middleware = Arc::new(move |ctx: &mut CorrelationContext| {
815                    let headers = ctx.headers();
816                    let path_inner = path_clone.clone();
817                    let fut = async move {
818                        let limiter = LIMITER.get_or_init(|| create_from_env()).clone();
819                        let key = ctx.user_id().unwrap_or_else(|| {
820                            headers
821                                .get("x-forwarded-for")
822                                .and_then(|v| v.to_str().ok())
823                                .unwrap_or("anonymous")
824                                .to_string()
825                        }) + ":"
826                            + &path_inner.path;
827                        if limiter.is_allowed(&key, path_inner.limit).await {
828                            return Ok(());
829                        }
830                        Err(ErrorResult::from_error(
831                            "Too many requests. Please try again later.",
832                            429,
833                        ))
834                    };
835                    FutureExt::boxed(fut)
836                });
837                all_middlewares.push(m);
838            }
839        }
840        all_middlewares.extend(middlewares);
841
842        // Declared per-parameter send caps (`FileParameter.count`), enforced in
843        // place before the handler runs just like body validation.
844        let file_counts: Vec<(String, u32)> = description
845            .file_parameters
846            .iter()
847            .filter_map(|(name, p)| p.count.map(|c| (name.clone(), c)))
848            .collect();
849        let handler = Arc::new(handler);
850
851        let boxed: BoxHandler = Arc::new(move |ctx, _headers, _method, _path, _body| {
852            let file_counts = file_counts.clone();
853            let handler = handler.clone();
854            Box::pin(async move {
855                let ctx = Arc::new(ctx);
856                if let Some(err) = file_count_violation(ctx.clone(), &file_counts) {
857                    return crate::response::error_response(&err, ctx.clone());
858                }
859                match handler((*ctx).clone()).await {
860                    Ok(typed) => crate::response::build_response(typed.as_ref(), ctx.clone()),
861                    Err(e) => crate::response::error_response(&e, ctx.clone()),
862                }
863            })
864        });
865
866        self.insert(RouteEntry {
867            method: method.clone(),
868            path: full_path.clone(),
869            handler: boxed,
870            middlewares: all_middlewares,
871        });
872        info!(
873            handler = "Controller",
874            method = method.as_str(),
875            path = %full_path,
876            "Mounted a '{}' handler which listens on  '{}'",
877            method.as_str(),
878            full_path
879        );
880        TOTAL_COUNT.fetch_add(1, Ordering::SeqCst);
881    }
882
883    /// Registers a pre-built [`BoxHandler`] at the resolved path with the given middlewares.
884    /// A trailing `/*` on the resolved path acts as a prefix wildcard when matching.
885    /// Unlike [`Router::mount`], this performs no docs registration and imposes no
886    /// body contract, making it suitable for static-asset serving and custom handlers.
887    pub fn mount_raw(
888        &mut self,
889        base_path: &str,
890        version: u32,
891        path: &str,
892        method: Method,
893        handler: BoxHandler,
894        middlewares: Vec<Middleware>,
895    ) {
896        self.ensure_global_middleware();
897        let full_path = calculate_full_path(base_path, version, path);
898        self.insert(RouteEntry {
899            method: method.clone(),
900            path: full_path.clone(),
901            handler,
902            middlewares,
903        });
904        info!(
905            handler = "raw",
906            method = method.as_str(),
907            path = %full_path,
908            "Mounted a '{}' handler which listens on  '{}'",
909            method.as_str(),
910            full_path
911        );
912        TOTAL_COUNT.fetch_add(1, Ordering::SeqCst);
913    }
914
915    /// Serves files under `fs_path` at `base_path` + version + `path` with a `/*` wildcard.
916    /// Requests for `/` resolve to `index.html` when present; missing files fall back
917    /// to a plain-text placeholder response instead of a 404.
918    pub fn mount_static(
919        &mut self,
920        base_path: &str,
921        version: u32,
922        path: &str,
923        fs_path: String,
924        middlewares: Vec<Middleware>,
925    ) {
926        let full_path = if version == 0 {
927            no_trailing_slash(&format!(
928                "/{}/{}/*",
929                base_path.trim_matches('/'),
930                path.trim_matches('/')
931            ))
932        } else {
933            no_trailing_slash(&format!(
934                "/v{}/{}/{}/*",
935                version,
936                base_path.trim_matches('/'),
937                path.trim_matches('/')
938            ))
939        };
940        let fs_path_clone = fs_path.clone();
941        let full_path_no_wildcard = full_path.trim_end_matches("/*").to_string();
942        let handler: BoxHandler = Arc::new(move |ctx, _headers, _method, req_path, _body| {
943            let fs_path = fs_path_clone.clone();
944            let req_path = req_path.clone();
945            let ctx = ctx.clone();
946            let prefix = full_path_no_wildcard.clone();
947            Box::pin(async move {
948                let rel = req_path.trim_start_matches(&prefix).trim_start_matches('/');
949                // Resolve to a real file under fs_path when present (Swagger UI `doc/` dir).
950                let candidates = if rel.is_empty() {
951                    vec![
952                        format!("{}/index.html", fs_path.trim_end_matches('/')),
953                        fs_path.clone(),
954                    ]
955                } else {
956                    vec![format!("{}/{}", fs_path.trim_end_matches('/'), rel)]
957                };
958                for candidate in &candidates {
959                    if let Ok(bytes) = std::fs::read(candidate) {
960                        let ct = guess_content_type(candidate);
961                        let body = String::from_utf8_lossy(&bytes).into_owned();
962                        return Response::builder()
963                            .status(200)
964                            .header("X-Request-ID", ctx.request_id())
965                            .header("Content-Type", ct)
966                            .body(body)
967                            .unwrap();
968                    }
969                }
970                // Fallback placeholder (keeps offline/dev behavior useful)
971                let file_path = if rel.is_empty() {
972                    fs_path.clone()
973                } else {
974                    format!("{}/{}", fs_path.trim_end_matches('/'), rel)
975                };
976                let typed = ServiceResult::new(
977                    "success",
978                    "OK",
979                    Some(format!("Serving static file: {}", file_path)),
980                    200,
981                )
982                .with_response_type(ResponseType::Text);
983                let body = TypedServiceResult::serialize(&typed).unwrap();
984                Response::builder()
985                    .status(TypedServiceResult::code(&typed))
986                    .header("X-Request-ID", ctx.request_id())
987                    .header("Content-Type", "text/plain")
988                    .body(body)
989                    .unwrap()
990            })
991        });
992        self.insert(RouteEntry {
993            method: Method::GET,
994            path: full_path.clone(),
995            handler,
996            middlewares,
997        });
998        info!(
999            handler = "static",
1000            path = %full_path,
1001            "Mounted a '{}' handler which listens on  '{}'",
1002            "static file",
1003            full_path
1004        );
1005        TOTAL_COUNT.fetch_add(1, Ordering::SeqCst);
1006    }
1007
1008    fn find<'a>(
1009        node: &'a TrieNode,
1010        segments: &[&str],
1011        method: &Method,
1012        params: &mut Vec<(String, String)>,
1013    ) -> Option<&'a RouteEntry> {
1014        match segments.split_first() {
1015            None => node
1016                .exact_routes
1017                .get(method)
1018                .or_else(|| node.wildcard_routes.get(method)),
1019            Some((seg, rest)) => {
1020                // static beats param beats wildcard, tried in that order with backtracking
1021                if let Some(child) = node.static_children.get(*seg) {
1022                    if let Some(r) = Self::find(child, rest, method, params) {
1023                        return Some(r);
1024                    }
1025                }
1026
1027                if let Some((name, child)) = &node.param_child {
1028                    params.push((name.clone(), (*seg).to_string()));
1029                    if let Some(r) = Self::find(child, rest, method, params) {
1030                        return Some(r);
1031                    }
1032                    params.pop();
1033                }
1034
1035                node.wildcard_routes.get(method)
1036            }
1037        }
1038    }
1039}
1040
1041impl Default for Router {
1042    fn default() -> Self {
1043        Self::new()
1044    }
1045}
1046
1047fn guess_content_type(path: &str) -> &'static str {
1048    let lower = path.to_ascii_lowercase();
1049    if lower.ends_with(".html") || lower.ends_with(".htm") {
1050        "text/html"
1051    } else if lower.ends_with(".js") {
1052        "application/javascript"
1053    } else if lower.ends_with(".css") {
1054        "text/css"
1055    } else if lower.ends_with(".json") {
1056        "application/json"
1057    } else if lower.ends_with(".png") {
1058        "image/png"
1059    } else if lower.ends_with(".jpg") || lower.ends_with(".jpeg") {
1060        "image/jpeg"
1061    } else if lower.ends_with(".svg") {
1062        "image/svg+xml"
1063    } else if lower.ends_with(".yaml") || lower.ends_with(".yml") {
1064        "application/yaml"
1065    } else {
1066        "text/plain"
1067    }
1068}
1069fn normalize_path(p: &str) -> String {
1070    let p = p.trim().replace("//", "/");
1071    if p.ends_with('/') && p.len() > 1 {
1072        p[..p.len() - 1].to_string()
1073    } else if p.is_empty() {
1074        "/".to_string()
1075    } else {
1076        p
1077    }
1078}
1079fn no_trailing_slash(path: &str) -> String {
1080    let p = path.trim().replace("//", "/");
1081    if p.is_empty() || p == "/" {
1082        return "/".to_string();
1083    }
1084    if p.ends_with('/') {
1085        p[..p.rfind('/').unwrap()].to_string()
1086    } else {
1087        p
1088    }
1089}
1090/// Enforce declared `FileParameter.count` caps against a dispatched context.
1091/// Counts every part bearing the parameter name (files plus text fields) —
1092/// `count` is the maximum number of times the parameter may be sent.
1093/// Non-multipart requests and undeclared parameters are never rejected.
1094fn file_count_violation(
1095    ctx: Arc<CorrelationContext>,
1096    counts: &[(String, u32)],
1097) -> Option<ErrorResult> {
1098    if counts.is_empty() {
1099        return None;
1100    }
1101    let mp = ctx.multipart()?;
1102    for (name, max) in counts {
1103        let occurrences = mp.files.iter().filter(|f| &f.field_name == name).count()
1104            + mp.fields.get(name).map_or(0, |v| v.len());
1105        if occurrences as u32 > *max {
1106            return Some(ErrorResult::bad_request(format!(
1107                "Too many '{}' parts: got {}, maximum is {}",
1108                name, occurrences, max
1109            )));
1110        }
1111    }
1112    None
1113}
1114
1115fn calculate_full_path(base_path: &str, version: u32, path: &str) -> String {
1116    let decoded = if version == 0 {
1117        let dddd = no_trailing_slash(&format!(
1118            "/{}/{}",
1119            base_path.trim_matches('/'),
1120            path.trim_start_matches('/')
1121        ));
1122        urlencoding::decode(dddd.leak())
1123    } else {
1124        let dddd = no_trailing_slash(&format!(
1125            "/v{}/{}/{}",
1126            version,
1127            base_path.trim_matches('/'),
1128            path.trim_start_matches('/')
1129        ));
1130        urlencoding::decode(dddd.leak())
1131    };
1132
1133    if decoded.is_err() {
1134        return base_path.to_string();
1135    }
1136
1137    decoded.unwrap().to_string()
1138}
1139
1140// ── RouteController trait ──────────────────────────────────────────────────
1141
1142#[async_trait::async_trait]
1143/// Groups related routes under a shared base path and version.
1144/// Implement [`RouteController::base_path`] and [`RouteController::register_routes`];
1145/// override [`RouteController::version`] to prefix paths with `/v{version}`.
1146pub trait RouteController: Send + Sync {
1147    /// Base path prefix for every route registered by this controller.
1148    fn base_path(&self) -> &str;
1149    /// API version prefix; `0` means no `/v{version}` segment is added.
1150    fn version(&self) -> u32 {
1151        0
1152    }
1153    /// Resolves a relative `path` to its full mounted path including base path and version.
1154    fn full_path(&self, path: &str) -> String {
1155        calculate_full_path(self.base_path(), self.version(), path)
1156    }
1157    /// Registers this controller's routes on `router`, typically via [`RouteControllerExt`].
1158    async fn register_routes(&self, router: &mut Router);
1159    /// Default registration entry point; delegates to [`RouteController::register_routes`].
1160    async fn register(&self, router: &mut Router) {
1161        self.register_routes(router).await;
1162    }
1163
1164    ///
1165    fn type_name(&self) -> &'static str {
1166        std::any::type_name::<Self>()
1167    }
1168}
1169
1170/// Convenience mounts that fill in this controller's base path and version.
1171/// Each `mount_*` takes an `Fn(CorrelationContext)` handler returning
1172/// `Result<ServiceResult<T>, ErrorResult>`; the `*_typed` variants instead return
1173/// `Result<Box<dyn TypedServiceResult>, ErrorResult>` for heterogeneous payloads.
1174/// ```ignore
1175/// self.mount_get(&mut router, "/list", desc, handler, vec![]);
1176/// ```
1177pub trait RouteControllerExt: RouteController {
1178    /// Mounts a GET handler with no required body at `path`.
1179    fn mount_get<T, F, Fut>(
1180        &self,
1181        router: &mut Router,
1182        path: &str,
1183        description: RouteDescription,
1184        handler: F,
1185        middlewares: Vec<Middleware>,
1186    ) where
1187        T: serde::Serialize + Send + Sync + 'static,
1188        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1189        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1190    {
1191        router.mount(
1192            self.base_path(),
1193            self.version(),
1194            path,
1195            false,
1196            Method::GET,
1197            description,
1198            handler,
1199            middlewares,
1200        );
1201    }
1202    /// Mounts a POST handler; requires a declared request body or file parameter.
1203    fn mount_post<T, F, Fut>(
1204        &self,
1205        router: &mut Router,
1206        path: &str,
1207        description: RouteDescription,
1208        handler: F,
1209        middlewares: Vec<Middleware>,
1210    ) where
1211        T: serde::Serialize + Send + Sync + 'static,
1212        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1213        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1214    {
1215        router.mount(
1216            self.base_path(),
1217            self.version(),
1218            path,
1219            true,
1220            Method::POST,
1221            description,
1222            handler,
1223            middlewares,
1224        );
1225    }
1226    /// Mounts a PUT handler; requires a declared request body or file parameter.
1227    fn mount_put<T, F, Fut>(
1228        &self,
1229        router: &mut Router,
1230        path: &str,
1231        description: RouteDescription,
1232        handler: F,
1233        middlewares: Vec<Middleware>,
1234    ) where
1235        T: serde::Serialize + Send + Sync + 'static,
1236        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1237        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1238    {
1239        router.mount(
1240            self.base_path(),
1241            self.version(),
1242            path,
1243            true,
1244            Method::PUT,
1245            description,
1246            handler,
1247            middlewares,
1248        );
1249    }
1250    /// Mounts a PATCH handler; `has_body` selects whether a request body is required.
1251    fn mount_patch<T, F, Fut>(
1252        &self,
1253        router: &mut Router,
1254        path: &str,
1255        has_body: bool,
1256        description: RouteDescription,
1257        handler: F,
1258        middlewares: Vec<Middleware>,
1259    ) where
1260        T: serde::Serialize + Send + Sync + 'static,
1261        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1262        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1263    {
1264        router.mount(
1265            self.base_path(),
1266            self.version(),
1267            path,
1268            has_body,
1269            Method::PATCH,
1270            description,
1271            handler,
1272            middlewares,
1273        );
1274    }
1275    /// Mounts a PATCH handler that requires a request body.
1276    fn mount_patch_with_body<T, F, Fut>(
1277        &self,
1278        router: &mut Router,
1279        path: &str,
1280        description: RouteDescription,
1281        handler: F,
1282        middlewares: Vec<Middleware>,
1283    ) where
1284        T: serde::Serialize + Send + Sync + 'static,
1285        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1286        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1287    {
1288        self.mount_patch(router, path, true, description, handler, middlewares);
1289    }
1290    /// Mounts a PATCH handler that requires no request body.
1291    fn mount_patch_without_body<T, F, Fut>(
1292        &self,
1293        router: &mut Router,
1294        path: &str,
1295        description: RouteDescription,
1296        handler: F,
1297        middlewares: Vec<Middleware>,
1298    ) where
1299        T: serde::Serialize + Send + Sync + 'static,
1300        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1301        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1302    {
1303        self.mount_patch(router, path, false, description, handler, middlewares);
1304    }
1305    /// Mounts a DELETE handler with no required body at `path`.
1306    fn mount_delete<T, F, Fut>(
1307        &self,
1308        router: &mut Router,
1309        path: &str,
1310        description: RouteDescription,
1311        handler: F,
1312        middlewares: Vec<Middleware>,
1313    ) where
1314        T: serde::Serialize + Send + Sync + 'static,
1315        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1316        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1317    {
1318        router.mount(
1319            self.base_path(),
1320            self.version(),
1321            path,
1322            false,
1323            Method::DELETE,
1324            description,
1325            handler,
1326            middlewares,
1327        );
1328    }
1329    /// Mounts an OPTIONS handler with no required body at `path`.
1330    fn mount_options<T, F, Fut>(
1331        &self,
1332        router: &mut Router,
1333        path: &str,
1334        description: RouteDescription,
1335        handler: F,
1336        middlewares: Vec<Middleware>,
1337    ) where
1338        T: serde::Serialize + Send + Sync + 'static,
1339        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1340        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1341    {
1342        router.mount(
1343            self.base_path(),
1344            self.version(),
1345            path,
1346            false,
1347            Method::OPTIONS,
1348            description,
1349            handler,
1350            middlewares,
1351        );
1352    }
1353    /// Mounts a HEAD handler with no required body at `path`.
1354    fn mount_head<T, F, Fut>(
1355        &self,
1356        router: &mut Router,
1357        path: &str,
1358        description: RouteDescription,
1359        handler: F,
1360        middlewares: Vec<Middleware>,
1361    ) where
1362        T: serde::Serialize + Send + Sync + 'static,
1363        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1364        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1365    {
1366        router.mount(
1367            self.base_path(),
1368            self.version(),
1369            path,
1370            false,
1371            Method::HEAD,
1372            description,
1373            handler,
1374            middlewares,
1375        );
1376    }
1377    /// Mounts a TRACE handler with no required body at `path`.
1378    fn mount_trace<T, F, Fut>(
1379        &self,
1380        router: &mut Router,
1381        path: &str,
1382        description: RouteDescription,
1383        handler: F,
1384        middlewares: Vec<Middleware>,
1385    ) where
1386        T: serde::Serialize + Send + Sync + 'static,
1387        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1388        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1389    {
1390        router.mount(
1391            self.base_path(),
1392            self.version(),
1393            path,
1394            false,
1395            Method::TRACE,
1396            description,
1397            handler,
1398            middlewares,
1399        );
1400    }
1401    /// Mounts a CONNECT handler with no required body at `path`.
1402    fn mount_connect<T, F, Fut>(
1403        &self,
1404        router: &mut Router,
1405        path: &str,
1406        description: RouteDescription,
1407        handler: F,
1408        middlewares: Vec<Middleware>,
1409    ) where
1410        T: serde::Serialize + Send + Sync + 'static,
1411        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1412        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1413    {
1414        router.mount(
1415            self.base_path(),
1416            self.version(),
1417            path,
1418            false,
1419            Method::CONNECT,
1420            description,
1421            handler,
1422            middlewares,
1423        );
1424    }
1425    /// Mounts a WebDAV COPY handler with no required body at `path`.
1426    fn mount_copy<T, F, Fut>(
1427        &self,
1428        router: &mut Router,
1429        path: &str,
1430        description: RouteDescription,
1431        handler: F,
1432        middlewares: Vec<Middleware>,
1433    ) where
1434        T: serde::Serialize + Send + Sync + 'static,
1435        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1436        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1437    {
1438        router.mount(
1439            self.base_path(),
1440            self.version(),
1441            path,
1442            false,
1443            Method::from_bytes(b"COPY").unwrap(),
1444            description,
1445            handler,
1446            middlewares,
1447        );
1448    }
1449    /// Mounts a WebDAV MOVE handler with no required body at `path`.
1450    fn mount_move<T, F, Fut>(
1451        &self,
1452        router: &mut Router,
1453        path: &str,
1454        description: RouteDescription,
1455        handler: F,
1456        middlewares: Vec<Middleware>,
1457    ) where
1458        T: serde::Serialize + Send + Sync + 'static,
1459        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1460        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1461    {
1462        router.mount(
1463            self.base_path(),
1464            self.version(),
1465            path,
1466            false,
1467            Method::from_bytes(b"MOVE").unwrap(),
1468            description,
1469            handler,
1470            middlewares,
1471        );
1472    }
1473    /// Mounts a WebDAV LOCK handler; requires a declared request body or file parameter.
1474    fn mount_lock<T, F, Fut>(
1475        &self,
1476        router: &mut Router,
1477        path: &str,
1478        description: RouteDescription,
1479        handler: F,
1480        middlewares: Vec<Middleware>,
1481    ) where
1482        T: serde::Serialize + Send + Sync + 'static,
1483        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1484        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1485    {
1486        router.mount(
1487            self.base_path(),
1488            self.version(),
1489            path,
1490            true,
1491            Method::from_bytes(b"LOCK").unwrap(),
1492            description,
1493            handler,
1494            middlewares,
1495        );
1496    }
1497    /// Mounts a WebDAV UNLOCK handler with no required body at `path`.
1498    fn mount_unlock<T, F, Fut>(
1499        &self,
1500        router: &mut Router,
1501        path: &str,
1502        description: RouteDescription,
1503        handler: F,
1504        middlewares: Vec<Middleware>,
1505    ) where
1506        T: serde::Serialize + Send + Sync + 'static,
1507        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1508        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1509    {
1510        router.mount(
1511            self.base_path(),
1512            self.version(),
1513            path,
1514            false,
1515            Method::from_bytes(b"UNLOCK").unwrap(),
1516            description,
1517            handler,
1518            middlewares,
1519        );
1520    }
1521    /// Mounts a WebDAV PROPFIND handler; requires a declared request body or file parameter.
1522    fn mount_propfind<T, F, Fut>(
1523        &self,
1524        router: &mut Router,
1525        path: &str,
1526        description: RouteDescription,
1527        handler: F,
1528        middlewares: Vec<Middleware>,
1529    ) where
1530        T: serde::Serialize + Send + Sync + 'static,
1531        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1532        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1533    {
1534        router.mount(
1535            self.base_path(),
1536            self.version(),
1537            path,
1538            true,
1539            Method::from_bytes(b"PROPFIND").unwrap(),
1540            description,
1541            handler,
1542            middlewares,
1543        );
1544    }
1545    /// Mounts a WebDAV MKCOL handler with no required body at `path`.
1546    fn mount_mkcol<T, F, Fut>(
1547        &self,
1548        router: &mut Router,
1549        path: &str,
1550        description: RouteDescription,
1551        handler: F,
1552        middlewares: Vec<Middleware>,
1553    ) where
1554        T: serde::Serialize + Send + Sync + 'static,
1555        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1556        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1557    {
1558        router.mount(
1559            self.base_path(),
1560            self.version(),
1561            path,
1562            false,
1563            Method::from_bytes(b"MKCOL").unwrap(),
1564            description,
1565            handler,
1566            middlewares,
1567        );
1568    }
1569    /// Mounts a WebDAV SEARCH handler; requires a declared request body or file parameter.
1570    fn mount_search<T, F, Fut>(
1571        &self,
1572        router: &mut Router,
1573        path: &str,
1574        description: RouteDescription,
1575        handler: F,
1576        middlewares: Vec<Middleware>,
1577    ) where
1578        T: serde::Serialize + Send + Sync + 'static,
1579        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1580        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1581    {
1582        router.mount(
1583            self.base_path(),
1584            self.version(),
1585            path,
1586            true,
1587            Method::from_bytes(b"SEARCH").unwrap(),
1588            description,
1589            handler,
1590            middlewares,
1591        );
1592    }
1593    /// Mounts a WebDAV REPORT handler; requires a declared request body or file parameter.
1594    fn mount_report<T, F, Fut>(
1595        &self,
1596        router: &mut Router,
1597        path: &str,
1598        description: RouteDescription,
1599        handler: F,
1600        middlewares: Vec<Middleware>,
1601    ) where
1602        T: serde::Serialize + Send + Sync + 'static,
1603        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1604        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1605    {
1606        router.mount(
1607            self.base_path(),
1608            self.version(),
1609            path,
1610            true,
1611            Method::from_bytes(b"REPORT").unwrap(),
1612            description,
1613            handler,
1614            middlewares,
1615        );
1616    }
1617    /// Mounts a versioning CHECKIN handler with no required body at `path`.
1618    fn mount_checkin<T, F, Fut>(
1619        &self,
1620        router: &mut Router,
1621        path: &str,
1622        description: RouteDescription,
1623        handler: F,
1624        middlewares: Vec<Middleware>,
1625    ) where
1626        T: serde::Serialize + Send + Sync + 'static,
1627        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1628        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1629    {
1630        router.mount(
1631            self.base_path(),
1632            self.version(),
1633            path,
1634            false,
1635            Method::from_bytes(b"CHECKIN").unwrap(),
1636            description,
1637            handler,
1638            middlewares,
1639        );
1640    }
1641    /// Mounts a versioning CHECKOUT handler with no required body at `path`.
1642    fn mount_checkout<T, F, Fut>(
1643        &self,
1644        router: &mut Router,
1645        path: &str,
1646        description: RouteDescription,
1647        handler: F,
1648        middlewares: Vec<Middleware>,
1649    ) where
1650        T: serde::Serialize + Send + Sync + 'static,
1651        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1652        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1653    {
1654        router.mount(
1655            self.base_path(),
1656            self.version(),
1657            path,
1658            false,
1659            Method::from_bytes(b"CHECKOUT").unwrap(),
1660            description,
1661            handler,
1662            middlewares,
1663        );
1664    }
1665    /// Mounts a versioning UNCHECKOUT handler with no required body at `path`.
1666    fn mount_uncheckout<T, F, Fut>(
1667        &self,
1668        router: &mut Router,
1669        path: &str,
1670        description: RouteDescription,
1671        handler: F,
1672        middlewares: Vec<Middleware>,
1673    ) where
1674        T: serde::Serialize + Send + Sync + 'static,
1675        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1676        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1677    {
1678        router.mount(
1679            self.base_path(),
1680            self.version(),
1681            path,
1682            false,
1683            Method::from_bytes(b"UNCHECKOUT").unwrap(),
1684            description,
1685            handler,
1686            middlewares,
1687        );
1688    }
1689    /// Mounts a WebDAV MERGE handler; requires a declared request body or file parameter.
1690    fn mount_merge<T, F, Fut>(
1691        &self,
1692        router: &mut Router,
1693        path: &str,
1694        description: RouteDescription,
1695        handler: F,
1696        middlewares: Vec<Middleware>,
1697    ) where
1698        T: serde::Serialize + Send + Sync + 'static,
1699        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1700        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1701    {
1702        router.mount(
1703            self.base_path(),
1704            self.version(),
1705            path,
1706            true,
1707            Method::from_bytes(b"MERGE").unwrap(),
1708            description,
1709            handler,
1710            middlewares,
1711        );
1712    }
1713    /// Mounts a WebDAV ACL handler; requires a declared request body or file parameter.
1714    fn mount_acl<T, F, Fut>(
1715        &self,
1716        router: &mut Router,
1717        path: &str,
1718        description: RouteDescription,
1719        handler: F,
1720        middlewares: Vec<Middleware>,
1721    ) where
1722        T: serde::Serialize + Send + Sync + 'static,
1723        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1724        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1725    {
1726        router.mount(
1727            self.base_path(),
1728            self.version(),
1729            path,
1730            true,
1731            Method::from_bytes(b"ACL").unwrap(),
1732            description,
1733            handler,
1734            middlewares,
1735        );
1736    }
1737    /// Mounts a handler for an arbitrary `method` with no required body at `path`.
1738    fn mount_custom<T, F, Fut>(
1739        &self,
1740        router: &mut Router,
1741        path: &str,
1742        method: Method,
1743        description: RouteDescription,
1744        handler: F,
1745        middlewares: Vec<Middleware>,
1746    ) where
1747        T: serde::Serialize + Send + Sync + 'static,
1748        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1749        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1750    {
1751        router.mount(
1752            self.base_path(),
1753            self.version(),
1754            path,
1755            false,
1756            method,
1757            description,
1758            handler,
1759            middlewares,
1760        );
1761    }
1762    /// Serves files under `fs_path` at this controller's base path plus `path`.
1763    fn mount_static(
1764        &self,
1765        router: &mut Router,
1766        path: &str,
1767        fs_path: String,
1768        middlewares: Vec<Middleware>,
1769    ) {
1770        router.mount_static(self.base_path(), self.version(), path, fs_path, middlewares);
1771    }
1772    /// Mounts a handler returning `Box<dyn TypedServiceResult>` for heterogeneous payloads.
1773    fn mount_typed<F, Fut>(
1774        &self,
1775        router: &mut Router,
1776        path: &str,
1777        method: Method,
1778        description: RouteDescription,
1779        handler: F,
1780        middlewares: Vec<Middleware>,
1781    ) where
1782        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1783        Fut: Future<Output = Result<Box<dyn TypedServiceResult>, ErrorResult>> + Send + 'static,
1784    {
1785        router.mount_typed(
1786            self.base_path(),
1787            self.version(),
1788            path,
1789            method,
1790            description,
1791            handler,
1792            middlewares,
1793        );
1794    }
1795    /// Mounts a boxed-result GET handler returning heterogeneous payloads.
1796    fn mount_get_typed<F, Fut>(
1797        &self,
1798        router: &mut Router,
1799        path: &str,
1800        description: RouteDescription,
1801        handler: F,
1802        middlewares: Vec<Middleware>,
1803    ) where
1804        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1805        Fut: Future<Output = Result<Box<dyn TypedServiceResult>, ErrorResult>> + Send + 'static,
1806    {
1807        self.mount_typed(router, path, Method::GET, description, handler, middlewares);
1808    }
1809    /// Mounts a boxed-result POST handler returning heterogeneous payloads.
1810    fn mount_post_typed<F, Fut>(
1811        &self,
1812        router: &mut Router,
1813        path: &str,
1814        description: RouteDescription,
1815        handler: F,
1816        middlewares: Vec<Middleware>,
1817    ) where
1818        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1819        Fut: Future<Output = Result<Box<dyn TypedServiceResult>, ErrorResult>> + Send + 'static,
1820    {
1821        self.mount_typed(
1822            router,
1823            path,
1824            Method::POST,
1825            description,
1826            handler,
1827            middlewares,
1828        );
1829    }
1830}
1831impl<T: RouteController> RouteControllerExt for T {}
1832
1833// ── ConfigurationRegistrant — Hyper server bootstrap ───────────────────────
1834
1835/// Hyper server bootstrap: owns the shared [`Router`] and the bind address.
1836/// Mount controllers and global middlewares, then call [`ConfigurationRegistrant::serve`].
1837/// ```ignore
1838/// let server = std::sync::Arc::new(ConfigurationRegistrant::new(addr));
1839/// server.mount_controller(MyController).await;
1840/// let bound = server.serve().await?;
1841/// ```
1842pub struct ConfigurationRegistrant {
1843    router: Arc<tokio::sync::RwLock<Router>>,
1844    addr: SocketAddr,
1845}
1846
1847impl ConfigurationRegistrant {
1848    /// Creates a bootstrap with a fresh router bound to `addr` on [`ConfigurationRegistrant::serve`].
1849    pub fn new(addr: SocketAddr) -> Self {
1850        Self {
1851            router: Arc::new(tokio::sync::RwLock::new(Router::new())),
1852            addr,
1853        }
1854    }
1855
1856    /// Registers every route of `controller` on the shared router.
1857    pub async fn mount_controller<C: RouteController + 'static>(&self, controller: C) {
1858        let mut r = self.router.write().await;
1859        info!(
1860            target: "routing",
1861            handler = std::any::type_name::<C>(),
1862            path = %controller.base_path(),
1863            "Mounted controller '{}' at '{}'",
1864            std::any::type_name::<C>(),
1865            controller.base_path()
1866        );
1867        controller.register_routes(&mut r).await;
1868    }
1869
1870    /// Pushes a middleware that runs for every request before route matching.
1871    pub async fn mount_middleware(&self, _mw: Middleware) {
1872        let mut r = self.router.write().await;
1873        r.global_middlewares.push(_mw);
1874    }
1875
1876    /// Binds the socket, spawns the Hyper accept loop in the background, and returns the bound address.
1877    /// Returns an error if the socket cannot be bound.
1878    pub async fn serve(self: Arc<Self>) -> anyhow::Result<SocketAddr> {
1879        let listener = TcpListener::bind(self.addr).await?;
1880        let addr = listener.local_addr()?;
1881        info!(address = %addr, "HTTP server listening");
1882        let router = self.router.clone();
1883        tokio::spawn(async move {
1884            loop {
1885                let (stream, remote) = match listener.accept().await {
1886                    Ok(v) => v,
1887                    Err(e) => {
1888                        warn!(error = %e, "Failed to accept HTTP connection");
1889                        continue;
1890                    }
1891                };
1892                let io = TokioIo::new(stream);
1893                let router = router.clone();
1894                tokio::spawn(async move {
1895                    let svc = service_fn(move |req: Request<Incoming>| {
1896                        let router = router.clone();
1897                        let remote = remote;
1898                        async move { handle_request(router, req, remote).await }
1899                    });
1900                    if let Err(e) = http1::Builder::new().serve_connection(io, svc).await {
1901                        tracing::debug!(remote_addr = %remote, error = %e, "HTTP connection ended with an error");
1902                    }
1903                });
1904            }
1905        });
1906        Ok(addr)
1907    }
1908
1909    /// Returns a clone of the shared router handle for direct route registration.
1910    pub fn router_handle(&self) -> Arc<tokio::sync::RwLock<Router>> {
1911        self.router.clone()
1912    }
1913}
1914
1915async fn handle_request(
1916    router: Arc<tokio::sync::RwLock<Router>>,
1917    req: Request<Incoming>,
1918    remote_addr: SocketAddr,
1919) -> Result<Response<String>, ErrorResult> {
1920    let request_started = std::time::Instant::now();
1921    let method = req.method().clone();
1922    let path = req.uri().path();
1923    let old_path = path;
1924    let path = urlencoding::decode(path);
1925    if path.is_err() {
1926        tracing::error!(
1927            "This should not be possible. Encountered an error while processing the URL {}",
1928            old_path
1929        );
1930        return Err(ErrorResult::bad_request("Invalid path parameter"));
1931    }
1932    let path = path.ok().unwrap().to_string();
1933    let query = req.uri().query().unwrap_or("").to_string();
1934    let headers = req.headers().clone();
1935    let (_parts, body) = req.into_parts();
1936    let body_bytes = match body.collect().await {
1937        Ok(collected) => collected.to_bytes().to_vec(),
1938        Err(e) => {
1939            warn!(remote_addr = %remote_addr, error = %e, "Failed to read request body");
1940            vec![]
1941        }
1942    };
1943
1944    let params: HashMap<String, String> = serde_urlencoded::from_str(&query).unwrap_or_default();
1945    let mut ctx = build_correlation_context(&headers, &params, &body_bytes);
1946
1947    // Decode multipart eagerly so handlers and middlewares read fields/files
1948    // off the context via `form_field`/`files` and `ctx.body::<T>()`
1949    // instead of parsing raw bytes themselves.
1950    if let Some(ct) = headers
1951        .get(http::header::CONTENT_TYPE)
1952        .and_then(|v| v.to_str().ok())
1953    {
1954        if ct.to_lowercase().starts_with("multipart/form-data") {
1955            if let Some(boundary) =
1956                crate::utils::request_parser::RequestParser::multipart_boundary(ct)
1957            {
1958                let mp = crate::utils::request_parser::RequestParser::parse_multipart(
1959                    &body_bytes,
1960                    &boundary,
1961                );
1962                ctx.set_multipart(mp);
1963            }
1964        }
1965    }
1966    {
1967        let guard = router.read().await;
1968        for mw in &guard.global_middlewares {
1969            if let Err(e) = mw(&mut ctx).await {
1970                let resp = crate::response::error_response(&e, Arc::new(ctx.clone()));
1971                return Ok(finish_request(
1972                    &method,
1973                    &path,
1974                    request_started,
1975                    adapt_response(resp),
1976                ));
1977            }
1978        }
1979    }
1980
1981    let limit = ctx
1982        .query_param("limit")
1983        .and_then(|v| v.parse().ok())
1984        .unwrap_or(15usize);
1985    let cursor = ctx.query_param("cursor");
1986    ctx.set_pagination(cursor, limit);
1987
1988    // Reserved test-client interception — catch-all guard on
1989    // `X-Moovable-Test-Client`. Only intercepts when the header is present and
1990    // DocumentationController installed the hook.
1991    let test_hook = {
1992        let guard = router.read().await;
1993        let has_header = headers.contains_key("x-moovable-test-client")
1994            || headers.contains_key("x-tm30-test-client");
1995        if has_header {
1996            guard.test_client_handler.clone()
1997        } else {
1998            None
1999        }
2000    };
2001    if let Some(hook) = test_hook {
2002        let resp = hook(
2003            ctx.clone(),
2004            headers.clone(),
2005            method.clone(),
2006            path.clone(),
2007            body_bytes,
2008        )
2009        .await;
2010        return Ok(finish_request(
2011            &method,
2012            &path,
2013            request_started,
2014            adapt_string_response(resp, Arc::new(ctx.clone())),
2015        ));
2016    }
2017
2018    let guard = router.read().await;
2019    if let Some((entry, params)) = guard.resolve(&method, &path) {
2020        for mw in &entry.middlewares {
2021            if let Err(e) = mw(&mut ctx).await {
2022                let resp = crate::response::error_response(&e, Arc::new(ctx.clone()));
2023                return Ok(finish_request(
2024                    &method,
2025                    &path,
2026                    request_started,
2027                    adapt_response(resp),
2028                ));
2029            }
2030        }
2031        ctx.set_params(params);
2032        let handler = entry.handler.clone();
2033        let path_clone = path.clone();
2034        let headers_clone = headers.clone();
2035        let method_clone = method.clone();
2036        drop(guard);
2037        let resp = handler(
2038            ctx.clone(),
2039            headers_clone,
2040            method_clone,
2041            path_clone,
2042            body_bytes,
2043        )
2044        .await;
2045        Ok(finish_request(
2046            &method,
2047            &path,
2048            request_started,
2049            adapt_string_response(resp, Arc::new(ctx.clone())),
2050        ))
2051    } else {
2052        drop(guard);
2053        let err = ErrorResult::not_found(format!("The requested resource was not found: {path}"));
2054        let resp = crate::response::error_response(&err, Arc::new(ctx.clone()));
2055        Ok(finish_request(
2056            &method,
2057            &path,
2058            request_started,
2059            adapt_response(resp),
2060        ))
2061    }
2062}
2063
2064fn finish_request(
2065    method: &Method,
2066    path: &str,
2067    started: std::time::Instant,
2068    response: Response<String>,
2069) -> Response<String> {
2070    crate::middleware::monitoring::RequestLogger::log(
2071        path,
2072        method.as_str(),
2073        response.status().as_u16(),
2074        started.elapsed(),
2075    );
2076    response
2077}
2078
2079fn build_correlation_context(
2080    headers: &HeaderMap,
2081    query: &HashMap<String, String>,
2082    body: &[u8],
2083) -> CorrelationContext {
2084    let corr_id = headers
2085        .get("x-correlation-id")
2086        .and_then(|v| v.to_str().ok())
2087        .unwrap_or("");
2088    let flow_str = headers
2089        .get("x-correlation-flow")
2090        .and_then(|v| v.to_str().ok())
2091        .unwrap_or("ONCE");
2092    let flow = flow_str
2093        .parse()
2094        .unwrap_or(crate::logging::CorrelationFlow::Once);
2095    let ctx = if corr_id.is_empty() {
2096        CorrelationContext::new()
2097    } else {
2098        CorrelationContext::with_ids(corr_id, &hex::encode(rand::random::<[u8; 8]>()))
2099    };
2100    ctx.set_flow(flow);
2101    // Attach request-scoped data in place — handlers read body, headers, and
2102    // query parameters off the context, so handler signatures stay context-only.
2103    ctx.set_headers(headers.clone());
2104    ctx.set_query_params(query.clone());
2105    ctx.set_body(body.to_vec());
2106    let req_id = headers.get("x-request-id").and_then(|v| v.to_str().ok());
2107    if let Some(rid) = req_id {
2108        ctx.set_request_id(rid);
2109    } else {
2110        let rid = hex::encode(rand::random::<[u8; 8]>());
2111        ctx.set_request_id(&rid);
2112    }
2113    ctx
2114}
2115
2116fn adapt_response(r: Response<String>) -> Response<String> {
2117    r
2118}
2119fn adapt_string_response(
2120    mut r: Response<String>,
2121    ctx: Arc<CorrelationContext>,
2122) -> Response<String> {
2123    let headers = r.headers_mut();
2124    headers
2125        .entry("x-request-id")
2126        .or_insert(ctx.request_id().parse().unwrap());
2127    headers
2128        .entry("x-correlation-id")
2129        .or_insert(ctx.correlation_id().parse().unwrap());
2130    headers
2131        .entry("x-correlation-flow")
2132        .or_insert(ctx.flow().to_string().parse().unwrap());
2133    r
2134}
2135
2136/// Builds a complete HTTP response from a `ServiceResult<T>`, setting correlation
2137/// headers and the content type from the result's response type.
2138pub fn json_response<T: serde::Serialize + Send + Sync>(
2139    result: ServiceResult<T>,
2140    ctx: Arc<CorrelationContext>,
2141) -> Response<String> {
2142    crate::response::build_response(&result, ctx)
2143}
2144
2145/// Builds a complete HTTP response from any `TypedServiceResult`, setting correlation
2146/// headers and the content type. Serialization failures fall back to a 500 error message body.
2147pub fn typed_response(
2148    result: &dyn TypedServiceResult,
2149    ctx: Arc<CorrelationContext>,
2150) -> Response<String> {
2151    let body = result.serialize().unwrap_or_else(|e| {
2152        ErrorResult::from_error(&anyhow::anyhow!(e.to_string()), 500)
2153            .message
2154            .clone()
2155    });
2156    let mut builder = Response::builder()
2157        .status(result.code())
2158        .header("X-Request-ID", ctx.request_id())
2159        .header("X-Correlation-ID", ctx.correlation_id())
2160        .header("X-Correlation-Flow", ctx.flow().to_string());
2161    let ct = match result.response_type() {
2162        ResponseType::Json => "application/json",
2163        ResponseType::File => {
2164            let filename = body.rsplit('/').next().unwrap_or("file");
2165            builder = builder.header(
2166                "Content-Disposition",
2167                format!("attachment; filename=\"{}\"", filename),
2168            );
2169            "application/octet-stream"
2170        }
2171        ResponseType::Xml => "application/xml",
2172        ResponseType::Javascript => "application/javascript",
2173        ResponseType::Html => "text/html",
2174        ResponseType::Text => "text/plain",
2175    };
2176    builder = builder.header("Content-Type", ct);
2177    builder.body(body).unwrap()
2178}