rust_webx_core/route/scan.rs
1//! Type scanning and automatic service registration logic.
2//!
3//! In the rust-webx framework, "scanning" is achieved at compile time via:
4//!
5//! 1. `#[rust_dix::module]` + `rust_dix::inject!` — declare handlers in a module group
6//! 2. `#[endpoint]` (or shortcuts `#[get]`, `#[post]`, `#[put]`, `#[delete]`) — register route metadata via `inventory::submit!`
7//!
8//! This module provides the `RouteEntry` type that connects compile-time
9//! macro output to runtime routing.
10
11use crate::routing::HttpMethod;
12use std::any::Any;
13use std::collections::HashMap;
14use std::sync::{Arc, OnceLock};
15
16// --- Global Service Provider (for DI-based handler construction) ---
17
18static GLOBAL_PROVIDER: OnceLock<Arc<rust_dix::ServiceProvider>> = OnceLock::new();
19
20/// Set the global service provider. Called once at `Host::build()` time.
21pub fn set_global_provider(provider: Arc<rust_dix::ServiceProvider>) {
22 GLOBAL_PROVIDER.set(provider).ok();
23}
24
25/// Get the global service provider. Used by `#[handler]` factories
26/// when the handler struct has `#[inject_attr]` for DI-based construction.
27pub fn global_provider() -> &'static Arc<rust_dix::ServiceProvider> {
28 GLOBAL_PROVIDER
29 .get()
30 .expect("Global provider not initialized")
31}
32
33/// Metadata about a request parameter for OpenAPI generation.
34#[derive(Debug, Clone)]
35pub struct ParamMeta {
36 /// Field name (e.g., "id", "body").
37 pub name: &'static str,
38
39 /// Parameter location: "path", "query", or "body".
40 pub source: &'static str,
41
42 /// Type hint for OpenAPI schema (e.g., "string", "integer", "object").
43 pub type_hint: &'static str,
44}
45
46/// A route entry registered at compile time via `#[endpoint]` (or its shortcuts `#[get]`/`#[post]`/...).
47///
48/// Collected by the `inventory` crate and read at application startup.
49#[derive(Debug, Clone)]
50pub struct RouteEntry {
51 /// HTTP method for this route.
52 pub method: HttpMethod,
53
54 /// Route path pattern (e.g., "/users/{id}").
55 pub path: &'static str,
56
57 /// Type name of the request handler.
58 /// Used to dispatch to the correct handler at runtime.
59 pub handler_type: &'static str,
60
61 /// OpenAPI response type name (e.g., "UserModel", "Vec<UserModel>", "String").
62 pub rsp_type: &'static str,
63
64 /// Human-readable summary for OpenAPI docs (e.g., "Get user by ID").
65 pub summary: &'static str,
66
67 /// Optional long-form description from `///` doc comments on the impl block.
68 pub description: &'static str,
69
70 /// OpenAPI parameter metadata: path params, body params, etc.
71 pub params: &'static [ParamMeta],
72
73 /// "" = public, "authenticated" = any valid JWT, otherwise specific role name.
74 pub required_role: &'static str,
75}
76
77// Collect RouteEntry instances at compile time using `inventory`.
78inventory::collect!(RouteEntry);
79
80/// Handler registration collected at compile time.
81/// Each `#[handler]` annotation submits one of these to inventory.
82///
83/// The factory is called **per request** with the request-scoped `IServiceResolver`
84/// so it can resolve Scoped dependencies (e.g. `DbContext`) via `get_owned`.
85/// The result is an owned, type-erased handler — no `Arc<dyn Trait>` caching,
86/// so `handle(&mut self)` works without interior mutability.
87pub struct HandlerRegistration {
88 /// Compile-time type id of the request type — primary dispatch key.
89 pub req_type_id: std::any::TypeId,
90 /// Request type name (e.g., "HelloRequest") — used for diagnostics and route matching.
91 pub req_type_name: &'static str,
92 /// Constructs a fresh owned handler, boxed as `Box<dyn Any + Send>`.
93 /// Called per-request with the per-request scope as resolver.
94 pub factory: fn(&dyn rust_dix::IServiceResolver) -> Box<dyn std::any::Any + Send>,
95 /// Type-erased call bridge: receives the owned handler and the boxed request,
96 /// invokes `handle(&mut self, req)`, and returns the boxed response.
97 #[allow(clippy::type_complexity)]
98 pub call: fn(
99 handler: Box<dyn std::any::Any + Send>,
100 request: Box<dyn std::any::Any + Send>,
101 ) -> std::pin::Pin<
102 Box<dyn std::future::Future<Output = crate::error::Result<Box<dyn std::any::Any + Send>>> + Send>,
103 >,
104}
105
106inventory::collect!(HandlerRegistration);
107
108/// A dispatch function registered at compile time via the endpoint macros.
109///
110/// Each `#[get]`, `#[post]`, etc. macro generates one of these with a
111/// function that constructs the request, looks up the handler, and
112/// calls through the HandlerCache call bridge.
113pub struct RouteDispatch {
114 pub handler_type: &'static str,
115 #[allow(clippy::type_complexity)]
116 pub dispatch: fn(
117 body_bytes: Vec<u8>,
118 route_params: std::collections::HashMap<String, String>,
119 query_params: std::collections::HashMap<String, String>,
120 claims: Option<Box<dyn crate::auth::IClaims>>,
121 ) -> std::pin::Pin<
122 Box<dyn std::future::Future<Output = crate::error::Result<ResponseData>> + Send>,
123 >,
124}
125
126/// Response data produced by a dispatch function.
127#[derive(Debug)]
128pub struct ResponseData {
129 pub status: u16,
130 pub content_type: String,
131 pub body: Vec<u8>,
132}
133
134inventory::collect!(RouteDispatch);
135
136// SAFETY: RouteDispatch is only used at startup, single-threaded context.
137unsafe impl Sync for RouteDispatch {}
138unsafe impl Send for RouteDispatch {}
139
140impl RouteEntry {
141 /// Create a new route entry.
142 #[allow(clippy::too_many_arguments)]
143 pub const fn new(
144 method: HttpMethod,
145 path: &'static str,
146 handler_type: &'static str,
147 rsp_type: &'static str,
148 summary: &'static str,
149 description: &'static str,
150 params: &'static [ParamMeta],
151 required_role: &'static str,
152 ) -> Self {
153 Self {
154 method,
155 path,
156 handler_type,
157 rsp_type,
158 summary,
159 description,
160 params,
161 required_role,
162 }
163 }
164}
165
166// --- Handler Cache ---
167
168/// Runtime registry of compiled handler registrations.
169///
170/// Built at startup from `HandlerRegistration` inventory items.
171/// Maps request type name to handler entry (factory + call bridge).
172///
173/// Unlike the previous design, entries do **not** cache a handler instance:
174/// the factory is invoked per request with the request-scoped resolver so
175/// Scoped dependencies (DbContext) are freshly owned each time.
176pub struct HandlerCache {
177 pub entries: HashMap<&'static str, HandlerEntry>,
178 pub entries_by_id: HashMap<std::any::TypeId, HandlerEntry>,
179}
180
181static HANDLER_CACHE: OnceLock<HandlerCache> = OnceLock::new();
182
183impl HandlerCache {
184 /// Build the cache from all `HandlerRegistration` inventory items.
185 pub fn build() -> Self {
186 let mut entries = HashMap::new();
187 let mut entries_by_id = HashMap::new();
188 for reg in inventory::iter::<HandlerRegistration> {
189 let entry = HandlerEntry {
190 factory: reg.factory,
191 call: reg.call,
192 };
193 entries.insert(reg.req_type_name, entry.clone());
194 entries_by_id.insert(reg.req_type_id, entry);
195 }
196 Self {
197 entries,
198 entries_by_id,
199 }
200 }
201
202 /// Initialize the global cache. Called once at host build time.
203 pub fn init_global() {
204 let cache = Self::build();
205 HANDLER_CACHE.set(cache).ok();
206 }
207
208 /// Get a reference to the global handler cache.
209 /// Must be called after `init_global()`.
210 pub fn get_or_init() -> &'static HandlerCache {
211 HANDLER_CACHE.get_or_init(Self::build)
212 }
213
214 /// Look up a handler entry by request type name (route / OpenAPI diagnostics).
215 pub fn get(&self, req_type_name: &str) -> Option<&HandlerEntry> {
216 self.entries.get(req_type_name)
217 }
218
219 /// Look up a handler entry by request type id (Mediator / in-process dispatch).
220 pub fn get_by_type_id(&self, type_id: std::any::TypeId) -> Option<&HandlerEntry> {
221 self.entries_by_id.get(&type_id)
222 }
223}
224
225/// Preferred name for [`HandlerCache`] — emphasizes registry semantics over caching.
226pub type HandlerRegistry = HandlerCache;
227
228/// A single compiled handler entry in the registry.
229///
230/// Stores only the factory and call bridge — no cached instance.
231/// The factory is called per request to produce a fresh owned handler.
232#[derive(Clone)]
233pub struct HandlerEntry {
234 /// Per-request factory: receives the scoped resolver, returns an owned handler.
235 pub factory: fn(&dyn rust_dix::IServiceResolver) -> Box<dyn Any + Send>,
236 /// Type-erased call bridge: invokes `handle(&mut self, req)` on the owned handler.
237 #[allow(clippy::type_complexity)]
238 pub call: fn(
239 handler: Box<dyn Any + Send>,
240 request: Box<dyn Any + Send>,
241 ) -> std::pin::Pin<
242 Box<dyn std::future::Future<Output = crate::error::Result<Box<dyn Any + Send>>> + Send>,
243 >,
244}