Skip to main content

rust_webx_core/route/
diagnostics.rs

1//! Compile-time route and handler diagnostics.
2
3use std::collections::HashSet;
4
5use crate::route::scan::{HandlerCache, HandlerRegistration, RouteEntry};
6
7/// Snapshot of a registered HTTP route.
8#[derive(Debug, Clone)]
9pub struct RouteSnapshot {
10    pub method: String,
11    pub path: &'static str,
12    pub request_type: &'static str,
13    pub response_type: &'static str,
14    pub has_handler: bool,
15}
16
17/// Collect all inventory routes and whether a matching `#[handler]` exists.
18pub fn route_snapshots() -> Vec<RouteSnapshot> {
19    let cache = HandlerCache::build();
20    let mut routes: Vec<RouteSnapshot> = inventory::iter::<RouteEntry>()
21        .map(|entry| RouteSnapshot {
22            method: entry.method.as_str().to_string(),
23            path: entry.path,
24            request_type: entry.handler_type,
25            response_type: entry.rsp_type,
26            has_handler: cache.get(entry.handler_type).is_some(),
27        })
28        .collect();
29    routes.sort_by(|a, b| a.path.cmp(b.path).then(a.method.cmp(&b.method)));
30    routes
31}
32
33/// Request types with a route but no registered handler.
34pub fn orphan_routes() -> Vec<&'static str> {
35    route_snapshots()
36        .into_iter()
37        .filter(|r| !r.has_handler)
38        .map(|r| r.request_type)
39        .collect()
40}
41
42/// Routes with inventory entries but no matching `#[handler]` (includes path/method).
43pub fn orphan_route_details() -> Vec<RouteSnapshot> {
44    route_snapshots()
45        .into_iter()
46        .filter(|r| !r.has_handler)
47        .collect()
48}
49
50/// Request types registered via `#[handler]` without a matching route.
51pub fn orphan_handlers() -> Vec<&'static str> {
52    let routed: HashSet<&'static str> = inventory::iter::<RouteEntry>()
53        .map(|e| e.handler_type)
54        .collect();
55
56    let mut orphans = Vec::new();
57    for reg in inventory::iter::<HandlerRegistration>() {
58        if !routed.contains(reg.req_type_name) {
59            orphans.push(reg.req_type_name);
60        }
61    }
62    orphans.sort_unstable();
63    orphans
64}
65
66/// Request types with more than one `#[handler]` registration (last wins at runtime).
67pub fn duplicate_handlers() -> Vec<(&'static str, usize)> {
68    let mut counts: std::collections::HashMap<&'static str, usize> =
69        std::collections::HashMap::new();
70    for reg in inventory::iter::<HandlerRegistration>() {
71        *counts.entry(reg.req_type_name).or_default() += 1;
72    }
73    let mut dupes: Vec<_> = counts.into_iter().filter(|(_, count)| *count > 1).collect();
74    dupes.sort_by_key(|(name, _)| *name);
75    dupes
76}
77
78/// Human-readable route/handler diagnostic report (no tracing dependency).
79pub fn format_route_diagnostics() -> String {
80    use std::fmt::Write;
81
82    let routes = route_snapshots();
83    let mut out = format!("Registered HTTP routes: {}\n", routes.len());
84
85    for route in &routes {
86        let status = if route.has_handler { "ok" } else { "ORPHAN" };
87        let _ = writeln!(
88            out,
89            "  [{status}] {} {}  {} => {}",
90            route.method, route.path, route.request_type, route.response_type
91        );
92    }
93
94    let orphan_route_rows = orphan_route_details();
95    if !orphan_route_rows.is_empty() {
96        let _ = writeln!(
97            out,
98            "\nRoutes without #[handler] ({}):",
99            orphan_route_rows.len()
100        );
101        let _ = writeln!(
102            out,
103            "  Fix: add #[handler] or #[handler(inject)] impl IRequestHandler<Request, Response>"
104        );
105        for route in &orphan_route_rows {
106            let _ = writeln!(
107                out,
108                "  - {} {} ({})",
109                route.method, route.path, route.request_type
110            );
111        }
112    }
113
114    let missing_routes = orphan_handlers();
115    if !missing_routes.is_empty() {
116        let _ = writeln!(out, "\nHandlers without route ({}):", missing_routes.len());
117        let _ = writeln!(
118            out,
119            "  Fix: add #[get]/#[post]/... impl IRequest<R> for the request type, or remove the orphan #[handler]"
120        );
121        for req in &missing_routes {
122            let _ = writeln!(out, "  - {req}");
123        }
124    }
125
126    let dupes = duplicate_handlers();
127    if !dupes.is_empty() {
128        let _ = writeln!(
129            out,
130            "\nDuplicate #[handler] registrations ({}); last inventory entry wins:",
131            dupes.len()
132        );
133        let _ = writeln!(
134            out,
135            "  Fix: keep one #[handler] per request type; remove duplicate impl blocks"
136        );
137        for (req, count) in &dupes {
138            let _ = writeln!(out, "  - {req} ({count} handlers)");
139        }
140    }
141
142    if orphan_route_rows.is_empty() && missing_routes.is_empty() && dupes.is_empty() {
143        let _ = writeln!(
144            out,
145            "\nRoute table OK — every route has a handler and every handler has a route."
146        );
147    } else {
148        let _ = writeln!(
149            out,
150            "\nStartup will panic on orphan routes/handlers. Run: cargo run -p <host-crate> -- --doctor"
151        );
152    }
153
154    out
155}