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> = std::collections::HashMap::new();
69    for reg in inventory::iter::<HandlerRegistration>() {
70        *counts.entry(reg.req_type_name).or_default() += 1;
71    }
72    let mut dupes: Vec<_> = counts
73        .into_iter()
74        .filter(|(_, count)| *count > 1)
75        .collect();
76    dupes.sort_by_key(|(name, _)| *name);
77    dupes
78}
79
80/// Human-readable route/handler diagnostic report (no tracing dependency).
81pub fn format_route_diagnostics() -> String {
82    use std::fmt::Write;
83
84    let routes = route_snapshots();
85    let mut out = format!("Registered HTTP routes: {}\n", routes.len());
86
87    for route in &routes {
88        let status = if route.has_handler { "ok" } else { "ORPHAN" };
89        let _ = writeln!(
90            out,
91            "  [{status}] {} {}  {} => {}",
92            route.method, route.path, route.request_type, route.response_type
93        );
94    }
95
96    let orphan_route_rows = orphan_route_details();
97    if !orphan_route_rows.is_empty() {
98        let _ = writeln!(
99            out,
100            "\nRoutes without #[handler] ({}):",
101            orphan_route_rows.len()
102        );
103        let _ = writeln!(
104            out,
105            "  Fix: add #[handler] or #[handler(inject)] impl IRequestHandler<Request, Response>"
106        );
107        for route in &orphan_route_rows {
108            let _ = writeln!(
109                out,
110                "  - {} {} ({})",
111                route.method, route.path, route.request_type
112            );
113        }
114    }
115
116    let missing_routes = orphan_handlers();
117    if !missing_routes.is_empty() {
118        let _ = writeln!(
119            out,
120            "\nHandlers without route ({}):",
121            missing_routes.len()
122        );
123        let _ = writeln!(
124            out,
125            "  Fix: add #[get]/#[post]/... impl IRequest<R> for the request type, or remove the orphan #[handler]"
126        );
127        for req in &missing_routes {
128            let _ = writeln!(out, "  - {req}");
129        }
130    }
131
132    let dupes = duplicate_handlers();
133    if !dupes.is_empty() {
134        let _ = writeln!(
135            out,
136            "\nDuplicate #[handler] registrations ({}); last inventory entry wins:",
137            dupes.len()
138        );
139        let _ = writeln!(
140            out,
141            "  Fix: keep one #[handler] per request type; remove duplicate impl blocks"
142        );
143        for (req, count) in &dupes {
144            let _ = writeln!(out, "  - {req} ({count} handlers)");
145        }
146    }
147
148    if orphan_route_rows.is_empty() && missing_routes.is_empty() && dupes.is_empty() {
149        let _ = writeln!(out, "\nRoute table OK — every route has a handler and every handler has a route.");
150    } else {
151        let _ = writeln!(
152            out,
153            "\nStartup will panic on orphan routes/handlers. Run: cargo run -p <host-crate> -- --doctor"
154        );
155    }
156
157    out
158}