Skip to main content

proxima_primitives/pipe/resilience/
fallback.rs

1use crate::pipe::primitives::Pipe;
2use core::future::Future;
3
4/// A [`Pipe`] combinator that routes to `secondary` when `primary` fails.
5///
6/// `primary.call(input.clone())` is tried first; on any error,
7/// `secondary.call(input)` is called with the original input. On success,
8/// `secondary` is never invoked.
9///
10/// Requires `P::In: Clone` so the input can be replayed.
11#[derive(Debug, Clone)]
12pub struct Fallback<P, S> {
13    pub primary: P,
14    pub secondary: S,
15}
16
17impl<P, S> Pipe for Fallback<P, S>
18where
19    P: Pipe,
20    S: Pipe<In = P::In, Out = P::Out, Err = P::Err>,
21    P::In: Clone,
22{
23    type In = P::In;
24    type Out = P::Out;
25    type Err = P::Err;
26
27    fn call(&self, input: Self::In) -> impl Future<Output = Result<Self::Out, Self::Err>> {
28        async move {
29            match self.primary.call(input.clone()).await {
30                Ok(out) => Ok(out),
31                Err(_) => self.secondary.call(input).await,
32            }
33        }
34    }
35}
36
37#[cfg(test)]
38#[allow(clippy::unwrap_used, clippy::expect_used)]
39mod tests {
40    use super::*;
41    use crate::block_on;
42    use core::future::Future;
43
44    #[derive(Debug, Clone, PartialEq)]
45    struct AlwaysFail;
46
47    impl Pipe for AlwaysFail {
48        type In = u32;
49        type Out = u32;
50        type Err = &'static str;
51
52        fn call(&self, _input: u32) -> impl Future<Output = Result<u32, &'static str>> {
53            async { Err("primary failed") }
54        }
55    }
56
57    #[derive(Debug, Clone)]
58    struct AlwaysOk(u32);
59
60    impl Pipe for AlwaysOk {
61        type In = u32;
62        type Out = u32;
63        type Err = &'static str;
64
65        fn call(&self, _input: u32) -> impl Future<Output = Result<u32, &'static str>> {
66            let value = self.0;
67            async move { Ok(value) }
68        }
69    }
70
71    #[derive(Debug, Clone)]
72    struct DoubleInput;
73
74    impl Pipe for DoubleInput {
75        type In = u32;
76        type Out = u32;
77        type Err = &'static str;
78
79        fn call(&self, input: u32) -> impl Future<Output = Result<u32, &'static str>> {
80            async move { Ok(input * 2) }
81        }
82    }
83
84    /// secondary is AlwaysFail but primary succeeds — secondary must never be called
85    #[test]
86    fn fallback_uses_primary_when_it_succeeds() {
87        let fallback = Fallback {
88            primary: AlwaysOk(10),
89            secondary: AlwaysFail,
90        };
91        let result = block_on(Pipe::call(&fallback, 0));
92        assert_eq!(result, Ok(10), "primary result returned unchanged");
93    }
94
95    #[test]
96    fn fallback_uses_secondary_when_primary_fails() {
97        let fallback = Fallback {
98            primary: AlwaysFail,
99            secondary: AlwaysOk(42),
100        };
101        let result = block_on(Pipe::call(&fallback, 0));
102        assert_eq!(result, Ok(42), "secondary result after primary error");
103    }
104
105    #[test]
106    fn fallback_passes_original_input_to_secondary() {
107        let fallback = Fallback {
108            primary: AlwaysFail,
109            secondary: DoubleInput,
110        };
111        let result = block_on(Pipe::call(&fallback, 7));
112        assert_eq!(result, Ok(14), "secondary receives original input");
113    }
114
115    #[test]
116    fn fallback_does_not_call_secondary_when_primary_ok() {
117        // secondary would return Err; getting Ok proves secondary was skipped
118        struct SuccessOrBust(u32);
119        impl Pipe for SuccessOrBust {
120            type In = u32;
121            type Out = u32;
122            type Err = &'static str;
123            fn call(&self, _input: u32) -> impl Future<Output = Result<u32, &'static str>> {
124                let value = self.0;
125                async move { Ok(value) }
126            }
127        }
128        let fallback = Fallback {
129            primary: SuccessOrBust(99),
130            secondary: AlwaysFail,
131        };
132        let result = block_on(Pipe::call(&fallback, 0));
133        assert_eq!(result, Ok(99));
134    }
135}