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