Skip to main content

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, RwLock};
15
16// --- Deprecated process-wide fallback (prefer [`DispatchRuntime`] on `Host`) ---
17
18static GLOBAL_PROVIDER: RwLock<Option<Arc<rust_dix::ServiceProvider>>> = RwLock::new(None);
19
20/// Deprecated shim: set a process-wide `ServiceProvider` fallback.
21///
22/// `Host::build()` no longer calls this. Prefer `Host::provider()` or run work
23/// inside `Host::dispatch_runtime().run()` so `dispatch_provider()` resolves
24/// from the active runtime.
25#[deprecated(
26    since = "0.2.0",
27    note = "Use Host::provider() or dispatch_provider() within an active DispatchRuntime"
28)]
29pub fn set_global_provider(provider: Arc<rust_dix::ServiceProvider>) {
30    let mut guard = GLOBAL_PROVIDER.write().expect("global provider lock poisoned");
31    if guard.is_some() {
32        tracing::warn!(
33            "Replacing deprecated global ServiceProvider. \
34             Prefer Host::dispatch_runtime() for per-host isolation."
35        );
36    }
37    *guard = Some(provider);
38}
39
40/// Deprecated shim: read the process-wide `ServiceProvider` fallback.
41///
42/// Panics when no shim was set and no [`DispatchRuntime`] is active on this task.
43#[deprecated(
44    since = "0.2.0",
45    note = "Use Host::provider() or dispatch_provider() within an active DispatchRuntime"
46)]
47pub fn global_provider() -> Arc<rust_dix::ServiceProvider> {
48    try_global_provider().unwrap_or_else(|| {
49        panic!(
50            "global_provider() is deprecated and not set. \
51             Use host.provider() for the host instance, or dispatch_provider() \
52             inside Host::dispatch_runtime().run() (HTTP and hosted services are scoped automatically)."
53        )
54    })
55}
56
57/// Internal fallback for `dispatch_provider()` when no task-local runtime is active.
58pub(crate) fn try_global_provider() -> Option<Arc<rust_dix::ServiceProvider>> {
59    GLOBAL_PROVIDER
60        .read()
61        .expect("global provider lock poisoned")
62        .clone()
63}
64
65/// Metadata about a request parameter for OpenAPI generation.
66#[derive(Debug, Clone)]
67pub struct ParamMeta {
68    /// Field name (e.g., "id", "body").
69    pub name: &'static str,
70
71    /// Parameter location: "path", "query", or "body".
72    pub source: &'static str,
73
74    /// Type hint for OpenAPI schema (e.g., "string", "integer", "object").
75    pub type_hint: &'static str,
76}
77
78/// A route entry registered at compile time via `#[endpoint]` (or its shortcuts `#[get]`/`#[post]`/...).
79///
80/// Collected by the `inventory` crate and read at application startup.
81#[derive(Debug, Clone)]
82pub struct RouteEntry {
83    /// HTTP method for this route.
84    pub method: HttpMethod,
85
86    /// Route path pattern (e.g., "/users/{id}").
87    pub path: &'static str,
88
89    /// Type name of the request handler.
90    /// Used to dispatch to the correct handler at runtime.
91    pub handler_type: &'static str,
92
93    /// OpenAPI response type name (e.g., "UserModel", "Vec<UserModel>", "String").
94    pub rsp_type: &'static str,
95
96    /// Human-readable summary for OpenAPI docs (e.g., "Get user by ID").
97    pub summary: &'static str,
98
99    /// Optional long-form description from `///` doc comments on the impl block.
100    pub description: &'static str,
101
102    /// OpenAPI parameter metadata: path params, body params, etc.
103    pub params: &'static [ParamMeta],
104
105    /// "" = public, "authenticated" = any valid JWT, otherwise specific role name.
106    pub required_role: &'static str,
107
108    /// Non-empty when `#[authorize(permission = "...")]` is declared.
109    pub required_permission: &'static str,
110}
111
112// Collect RouteEntry instances at compile time using `inventory`.
113inventory::collect!(RouteEntry);
114
115/// Optional query/path/body metadata for a request type, collected via
116/// `#[derive(WebxRequestMeta)]` on the request struct.
117#[derive(Debug, Clone)]
118pub struct RequestParamEntry {
119    pub request_type: &'static str,
120    pub params: &'static [ParamMeta],
121}
122
123inventory::collect!(RequestParamEntry);
124
125/// Handler registration collected at compile time.
126/// Each `#[handler]` annotation submits one of these to inventory.
127///
128/// The factory is called **per request** with the request-scoped `IServiceResolver`
129/// so it can resolve Scoped dependencies (e.g. `DbContext`) via `get_owned`.
130/// The result is an owned, type-erased handler — no `Arc<dyn Trait>` caching,
131/// so `handle(&mut self)` works without interior mutability.
132pub struct HandlerRegistration {
133    /// Compile-time type id of the request type — primary dispatch key.
134    pub req_type_id: std::any::TypeId,
135    /// Request type name (e.g., "HelloRequest") — used for diagnostics and route matching.
136    pub req_type_name: &'static str,
137    /// Constructs a fresh owned handler, boxed as `Box<dyn Any + Send>`.
138    /// Called per-request with the per-request scope as resolver.
139    pub factory: fn(
140        &dyn rust_dix::IServiceResolver,
141    ) -> crate::error::Result<Box<dyn std::any::Any + Send>>,
142    /// Type-erased call bridge: receives the owned handler and the boxed request,
143    /// invokes `handle(&mut self, req)`, and returns the boxed response.
144    #[allow(clippy::type_complexity)]
145    pub call: fn(
146        handler: Box<dyn std::any::Any + Send>,
147        request: Box<dyn std::any::Any + Send>,
148    ) -> std::pin::Pin<
149        Box<dyn std::future::Future<Output = crate::error::Result<Box<dyn std::any::Any + Send>>> + Send>,
150    >,
151}
152
153inventory::collect!(HandlerRegistration);
154
155/// A dispatch function registered at compile time via the endpoint macros.
156///
157/// Each `#[get]`, `#[post]`, etc. macro generates one of these with a
158/// function that constructs the request, looks up the handler, and
159/// calls through the HandlerCache call bridge.
160pub struct RouteDispatch {
161    pub handler_type: &'static str,
162    #[allow(clippy::type_complexity)]
163    pub dispatch: fn(
164        body_bytes: Vec<u8>,
165        route_params: std::collections::HashMap<String, String>,
166        query_params: std::collections::HashMap<String, String>,
167        claims: Option<Box<dyn crate::auth::IClaims>>,
168    ) -> std::pin::Pin<
169        Box<dyn std::future::Future<Output = crate::error::Result<ResponseData>> + Send>,
170    >,
171}
172
173/// Response data produced by a dispatch function.
174#[derive(Debug)]
175pub struct ResponseData {
176    pub status: u16,
177    pub content_type: String,
178    pub body: Vec<u8>,
179}
180
181inventory::collect!(RouteDispatch);
182
183impl RouteEntry {
184    /// Create a new route entry.
185    #[allow(clippy::too_many_arguments)]
186    pub const fn new(
187        method: HttpMethod,
188        path: &'static str,
189        handler_type: &'static str,
190        rsp_type: &'static str,
191        summary: &'static str,
192        description: &'static str,
193        params: &'static [ParamMeta],
194        required_role: &'static str,
195        required_permission: &'static str,
196    ) -> Self {
197        Self {
198            method,
199            path,
200            handler_type,
201            rsp_type,
202            summary,
203            description,
204            params,
205            required_role,
206            required_permission,
207        }
208    }
209}
210
211// --- Handler Cache ---
212
213/// Runtime registry of compiled handler registrations.
214///
215/// Built at startup from `HandlerRegistration` inventory items.
216/// Maps request type name to handler entry (factory + call bridge).
217///
218/// Unlike the previous design, entries do **not** cache a handler instance:
219/// the factory is invoked per request with the request-scoped resolver so
220/// Scoped dependencies (DbContext) are freshly owned each time.
221pub struct HandlerCache {
222    pub entries: HashMap<&'static str, HandlerEntry>,
223    pub entries_by_id: HashMap<std::any::TypeId, HandlerEntry>,
224}
225
226/// Deprecated process-wide handler registry fallback (inventory is process-wide).
227static HANDLER_CACHE: OnceLock<Arc<HandlerCache>> = OnceLock::new();
228
229impl HandlerCache {
230    /// Build the cache from all `HandlerRegistration` inventory items.
231    pub fn build() -> Self {
232        let mut entries = HashMap::new();
233        let mut entries_by_id = HashMap::new();
234        for reg in inventory::iter::<HandlerRegistration> {
235            let entry = HandlerEntry {
236                factory: reg.factory,
237                call: reg.call,
238            };
239            entries.insert(reg.req_type_name, entry.clone());
240            entries_by_id.insert(reg.req_type_id, entry);
241        }
242        Self {
243            entries,
244            entries_by_id,
245        }
246    }
247
248    /// Deprecated shim: lazy-build the process-wide handler registry.
249    ///
250    /// `Host::build()` uses `HandlerCache::build()` per instance. This remains only
251    /// as a fallback for in-process `Mediator::send` without an active runtime.
252    #[deprecated(
253        since = "0.2.0",
254        note = "Prefer dispatch_handler_cache() within an active DispatchRuntime"
255    )]
256    pub fn get_or_init() -> Arc<Self> {
257        Arc::clone(HANDLER_CACHE.get_or_init(|| Arc::new(Self::build())))
258    }
259
260    /// Look up a handler entry by request type name (route / OpenAPI diagnostics).
261    pub fn get(&self, req_type_name: &str) -> Option<&HandlerEntry> {
262        self.entries.get(req_type_name)
263    }
264
265    /// Look up a handler entry by request type id (Mediator / in-process dispatch).
266    pub fn get_by_type_id(&self, type_id: std::any::TypeId) -> Option<&HandlerEntry> {
267        self.entries_by_id.get(&type_id)
268    }
269}
270
271/// Preferred name for [`HandlerCache`] — emphasizes registry semantics over caching.
272pub type HandlerRegistry = HandlerCache;
273
274/// A single compiled handler entry in the registry.
275///
276/// Stores only the factory and call bridge — no cached instance.
277/// The factory is called per request to produce a fresh owned handler.
278#[derive(Clone)]
279pub struct HandlerEntry {
280    /// Per-request factory: receives the scoped resolver, returns an owned handler.
281    pub factory: fn(
282        &dyn rust_dix::IServiceResolver,
283    ) -> crate::error::Result<Box<dyn Any + Send>>,
284    /// Type-erased call bridge: invokes `handle(&mut self, req)` on the owned handler.
285    #[allow(clippy::type_complexity)]
286    pub call: fn(
287        handler: Box<dyn Any + Send>,
288        request: Box<dyn Any + Send>,
289    ) -> std::pin::Pin<
290        Box<dyn std::future::Future<Output = crate::error::Result<Box<dyn Any + Send>>> + Send>,
291    >,
292}