rust_webx_core/route/
diagnostics.rs1use std::collections::HashSet;
4
5use crate::route::scan::{HandlerCache, HandlerRegistration, RouteEntry};
6
7#[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
17pub 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
33pub 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
42pub fn orphan_route_details() -> Vec<RouteSnapshot> {
44 route_snapshots()
45 .into_iter()
46 .filter(|r| !r.has_handler)
47 .collect()
48}
49
50pub 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
66pub 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
78pub 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}