Skip to main content

rust_webx_host/
diagnostics.rs

1//! Route/handler startup diagnostics (uses tracing — host layer only).
2
3use rust_webx_core::route::diagnostics::{
4    duplicate_handlers, orphan_handlers, orphan_route_details, route_snapshots,
5};
6use rust_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
14pub fn assert_route_configuration_valid() {
15    let orphan_route_rows = orphan_route_details();
16    let missing_routes = orphan_handlers();
17
18    let mut missing_dispatch: Vec<(&'static str, &'static str, &'static str)> = Vec::new();
19    let cache = rust_webx_core::route::scan::HandlerCache::build();
20    let dispatch_types: std::collections::HashSet<&'static str> = inventory::iter::<RouteDispatch>()
21        .map(|d| d.handler_type)
22        .collect();
23
24    for entry in inventory::iter::<RouteEntry> {
25        if cache.get(entry.handler_type).is_some() && !dispatch_types.contains(entry.handler_type) {
26            missing_dispatch.push((
27                entry.method.as_str(),
28                entry.path,
29                entry.handler_type,
30            ));
31        }
32    }
33
34    if orphan_route_rows.is_empty() && missing_routes.is_empty() && missing_dispatch.is_empty() {
35        return;
36    }
37
38    let mut message = String::from("Route configuration errors:\n");
39    if !orphan_route_rows.is_empty() {
40        message.push_str("\nRoutes without #[handler]:\n");
41        message.push_str(
42            "  Fix: add #[handler] or #[handler(inject)] impl IRequestHandler<Request, Response>\n",
43        );
44        for route in &orphan_route_rows {
45            message.push_str(&format!(
46                "  - {} {} ({})\n",
47                route.method, route.path, route.request_type
48            ));
49        }
50    }
51    if !missing_routes.is_empty() {
52        message.push_str("\nHandlers without route:\n");
53        message.push_str(
54            "  Fix: add #[get]/#[post]/... impl IRequest<R> for the request type, or remove the orphan #[handler]\n",
55        );
56        for req in &missing_routes {
57            message.push_str(&format!("  - {req}\n"));
58        }
59    }
60    if !missing_dispatch.is_empty() {
61        message.push_str("\nRoutes with handler but no RouteDispatch:\n");
62        message.push_str(
63            "  Fix: ensure the request type has a route macro (#[get]/#[post]/...) on its IRequest impl\n",
64        );
65        for (method, path, req) in &missing_dispatch {
66            message.push_str(&format!("  - {method} {path} ({req})\n"));
67        }
68    }
69    message.push_str("\nRun: cargo run -p <host-crate> -- --doctor");
70    panic!("{message}");
71}
72
73/// Log route table and warn on orphan routes/handlers.
74pub fn log_startup_diagnostics() {
75    let routes = route_snapshots();
76    tracing::info!(count = routes.len(), "Registered HTTP routes");
77    for route in &routes {
78        if route.has_handler {
79            tracing::debug!(
80                method = %route.method,
81                path = route.path,
82                request = route.request_type,
83                response = route.response_type,
84                "route"
85            );
86        } else {
87            tracing::warn!(
88                method = %route.method,
89                path = route.path,
90                request = route.request_type,
91                "route has no #[handler] registration — add #[handler] on IRequestHandler impl"
92            );
93        }
94    }
95
96    for handler in orphan_handlers() {
97        tracing::warn!(
98            request = handler,
99            "handler has no matching route — add #[get]/#[post] on IRequest impl or remove #[handler]"
100        );
101    }
102
103    for (handler, count) in duplicate_handlers() {
104        tracing::warn!(
105            request = handler,
106            count,
107            "duplicate #[handler] registration; last inventory entry wins"
108        );
109    }
110}