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> = 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
80pub 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}