synwire_core/runnables/
fallbacks.rs1use crate::BoxFuture;
4use crate::error::SynwireError;
5use crate::runnables::config::RunnableConfig;
6use crate::runnables::core::RunnableCore;
7use serde_json::Value;
8
9pub struct RunnableWithFallbacks {
15 primary: Box<dyn RunnableCore>,
16 fallbacks: Vec<Box<dyn RunnableCore>>,
17}
18
19impl RunnableWithFallbacks {
20 pub fn new(primary: Box<dyn RunnableCore>, fallbacks: Vec<Box<dyn RunnableCore>>) -> Self {
22 Self { primary, fallbacks }
23 }
24}
25
26impl RunnableCore for RunnableWithFallbacks {
27 fn invoke<'a>(
28 &'a self,
29 input: Value,
30 config: Option<&'a RunnableConfig>,
31 ) -> BoxFuture<'a, Result<Value, SynwireError>> {
32 Box::pin(async move {
33 let mut last_error = match self.primary.invoke(input.clone(), config).await {
34 Ok(v) => return Ok(v),
35 Err(e) => e,
36 };
37
38 for fallback in &self.fallbacks {
39 match fallback.invoke(input.clone(), config).await {
40 Ok(v) => return Ok(v),
41 Err(e) => last_error = e,
42 }
43 }
44
45 Err(last_error)
46 })
47 }
48
49 #[allow(clippy::unnecessary_literal_bound)]
50 fn name(&self) -> &str {
51 "RunnableWithFallbacks"
52 }
53}
54
55pub fn with_fallbacks(
57 primary: Box<dyn RunnableCore>,
58 fallbacks: Vec<Box<dyn RunnableCore>>,
59) -> RunnableWithFallbacks {
60 RunnableWithFallbacks::new(primary, fallbacks)
61}
62
63#[cfg(test)]
64#[allow(clippy::unwrap_used)]
65mod tests {
66 use super::*;
67 use crate::runnables::lambda::RunnableLambda;
68
69 #[tokio::test]
70 async fn test_fallback_on_primary_failure() {
71 let failing = RunnableLambda::new(|_: Value| {
72 Box::pin(async { Err(SynwireError::Other("primary failed".into())) })
73 });
74
75 let fallback =
76 RunnableLambda::new(|_: Value| Box::pin(async { Ok(Value::from("fallback_result")) }));
77
78 let composed = with_fallbacks(Box::new(failing), vec![Box::new(fallback)]);
79 let result = composed.invoke(Value::from("input"), None).await.unwrap();
80 assert_eq!(result, Value::from("fallback_result"));
81 }
82
83 #[tokio::test]
84 async fn test_primary_succeeds_no_fallback() {
85 let primary = RunnableLambda::new(|v: Value| Box::pin(async { Ok(v) }));
86 let fallback =
87 RunnableLambda::new(|_: Value| Box::pin(async { Ok(Value::from("should_not_reach")) }));
88
89 let composed = with_fallbacks(Box::new(primary), vec![Box::new(fallback)]);
90 let result = composed
91 .invoke(Value::from("original"), None)
92 .await
93 .unwrap();
94 assert_eq!(result, Value::from("original"));
95 }
96
97 #[tokio::test]
98 async fn test_all_fallbacks_fail() {
99 let failing = RunnableLambda::new(|_: Value| {
100 Box::pin(async { Err(SynwireError::Other("fail".into())) })
101 });
102 let also_failing = RunnableLambda::new(|_: Value| {
103 Box::pin(async { Err(SynwireError::Other("also fail".into())) })
104 });
105
106 let composed = with_fallbacks(Box::new(failing), vec![Box::new(also_failing)]);
107 let result = composed.invoke(Value::from("input"), None).await;
108 assert!(result.is_err());
109 }
110}