rust_webx_core/handler.rs
1//! Handler traits: IRequestHandler and IEventHandler.
2//!
3//! ## IRequestHandler<T, R>
4//!
5//! Dual-type-parameter handler: `T` is the request type, `R` is the response type.
6//! The constraint `T: IRequest<R>` ensures type safety between request and response.
7//!
8//! `handle` takes `&mut self` so handlers that own a `DbContext` (resolved via
9//! `get_owned` — EFCore-style per-request unit-of-work) can call `ctx.set::<T>()`
10//! and `ctx.save_changes()` which require `&mut self`.
11//!
12//! ```ignore
13//! #[async_trait]
14//! impl IRequestHandler<GetUserRequest, UserModel> for GetUserHandler {
15//! async fn handle(&mut self, req: GetUserRequest) -> Result<UserModel> { ... }
16//! }
17//! ```
18
19use crate::auth::IClaims;
20use crate::error::Result;
21use crate::mediator::{IEventRequest, IRequest};
22
23/// Handles a single `IRequest<R>`, producing its associated response `R`.
24///
25/// Authentication claims are NOT passed as a method parameter — this trait stays
26/// free of auth concerns. Instead, requests that need claims implement the
27/// inherent `set_claims` method (see `IClaimsCarrier`); the dispatcher injects
28/// claims into the request *before* calling `handle`.
29///
30/// Register via `#[handler]` proc macro for compile-time collection,
31/// or use `register_handlers!` for manual DI registration.
32///
33/// `handle(&mut self, ...)` enables the EFCore-style owned-DbContext pattern:
34/// handlers declare a bare `ctx: DbContext` field, resolved per-request via
35/// `IServiceResolver::get_owned`, and mutate it directly without `Arc<Mutex>`.
36///
37/// ```ignore
38/// #[async_trait]
39/// impl IRequestHandler<GetUserRequest, UserModel> for GetUserHandler {
40/// async fn handle(&mut self, req: GetUserRequest) -> Result<UserModel> { ... }
41/// }
42/// ```
43#[async_trait::async_trait]
44pub trait IRequestHandler<T, R>: Send + Sync
45where
46 T: IRequest<R> + Send + 'static,
47 R: serde::Serialize + Send + 'static,
48{
49 /// Handle the request. Claims (if any) are already in `req` via `set_claims`.
50 async fn handle(&mut self, req: T) -> Result<R>;
51}
52
53/// Blanket trait that enables claims injection on request structs.
54///
55/// The default implementation is a **no-op**, so every `T: Send` satisfies it
56/// without any boilerplate. Requests that actually carry claims shadow the
57/// trait method with an **inherent** `set_claims(&mut self, …)` method; Rust's
58/// method resolution picks the inherent method over the trait default at
59/// compile time, with zero runtime cost.
60///
61/// # Why not specialization?
62///
63/// Stable Rust has no specialization. The inherent-method-shadows-trait-default
64/// pattern achieves the same "override per-type" behavior without nightly
65/// features.
66///
67/// # Usage in contracts
68///
69/// Use the `#[claims]` attribute macro to automatically inject the `claims`
70/// field and generate the inherent `set_claims` method:
71///
72/// ```ignore
73/// use rust_webx::*;
74/// use serde::Deserialize;
75///
76/// #[claims]
77/// #[derive(Default, Deserialize)]
78/// pub struct CreateBlogPostRequest {
79/// pub slug: String,
80/// // ...
81/// }
82/// ```
83///
84/// The macro expands to (conceptually):
85///
86/// ```ignore
87/// pub struct CreateBlogPostRequest {
88/// pub slug: String,
89/// // ...
90/// #[serde(skip)]
91/// pub claims: Option<Box<dyn IClaims>>,
92/// }
93///
94/// impl CreateBlogPostRequest {
95/// pub fn set_claims(&mut self, claims: Option<Box<dyn IClaims>>) {
96/// self.claims = claims;
97/// }
98/// }
99/// ```
100///
101/// # Usage in handlers
102///
103/// ```ignore
104/// async fn handle(&mut self, req: CreateBlogPostRequest) -> Result<BlogPostModel> {
105/// let uid = req.claims.as_ref()
106/// .and_then(|c| c.subject().parse().ok())
107/// .ok_or(Error::Unauthorized)?;
108/// // ...
109/// }
110/// ```
111pub trait IClaimsCarrier: Send {
112 /// Default no-op. Overridden by inherent `set_claims` on types that carry claims.
113 fn set_claims(&mut self, _claims: Option<Box<dyn IClaims>>) {}
114}
115
116/// Blanket no-op implementation — every `Send` type is a carrier by default.
117impl<T: Send> IClaimsCarrier for T {}
118
119/// Handles a single `IEventRequest`, performing side effects.
120///
121/// ```ignore
122/// #[async_trait]
123/// impl IEventHandler<UserCreatedEvent> for SendWelcomeEmailHandler {
124/// async fn handle(&self, event: UserCreatedEvent) -> Result<()> { ... }
125/// }
126/// ```
127#[async_trait::async_trait]
128pub trait IEventHandler<T: IEventRequest>: Send + Sync {
129 async fn handle(&self, event: T) -> Result<()>;
130}
131
132/// Background service that is started when the host starts and
133/// stopped when the host performs a graceful shutdown.
134///
135/// Analogous to ASP.NET Core's IHostedService.
136///
137/// Use this for:
138/// - Data initialization / seeding at application startup
139/// - Background polling loops
140/// - Queue consumers
141/// - Connection pool warmup
142///
143/// # Example
144///
145/// ```ignore
146/// #[derive(Default)]
147/// struct DbInitService;
148///
149/// #[async_trait]
150/// impl IHostedService for DbInitService {
151/// async fn start(&self) -> Result<()> {
152/// tracing::info!("[DbInitService] Running migrations...");
153/// run_migrations().await?;
154/// tracing::info!("[DbInitService] Seeding data...");
155/// seed_data().await?;
156/// Ok(())
157/// }
158///
159/// async fn stop(&self) -> Result<()> {
160/// tracing::info!("[DbInitService] Shutting down...");
161/// Ok(())
162/// }
163/// }
164/// ```
165#[async_trait::async_trait]
166pub trait IHostedService: Send + Sync {
167 /// Called when the host starts.
168 ///
169 /// The host waits for all hosted services to finish `start()`
170 /// before beginning to accept incoming requests.
171 async fn start(&self) -> Result<()>;
172
173 /// Called during a graceful shutdown.
174 ///
175 /// The host calls `stop()` on all hosted services concurrently
176 /// after the HTTP server has stopped accepting new connections.
177 async fn stop(&self) -> Result<()> {
178 Ok(())
179 }
180}