Skip to main content

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>(&self, req: Req) -> RequestHookFutureSend<'a, Req, Resp>;
33
34    /// Execute hook with a response
35    fn execute_response<'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>(&self, req: Req) -> RequestHookFutureLocal<'a, Req, Resp>;
45
46    /// Execute hook with a response
47    fn execute_response<'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    #[must_use]
101    pub fn new() -> Self {
102        Self::default()
103    }
104
105    /// Builder constructor for ergonomic hook registration
106    #[must_use]
107    pub fn builder() -> LifecycleHooksBuilder<Req, Resp> {
108        LifecycleHooksBuilder::new()
109    }
110
111    /// Check if any hooks are registered
112    #[must_use]
113    pub fn is_empty(&self) -> bool {
114        self.on_request.is_empty()
115            && self.pre_validation.is_empty()
116            && self.pre_handler.is_empty()
117            && self.on_response.is_empty()
118            && self.on_error.is_empty()
119    }
120
121    pub fn add_on_request(&mut self, hook: Arc<CoreHook<Req, Resp>>) {
122        self.on_request.push(hook);
123    }
124
125    pub fn add_pre_validation(&mut self, hook: Arc<CoreHook<Req, Resp>>) {
126        self.pre_validation.push(hook);
127    }
128
129    pub fn add_pre_handler(&mut self, hook: Arc<CoreHook<Req, Resp>>) {
130        self.pre_handler.push(hook);
131    }
132
133    pub fn add_on_response(&mut self, hook: Arc<CoreHook<Req, Resp>>) {
134        self.on_response.push(hook);
135    }
136
137    pub fn add_on_error(&mut self, hook: Arc<CoreHook<Req, Resp>>) {
138        self.on_error.push(hook);
139    }
140
141    /// # Errors
142    /// Returns an error string if a hook execution fails.
143    pub async fn execute_on_request(&self, mut req: Req) -> Result<HookResult<Req, Resp>, String> {
144        if self.on_request.is_empty() {
145            return Ok(HookResult::Continue(req));
146        }
147
148        for hook in &self.on_request {
149            match hook.execute_request(req).await? {
150                HookResult::Continue(r) => req = r,
151                HookResult::ShortCircuit(response) => return Ok(HookResult::ShortCircuit(response)),
152            }
153        }
154
155        Ok(HookResult::Continue(req))
156    }
157
158    /// # Errors
159    /// Returns an error string if a hook execution fails.
160    pub async fn execute_pre_validation(&self, mut req: Req) -> Result<HookResult<Req, Resp>, String> {
161        if self.pre_validation.is_empty() {
162            return Ok(HookResult::Continue(req));
163        }
164
165        for hook in &self.pre_validation {
166            match hook.execute_request(req).await? {
167                HookResult::Continue(r) => req = r,
168                HookResult::ShortCircuit(response) => return Ok(HookResult::ShortCircuit(response)),
169            }
170        }
171
172        Ok(HookResult::Continue(req))
173    }
174
175    /// # Errors
176    /// Returns an error string if a hook execution fails.
177    pub async fn execute_pre_handler(&self, mut req: Req) -> Result<HookResult<Req, Resp>, String> {
178        if self.pre_handler.is_empty() {
179            return Ok(HookResult::Continue(req));
180        }
181
182        for hook in &self.pre_handler {
183            match hook.execute_request(req).await? {
184                HookResult::Continue(r) => req = r,
185                HookResult::ShortCircuit(response) => return Ok(HookResult::ShortCircuit(response)),
186            }
187        }
188
189        Ok(HookResult::Continue(req))
190    }
191
192    /// # Errors
193    /// Returns an error string if a hook execution fails.
194    pub async fn execute_on_response(&self, mut resp: Resp) -> Result<Resp, String> {
195        if self.on_response.is_empty() {
196            return Ok(resp);
197        }
198
199        for hook in &self.on_response {
200            match hook.execute_response(resp).await? {
201                HookResult::Continue(r) | HookResult::ShortCircuit(r) => resp = r,
202            }
203        }
204
205        Ok(resp)
206    }
207
208    /// # Errors
209    /// Returns an error string if a hook execution fails.
210    pub async fn execute_on_error(&self, mut resp: Resp) -> Result<Resp, String> {
211        if self.on_error.is_empty() {
212            return Ok(resp);
213        }
214
215        for hook in &self.on_error {
216            match hook.execute_response(resp).await? {
217                HookResult::Continue(r) | HookResult::ShortCircuit(r) => resp = r,
218            }
219        }
220
221        Ok(resp)
222    }
223}
224
225/// Helper struct for implementing request hooks from closures
226struct RequestHookFn<F, Req, Resp> {
227    name: String,
228    func: F,
229    _marker: std::marker::PhantomData<fn(Req, Resp)>,
230}
231
232struct ResponseHookFn<F, Req, Resp> {
233    name: String,
234    func: F,
235    _marker: std::marker::PhantomData<fn(Req, Resp)>,
236}
237
238#[cfg(not(target_arch = "wasm32"))]
239impl<F, Fut, Req, Resp> NativeLifecycleHook<Req, Resp> for RequestHookFn<F, Req, Resp>
240where
241    F: Fn(Req) -> Fut + Send + Sync,
242    Fut: Future<Output = Result<HookResult<Req, Resp>, String>> + Send + 'static,
243    Req: Send + 'static,
244    Resp: Send + 'static,
245{
246    fn name(&self) -> &str {
247        &self.name
248    }
249
250    fn execute_request<'a>(&self, req: Req) -> RequestHookFutureSend<'a, Req, Resp> {
251        Box::pin((self.func)(req))
252    }
253
254    fn execute_response<'a>(&self, _resp: Resp) -> ResponseHookFutureSend<'a, Resp> {
255        Box::pin(async move { Err("Request hook called with response - this is a bug".to_string()) })
256    }
257}
258
259#[cfg(target_arch = "wasm32")]
260impl<F, Fut, Req, Resp> LocalLifecycleHook<Req, Resp> for RequestHookFn<F, Req, Resp>
261where
262    F: Fn(Req) -> Fut + Send + Sync,
263    Fut: Future<Output = Result<HookResult<Req, Resp>, String>> + 'static,
264    Req: 'static,
265    Resp: 'static,
266{
267    fn name(&self) -> &str {
268        &self.name
269    }
270
271    fn execute_request<'a>(&self, req: Req) -> RequestHookFutureLocal<'a, Req, Resp> {
272        Box::pin((self.func)(req))
273    }
274
275    fn execute_response<'a>(&self, _resp: Resp) -> ResponseHookFutureLocal<'a, Resp> {
276        Box::pin(async move { Err("Request hook called with response - this is a bug".to_string()) })
277    }
278}
279
280#[cfg(not(target_arch = "wasm32"))]
281impl<F, Fut, Req, Resp> NativeLifecycleHook<Req, Resp> for ResponseHookFn<F, Req, Resp>
282where
283    F: Fn(Resp) -> Fut + Send + Sync,
284    Fut: Future<Output = Result<HookResult<Resp, Resp>, String>> + Send + 'static,
285    Req: Send + 'static,
286    Resp: Send + 'static,
287{
288    fn name(&self) -> &str {
289        &self.name
290    }
291
292    fn execute_request<'a>(&self, _req: Req) -> RequestHookFutureSend<'a, Req, Resp> {
293        Box::pin(async move { Err("Response hook called with request - this is a bug".to_string()) })
294    }
295
296    fn execute_response<'a>(&self, resp: Resp) -> ResponseHookFutureSend<'a, Resp> {
297        Box::pin((self.func)(resp))
298    }
299}
300
301#[cfg(target_arch = "wasm32")]
302impl<F, Fut, Req, Resp> LocalLifecycleHook<Req, Resp> for ResponseHookFn<F, Req, Resp>
303where
304    F: Fn(Resp) -> Fut + Send + Sync,
305    Fut: Future<Output = Result<HookResult<Resp, Resp>, String>> + 'static,
306    Req: 'static,
307    Resp: 'static,
308{
309    fn name(&self) -> &str {
310        &self.name
311    }
312
313    fn execute_request<'a>(&self, _req: Req) -> RequestHookFutureLocal<'a, Req, Resp> {
314        Box::pin(async move { Err("Response hook called with request - this is a bug".to_string()) })
315    }
316
317    fn execute_response<'a>(&self, resp: Resp) -> ResponseHookFutureLocal<'a, Resp> {
318        Box::pin((self.func)(resp))
319    }
320}
321
322/// Builder pattern for `LifecycleHooks`
323pub struct LifecycleHooksBuilder<Req, Resp> {
324    hooks: LifecycleHooks<Req, Resp>,
325}
326
327impl<Req, Resp> LifecycleHooksBuilder<Req, Resp> {
328    /// Create a new builder
329    #[must_use]
330    pub fn new() -> Self {
331        Self {
332            hooks: LifecycleHooks::default(),
333        }
334    }
335
336    /// Add an `on_request` hook
337    #[must_use]
338    pub fn on_request(mut self, hook: Arc<CoreHook<Req, Resp>>) -> Self {
339        self.hooks.add_on_request(hook);
340        self
341    }
342
343    /// Add a `pre_validation` hook
344    #[must_use]
345    pub fn pre_validation(mut self, hook: Arc<CoreHook<Req, Resp>>) -> Self {
346        self.hooks.add_pre_validation(hook);
347        self
348    }
349
350    /// Add a `pre_handler` hook
351    #[must_use]
352    pub fn pre_handler(mut self, hook: Arc<CoreHook<Req, Resp>>) -> Self {
353        self.hooks.add_pre_handler(hook);
354        self
355    }
356
357    /// Add an `on_response` hook
358    #[must_use]
359    pub fn on_response(mut self, hook: Arc<CoreHook<Req, Resp>>) -> Self {
360        self.hooks.add_on_response(hook);
361        self
362    }
363
364    /// Add an `on_error` hook
365    #[must_use]
366    pub fn on_error(mut self, hook: Arc<CoreHook<Req, Resp>>) -> Self {
367        self.hooks.add_on_error(hook);
368        self
369    }
370
371    /// Build the `LifecycleHooks` instance
372    #[must_use]
373    pub fn build(self) -> LifecycleHooks<Req, Resp> {
374        self.hooks
375    }
376}
377
378impl<Req, Resp> Default for LifecycleHooksBuilder<Req, Resp> {
379    fn default() -> Self {
380        Self::new()
381    }
382}
383
384/// Create a request hook from an async function or closure (native targets).
385#[cfg(not(target_arch = "wasm32"))]
386pub fn request_hook<Req, Resp, F, Fut>(name: impl Into<String>, func: F) -> Arc<dyn LifecycleHook<Req, Resp>>
387where
388    F: Fn(Req) -> Fut + Send + Sync + 'static,
389    Fut: Future<Output = Result<HookResult<Req, Resp>, String>> + Send + 'static,
390    Req: Send + 'static,
391    Resp: Send + 'static,
392{
393    Arc::new(RequestHookFn {
394        name: name.into(),
395        func,
396        _marker: std::marker::PhantomData,
397    })
398}
399
400/// Create a request hook from an async function or closure (wasm targets).
401#[cfg(target_arch = "wasm32")]
402pub fn request_hook<Req, Resp, F, Fut>(name: impl Into<String>, func: F) -> Arc<dyn LifecycleHook<Req, Resp>>
403where
404    F: Fn(Req) -> Fut + Send + Sync + 'static,
405    Fut: Future<Output = Result<HookResult<Req, Resp>, String>> + 'static,
406    Req: 'static,
407    Resp: 'static,
408{
409    Arc::new(RequestHookFn {
410        name: name.into(),
411        func,
412        _marker: std::marker::PhantomData,
413    })
414}
415
416/// Create a response hook from an async function or closure (native targets).
417#[cfg(not(target_arch = "wasm32"))]
418pub fn response_hook<Req, Resp, F, Fut>(name: impl Into<String>, func: F) -> Arc<dyn LifecycleHook<Req, Resp>>
419where
420    F: Fn(Resp) -> Fut + Send + Sync + 'static,
421    Fut: Future<Output = Result<HookResult<Resp, Resp>, String>> + Send + 'static,
422    Req: Send + 'static,
423    Resp: Send + 'static,
424{
425    Arc::new(ResponseHookFn {
426        name: name.into(),
427        func,
428        _marker: std::marker::PhantomData,
429    })
430}
431
432/// Create a response hook from an async function or closure (wasm targets).
433#[cfg(target_arch = "wasm32")]
434pub fn response_hook<Req, Resp, F, Fut>(name: impl Into<String>, func: F) -> Arc<dyn LifecycleHook<Req, Resp>>
435where
436    F: Fn(Resp) -> Fut + Send + Sync + 'static,
437    Fut: Future<Output = Result<HookResult<Resp, Resp>, String>> + 'static,
438    Req: 'static,
439    Resp: 'static,
440{
441    Arc::new(ResponseHookFn {
442        name: name.into(),
443        func,
444        _marker: std::marker::PhantomData,
445    })
446}
447
448#[cfg(test)]
449mod tests {
450    use super::*;
451    use tokio_test::block_on;
452
453    #[test]
454    fn test_hook_result_continue_variant() {
455        let result: HookResult<i32, String> = HookResult::Continue(42);
456        assert!(matches!(result, HookResult::Continue(42)));
457    }
458
459    #[test]
460    fn test_hook_result_short_circuit_variant() {
461        let result: HookResult<i32, String> = HookResult::ShortCircuit("response".to_string());
462        assert!(matches!(result, HookResult::ShortCircuit(ref s) if s == "response"));
463    }
464
465    #[test]
466    fn test_hook_result_debug_format() {
467        let continue_result: HookResult<i32, String> = HookResult::Continue(100);
468        let debug_str = format!("{continue_result:?}");
469        assert!(debug_str.contains("Continue"));
470
471        let short_circuit_result: HookResult<i32, String> = HookResult::ShortCircuit("err".to_string());
472        let debug_str = format!("{short_circuit_result:?}");
473        assert!(debug_str.contains("ShortCircuit"));
474    }
475
476    #[test]
477    fn test_lifecycle_hooks_default() {
478        let hooks: LifecycleHooks<String, String> = LifecycleHooks::default();
479        assert!(hooks.is_empty());
480    }
481
482    #[test]
483    fn test_lifecycle_hooks_new() {
484        let hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
485        assert!(hooks.is_empty());
486    }
487
488    #[test]
489    fn test_lifecycle_hooks_is_empty_true() {
490        let hooks: LifecycleHooks<String, String> = LifecycleHooks::default();
491        assert!(hooks.is_empty());
492    }
493
494    #[test]
495    fn test_lifecycle_hooks_debug_format_empty() {
496        let hooks: LifecycleHooks<String, String> = LifecycleHooks::default();
497        let debug_str = format!("{hooks:?}");
498        assert!(debug_str.contains("LifecycleHooks"));
499        assert!(debug_str.contains("on_request_count"));
500        assert!(debug_str.contains('0'));
501    }
502
503    #[test]
504    fn test_lifecycle_hooks_clone() {
505        let hooks1: LifecycleHooks<String, String> = LifecycleHooks::default();
506        let hooks2 = hooks1;
507        assert!(hooks2.is_empty());
508    }
509
510    #[test]
511    fn test_lifecycle_hooks_builder_new() {
512        let builder: LifecycleHooksBuilder<String, String> = LifecycleHooksBuilder::new();
513        let hooks = builder.build();
514        assert!(hooks.is_empty());
515    }
516
517    #[test]
518    fn test_lifecycle_hooks_builder_default() {
519        let builder: LifecycleHooksBuilder<String, String> = LifecycleHooksBuilder::default();
520        let hooks = builder.build();
521        assert!(hooks.is_empty());
522    }
523
524    #[test]
525    fn test_lifecycle_hooks_builder_method() {
526        let hooks: LifecycleHooks<String, String> = LifecycleHooks::builder().build();
527        assert!(hooks.is_empty());
528    }
529
530    #[test]
531    fn test_add_on_request_hook() {
532        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
533
534        #[cfg(not(target_arch = "wasm32"))]
535        let hook = Arc::new(TestRequestHook);
536        #[cfg(target_arch = "wasm32")]
537        let hook = Arc::new(TestRequestHookLocal);
538
539        hooks.add_on_request(hook);
540        assert!(!hooks.is_empty());
541    }
542
543    #[test]
544    fn request_hook_errors_if_called_with_response() {
545        #[cfg(not(target_arch = "wasm32"))]
546        {
547            let hook = request_hook::<String, String, _, _>("req", |req| async move { Ok(HookResult::Continue(req)) });
548            let err = block_on(async { hook.execute_response("resp".to_string()).await }).unwrap_err();
549            assert!(err.contains("Request hook called with response"));
550        }
551    }
552
553    #[test]
554    fn response_hook_errors_if_called_with_request() {
555        #[cfg(not(target_arch = "wasm32"))]
556        {
557            let hook =
558                response_hook::<String, String, _, _>("resp", |resp| async move { Ok(HookResult::Continue(resp)) });
559            let err = block_on(async { hook.execute_request("req".to_string()).await }).unwrap_err();
560            assert!(err.contains("Response hook called with request"));
561        }
562    }
563
564    #[cfg(not(target_arch = "wasm32"))]
565    struct TestRequestHook;
566
567    #[cfg(not(target_arch = "wasm32"))]
568    impl NativeLifecycleHook<String, String> for TestRequestHook {
569        fn name(&self) -> &'static str {
570            "test_request_hook"
571        }
572
573        fn execute_request<'a>(&self, req: String) -> RequestHookFutureSend<'a, String, String> {
574            Box::pin(async move { Ok(HookResult::Continue(req + "_modified")) })
575        }
576
577        fn execute_response<'a>(&self, _resp: String) -> ResponseHookFutureSend<'a, String> {
578            Box::pin(async { Err("not implemented".to_string()) })
579        }
580    }
581
582    #[cfg(target_arch = "wasm32")]
583    struct TestRequestHookLocal;
584
585    #[cfg(target_arch = "wasm32")]
586    impl LocalLifecycleHook<String, String> for TestRequestHookLocal {
587        fn name(&self) -> &str {
588            "test_request_hook"
589        }
590
591        fn execute_request<'a>(&self, req: String) -> RequestHookFutureLocal<'a, String, String> {
592            Box::pin(async move { Ok(HookResult::Continue(req + "_modified")) })
593        }
594
595        fn execute_response<'a>(&self, _resp: String) -> ResponseHookFutureLocal<'a, String> {
596            Box::pin(async { Err("not implemented".to_string()) })
597        }
598    }
599
600    #[test]
601    fn test_add_pre_validation_hook() {
602        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
603
604        #[cfg(not(target_arch = "wasm32"))]
605        let hook = Arc::new(TestRequestHook);
606        #[cfg(target_arch = "wasm32")]
607        let hook = Arc::new(TestRequestHookLocal);
608
609        hooks.add_pre_validation(hook);
610        assert!(!hooks.is_empty());
611    }
612
613    #[test]
614    fn test_add_pre_handler_hook() {
615        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
616
617        #[cfg(not(target_arch = "wasm32"))]
618        let hook = Arc::new(TestRequestHook);
619        #[cfg(target_arch = "wasm32")]
620        let hook = Arc::new(TestRequestHookLocal);
621
622        hooks.add_pre_handler(hook);
623        assert!(!hooks.is_empty());
624    }
625
626    #[cfg(not(target_arch = "wasm32"))]
627    struct TestResponseHook;
628
629    #[cfg(not(target_arch = "wasm32"))]
630    impl NativeLifecycleHook<String, String> for TestResponseHook {
631        fn name(&self) -> &'static str {
632            "test_response_hook"
633        }
634
635        fn execute_request<'a>(&self, _req: String) -> RequestHookFutureSend<'a, String, String> {
636            Box::pin(async { Err("not implemented".to_string()) })
637        }
638
639        fn execute_response<'a>(&self, resp: String) -> ResponseHookFutureSend<'a, String> {
640            Box::pin(async move { Ok(HookResult::Continue(resp + "_processed")) })
641        }
642    }
643
644    #[cfg(target_arch = "wasm32")]
645    struct TestResponseHookLocal;
646
647    #[cfg(target_arch = "wasm32")]
648    impl LocalLifecycleHook<String, String> for TestResponseHookLocal {
649        fn name(&self) -> &str {
650            "test_response_hook"
651        }
652
653        fn execute_request<'a>(&self, _req: String) -> RequestHookFutureLocal<'a, String, String> {
654            Box::pin(async { Err("not implemented".to_string()) })
655        }
656
657        fn execute_response<'a>(&self, resp: String) -> ResponseHookFutureLocal<'a, String> {
658            Box::pin(async move { Ok(HookResult::Continue(resp + "_processed")) })
659        }
660    }
661
662    #[test]
663    fn test_add_on_response_hook() {
664        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
665
666        #[cfg(not(target_arch = "wasm32"))]
667        let hook = Arc::new(TestResponseHook);
668        #[cfg(target_arch = "wasm32")]
669        let hook = Arc::new(TestResponseHookLocal);
670
671        hooks.add_on_response(hook);
672        assert!(!hooks.is_empty());
673    }
674
675    #[test]
676    fn test_add_on_error_hook() {
677        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
678
679        #[cfg(not(target_arch = "wasm32"))]
680        let hook = Arc::new(TestResponseHook);
681        #[cfg(target_arch = "wasm32")]
682        let hook = Arc::new(TestResponseHookLocal);
683
684        hooks.add_on_error(hook);
685        assert!(!hooks.is_empty());
686    }
687
688    #[test]
689    fn test_execute_on_request_no_hooks() {
690        let hooks: LifecycleHooks<String, String> = LifecycleHooks::default();
691        assert!(hooks.is_empty());
692    }
693
694    #[test]
695    fn test_execute_pre_validation_no_hooks() {
696        let hooks: LifecycleHooks<String, String> = LifecycleHooks::default();
697        assert!(hooks.is_empty());
698    }
699
700    #[test]
701    fn test_execute_pre_handler_no_hooks() {
702        let hooks: LifecycleHooks<String, String> = LifecycleHooks::default();
703        assert!(hooks.is_empty());
704    }
705
706    #[test]
707    fn test_execute_on_response_no_hooks() {
708        let hooks: LifecycleHooks<String, String> = LifecycleHooks::default();
709        assert!(hooks.is_empty());
710    }
711
712    #[test]
713    fn test_execute_on_error_no_hooks() {
714        let hooks: LifecycleHooks<String, String> = LifecycleHooks::default();
715        assert!(hooks.is_empty());
716    }
717
718    #[test]
719    fn test_execute_request_hooks_continue_flow() {
720        #[cfg(not(target_arch = "wasm32"))]
721        {
722            let hooks: LifecycleHooks<String, String> = LifecycleHooks::builder()
723                .on_request(request_hook("req", |req| async move {
724                    Ok(HookResult::Continue(req + "_a"))
725                }))
726                .pre_validation(request_hook("pre", |req| async move {
727                    Ok(HookResult::Continue(req + "_b"))
728                }))
729                .pre_handler(request_hook("handler", |req| async move {
730                    Ok(HookResult::Continue(req + "_c"))
731                }))
732                .build();
733
734            let on_request = block_on(hooks.execute_on_request("start".to_string())).unwrap();
735            assert!(matches!(on_request, HookResult::Continue(ref val) if val == "start_a"));
736
737            let pre_validation = block_on(hooks.execute_pre_validation("start".to_string())).unwrap();
738            assert!(matches!(pre_validation, HookResult::Continue(ref val) if val == "start_b"));
739
740            let pre_handler = block_on(hooks.execute_pre_handler("start".to_string())).unwrap();
741            assert!(matches!(pre_handler, HookResult::Continue(ref val) if val == "start_c"));
742        }
743    }
744
745    #[test]
746    fn test_execute_request_hooks_short_circuit_flow() {
747        #[cfg(not(target_arch = "wasm32"))]
748        {
749            let hooks: LifecycleHooks<String, String> = LifecycleHooks::builder()
750                .on_request(request_hook("req", |_req| async move {
751                    Ok(HookResult::ShortCircuit("stop".to_string()))
752                }))
753                .build();
754
755            let result = block_on(hooks.execute_on_request("start".to_string())).unwrap();
756            assert!(matches!(result, HookResult::ShortCircuit(ref val) if val == "stop"));
757        }
758    }
759
760    #[test]
761    fn test_execute_response_hooks_continue_and_short_circuit() {
762        #[cfg(not(target_arch = "wasm32"))]
763        {
764            let hooks: LifecycleHooks<String, String> = LifecycleHooks::builder()
765                .on_response(response_hook("resp", |resp| async move {
766                    Ok(HookResult::Continue(resp + "_ok"))
767                }))
768                .on_error(response_hook("err", |resp| async move {
769                    Ok(HookResult::ShortCircuit(resp + "_err"))
770                }))
771                .build();
772
773            let on_response = block_on(hooks.execute_on_response("start".to_string())).unwrap();
774            assert_eq!(on_response, "start_ok");
775
776            let on_error = block_on(hooks.execute_on_error("start".to_string())).unwrap();
777            assert_eq!(on_error, "start_err");
778        }
779    }
780
781    #[cfg(not(target_arch = "wasm32"))]
782    struct TestShortCircuitHook;
783
784    #[cfg(not(target_arch = "wasm32"))]
785    impl NativeLifecycleHook<String, String> for TestShortCircuitHook {
786        fn name(&self) -> &'static str {
787            "short_circuit"
788        }
789
790        fn execute_request<'a>(&self, _req: String) -> RequestHookFutureSend<'a, String, String> {
791            Box::pin(async { Ok(HookResult::ShortCircuit("short_circuit_response".to_string())) })
792        }
793
794        fn execute_response<'a>(&self, _resp: String) -> ResponseHookFutureSend<'a, String> {
795            Box::pin(async { Err("not implemented".to_string()) })
796        }
797    }
798
799    #[cfg(target_arch = "wasm32")]
800    struct TestShortCircuitHookLocal;
801
802    #[cfg(target_arch = "wasm32")]
803    impl LocalLifecycleHook<String, String> for TestShortCircuitHookLocal {
804        fn name(&self) -> &str {
805            "short_circuit"
806        }
807
808        fn execute_request<'a>(&self, _req: String) -> RequestHookFutureLocal<'a, String, String> {
809            Box::pin(async { Ok(HookResult::ShortCircuit("short_circuit_response".to_string())) })
810        }
811
812        fn execute_response<'a>(&self, _resp: String) -> ResponseHookFutureLocal<'a, String> {
813            Box::pin(async { Err("not implemented".to_string()) })
814        }
815    }
816
817    #[test]
818    fn test_on_request_short_circuit() {
819        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
820
821        #[cfg(not(target_arch = "wasm32"))]
822        let hook = Arc::new(TestShortCircuitHook);
823        #[cfg(target_arch = "wasm32")]
824        let hook = Arc::new(TestShortCircuitHookLocal);
825
826        hooks.add_on_request(hook);
827        assert!(!hooks.is_empty());
828    }
829
830    #[test]
831    fn test_pre_validation_short_circuit() {
832        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
833
834        #[cfg(not(target_arch = "wasm32"))]
835        let hook = Arc::new(TestShortCircuitHook);
836        #[cfg(target_arch = "wasm32")]
837        let hook = Arc::new(TestShortCircuitHookLocal);
838
839        hooks.add_pre_validation(hook);
840        assert!(!hooks.is_empty());
841    }
842
843    #[test]
844    fn test_pre_handler_short_circuit() {
845        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
846
847        #[cfg(not(target_arch = "wasm32"))]
848        let hook = Arc::new(TestShortCircuitHook);
849        #[cfg(target_arch = "wasm32")]
850        let hook = Arc::new(TestShortCircuitHookLocal);
851
852        hooks.add_pre_handler(hook);
853        assert!(!hooks.is_empty());
854    }
855
856    #[cfg(not(target_arch = "wasm32"))]
857    struct TestResponseShortCircuitHook;
858
859    #[cfg(not(target_arch = "wasm32"))]
860    impl NativeLifecycleHook<String, String> for TestResponseShortCircuitHook {
861        fn name(&self) -> &'static str {
862            "response_short_circuit"
863        }
864
865        fn execute_request<'a>(&self, _req: String) -> RequestHookFutureSend<'a, String, String> {
866            Box::pin(async { Err("not implemented".to_string()) })
867        }
868
869        fn execute_response<'a>(&self, resp: String) -> ResponseHookFutureSend<'a, String> {
870            Box::pin(async move { Ok(HookResult::ShortCircuit("short_circuit_".to_string() + &resp)) })
871        }
872    }
873
874    #[cfg(target_arch = "wasm32")]
875    struct TestResponseShortCircuitHookLocal;
876
877    #[cfg(target_arch = "wasm32")]
878    impl LocalLifecycleHook<String, String> for TestResponseShortCircuitHookLocal {
879        fn name(&self) -> &str {
880            "response_short_circuit"
881        }
882
883        fn execute_request<'a>(&self, _req: String) -> RequestHookFutureLocal<'a, String, String> {
884            Box::pin(async { Err("not implemented".to_string()) })
885        }
886
887        fn execute_response<'a>(&self, resp: String) -> ResponseHookFutureLocal<'a, String> {
888            Box::pin(async move { Ok(HookResult::ShortCircuit("short_circuit_".to_string() + &resp)) })
889        }
890    }
891
892    #[test]
893    fn test_on_response_short_circuit() {
894        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
895
896        #[cfg(not(target_arch = "wasm32"))]
897        let hook = Arc::new(TestResponseShortCircuitHook);
898        #[cfg(target_arch = "wasm32")]
899        let hook = Arc::new(TestResponseShortCircuitHookLocal);
900
901        hooks.add_on_response(hook);
902        assert!(!hooks.is_empty());
903    }
904
905    #[test]
906    fn test_on_error_short_circuit() {
907        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
908
909        #[cfg(not(target_arch = "wasm32"))]
910        let hook = Arc::new(TestResponseShortCircuitHook);
911        #[cfg(target_arch = "wasm32")]
912        let hook = Arc::new(TestResponseShortCircuitHookLocal);
913
914        hooks.add_on_error(hook);
915        assert!(!hooks.is_empty());
916    }
917
918    #[test]
919    fn test_multiple_on_request_hooks_in_sequence() {
920        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
921
922        #[cfg(not(target_arch = "wasm32"))]
923        {
924            hooks.add_on_request(Arc::new(TestAppendHook("_first")));
925            hooks.add_on_request(Arc::new(TestAppendHook("_second")));
926        }
927        #[cfg(target_arch = "wasm32")]
928        {
929            hooks.add_on_request(Arc::new(TestAppendHookLocal("_first")));
930            hooks.add_on_request(Arc::new(TestAppendHookLocal("_second")));
931        }
932
933        assert_eq!(hooks.on_request.len(), 2);
934    }
935
936    #[cfg(not(target_arch = "wasm32"))]
937    struct TestAppendHook(&'static str);
938
939    #[cfg(not(target_arch = "wasm32"))]
940    impl NativeLifecycleHook<String, String> for TestAppendHook {
941        fn name(&self) -> &'static str {
942            "append"
943        }
944
945        fn execute_request<'a>(&self, req: String) -> RequestHookFutureSend<'a, String, String> {
946            let suffix = self.0;
947            Box::pin(async move { Ok(HookResult::Continue(req + suffix)) })
948        }
949
950        fn execute_response<'a>(&self, _resp: String) -> ResponseHookFutureSend<'a, String> {
951            Box::pin(async { Err("not implemented".to_string()) })
952        }
953    }
954
955    #[cfg(target_arch = "wasm32")]
956    struct TestAppendHookLocal(&'static str);
957
958    #[cfg(target_arch = "wasm32")]
959    impl LocalLifecycleHook<String, String> for TestAppendHookLocal {
960        fn name(&self) -> &str {
961            "append"
962        }
963
964        fn execute_request<'a>(&self, req: String) -> RequestHookFutureLocal<'a, String, String> {
965            let suffix = self.0;
966            Box::pin(async move { Ok(HookResult::Continue(req + suffix)) })
967        }
968
969        fn execute_response<'a>(&self, _resp: String) -> ResponseHookFutureLocal<'a, String> {
970            Box::pin(async { Err("not implemented".to_string()) })
971        }
972    }
973
974    #[test]
975    fn test_multiple_response_hooks_in_sequence() {
976        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
977
978        #[cfg(not(target_arch = "wasm32"))]
979        {
980            hooks.add_on_response(Arc::new(TestAppendResponseHook("_first")));
981            hooks.add_on_response(Arc::new(TestAppendResponseHook("_second")));
982        }
983        #[cfg(target_arch = "wasm32")]
984        {
985            hooks.add_on_response(Arc::new(TestAppendResponseHookLocal("_first")));
986            hooks.add_on_response(Arc::new(TestAppendResponseHookLocal("_second")));
987        }
988
989        assert_eq!(hooks.on_response.len(), 2);
990    }
991
992    #[cfg(not(target_arch = "wasm32"))]
993    struct TestAppendResponseHook(&'static str);
994
995    #[cfg(not(target_arch = "wasm32"))]
996    impl NativeLifecycleHook<String, String> for TestAppendResponseHook {
997        fn name(&self) -> &'static str {
998            "append_response"
999        }
1000
1001        fn execute_request<'a>(&self, _req: String) -> RequestHookFutureSend<'a, String, String> {
1002            Box::pin(async { Err("not implemented".to_string()) })
1003        }
1004
1005        fn execute_response<'a>(&self, resp: String) -> ResponseHookFutureSend<'a, String> {
1006            let suffix = self.0;
1007            Box::pin(async move { Ok(HookResult::Continue(resp + suffix)) })
1008        }
1009    }
1010
1011    #[cfg(target_arch = "wasm32")]
1012    struct TestAppendResponseHookLocal(&'static str);
1013
1014    #[cfg(target_arch = "wasm32")]
1015    impl LocalLifecycleHook<String, String> for TestAppendResponseHookLocal {
1016        fn name(&self) -> &str {
1017            "append_response"
1018        }
1019
1020        fn execute_request<'a>(&self, _req: String) -> RequestHookFutureLocal<'a, String, String> {
1021            Box::pin(async { Err("not implemented".to_string()) })
1022        }
1023
1024        fn execute_response<'a>(&self, resp: String) -> ResponseHookFutureLocal<'a, String> {
1025            let suffix = self.0;
1026            Box::pin(async move { Ok(HookResult::Continue(resp + suffix)) })
1027        }
1028    }
1029
1030    #[test]
1031    fn test_builder_chain_multiple_hooks() {
1032        #[cfg(not(target_arch = "wasm32"))]
1033        let hooks = LifecycleHooks::builder()
1034            .on_request(Arc::new(TestRequestHook))
1035            .pre_validation(Arc::new(TestRequestHook))
1036            .pre_handler(Arc::new(TestRequestHook))
1037            .on_response(Arc::new(TestResponseHook))
1038            .on_error(Arc::new(TestResponseHook))
1039            .build();
1040
1041        #[cfg(target_arch = "wasm32")]
1042        let hooks = LifecycleHooks::builder()
1043            .on_request(Arc::new(TestRequestHookLocal))
1044            .pre_validation(Arc::new(TestRequestHookLocal))
1045            .pre_handler(Arc::new(TestRequestHookLocal))
1046            .on_response(Arc::new(TestResponseHookLocal))
1047            .on_error(Arc::new(TestResponseHookLocal))
1048            .build();
1049
1050        assert!(!hooks.is_empty());
1051    }
1052
1053    #[cfg(not(target_arch = "wasm32"))]
1054    struct TestErrorHook;
1055
1056    #[cfg(not(target_arch = "wasm32"))]
1057    impl NativeLifecycleHook<String, String> for TestErrorHook {
1058        fn name(&self) -> &'static str {
1059            "error_hook"
1060        }
1061
1062        fn execute_request<'a>(&self, _req: String) -> RequestHookFutureSend<'a, String, String> {
1063            Box::pin(async { Err("hook_error".to_string()) })
1064        }
1065
1066        fn execute_response<'a>(&self, _resp: String) -> ResponseHookFutureSend<'a, String> {
1067            Box::pin(async { Err("hook_error".to_string()) })
1068        }
1069    }
1070
1071    #[cfg(target_arch = "wasm32")]
1072    struct TestErrorHookLocal;
1073
1074    #[cfg(target_arch = "wasm32")]
1075    impl LocalLifecycleHook<String, String> for TestErrorHookLocal {
1076        fn name(&self) -> &str {
1077            "error_hook"
1078        }
1079
1080        fn execute_request<'a>(&self, _req: String) -> RequestHookFutureLocal<'a, String, String> {
1081            Box::pin(async { Err("hook_error".to_string()) })
1082        }
1083
1084        fn execute_response<'a>(&self, _resp: String) -> ResponseHookFutureLocal<'a, String> {
1085            Box::pin(async { Err("hook_error".to_string()) })
1086        }
1087    }
1088
1089    #[test]
1090    fn test_on_request_hook_error_propagates() {
1091        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
1092
1093        #[cfg(not(target_arch = "wasm32"))]
1094        let hook = Arc::new(TestErrorHook);
1095        #[cfg(target_arch = "wasm32")]
1096        let hook = Arc::new(TestErrorHookLocal);
1097
1098        hooks.add_on_request(hook);
1099        assert!(!hooks.is_empty());
1100    }
1101
1102    #[test]
1103    fn test_pre_validation_hook_error_propagates() {
1104        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
1105
1106        #[cfg(not(target_arch = "wasm32"))]
1107        let hook = Arc::new(TestErrorHook);
1108        #[cfg(target_arch = "wasm32")]
1109        let hook = Arc::new(TestErrorHookLocal);
1110
1111        hooks.add_pre_validation(hook);
1112        assert!(!hooks.is_empty());
1113    }
1114
1115    #[test]
1116    fn test_pre_handler_hook_error_propagates() {
1117        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
1118
1119        #[cfg(not(target_arch = "wasm32"))]
1120        let hook = Arc::new(TestErrorHook);
1121        #[cfg(target_arch = "wasm32")]
1122        let hook = Arc::new(TestErrorHookLocal);
1123
1124        hooks.add_pre_handler(hook);
1125        assert!(!hooks.is_empty());
1126    }
1127
1128    #[test]
1129    fn test_on_response_hook_error_propagates() {
1130        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
1131
1132        #[cfg(not(target_arch = "wasm32"))]
1133        let hook = Arc::new(TestErrorHook);
1134        #[cfg(target_arch = "wasm32")]
1135        let hook = Arc::new(TestErrorHookLocal);
1136
1137        hooks.add_on_response(hook);
1138        assert!(!hooks.is_empty());
1139    }
1140
1141    #[test]
1142    fn test_on_error_hook_error_propagates() {
1143        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
1144
1145        #[cfg(not(target_arch = "wasm32"))]
1146        let hook = Arc::new(TestErrorHook);
1147        #[cfg(target_arch = "wasm32")]
1148        let hook = Arc::new(TestErrorHookLocal);
1149
1150        hooks.add_on_error(hook);
1151        assert!(!hooks.is_empty());
1152    }
1153
1154    #[test]
1155    fn test_debug_format_with_hooks() {
1156        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
1157
1158        #[cfg(not(target_arch = "wasm32"))]
1159        hooks.add_on_request(Arc::new(TestRequestHook));
1160        #[cfg(target_arch = "wasm32")]
1161        hooks.add_on_request(Arc::new(TestRequestHookLocal));
1162
1163        let debug_str = format!("{hooks:?}");
1164        assert!(debug_str.contains("on_request_count"));
1165        assert!(debug_str.contains('1'));
1166    }
1167
1168    #[cfg(not(target_arch = "wasm32"))]
1169    #[test]
1170    fn test_request_hook_called_with_response_returns_error() {
1171        let hook = TestRequestHook;
1172        assert_eq!(hook.name(), "test_request_hook");
1173    }
1174
1175    #[cfg(not(target_arch = "wasm32"))]
1176    #[test]
1177    fn test_response_hook_called_with_request_returns_error() {
1178        let hook = TestResponseHook;
1179        assert_eq!(hook.name(), "test_response_hook");
1180    }
1181
1182    #[cfg(not(target_arch = "wasm32"))]
1183    #[test]
1184    fn test_request_hook_name() {
1185        let hook = TestRequestHook;
1186        assert_eq!(hook.name(), "test_request_hook");
1187    }
1188
1189    #[cfg(not(target_arch = "wasm32"))]
1190    #[test]
1191    fn test_response_hook_name() {
1192        let hook = TestResponseHook;
1193        assert_eq!(hook.name(), "test_response_hook");
1194    }
1195
1196    #[test]
1197    fn test_first_hook_short_circuits_subsequent_hooks_not_executed() {
1198        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
1199
1200        #[cfg(not(target_arch = "wasm32"))]
1201        {
1202            hooks.add_on_request(Arc::new(TestShortCircuitHook));
1203            hooks.add_on_request(Arc::new(TestRequestHook));
1204        }
1205        #[cfg(target_arch = "wasm32")]
1206        {
1207            hooks.add_on_request(Arc::new(TestShortCircuitHookLocal));
1208            hooks.add_on_request(Arc::new(TestRequestHookLocal));
1209        }
1210
1211        assert_eq!(hooks.on_request.len(), 2);
1212    }
1213
1214    #[test]
1215    fn test_hook_count_accessors() {
1216        let hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
1217        assert_eq!(hooks.on_request.len(), 0);
1218        assert_eq!(hooks.pre_validation.len(), 0);
1219        assert_eq!(hooks.pre_handler.len(), 0);
1220        assert_eq!(hooks.on_response.len(), 0);
1221        assert_eq!(hooks.on_error.len(), 0);
1222    }
1223
1224    #[test]
1225    fn test_lifecycle_hooks_clone_with_hooks() {
1226        let mut hooks1: LifecycleHooks<String, String> = LifecycleHooks::new();
1227
1228        #[cfg(not(target_arch = "wasm32"))]
1229        hooks1.add_on_request(Arc::new(TestRequestHook));
1230        #[cfg(target_arch = "wasm32")]
1231        hooks1.add_on_request(Arc::new(TestRequestHookLocal));
1232
1233        let hooks2 = hooks1.clone();
1234        assert_eq!(hooks1.on_request.len(), hooks2.on_request.len());
1235        assert!(!hooks2.is_empty());
1236    }
1237
1238    #[test]
1239    fn test_builder_as_default() {
1240        let builder = LifecycleHooksBuilder::<String, String>::default();
1241        let hooks = builder.build();
1242        assert!(hooks.is_empty());
1243    }
1244
1245    #[test]
1246    fn test_is_empty_before_and_after_adding_hooks() {
1247        let mut hooks: LifecycleHooks<String, String> = LifecycleHooks::new();
1248        assert!(hooks.is_empty());
1249
1250        #[cfg(not(target_arch = "wasm32"))]
1251        hooks.add_on_request(Arc::new(TestRequestHook));
1252        #[cfg(target_arch = "wasm32")]
1253        hooks.add_on_request(Arc::new(TestRequestHookLocal));
1254
1255        assert!(!hooks.is_empty());
1256    }
1257
1258    #[test]
1259    fn test_hook_result_enum_value() {
1260        let val1: HookResult<String, String> = HookResult::Continue(String::from("test"));
1261        let val2: HookResult<String, String> = HookResult::ShortCircuit(String::from("response"));
1262
1263        match val1 {
1264            HookResult::Continue(s) => assert_eq!(s, "test"),
1265            HookResult::ShortCircuit(_) => panic!("Wrong variant"),
1266        }
1267
1268        match val2 {
1269            HookResult::Continue(_) => panic!("Wrong variant"),
1270            HookResult::ShortCircuit(s) => assert_eq!(s, "response"),
1271        }
1272    }
1273}