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}
423
424#[cfg(test)]
425mod tests {
426    use super::*;
427    use tokio_test::block_on;
428
429    #[test]
430    fn test_hook_result_continue_variant() {
431        let result: HookResult<i32, String> = HookResult::Continue(42);
432        assert!(matches!(result, HookResult::Continue(42)));
433    }
434
435    #[test]
436    fn test_hook_result_short_circuit_variant() {
437        let result: HookResult<i32, String> = HookResult::ShortCircuit("response".to_string());
438        assert!(matches!(result, HookResult::ShortCircuit(ref s) if s == "response"));
439    }
440
441    #[test]
442    fn test_hook_result_debug_format() {
443        let continue_result: HookResult<i32, String> = HookResult::Continue(100);
444        let debug_str = format!("{:?}", continue_result);
445        assert!(debug_str.contains("Continue"));
446
447        let short_circuit_result: HookResult<i32, String> = HookResult::ShortCircuit("err".to_string());
448        let debug_str = format!("{:?}", short_circuit_result);
449        assert!(debug_str.contains("ShortCircuit"));
450    }
451
452    #[test]
453    fn test_lifecycle_hooks_default() {
454        let hooks: LifecycleHooks<String, String> = LifecycleHooks::default();
455        assert!(hooks.is_empty());
456    }
457
458    #[test]
459    fn test_lifecycle_hooks_new() {
460        let hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
461        assert!(hooks.is_empty());
462    }
463
464    #[test]
465    fn test_lifecycle_hooks_is_empty_true() {
466        let hooks: LifecycleHooks<String, String> = LifecycleHooks::default();
467        assert!(hooks.is_empty());
468    }
469
470    #[test]
471    fn test_lifecycle_hooks_debug_format_empty() {
472        let hooks: LifecycleHooks<String, String> = LifecycleHooks::default();
473        let debug_str = format!("{:?}", hooks);
474        assert!(debug_str.contains("LifecycleHooks"));
475        assert!(debug_str.contains("on_request_count"));
476        assert!(debug_str.contains("0"));
477    }
478
479    #[test]
480    fn test_lifecycle_hooks_clone() {
481        let hooks1: LifecycleHooks<String, String> = LifecycleHooks::default();
482        let hooks2 = hooks1.clone();
483        assert!(hooks2.is_empty());
484    }
485
486    #[test]
487    fn test_lifecycle_hooks_builder_new() {
488        let builder: LifecycleHooksBuilder<String, String> = LifecycleHooksBuilder::new();
489        let hooks = builder.build();
490        assert!(hooks.is_empty());
491    }
492
493    #[test]
494    fn test_lifecycle_hooks_builder_default() {
495        let builder: LifecycleHooksBuilder<String, String> = LifecycleHooksBuilder::default();
496        let hooks = builder.build();
497        assert!(hooks.is_empty());
498    }
499
500    #[test]
501    fn test_lifecycle_hooks_builder_method() {
502        let hooks: LifecycleHooks<String, String> = LifecycleHooks::builder().build();
503        assert!(hooks.is_empty());
504    }
505
506    #[test]
507    fn test_add_on_request_hook() {
508        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
509
510        #[cfg(not(target_arch = "wasm32"))]
511        let hook = Arc::new(TestRequestHook);
512        #[cfg(target_arch = "wasm32")]
513        let hook = Arc::new(TestRequestHookLocal);
514
515        hooks.add_on_request(hook);
516        assert!(!hooks.is_empty());
517    }
518
519    #[test]
520    fn request_hook_errors_if_called_with_response() {
521        #[cfg(not(target_arch = "wasm32"))]
522        {
523            let hook = request_hook::<String, String, _, _>("req", |req| async move { Ok(HookResult::Continue(req)) });
524            let err = block_on(async { hook.execute_response("resp".to_string()).await }).unwrap_err();
525            assert!(err.contains("Request hook called with response"));
526        }
527    }
528
529    #[test]
530    fn response_hook_errors_if_called_with_request() {
531        #[cfg(not(target_arch = "wasm32"))]
532        {
533            let hook =
534                response_hook::<String, String, _, _>("resp", |resp| async move { Ok(HookResult::Continue(resp)) });
535            let err = block_on(async { hook.execute_request("req".to_string()).await }).unwrap_err();
536            assert!(err.contains("Response hook called with request"));
537        }
538    }
539
540    #[cfg(not(target_arch = "wasm32"))]
541    struct TestRequestHook;
542
543    #[cfg(not(target_arch = "wasm32"))]
544    impl NativeLifecycleHook<String, String> for TestRequestHook {
545        fn name(&self) -> &str {
546            "test_request_hook"
547        }
548
549        fn execute_request<'a>(&'a self, req: String) -> RequestHookFutureSend<'a, String, String> {
550            Box::pin(async move { Ok(HookResult::Continue(req + "_modified")) })
551        }
552
553        fn execute_response<'a>(&'a self, _resp: String) -> ResponseHookFutureSend<'a, String> {
554            Box::pin(async { Err("not implemented".to_string()) })
555        }
556    }
557
558    #[cfg(target_arch = "wasm32")]
559    struct TestRequestHookLocal;
560
561    #[cfg(target_arch = "wasm32")]
562    impl LocalLifecycleHook<String, String> for TestRequestHookLocal {
563        fn name(&self) -> &str {
564            "test_request_hook"
565        }
566
567        fn execute_request<'a>(&'a self, req: String) -> RequestHookFutureLocal<'a, String, String> {
568            Box::pin(async move { Ok(HookResult::Continue(req + "_modified")) })
569        }
570
571        fn execute_response<'a>(&'a self, _resp: String) -> ResponseHookFutureLocal<'a, String> {
572            Box::pin(async { Err("not implemented".to_string()) })
573        }
574    }
575
576    #[test]
577    fn test_add_pre_validation_hook() {
578        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
579
580        #[cfg(not(target_arch = "wasm32"))]
581        let hook = Arc::new(TestRequestHook);
582        #[cfg(target_arch = "wasm32")]
583        let hook = Arc::new(TestRequestHookLocal);
584
585        hooks.add_pre_validation(hook);
586        assert!(!hooks.is_empty());
587    }
588
589    #[test]
590    fn test_add_pre_handler_hook() {
591        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
592
593        #[cfg(not(target_arch = "wasm32"))]
594        let hook = Arc::new(TestRequestHook);
595        #[cfg(target_arch = "wasm32")]
596        let hook = Arc::new(TestRequestHookLocal);
597
598        hooks.add_pre_handler(hook);
599        assert!(!hooks.is_empty());
600    }
601
602    #[cfg(not(target_arch = "wasm32"))]
603    struct TestResponseHook;
604
605    #[cfg(not(target_arch = "wasm32"))]
606    impl NativeLifecycleHook<String, String> for TestResponseHook {
607        fn name(&self) -> &str {
608            "test_response_hook"
609        }
610
611        fn execute_request<'a>(&'a self, _req: String) -> RequestHookFutureSend<'a, String, String> {
612            Box::pin(async { Err("not implemented".to_string()) })
613        }
614
615        fn execute_response<'a>(&'a self, resp: String) -> ResponseHookFutureSend<'a, String> {
616            Box::pin(async move { Ok(HookResult::Continue(resp + "_processed")) })
617        }
618    }
619
620    #[cfg(target_arch = "wasm32")]
621    struct TestResponseHookLocal;
622
623    #[cfg(target_arch = "wasm32")]
624    impl LocalLifecycleHook<String, String> for TestResponseHookLocal {
625        fn name(&self) -> &str {
626            "test_response_hook"
627        }
628
629        fn execute_request<'a>(&'a self, _req: String) -> RequestHookFutureLocal<'a, String, String> {
630            Box::pin(async { Err("not implemented".to_string()) })
631        }
632
633        fn execute_response<'a>(&'a self, resp: String) -> ResponseHookFutureLocal<'a, String> {
634            Box::pin(async move { Ok(HookResult::Continue(resp + "_processed")) })
635        }
636    }
637
638    #[test]
639    fn test_add_on_response_hook() {
640        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
641
642        #[cfg(not(target_arch = "wasm32"))]
643        let hook = Arc::new(TestResponseHook);
644        #[cfg(target_arch = "wasm32")]
645        let hook = Arc::new(TestResponseHookLocal);
646
647        hooks.add_on_response(hook);
648        assert!(!hooks.is_empty());
649    }
650
651    #[test]
652    fn test_add_on_error_hook() {
653        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
654
655        #[cfg(not(target_arch = "wasm32"))]
656        let hook = Arc::new(TestResponseHook);
657        #[cfg(target_arch = "wasm32")]
658        let hook = Arc::new(TestResponseHookLocal);
659
660        hooks.add_on_error(hook);
661        assert!(!hooks.is_empty());
662    }
663
664    #[test]
665    fn test_execute_on_request_no_hooks() {
666        let hooks: LifecycleHooks<String, String> = LifecycleHooks::default();
667        assert!(hooks.is_empty());
668    }
669
670    #[test]
671    fn test_execute_pre_validation_no_hooks() {
672        let hooks: LifecycleHooks<String, String> = LifecycleHooks::default();
673        assert!(hooks.is_empty());
674    }
675
676    #[test]
677    fn test_execute_pre_handler_no_hooks() {
678        let hooks: LifecycleHooks<String, String> = LifecycleHooks::default();
679        assert!(hooks.is_empty());
680    }
681
682    #[test]
683    fn test_execute_on_response_no_hooks() {
684        let hooks: LifecycleHooks<String, String> = LifecycleHooks::default();
685        assert!(hooks.is_empty());
686    }
687
688    #[test]
689    fn test_execute_on_error_no_hooks() {
690        let hooks: LifecycleHooks<String, String> = LifecycleHooks::default();
691        assert!(hooks.is_empty());
692    }
693
694    #[cfg(not(target_arch = "wasm32"))]
695    struct TestShortCircuitHook;
696
697    #[cfg(not(target_arch = "wasm32"))]
698    impl NativeLifecycleHook<String, String> for TestShortCircuitHook {
699        fn name(&self) -> &str {
700            "short_circuit"
701        }
702
703        fn execute_request<'a>(&'a self, _req: String) -> RequestHookFutureSend<'a, String, String> {
704            Box::pin(async { Ok(HookResult::ShortCircuit("short_circuit_response".to_string())) })
705        }
706
707        fn execute_response<'a>(&'a self, _resp: String) -> ResponseHookFutureSend<'a, String> {
708            Box::pin(async { Err("not implemented".to_string()) })
709        }
710    }
711
712    #[cfg(target_arch = "wasm32")]
713    struct TestShortCircuitHookLocal;
714
715    #[cfg(target_arch = "wasm32")]
716    impl LocalLifecycleHook<String, String> for TestShortCircuitHookLocal {
717        fn name(&self) -> &str {
718            "short_circuit"
719        }
720
721        fn execute_request<'a>(&'a self, _req: String) -> RequestHookFutureLocal<'a, String, String> {
722            Box::pin(async { Ok(HookResult::ShortCircuit("short_circuit_response".to_string())) })
723        }
724
725        fn execute_response<'a>(&'a self, _resp: String) -> ResponseHookFutureLocal<'a, String> {
726            Box::pin(async { Err("not implemented".to_string()) })
727        }
728    }
729
730    #[test]
731    fn test_on_request_short_circuit() {
732        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
733
734        #[cfg(not(target_arch = "wasm32"))]
735        let hook = Arc::new(TestShortCircuitHook);
736        #[cfg(target_arch = "wasm32")]
737        let hook = Arc::new(TestShortCircuitHookLocal);
738
739        hooks.add_on_request(hook);
740        assert!(!hooks.is_empty());
741    }
742
743    #[test]
744    fn test_pre_validation_short_circuit() {
745        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
746
747        #[cfg(not(target_arch = "wasm32"))]
748        let hook = Arc::new(TestShortCircuitHook);
749        #[cfg(target_arch = "wasm32")]
750        let hook = Arc::new(TestShortCircuitHookLocal);
751
752        hooks.add_pre_validation(hook);
753        assert!(!hooks.is_empty());
754    }
755
756    #[test]
757    fn test_pre_handler_short_circuit() {
758        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
759
760        #[cfg(not(target_arch = "wasm32"))]
761        let hook = Arc::new(TestShortCircuitHook);
762        #[cfg(target_arch = "wasm32")]
763        let hook = Arc::new(TestShortCircuitHookLocal);
764
765        hooks.add_pre_handler(hook);
766        assert!(!hooks.is_empty());
767    }
768
769    #[cfg(not(target_arch = "wasm32"))]
770    struct TestResponseShortCircuitHook;
771
772    #[cfg(not(target_arch = "wasm32"))]
773    impl NativeLifecycleHook<String, String> for TestResponseShortCircuitHook {
774        fn name(&self) -> &str {
775            "response_short_circuit"
776        }
777
778        fn execute_request<'a>(&'a self, _req: String) -> RequestHookFutureSend<'a, String, String> {
779            Box::pin(async { Err("not implemented".to_string()) })
780        }
781
782        fn execute_response<'a>(&'a self, resp: String) -> ResponseHookFutureSend<'a, String> {
783            Box::pin(async move { Ok(HookResult::ShortCircuit("short_circuit_".to_string() + &resp)) })
784        }
785    }
786
787    #[cfg(target_arch = "wasm32")]
788    struct TestResponseShortCircuitHookLocal;
789
790    #[cfg(target_arch = "wasm32")]
791    impl LocalLifecycleHook<String, String> for TestResponseShortCircuitHookLocal {
792        fn name(&self) -> &str {
793            "response_short_circuit"
794        }
795
796        fn execute_request<'a>(&'a self, _req: String) -> RequestHookFutureLocal<'a, String, String> {
797            Box::pin(async { Err("not implemented".to_string()) })
798        }
799
800        fn execute_response<'a>(&'a self, resp: String) -> ResponseHookFutureLocal<'a, String> {
801            Box::pin(async move { Ok(HookResult::ShortCircuit("short_circuit_".to_string() + &resp)) })
802        }
803    }
804
805    #[test]
806    fn test_on_response_short_circuit() {
807        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
808
809        #[cfg(not(target_arch = "wasm32"))]
810        let hook = Arc::new(TestResponseShortCircuitHook);
811        #[cfg(target_arch = "wasm32")]
812        let hook = Arc::new(TestResponseShortCircuitHookLocal);
813
814        hooks.add_on_response(hook);
815        assert!(!hooks.is_empty());
816    }
817
818    #[test]
819    fn test_on_error_short_circuit() {
820        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
821
822        #[cfg(not(target_arch = "wasm32"))]
823        let hook = Arc::new(TestResponseShortCircuitHook);
824        #[cfg(target_arch = "wasm32")]
825        let hook = Arc::new(TestResponseShortCircuitHookLocal);
826
827        hooks.add_on_error(hook);
828        assert!(!hooks.is_empty());
829    }
830
831    #[test]
832    fn test_multiple_on_request_hooks_in_sequence() {
833        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
834
835        #[cfg(not(target_arch = "wasm32"))]
836        {
837            hooks.add_on_request(Arc::new(TestAppendHook("_first")));
838            hooks.add_on_request(Arc::new(TestAppendHook("_second")));
839        }
840        #[cfg(target_arch = "wasm32")]
841        {
842            hooks.add_on_request(Arc::new(TestAppendHookLocal("_first")));
843            hooks.add_on_request(Arc::new(TestAppendHookLocal("_second")));
844        }
845
846        assert_eq!(hooks.on_request.len(), 2);
847    }
848
849    #[cfg(not(target_arch = "wasm32"))]
850    struct TestAppendHook(&'static str);
851
852    #[cfg(not(target_arch = "wasm32"))]
853    impl NativeLifecycleHook<String, String> for TestAppendHook {
854        fn name(&self) -> &str {
855            "append"
856        }
857
858        fn execute_request<'a>(&'a self, req: String) -> RequestHookFutureSend<'a, String, String> {
859            let suffix = self.0;
860            Box::pin(async move { Ok(HookResult::Continue(req + suffix)) })
861        }
862
863        fn execute_response<'a>(&'a self, _resp: String) -> ResponseHookFutureSend<'a, String> {
864            Box::pin(async { Err("not implemented".to_string()) })
865        }
866    }
867
868    #[cfg(target_arch = "wasm32")]
869    struct TestAppendHookLocal(&'static str);
870
871    #[cfg(target_arch = "wasm32")]
872    impl LocalLifecycleHook<String, String> for TestAppendHookLocal {
873        fn name(&self) -> &str {
874            "append"
875        }
876
877        fn execute_request<'a>(&'a self, req: String) -> RequestHookFutureLocal<'a, String, String> {
878            let suffix = self.0;
879            Box::pin(async move { Ok(HookResult::Continue(req + suffix)) })
880        }
881
882        fn execute_response<'a>(&'a self, _resp: String) -> ResponseHookFutureLocal<'a, String> {
883            Box::pin(async { Err("not implemented".to_string()) })
884        }
885    }
886
887    #[test]
888    fn test_multiple_response_hooks_in_sequence() {
889        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
890
891        #[cfg(not(target_arch = "wasm32"))]
892        {
893            hooks.add_on_response(Arc::new(TestAppendResponseHook("_first")));
894            hooks.add_on_response(Arc::new(TestAppendResponseHook("_second")));
895        }
896        #[cfg(target_arch = "wasm32")]
897        {
898            hooks.add_on_response(Arc::new(TestAppendResponseHookLocal("_first")));
899            hooks.add_on_response(Arc::new(TestAppendResponseHookLocal("_second")));
900        }
901
902        assert_eq!(hooks.on_response.len(), 2);
903    }
904
905    #[cfg(not(target_arch = "wasm32"))]
906    struct TestAppendResponseHook(&'static str);
907
908    #[cfg(not(target_arch = "wasm32"))]
909    impl NativeLifecycleHook<String, String> for TestAppendResponseHook {
910        fn name(&self) -> &str {
911            "append_response"
912        }
913
914        fn execute_request<'a>(&'a self, _req: String) -> RequestHookFutureSend<'a, String, String> {
915            Box::pin(async { Err("not implemented".to_string()) })
916        }
917
918        fn execute_response<'a>(&'a self, resp: String) -> ResponseHookFutureSend<'a, String> {
919            let suffix = self.0;
920            Box::pin(async move { Ok(HookResult::Continue(resp + suffix)) })
921        }
922    }
923
924    #[cfg(target_arch = "wasm32")]
925    struct TestAppendResponseHookLocal(&'static str);
926
927    #[cfg(target_arch = "wasm32")]
928    impl LocalLifecycleHook<String, String> for TestAppendResponseHookLocal {
929        fn name(&self) -> &str {
930            "append_response"
931        }
932
933        fn execute_request<'a>(&'a self, _req: String) -> RequestHookFutureLocal<'a, String, String> {
934            Box::pin(async { Err("not implemented".to_string()) })
935        }
936
937        fn execute_response<'a>(&'a self, resp: String) -> ResponseHookFutureLocal<'a, String> {
938            let suffix = self.0;
939            Box::pin(async move { Ok(HookResult::Continue(resp + suffix)) })
940        }
941    }
942
943    #[test]
944    fn test_builder_chain_multiple_hooks() {
945        #[cfg(not(target_arch = "wasm32"))]
946        let hooks = LifecycleHooks::builder()
947            .on_request(Arc::new(TestRequestHook))
948            .pre_validation(Arc::new(TestRequestHook))
949            .pre_handler(Arc::new(TestRequestHook))
950            .on_response(Arc::new(TestResponseHook))
951            .on_error(Arc::new(TestResponseHook))
952            .build();
953
954        #[cfg(target_arch = "wasm32")]
955        let hooks = LifecycleHooks::builder()
956            .on_request(Arc::new(TestRequestHookLocal))
957            .pre_validation(Arc::new(TestRequestHookLocal))
958            .pre_handler(Arc::new(TestRequestHookLocal))
959            .on_response(Arc::new(TestResponseHookLocal))
960            .on_error(Arc::new(TestResponseHookLocal))
961            .build();
962
963        assert!(!hooks.is_empty());
964    }
965
966    #[cfg(not(target_arch = "wasm32"))]
967    struct TestErrorHook;
968
969    #[cfg(not(target_arch = "wasm32"))]
970    impl NativeLifecycleHook<String, String> for TestErrorHook {
971        fn name(&self) -> &str {
972            "error_hook"
973        }
974
975        fn execute_request<'a>(&'a self, _req: String) -> RequestHookFutureSend<'a, String, String> {
976            Box::pin(async { Err("hook_error".to_string()) })
977        }
978
979        fn execute_response<'a>(&'a self, _resp: String) -> ResponseHookFutureSend<'a, String> {
980            Box::pin(async { Err("hook_error".to_string()) })
981        }
982    }
983
984    #[cfg(target_arch = "wasm32")]
985    struct TestErrorHookLocal;
986
987    #[cfg(target_arch = "wasm32")]
988    impl LocalLifecycleHook<String, String> for TestErrorHookLocal {
989        fn name(&self) -> &str {
990            "error_hook"
991        }
992
993        fn execute_request<'a>(&'a self, _req: String) -> RequestHookFutureLocal<'a, String, String> {
994            Box::pin(async { Err("hook_error".to_string()) })
995        }
996
997        fn execute_response<'a>(&'a self, _resp: String) -> ResponseHookFutureLocal<'a, String> {
998            Box::pin(async { Err("hook_error".to_string()) })
999        }
1000    }
1001
1002    #[test]
1003    fn test_on_request_hook_error_propagates() {
1004        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
1005
1006        #[cfg(not(target_arch = "wasm32"))]
1007        let hook = Arc::new(TestErrorHook);
1008        #[cfg(target_arch = "wasm32")]
1009        let hook = Arc::new(TestErrorHookLocal);
1010
1011        hooks.add_on_request(hook);
1012        assert!(!hooks.is_empty());
1013    }
1014
1015    #[test]
1016    fn test_pre_validation_hook_error_propagates() {
1017        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
1018
1019        #[cfg(not(target_arch = "wasm32"))]
1020        let hook = Arc::new(TestErrorHook);
1021        #[cfg(target_arch = "wasm32")]
1022        let hook = Arc::new(TestErrorHookLocal);
1023
1024        hooks.add_pre_validation(hook);
1025        assert!(!hooks.is_empty());
1026    }
1027
1028    #[test]
1029    fn test_pre_handler_hook_error_propagates() {
1030        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
1031
1032        #[cfg(not(target_arch = "wasm32"))]
1033        let hook = Arc::new(TestErrorHook);
1034        #[cfg(target_arch = "wasm32")]
1035        let hook = Arc::new(TestErrorHookLocal);
1036
1037        hooks.add_pre_handler(hook);
1038        assert!(!hooks.is_empty());
1039    }
1040
1041    #[test]
1042    fn test_on_response_hook_error_propagates() {
1043        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
1044
1045        #[cfg(not(target_arch = "wasm32"))]
1046        let hook = Arc::new(TestErrorHook);
1047        #[cfg(target_arch = "wasm32")]
1048        let hook = Arc::new(TestErrorHookLocal);
1049
1050        hooks.add_on_response(hook);
1051        assert!(!hooks.is_empty());
1052    }
1053
1054    #[test]
1055    fn test_on_error_hook_error_propagates() {
1056        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
1057
1058        #[cfg(not(target_arch = "wasm32"))]
1059        let hook = Arc::new(TestErrorHook);
1060        #[cfg(target_arch = "wasm32")]
1061        let hook = Arc::new(TestErrorHookLocal);
1062
1063        hooks.add_on_error(hook);
1064        assert!(!hooks.is_empty());
1065    }
1066
1067    #[test]
1068    fn test_debug_format_with_hooks() {
1069        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
1070
1071        #[cfg(not(target_arch = "wasm32"))]
1072        hooks.add_on_request(Arc::new(TestRequestHook));
1073        #[cfg(target_arch = "wasm32")]
1074        hooks.add_on_request(Arc::new(TestRequestHookLocal));
1075
1076        let debug_str = format!("{:?}", hooks);
1077        assert!(debug_str.contains("on_request_count"));
1078        assert!(debug_str.contains("1"));
1079    }
1080
1081    #[cfg(not(target_arch = "wasm32"))]
1082    #[test]
1083    fn test_request_hook_called_with_response_returns_error() {
1084        let hook = TestRequestHook;
1085        assert_eq!(hook.name(), "test_request_hook");
1086    }
1087
1088    #[cfg(not(target_arch = "wasm32"))]
1089    #[test]
1090    fn test_response_hook_called_with_request_returns_error() {
1091        let hook = TestResponseHook;
1092        assert_eq!(hook.name(), "test_response_hook");
1093    }
1094
1095    #[cfg(not(target_arch = "wasm32"))]
1096    #[test]
1097    fn test_request_hook_name() {
1098        let hook = TestRequestHook;
1099        assert_eq!(hook.name(), "test_request_hook");
1100    }
1101
1102    #[cfg(not(target_arch = "wasm32"))]
1103    #[test]
1104    fn test_response_hook_name() {
1105        let hook = TestResponseHook;
1106        assert_eq!(hook.name(), "test_response_hook");
1107    }
1108
1109    #[test]
1110    fn test_first_hook_short_circuits_subsequent_hooks_not_executed() {
1111        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
1112
1113        #[cfg(not(target_arch = "wasm32"))]
1114        {
1115            hooks.add_on_request(Arc::new(TestShortCircuitHook));
1116            hooks.add_on_request(Arc::new(TestRequestHook));
1117        }
1118        #[cfg(target_arch = "wasm32")]
1119        {
1120            hooks.add_on_request(Arc::new(TestShortCircuitHookLocal));
1121            hooks.add_on_request(Arc::new(TestRequestHookLocal));
1122        }
1123
1124        assert_eq!(hooks.on_request.len(), 2);
1125    }
1126
1127    #[test]
1128    fn test_hook_count_accessors() {
1129        let hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
1130        assert_eq!(hooks.on_request.len(), 0);
1131        assert_eq!(hooks.pre_validation.len(), 0);
1132        assert_eq!(hooks.pre_handler.len(), 0);
1133        assert_eq!(hooks.on_response.len(), 0);
1134        assert_eq!(hooks.on_error.len(), 0);
1135    }
1136
1137    #[test]
1138    fn test_lifecycle_hooks_clone_with_hooks() {
1139        let mut hooks1: LifecycleHooks<String, String> = LifecycleHooks::new();
1140
1141        #[cfg(not(target_arch = "wasm32"))]
1142        hooks1.add_on_request(Arc::new(TestRequestHook));
1143        #[cfg(target_arch = "wasm32")]
1144        hooks1.add_on_request(Arc::new(TestRequestHookLocal));
1145
1146        let hooks2 = hooks1.clone();
1147        assert_eq!(hooks1.on_request.len(), hooks2.on_request.len());
1148        assert!(!hooks2.is_empty());
1149    }
1150
1151    #[test]
1152    fn test_builder_as_default() {
1153        let builder = LifecycleHooksBuilder::<String, String>::default();
1154        let hooks = builder.build();
1155        assert!(hooks.is_empty());
1156    }
1157
1158    #[test]
1159    fn test_is_empty_comprehensive() {
1160        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
1161        assert!(hooks.is_empty());
1162
1163        #[cfg(not(target_arch = "wasm32"))]
1164        hooks.add_on_request(Arc::new(TestRequestHook));
1165        #[cfg(target_arch = "wasm32")]
1166        hooks.add_on_request(Arc::new(TestRequestHookLocal));
1167
1168        assert!(!hooks.is_empty());
1169    }
1170
1171    #[test]
1172    fn test_hook_result_enum_value() {
1173        let val1: HookResult<String, String> = HookResult::Continue(String::from("test"));
1174        let val2: HookResult<String, String> = HookResult::ShortCircuit(String::from("response"));
1175
1176        match val1 {
1177            HookResult::Continue(s) => assert_eq!(s, "test"),
1178            HookResult::ShortCircuit(_) => panic!("Wrong variant"),
1179        }
1180
1181        match val2 {
1182            HookResult::Continue(_) => panic!("Wrong variant"),
1183            HookResult::ShortCircuit(s) => assert_eq!(s, "response"),
1184        }
1185    }
1186}