1use 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#[derive(Debug)]
19pub enum HookResult<T, U> {
20 Continue(T),
22 ShortCircuit(U),
24}
25
26pub trait NativeLifecycleHook<Req, Resp>: Send + Sync {
28 fn name(&self) -> &str;
30
31 fn execute_request<'a>(&self, req: Req) -> RequestHookFutureSend<'a, Req, Resp>;
33
34 fn execute_response<'a>(&self, resp: Resp) -> ResponseHookFutureSend<'a, Resp>;
36}
37
38pub trait LocalLifecycleHook<Req, Resp> {
40 fn name(&self) -> &str;
42
43 fn execute_request<'a>(&self, req: Req) -> RequestHookFutureLocal<'a, Req, Resp>;
45
46 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#[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
61pub type TargetLifecycleHooks<Req, Resp> = LifecycleHooks<Req, Resp>;
63
64#[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 #[must_use]
101 pub fn new() -> Self {
102 Self::default()
103 }
104
105 #[must_use]
107 pub fn builder() -> LifecycleHooksBuilder<Req, Resp> {
108 LifecycleHooksBuilder::new()
109 }
110
111 #[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 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 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 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 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 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
225struct 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
322pub struct LifecycleHooksBuilder<Req, Resp> {
324 hooks: LifecycleHooks<Req, Resp>,
325}
326
327impl<Req, Resp> LifecycleHooksBuilder<Req, Resp> {
328 #[must_use]
330 pub fn new() -> Self {
331 Self {
332 hooks: LifecycleHooks::default(),
333 }
334 }
335
336 #[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 #[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 #[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 #[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 #[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 #[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#[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#[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#[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#[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}