Skip to main content

lrwf_core/di/
scan.rs

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