Skip to main content

webx_host/
diagnostics.rs

1//! Route/handler startup diagnostics (uses tracing — host layer only).
2
3use webx_core::route::diagnostics::{
4    duplicate_handlers, orphan_handlers, orphan_route_details, route_snapshots,
5};
6use webx_core::route::scan::{RouteDispatch, RouteEntry};
7
8/// Fail fast when route/handler configuration is inconsistent.
9///
10/// Panics at `HostBuilder::build()` when:
11/// - a route exists without a matching `#[handler]` registration
12/// - a `#[handler]` exists without a matching route
13/// - a route has a handler but no generated `RouteDispatch` bridge
14///
15/// `HostBuilder::try_build()` calls [`validate_route_configuration`] instead, so
16/// it can report the same problem as an `Err`.
17pub fn assert_route_configuration_valid() {
18    if let Err(error) = validate_route_configuration() {
19        // Keep the historical panic text: the raw report, no `Error` prefix.
20        let message = match error {
21            webx_core::error::Error::Di(message) => message,
22            other => other.to_string(),
23        };
24        panic!("{message}");
25    }
26}
27
28/// Validate route/handler consistency, returning the same report as
29/// [`assert_route_configuration_valid`] would panic with.
30pub fn validate_route_configuration() -> webx_core::error::Result<()> {
31    let orphan_route_rows = orphan_route_details();
32    let missing_routes = orphan_handlers();
33
34    let mut missing_dispatch: Vec<(&'static str, &'static str, &'static str)> = Vec::new();
35    let cache = webx_core::route::scan::HandlerCache::build();
36    let dispatch_types: std::collections::HashSet<&'static str> =
37        inventory::iter::<RouteDispatch>()
38            .map(|d| d.handler_type)
39            .collect();
40
41    for entry in inventory::iter::<RouteEntry> {
42        if cache.get(entry.handler_type).is_some() && !dispatch_types.contains(entry.handler_type) {
43            missing_dispatch.push((entry.method.as_str(), entry.path, entry.handler_type));
44        }
45    }
46
47    if orphan_route_rows.is_empty() && missing_routes.is_empty() && missing_dispatch.is_empty() {
48        return Ok(());
49    }
50
51    let mut message = String::from("Route configuration errors:\n");
52    if !orphan_route_rows.is_empty() {
53        message.push_str("\nRoutes without #[handler]:\n");
54        message.push_str(
55            "  Fix: add #[handler] or #[handler(inject)] impl IRequestHandler<Request, Response>\n",
56        );
57        for route in &orphan_route_rows {
58            message.push_str(&format!(
59                "  - {} {} ({})\n",
60                route.method, route.path, route.request_type
61            ));
62        }
63    }
64    if !missing_routes.is_empty() {
65        message.push_str("\nHandlers without route:\n");
66        message.push_str(
67            "  Fix: add #[get]/#[post]/... impl IRequest<R> for the request type, or remove the orphan #[handler]\n",
68        );
69        for req in &missing_routes {
70            message.push_str(&format!("  - {req}\n"));
71        }
72    }
73    if !missing_dispatch.is_empty() {
74        message.push_str("\nRoutes with handler but no RouteDispatch:\n");
75        message.push_str(
76            "  Fix: ensure the request type has a route macro (#[get]/#[post]/...) on its IRequest impl\n",
77        );
78        for (method, path, req) in &missing_dispatch {
79            message.push_str(&format!("  - {method} {path} ({req})\n"));
80        }
81    }
82    message.push_str("\nRun: cargo run -p <host-crate> -- --doctor");
83    Err(webx_core::error::Error::Di(message))
84}
85
86/// Log route table and warn on orphan routes/handlers.
87pub fn log_startup_diagnostics() {
88    let routes = route_snapshots();
89    tracing::info!(count = routes.len(), "Registered HTTP routes");
90    for route in &routes {
91        if route.has_handler {
92            tracing::debug!(
93                method = %route.method,
94                path = route.path,
95                request = route.request_type,
96                response = route.response_type,
97                "route"
98            );
99        } else {
100            tracing::warn!(
101                method = %route.method,
102                path = route.path,
103                request = route.request_type,
104                "route has no #[handler] registration — add #[handler] on IRequestHandler impl"
105            );
106        }
107    }
108
109    for handler in orphan_handlers() {
110        tracing::warn!(
111            request = handler,
112            "handler has no matching route — add #[get]/#[post] on IRequest impl or remove #[handler]"
113        );
114    }
115
116    for (handler, count) in duplicate_handlers() {
117        tracing::warn!(
118            request = handler,
119            count,
120            "duplicate #[handler] registration; last inventory entry wins"
121        );
122    }
123}