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