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    #[test]
695    fn test_execute_request_hooks_continue_flow() {
696        #[cfg(not(target_arch = "wasm32"))]
697        {
698            let hooks: LifecycleHooks<String, String> = LifecycleHooks::builder()
699                .on_request(request_hook("req", |req| async move {
700                    Ok(HookResult::Continue(req + "_a"))
701                }))
702                .pre_validation(request_hook("pre", |req| async move {
703                    Ok(HookResult::Continue(req + "_b"))
704                }))
705                .pre_handler(request_hook("handler", |req| async move {
706                    Ok(HookResult::Continue(req + "_c"))
707                }))
708                .build();
709
710            let on_request = block_on(hooks.execute_on_request("start".to_string())).unwrap();
711            assert!(matches!(on_request, HookResult::Continue(ref val) if val == "start_a"));
712
713            let pre_validation = block_on(hooks.execute_pre_validation("start".to_string())).unwrap();
714            assert!(matches!(pre_validation, HookResult::Continue(ref val) if val == "start_b"));
715
716            let pre_handler = block_on(hooks.execute_pre_handler("start".to_string())).unwrap();
717            assert!(matches!(pre_handler, HookResult::Continue(ref val) if val == "start_c"));
718        }
719    }
720
721    #[test]
722    fn test_execute_request_hooks_short_circuit_flow() {
723        #[cfg(not(target_arch = "wasm32"))]
724        {
725            let hooks: LifecycleHooks<String, String> = LifecycleHooks::builder()
726                .on_request(request_hook("req", |_req| async move {
727                    Ok(HookResult::ShortCircuit("stop".to_string()))
728                }))
729                .build();
730
731            let result = block_on(hooks.execute_on_request("start".to_string())).unwrap();
732            assert!(matches!(result, HookResult::ShortCircuit(ref val) if val == "stop"));
733        }
734    }
735
736    #[test]
737    fn test_execute_response_hooks_continue_and_short_circuit() {
738        #[cfg(not(target_arch = "wasm32"))]
739        {
740            let hooks: LifecycleHooks<String, String> = LifecycleHooks::builder()
741                .on_response(response_hook("resp", |resp| async move {
742                    Ok(HookResult::Continue(resp + "_ok"))
743                }))
744                .on_error(response_hook("err", |resp| async move {
745                    Ok(HookResult::ShortCircuit(resp + "_err"))
746                }))
747                .build();
748
749            let on_response = block_on(hooks.execute_on_response("start".to_string())).unwrap();
750            assert_eq!(on_response, "start_ok");
751
752            let on_error = block_on(hooks.execute_on_error("start".to_string())).unwrap();
753            assert_eq!(on_error, "start_err");
754        }
755    }
756
757    #[cfg(not(target_arch = "wasm32"))]
758    struct TestShortCircuitHook;
759
760    #[cfg(not(target_arch = "wasm32"))]
761    impl NativeLifecycleHook<String, String> for TestShortCircuitHook {
762        fn name(&self) -> &str {
763            "short_circuit"
764        }
765
766        fn execute_request<'a>(&'a self, _req: String) -> RequestHookFutureSend<'a, String, String> {
767            Box::pin(async { Ok(HookResult::ShortCircuit("short_circuit_response".to_string())) })
768        }
769
770        fn execute_response<'a>(&'a self, _resp: String) -> ResponseHookFutureSend<'a, String> {
771            Box::pin(async { Err("not implemented".to_string()) })
772        }
773    }
774
775    #[cfg(target_arch = "wasm32")]
776    struct TestShortCircuitHookLocal;
777
778    #[cfg(target_arch = "wasm32")]
779    impl LocalLifecycleHook<String, String> for TestShortCircuitHookLocal {
780        fn name(&self) -> &str {
781            "short_circuit"
782        }
783
784        fn execute_request<'a>(&'a self, _req: String) -> RequestHookFutureLocal<'a, String, String> {
785            Box::pin(async { Ok(HookResult::ShortCircuit("short_circuit_response".to_string())) })
786        }
787
788        fn execute_response<'a>(&'a self, _resp: String) -> ResponseHookFutureLocal<'a, String> {
789            Box::pin(async { Err("not implemented".to_string()) })
790        }
791    }
792
793    #[test]
794    fn test_on_request_short_circuit() {
795        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
796
797        #[cfg(not(target_arch = "wasm32"))]
798        let hook = Arc::new(TestShortCircuitHook);
799        #[cfg(target_arch = "wasm32")]
800        let hook = Arc::new(TestShortCircuitHookLocal);
801
802        hooks.add_on_request(hook);
803        assert!(!hooks.is_empty());
804    }
805
806    #[test]
807    fn test_pre_validation_short_circuit() {
808        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
809
810        #[cfg(not(target_arch = "wasm32"))]
811        let hook = Arc::new(TestShortCircuitHook);
812        #[cfg(target_arch = "wasm32")]
813        let hook = Arc::new(TestShortCircuitHookLocal);
814
815        hooks.add_pre_validation(hook);
816        assert!(!hooks.is_empty());
817    }
818
819    #[test]
820    fn test_pre_handler_short_circuit() {
821        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
822
823        #[cfg(not(target_arch = "wasm32"))]
824        let hook = Arc::new(TestShortCircuitHook);
825        #[cfg(target_arch = "wasm32")]
826        let hook = Arc::new(TestShortCircuitHookLocal);
827
828        hooks.add_pre_handler(hook);
829        assert!(!hooks.is_empty());
830    }
831
832    #[cfg(not(target_arch = "wasm32"))]
833    struct TestResponseShortCircuitHook;
834
835    #[cfg(not(target_arch = "wasm32"))]
836    impl NativeLifecycleHook<String, String> for TestResponseShortCircuitHook {
837        fn name(&self) -> &str {
838            "response_short_circuit"
839        }
840
841        fn execute_request<'a>(&'a self, _req: String) -> RequestHookFutureSend<'a, String, String> {
842            Box::pin(async { Err("not implemented".to_string()) })
843        }
844
845        fn execute_response<'a>(&'a self, resp: String) -> ResponseHookFutureSend<'a, String> {
846            Box::pin(async move { Ok(HookResult::ShortCircuit("short_circuit_".to_string() + &resp)) })
847        }
848    }
849
850    #[cfg(target_arch = "wasm32")]
851    struct TestResponseShortCircuitHookLocal;
852
853    #[cfg(target_arch = "wasm32")]
854    impl LocalLifecycleHook<String, String> for TestResponseShortCircuitHookLocal {
855        fn name(&self) -> &str {
856            "response_short_circuit"
857        }
858
859        fn execute_request<'a>(&'a self, _req: String) -> RequestHookFutureLocal<'a, String, String> {
860            Box::pin(async { Err("not implemented".to_string()) })
861        }
862
863        fn execute_response<'a>(&'a self, resp: String) -> ResponseHookFutureLocal<'a, String> {
864            Box::pin(async move { Ok(HookResult::ShortCircuit("short_circuit_".to_string() + &resp)) })
865        }
866    }
867
868    #[test]
869    fn test_on_response_short_circuit() {
870        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
871
872        #[cfg(not(target_arch = "wasm32"))]
873        let hook = Arc::new(TestResponseShortCircuitHook);
874        #[cfg(target_arch = "wasm32")]
875        let hook = Arc::new(TestResponseShortCircuitHookLocal);
876
877        hooks.add_on_response(hook);
878        assert!(!hooks.is_empty());
879    }
880
881    #[test]
882    fn test_on_error_short_circuit() {
883        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
884
885        #[cfg(not(target_arch = "wasm32"))]
886        let hook = Arc::new(TestResponseShortCircuitHook);
887        #[cfg(target_arch = "wasm32")]
888        let hook = Arc::new(TestResponseShortCircuitHookLocal);
889
890        hooks.add_on_error(hook);
891        assert!(!hooks.is_empty());
892    }
893
894    #[test]
895    fn test_multiple_on_request_hooks_in_sequence() {
896        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
897
898        #[cfg(not(target_arch = "wasm32"))]
899        {
900            hooks.add_on_request(Arc::new(TestAppendHook("_first")));
901            hooks.add_on_request(Arc::new(TestAppendHook("_second")));
902        }
903        #[cfg(target_arch = "wasm32")]
904        {
905            hooks.add_on_request(Arc::new(TestAppendHookLocal("_first")));
906            hooks.add_on_request(Arc::new(TestAppendHookLocal("_second")));
907        }
908
909        assert_eq!(hooks.on_request.len(), 2);
910    }
911
912    #[cfg(not(target_arch = "wasm32"))]
913    struct TestAppendHook(&'static str);
914
915    #[cfg(not(target_arch = "wasm32"))]
916    impl NativeLifecycleHook<String, String> for TestAppendHook {
917        fn name(&self) -> &str {
918            "append"
919        }
920
921        fn execute_request<'a>(&'a self, req: String) -> RequestHookFutureSend<'a, String, String> {
922            let suffix = self.0;
923            Box::pin(async move { Ok(HookResult::Continue(req + suffix)) })
924        }
925
926        fn execute_response<'a>(&'a self, _resp: String) -> ResponseHookFutureSend<'a, String> {
927            Box::pin(async { Err("not implemented".to_string()) })
928        }
929    }
930
931    #[cfg(target_arch = "wasm32")]
932    struct TestAppendHookLocal(&'static str);
933
934    #[cfg(target_arch = "wasm32")]
935    impl LocalLifecycleHook<String, String> for TestAppendHookLocal {
936        fn name(&self) -> &str {
937            "append"
938        }
939
940        fn execute_request<'a>(&'a self, req: String) -> RequestHookFutureLocal<'a, String, String> {
941            let suffix = self.0;
942            Box::pin(async move { Ok(HookResult::Continue(req + suffix)) })
943        }
944
945        fn execute_response<'a>(&'a self, _resp: String) -> ResponseHookFutureLocal<'a, String> {
946            Box::pin(async { Err("not implemented".to_string()) })
947        }
948    }
949
950    #[test]
951    fn test_multiple_response_hooks_in_sequence() {
952        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
953
954        #[cfg(not(target_arch = "wasm32"))]
955        {
956            hooks.add_on_response(Arc::new(TestAppendResponseHook("_first")));
957            hooks.add_on_response(Arc::new(TestAppendResponseHook("_second")));
958        }
959        #[cfg(target_arch = "wasm32")]
960        {
961            hooks.add_on_response(Arc::new(TestAppendResponseHookLocal("_first")));
962            hooks.add_on_response(Arc::new(TestAppendResponseHookLocal("_second")));
963        }
964
965        assert_eq!(hooks.on_response.len(), 2);
966    }
967
968    #[cfg(not(target_arch = "wasm32"))]
969    struct TestAppendResponseHook(&'static str);
970
971    #[cfg(not(target_arch = "wasm32"))]
972    impl NativeLifecycleHook<String, String> for TestAppendResponseHook {
973        fn name(&self) -> &str {
974            "append_response"
975        }
976
977        fn execute_request<'a>(&'a self, _req: String) -> RequestHookFutureSend<'a, String, String> {
978            Box::pin(async { Err("not implemented".to_string()) })
979        }
980
981        fn execute_response<'a>(&'a self, resp: String) -> ResponseHookFutureSend<'a, String> {
982            let suffix = self.0;
983            Box::pin(async move { Ok(HookResult::Continue(resp + suffix)) })
984        }
985    }
986
987    #[cfg(target_arch = "wasm32")]
988    struct TestAppendResponseHookLocal(&'static str);
989
990    #[cfg(target_arch = "wasm32")]
991    impl LocalLifecycleHook<String, String> for TestAppendResponseHookLocal {
992        fn name(&self) -> &str {
993            "append_response"
994        }
995
996        fn execute_request<'a>(&'a self, _req: String) -> RequestHookFutureLocal<'a, String, String> {
997            Box::pin(async { Err("not implemented".to_string()) })
998        }
999
1000        fn execute_response<'a>(&'a self, resp: String) -> ResponseHookFutureLocal<'a, String> {
1001            let suffix = self.0;
1002            Box::pin(async move { Ok(HookResult::Continue(resp + suffix)) })
1003        }
1004    }
1005
1006    #[test]
1007    fn test_builder_chain_multiple_hooks() {
1008        #[cfg(not(target_arch = "wasm32"))]
1009        let hooks = LifecycleHooks::builder()
1010            .on_request(Arc::new(TestRequestHook))
1011            .pre_validation(Arc::new(TestRequestHook))
1012            .pre_handler(Arc::new(TestRequestHook))
1013            .on_response(Arc::new(TestResponseHook))
1014            .on_error(Arc::new(TestResponseHook))
1015            .build();
1016
1017        #[cfg(target_arch = "wasm32")]
1018        let hooks = LifecycleHooks::builder()
1019            .on_request(Arc::new(TestRequestHookLocal))
1020            .pre_validation(Arc::new(TestRequestHookLocal))
1021            .pre_handler(Arc::new(TestRequestHookLocal))
1022            .on_response(Arc::new(TestResponseHookLocal))
1023            .on_error(Arc::new(TestResponseHookLocal))
1024            .build();
1025
1026        assert!(!hooks.is_empty());
1027    }
1028
1029    #[cfg(not(target_arch = "wasm32"))]
1030    struct TestErrorHook;
1031
1032    #[cfg(not(target_arch = "wasm32"))]
1033    impl NativeLifecycleHook<String, String> for TestErrorHook {
1034        fn name(&self) -> &str {
1035            "error_hook"
1036        }
1037
1038        fn execute_request<'a>(&'a self, _req: String) -> RequestHookFutureSend<'a, String, String> {
1039            Box::pin(async { Err("hook_error".to_string()) })
1040        }
1041
1042        fn execute_response<'a>(&'a self, _resp: String) -> ResponseHookFutureSend<'a, String> {
1043            Box::pin(async { Err("hook_error".to_string()) })
1044        }
1045    }
1046
1047    #[cfg(target_arch = "wasm32")]
1048    struct TestErrorHookLocal;
1049
1050    #[cfg(target_arch = "wasm32")]
1051    impl LocalLifecycleHook<String, String> for TestErrorHookLocal {
1052        fn name(&self) -> &str {
1053            "error_hook"
1054        }
1055
1056        fn execute_request<'a>(&'a self, _req: String) -> RequestHookFutureLocal<'a, String, String> {
1057            Box::pin(async { Err("hook_error".to_string()) })
1058        }
1059
1060        fn execute_response<'a>(&'a self, _resp: String) -> ResponseHookFutureLocal<'a, String> {
1061            Box::pin(async { Err("hook_error".to_string()) })
1062        }
1063    }
1064
1065    #[test]
1066    fn test_on_request_hook_error_propagates() {
1067        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
1068
1069        #[cfg(not(target_arch = "wasm32"))]
1070        let hook = Arc::new(TestErrorHook);
1071        #[cfg(target_arch = "wasm32")]
1072        let hook = Arc::new(TestErrorHookLocal);
1073
1074        hooks.add_on_request(hook);
1075        assert!(!hooks.is_empty());
1076    }
1077
1078    #[test]
1079    fn test_pre_validation_hook_error_propagates() {
1080        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
1081
1082        #[cfg(not(target_arch = "wasm32"))]
1083        let hook = Arc::new(TestErrorHook);
1084        #[cfg(target_arch = "wasm32")]
1085        let hook = Arc::new(TestErrorHookLocal);
1086
1087        hooks.add_pre_validation(hook);
1088        assert!(!hooks.is_empty());
1089    }
1090
1091    #[test]
1092    fn test_pre_handler_hook_error_propagates() {
1093        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
1094
1095        #[cfg(not(target_arch = "wasm32"))]
1096        let hook = Arc::new(TestErrorHook);
1097        #[cfg(target_arch = "wasm32")]
1098        let hook = Arc::new(TestErrorHookLocal);
1099
1100        hooks.add_pre_handler(hook);
1101        assert!(!hooks.is_empty());
1102    }
1103
1104    #[test]
1105    fn test_on_response_hook_error_propagates() {
1106        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
1107
1108        #[cfg(not(target_arch = "wasm32"))]
1109        let hook = Arc::new(TestErrorHook);
1110        #[cfg(target_arch = "wasm32")]
1111        let hook = Arc::new(TestErrorHookLocal);
1112
1113        hooks.add_on_response(hook);
1114        assert!(!hooks.is_empty());
1115    }
1116
1117    #[test]
1118    fn test_on_error_hook_error_propagates() {
1119        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
1120
1121        #[cfg(not(target_arch = "wasm32"))]
1122        let hook = Arc::new(TestErrorHook);
1123        #[cfg(target_arch = "wasm32")]
1124        let hook = Arc::new(TestErrorHookLocal);
1125
1126        hooks.add_on_error(hook);
1127        assert!(!hooks.is_empty());
1128    }
1129
1130    #[test]
1131    fn test_debug_format_with_hooks() {
1132        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
1133
1134        #[cfg(not(target_arch = "wasm32"))]
1135        hooks.add_on_request(Arc::new(TestRequestHook));
1136        #[cfg(target_arch = "wasm32")]
1137        hooks.add_on_request(Arc::new(TestRequestHookLocal));
1138
1139        let debug_str = format!("{:?}", hooks);
1140        assert!(debug_str.contains("on_request_count"));
1141        assert!(debug_str.contains("1"));
1142    }
1143
1144    #[cfg(not(target_arch = "wasm32"))]
1145    #[test]
1146    fn test_request_hook_called_with_response_returns_error() {
1147        let hook = TestRequestHook;
1148        assert_eq!(hook.name(), "test_request_hook");
1149    }
1150
1151    #[cfg(not(target_arch = "wasm32"))]
1152    #[test]
1153    fn test_response_hook_called_with_request_returns_error() {
1154        let hook = TestResponseHook;
1155        assert_eq!(hook.name(), "test_response_hook");
1156    }
1157
1158    #[cfg(not(target_arch = "wasm32"))]
1159    #[test]
1160    fn test_request_hook_name() {
1161        let hook = TestRequestHook;
1162        assert_eq!(hook.name(), "test_request_hook");
1163    }
1164
1165    #[cfg(not(target_arch = "wasm32"))]
1166    #[test]
1167    fn test_response_hook_name() {
1168        let hook = TestResponseHook;
1169        assert_eq!(hook.name(), "test_response_hook");
1170    }
1171
1172    #[test]
1173    fn test_first_hook_short_circuits_subsequent_hooks_not_executed() {
1174        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
1175
1176        #[cfg(not(target_arch = "wasm32"))]
1177        {
1178            hooks.add_on_request(Arc::new(TestShortCircuitHook));
1179            hooks.add_on_request(Arc::new(TestRequestHook));
1180        }
1181        #[cfg(target_arch = "wasm32")]
1182        {
1183            hooks.add_on_request(Arc::new(TestShortCircuitHookLocal));
1184            hooks.add_on_request(Arc::new(TestRequestHookLocal));
1185        }
1186
1187        assert_eq!(hooks.on_request.len(), 2);
1188    }
1189
1190    #[test]
1191    fn test_hook_count_accessors() {
1192        let hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
1193        assert_eq!(hooks.on_request.len(), 0);
1194        assert_eq!(hooks.pre_validation.len(), 0);
1195        assert_eq!(hooks.pre_handler.len(), 0);
1196        assert_eq!(hooks.on_response.len(), 0);
1197        assert_eq!(hooks.on_error.len(), 0);
1198    }
1199
1200    #[test]
1201    fn test_lifecycle_hooks_clone_with_hooks() {
1202        let mut hooks1: LifecycleHooks<String, String> = LifecycleHooks::new();
1203
1204        #[cfg(not(target_arch = "wasm32"))]
1205        hooks1.add_on_request(Arc::new(TestRequestHook));
1206        #[cfg(target_arch = "wasm32")]
1207        hooks1.add_on_request(Arc::new(TestRequestHookLocal));
1208
1209        let hooks2 = hooks1.clone();
1210        assert_eq!(hooks1.on_request.len(), hooks2.on_request.len());
1211        assert!(!hooks2.is_empty());
1212    }
1213
1214    #[test]
1215    fn test_builder_as_default() {
1216        let builder = LifecycleHooksBuilder::<String, String>::default();
1217        let hooks = builder.build();
1218        assert!(hooks.is_empty());
1219    }
1220
1221    #[test]
1222    fn test_is_empty_comprehensive() {
1223        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
1224        assert!(hooks.is_empty());
1225
1226        #[cfg(not(target_arch = "wasm32"))]
1227        hooks.add_on_request(Arc::new(TestRequestHook));
1228        #[cfg(target_arch = "wasm32")]
1229        hooks.add_on_request(Arc::new(TestRequestHookLocal));
1230
1231        assert!(!hooks.is_empty());
1232    }
1233
1234    #[test]
1235    fn test_hook_result_enum_value() {
1236        let val1: HookResult<String, String> = HookResult::Continue(String::from("test"));
1237        let val2: HookResult<String, String> = HookResult::ShortCircuit(String::from("response"));
1238
1239        match val1 {
1240            HookResult::Continue(s) => assert_eq!(s, "test"),
1241            HookResult::ShortCircuit(_) => panic!("Wrong variant"),
1242        }
1243
1244        match val2 {
1245            HookResult::Continue(_) => panic!("Wrong variant"),
1246            HookResult::ShortCircuit(s) => assert_eq!(s, "response"),
1247        }
1248    }
1249}