spikard_core/
lifecycle.rs

1//! Lifecycle hooks for request/response processing
2//!
3//! Transport-agnostic lifecycle system shared across HTTP, WASM, and future runtimes.
4//! Hooks operate on generic request/response carriers so higher-level crates can
5//! plug in their own types without pulling in server frameworks.
6
7use std::{future::Future, pin::Pin, sync::Arc};
8
9type RequestHookFutureSend<'a, Req, Resp> =
10    Pin<Box<dyn Future<Output = Result<HookResult<Req, Resp>, String>> + Send + 'a>>;
11type ResponseHookFutureSend<'a, Resp> =
12    Pin<Box<dyn Future<Output = Result<HookResult<Resp, Resp>, String>> + Send + 'a>>;
13
14type RequestHookFutureLocal<'a, Req, Resp> = Pin<Box<dyn Future<Output = Result<HookResult<Req, Resp>, String>> + 'a>>;
15type ResponseHookFutureLocal<'a, Resp> = Pin<Box<dyn Future<Output = Result<HookResult<Resp, Resp>, String>> + 'a>>;
16
17/// Result of a lifecycle hook execution
18#[derive(Debug)]
19pub enum HookResult<T, U> {
20    /// Continue to the next phase with the (possibly modified) value
21    Continue(T),
22    /// Short-circuit the request pipeline and return this response immediately
23    ShortCircuit(U),
24}
25
26/// Trait for lifecycle hooks on native targets (Send + Sync, Send futures).
27pub trait NativeLifecycleHook<Req, Resp>: Send + Sync {
28    /// Hook name for debugging and error messages
29    fn name(&self) -> &str;
30
31    /// Execute hook with a request
32    fn execute_request<'a>(&'a self, req: Req) -> RequestHookFutureSend<'a, Req, Resp>;
33
34    /// Execute hook with a response
35    fn execute_response<'a>(&'a self, resp: Resp) -> ResponseHookFutureSend<'a, Resp>;
36}
37
38/// Trait for lifecycle hooks on local (wasm) targets (no Send requirements).
39pub trait LocalLifecycleHook<Req, Resp> {
40    /// Hook name for debugging and error messages
41    fn name(&self) -> &str;
42
43    /// Execute hook with a request
44    fn execute_request<'a>(&'a self, req: Req) -> RequestHookFutureLocal<'a, Req, Resp>;
45
46    /// Execute hook with a response
47    fn execute_response<'a>(&'a self, resp: Resp) -> ResponseHookFutureLocal<'a, Resp>;
48}
49
50#[cfg(target_arch = "wasm32")]
51pub use LocalLifecycleHook as LifecycleHook;
52#[cfg(not(target_arch = "wasm32"))]
53pub use NativeLifecycleHook as LifecycleHook;
54
55/// Target-specific hook alias used by the rest of the codebase.
56#[cfg(not(target_arch = "wasm32"))]
57type CoreHook<Req, Resp> = dyn NativeLifecycleHook<Req, Resp>;
58#[cfg(target_arch = "wasm32")]
59type CoreHook<Req, Resp> = dyn LocalLifecycleHook<Req, Resp>;
60
61/// Target-specific container alias to make downstream imports clearer.
62pub type TargetLifecycleHooks<Req, Resp> = LifecycleHooks<Req, Resp>;
63
64/// Container for all lifecycle hooks
65#[derive(Clone)]
66pub struct LifecycleHooks<Req, Resp> {
67    on_request: Vec<Arc<CoreHook<Req, Resp>>>,
68    pre_validation: Vec<Arc<CoreHook<Req, Resp>>>,
69    pre_handler: Vec<Arc<CoreHook<Req, Resp>>>,
70    on_response: Vec<Arc<CoreHook<Req, Resp>>>,
71    on_error: Vec<Arc<CoreHook<Req, Resp>>>,
72}
73
74impl<Req, Resp> Default for LifecycleHooks<Req, Resp> {
75    fn default() -> Self {
76        Self {
77            on_request: Vec::new(),
78            pre_validation: Vec::new(),
79            pre_handler: Vec::new(),
80            on_response: Vec::new(),
81            on_error: Vec::new(),
82        }
83    }
84}
85
86impl<Req, Resp> std::fmt::Debug for LifecycleHooks<Req, Resp> {
87    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88        f.debug_struct("LifecycleHooks")
89            .field("on_request_count", &self.on_request.len())
90            .field("pre_validation_count", &self.pre_validation.len())
91            .field("pre_handler_count", &self.pre_handler.len())
92            .field("on_response_count", &self.on_response.len())
93            .field("on_error_count", &self.on_error.len())
94            .finish()
95    }
96}
97
98impl<Req, Resp> LifecycleHooks<Req, Resp> {
99    /// Create a new empty hooks container
100    pub fn new() -> Self {
101        Self::default()
102    }
103
104    /// Builder constructor for ergonomic hook registration
105    pub fn builder() -> LifecycleHooksBuilder<Req, Resp> {
106        LifecycleHooksBuilder::new()
107    }
108
109    /// Check if any hooks are registered
110    #[inline(always)]
111    pub fn is_empty(&self) -> bool {
112        self.on_request.is_empty()
113            && self.pre_validation.is_empty()
114            && self.pre_handler.is_empty()
115            && self.on_response.is_empty()
116            && self.on_error.is_empty()
117    }
118
119    pub fn add_on_request(&mut self, hook: Arc<CoreHook<Req, Resp>>) {
120        self.on_request.push(hook);
121    }
122
123    pub fn add_pre_validation(&mut self, hook: Arc<CoreHook<Req, Resp>>) {
124        self.pre_validation.push(hook);
125    }
126
127    pub fn add_pre_handler(&mut self, hook: Arc<CoreHook<Req, Resp>>) {
128        self.pre_handler.push(hook);
129    }
130
131    pub fn add_on_response(&mut self, hook: Arc<CoreHook<Req, Resp>>) {
132        self.on_response.push(hook);
133    }
134
135    pub fn add_on_error(&mut self, hook: Arc<CoreHook<Req, Resp>>) {
136        self.on_error.push(hook);
137    }
138
139    pub async fn execute_on_request(&self, mut req: Req) -> Result<HookResult<Req, Resp>, String> {
140        if self.on_request.is_empty() {
141            return Ok(HookResult::Continue(req));
142        }
143
144        for hook in &self.on_request {
145            match hook.execute_request(req).await? {
146                HookResult::Continue(r) => req = r,
147                HookResult::ShortCircuit(response) => return Ok(HookResult::ShortCircuit(response)),
148            }
149        }
150
151        Ok(HookResult::Continue(req))
152    }
153
154    pub async fn execute_pre_validation(&self, mut req: Req) -> Result<HookResult<Req, Resp>, String> {
155        if self.pre_validation.is_empty() {
156            return Ok(HookResult::Continue(req));
157        }
158
159        for hook in &self.pre_validation {
160            match hook.execute_request(req).await? {
161                HookResult::Continue(r) => req = r,
162                HookResult::ShortCircuit(response) => return Ok(HookResult::ShortCircuit(response)),
163            }
164        }
165
166        Ok(HookResult::Continue(req))
167    }
168
169    pub async fn execute_pre_handler(&self, mut req: Req) -> Result<HookResult<Req, Resp>, String> {
170        if self.pre_handler.is_empty() {
171            return Ok(HookResult::Continue(req));
172        }
173
174        for hook in &self.pre_handler {
175            match hook.execute_request(req).await? {
176                HookResult::Continue(r) => req = r,
177                HookResult::ShortCircuit(response) => return Ok(HookResult::ShortCircuit(response)),
178            }
179        }
180
181        Ok(HookResult::Continue(req))
182    }
183
184    pub async fn execute_on_response(&self, mut resp: Resp) -> Result<Resp, String> {
185        if self.on_response.is_empty() {
186            return Ok(resp);
187        }
188
189        for hook in &self.on_response {
190            match hook.execute_response(resp).await? {
191                HookResult::Continue(r) => resp = r,
192                HookResult::ShortCircuit(r) => resp = r,
193            }
194        }
195
196        Ok(resp)
197    }
198
199    pub async fn execute_on_error(&self, mut resp: Resp) -> Result<Resp, String> {
200        if self.on_error.is_empty() {
201            return Ok(resp);
202        }
203
204        for hook in &self.on_error {
205            match hook.execute_response(resp).await? {
206                HookResult::Continue(r) => resp = r,
207                HookResult::ShortCircuit(r) => resp = r,
208            }
209        }
210
211        Ok(resp)
212    }
213}
214
215/// Helper struct for implementing request hooks from closures
216struct RequestHookFn<F, Req, Resp> {
217    name: String,
218    func: F,
219    _marker: std::marker::PhantomData<fn(Req, Resp)>,
220}
221
222struct ResponseHookFn<F, Req, Resp> {
223    name: String,
224    func: F,
225    _marker: std::marker::PhantomData<fn(Req, Resp)>,
226}
227
228#[cfg(not(target_arch = "wasm32"))]
229impl<F, Fut, Req, Resp> NativeLifecycleHook<Req, Resp> for RequestHookFn<F, Req, Resp>
230where
231    F: Fn(Req) -> Fut + Send + Sync,
232    Fut: Future<Output = Result<HookResult<Req, Resp>, String>> + Send + 'static,
233    Req: Send + 'static,
234    Resp: Send + 'static,
235{
236    fn name(&self) -> &str {
237        &self.name
238    }
239
240    fn execute_request<'a>(&'a self, req: Req) -> RequestHookFutureSend<'a, Req, Resp> {
241        Box::pin((self.func)(req))
242    }
243
244    fn execute_response<'a>(&'a self, _resp: Resp) -> ResponseHookFutureSend<'a, Resp> {
245        Box::pin(async move { Err("Request hook called with response - this is a bug".to_string()) })
246    }
247}
248
249#[cfg(target_arch = "wasm32")]
250impl<F, Fut, Req, Resp> LocalLifecycleHook<Req, Resp> for RequestHookFn<F, Req, Resp>
251where
252    F: Fn(Req) -> Fut + Send + Sync,
253    Fut: Future<Output = Result<HookResult<Req, Resp>, String>> + 'static,
254    Req: 'static,
255    Resp: 'static,
256{
257    fn name(&self) -> &str {
258        &self.name
259    }
260
261    fn execute_request<'a>(&'a self, req: Req) -> RequestHookFutureLocal<'a, Req, Resp> {
262        Box::pin((self.func)(req))
263    }
264
265    fn execute_response<'a>(&'a self, _resp: Resp) -> ResponseHookFutureLocal<'a, Resp> {
266        Box::pin(async move { Err("Request hook called with response - this is a bug".to_string()) })
267    }
268}
269
270#[cfg(not(target_arch = "wasm32"))]
271impl<F, Fut, Req, Resp> NativeLifecycleHook<Req, Resp> for ResponseHookFn<F, Req, Resp>
272where
273    F: Fn(Resp) -> Fut + Send + Sync,
274    Fut: Future<Output = Result<HookResult<Resp, Resp>, String>> + Send + 'static,
275    Req: Send + 'static,
276    Resp: Send + 'static,
277{
278    fn name(&self) -> &str {
279        &self.name
280    }
281
282    fn execute_request<'a>(&'a self, _req: Req) -> RequestHookFutureSend<'a, Req, Resp> {
283        Box::pin(async move { Err("Response hook called with request - this is a bug".to_string()) })
284    }
285
286    fn execute_response<'a>(&'a self, resp: Resp) -> ResponseHookFutureSend<'a, Resp> {
287        Box::pin((self.func)(resp))
288    }
289}
290
291#[cfg(target_arch = "wasm32")]
292impl<F, Fut, Req, Resp> LocalLifecycleHook<Req, Resp> for ResponseHookFn<F, Req, Resp>
293where
294    F: Fn(Resp) -> Fut + Send + Sync,
295    Fut: Future<Output = Result<HookResult<Resp, Resp>, String>> + 'static,
296    Req: 'static,
297    Resp: 'static,
298{
299    fn name(&self) -> &str {
300        &self.name
301    }
302
303    fn execute_request<'a>(&'a self, _req: Req) -> RequestHookFutureLocal<'a, Req, Resp> {
304        Box::pin(async move { Err("Response hook called with request - this is a bug".to_string()) })
305    }
306
307    fn execute_response<'a>(&'a self, resp: Resp) -> ResponseHookFutureLocal<'a, Resp> {
308        Box::pin((self.func)(resp))
309    }
310}
311
312/// Builder Pattern for LifecycleHooks
313pub struct LifecycleHooksBuilder<Req, Resp> {
314    hooks: LifecycleHooks<Req, Resp>,
315}
316
317impl<Req, Resp> LifecycleHooksBuilder<Req, Resp> {
318    pub fn new() -> Self {
319        Self {
320            hooks: LifecycleHooks::default(),
321        }
322    }
323
324    pub fn on_request(mut self, hook: Arc<CoreHook<Req, Resp>>) -> Self {
325        self.hooks.add_on_request(hook);
326        self
327    }
328
329    pub fn pre_validation(mut self, hook: Arc<CoreHook<Req, Resp>>) -> Self {
330        self.hooks.add_pre_validation(hook);
331        self
332    }
333
334    pub fn pre_handler(mut self, hook: Arc<CoreHook<Req, Resp>>) -> Self {
335        self.hooks.add_pre_handler(hook);
336        self
337    }
338
339    pub fn on_response(mut self, hook: Arc<CoreHook<Req, Resp>>) -> Self {
340        self.hooks.add_on_response(hook);
341        self
342    }
343
344    pub fn on_error(mut self, hook: Arc<CoreHook<Req, Resp>>) -> Self {
345        self.hooks.add_on_error(hook);
346        self
347    }
348
349    pub fn build(self) -> LifecycleHooks<Req, Resp> {
350        self.hooks
351    }
352}
353
354impl<Req, Resp> Default for LifecycleHooksBuilder<Req, Resp> {
355    fn default() -> Self {
356        Self::new()
357    }
358}
359
360/// Create a request hook from an async function or closure (native targets).
361#[cfg(not(target_arch = "wasm32"))]
362pub fn request_hook<Req, Resp, F, Fut>(name: impl Into<String>, func: F) -> Arc<dyn LifecycleHook<Req, Resp>>
363where
364    F: Fn(Req) -> Fut + Send + Sync + 'static,
365    Fut: Future<Output = Result<HookResult<Req, Resp>, String>> + Send + 'static,
366    Req: Send + 'static,
367    Resp: Send + 'static,
368{
369    Arc::new(RequestHookFn {
370        name: name.into(),
371        func,
372        _marker: std::marker::PhantomData,
373    })
374}
375
376/// Create a request hook from an async function or closure (wasm targets).
377#[cfg(target_arch = "wasm32")]
378pub fn request_hook<Req, Resp, F, Fut>(name: impl Into<String>, func: F) -> Arc<dyn LifecycleHook<Req, Resp>>
379where
380    F: Fn(Req) -> Fut + Send + Sync + 'static,
381    Fut: Future<Output = Result<HookResult<Req, Resp>, String>> + 'static,
382    Req: 'static,
383    Resp: 'static,
384{
385    Arc::new(RequestHookFn {
386        name: name.into(),
387        func,
388        _marker: std::marker::PhantomData,
389    })
390}
391
392/// Create a response hook from an async function or closure (native targets).
393#[cfg(not(target_arch = "wasm32"))]
394pub fn response_hook<Req, Resp, F, Fut>(name: impl Into<String>, func: F) -> Arc<dyn LifecycleHook<Req, Resp>>
395where
396    F: Fn(Resp) -> Fut + Send + Sync + 'static,
397    Fut: Future<Output = Result<HookResult<Resp, Resp>, String>> + Send + 'static,
398    Req: Send + 'static,
399    Resp: Send + 'static,
400{
401    Arc::new(ResponseHookFn {
402        name: name.into(),
403        func,
404        _marker: std::marker::PhantomData,
405    })
406}
407
408/// Create a response hook from an async function or closure (wasm targets).
409#[cfg(target_arch = "wasm32")]
410pub fn response_hook<Req, Resp, F, Fut>(name: impl Into<String>, func: F) -> Arc<dyn LifecycleHook<Req, Resp>>
411where
412    F: Fn(Resp) -> Fut + Send + Sync + 'static,
413    Fut: Future<Output = Result<HookResult<Resp, Resp>, String>> + 'static,
414    Req: 'static,
415    Resp: 'static,
416{
417    Arc::new(ResponseHookFn {
418        name: name.into(),
419        func,
420        _marker: std::marker::PhantomData,
421    })
422}