1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
use std::sync::{OnceLock, RwLock};
/// One HTTP response line for OpenAPI generation (per route).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct OpenApiResponseDesc {
pub status: u16,
pub description: &'static str,
}
/// Optional per-route OpenAPI metadata (from `#[openapi(...)]` / `impl_routes!` `openapi` clause).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct OpenApiRouteSpec {
pub summary: Option<&'static str>,
pub tag: Option<&'static str>,
pub responses: &'static [OpenApiResponseDesc],
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RouteInfo {
pub method: &'static str,
pub path: &'static str,
pub handler: &'static str,
pub openapi: Option<&'static OpenApiRouteSpec>,
}
fn store() -> &'static RwLock<Vec<RouteInfo>> {
static STORE: OnceLock<RwLock<Vec<RouteInfo>>> = OnceLock::new();
STORE.get_or_init(|| RwLock::new(Vec::new()))
}
/// Global route registry (used by OpenAPI generation and diagnostics).
pub struct RouteRegistry;
impl RouteRegistry {
/// Register a route with no OpenAPI overrides (defaults: inferred summary/tags, `200` only).
pub fn register(method: &'static str, path: &'static str, handler: &'static str) {
Self::register_spec(method, path, handler, None);
}
/// Register a route; `openapi` may point at a leaked or `const` [`OpenApiRouteSpec`].
pub fn register_spec(
method: &'static str,
path: &'static str,
handler: &'static str,
openapi: Option<&'static OpenApiRouteSpec>,
) {
let mut guard = store().write().expect("route registry lock poisoned");
guard.push(RouteInfo {
method,
path,
handler,
openapi,
});
}
pub fn list() -> Vec<RouteInfo> {
let guard = store().read().expect("route registry lock poisoned");
guard.clone()
}
/// Exact-match lookup of the handler name for `(method, path)`.
///
/// Used by app-level middleware (throttler, probe normalization) that
/// cannot read the `HandlerKey` request extension (only the route-level
/// guard layer inserts it) but still needs the route's metadata. Paths
/// must match the registered form exactly — the controller prefix joined
/// with the route path; trailing slashes are normalized upstream by
/// tower-http's `normalize-path` when enabled.
pub fn handler_for(method: &str, path: &str) -> Option<String> {
let guard = store().read().expect("route registry lock poisoned");
guard
.iter()
.find(|r| r.method.eq_ignore_ascii_case(method) && r.path == path)
.map(|r| r.handler.to_string())
}
/// Clears all registered HTTP routes in this process.
///
/// **Available only with the `test-hooks` feature.** Intended for integration tests that share
/// a process with other tests; production applications must never call this (routes would
/// disappear from OpenAPI / diagnostics).
#[cfg(feature = "test-hooks")]
pub fn clear_for_tests() {
let mut guard = store().write().expect("route registry lock poisoned");
guard.clear();
}
}