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