Skip to main content

lc_core/runnables/
lambda.rs

1// lc-core/src/runnables/lambda.rs
2//! RunnableLambda - wraps closures as Runnable steps.
3//!
4//! `RunnableLambda` allows inline closures to participate in LCEL
5//! pipelines. It supports both synchronous and asynchronous closures.
6
7use super::config::RunnableConfig;
8use super::error::LcelError;
9use super::runnable_trait::Runnable;
10use async_trait::async_trait;
11use std::future::Future;
12use std::pin::Pin;
13use std::sync::Arc;
14
15/// Type alias for the boxed async closure stored in `RunnableLambda`.
16type AsyncFn<I, O> =
17    Arc<dyn Fn(I) -> Pin<Box<dyn Future<Output = Result<O, LcelError>> + Send>> + Send + Sync>;
18
19/// A `Runnable` that wraps a closure.
20///
21/// Created via `RunnableLambda::new_sync` or `RunnableLambda::new_async`.
22///
23/// # Example
24///
25/// ```rust,ignore
26/// let doubler = RunnableLambda::new_sync(|x: i32| x * 2);
27/// let result = doubler.invoke(5, None).await?; // 10
28/// ```
29pub struct RunnableLambda<I: Send + Sync + 'static, O: Send + Sync + 'static> {
30    func: AsyncFn<I, O>,
31}
32
33impl<I: Send + Sync + 'static, O: Send + Sync + 'static> std::fmt::Debug for RunnableLambda<I, O> {
34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        f.debug_struct("RunnableLambda")
36            .field("input", &std::any::type_name::<I>())
37            .field("output", &std::any::type_name::<O>())
38            .finish()
39    }
40}
41
42impl<I: Send + Sync + 'static, O: Send + Sync + 'static> RunnableLambda<I, O> {
43    /// Create from a synchronous (blocking) closure.
44    ///
45    /// The closure's output is automatically wrapped in `Ok(...)`.
46    ///
47    /// # Example
48    ///
49    /// ```rust,ignore
50    /// let upper = RunnableLambda::new_sync(|s: String| s.to_uppercase());
51    /// ```
52    pub fn new_sync<F>(func: F) -> Self
53    where
54        F: Fn(I) -> O + Send + Sync + 'static,
55    {
56        let func = Arc::new(move |input: I| {
57            let result = func(input);
58            Box::pin(async move { Ok(result) })
59                as Pin<Box<dyn Future<Output = Result<O, LcelError>> + Send>>
60        });
61        Self { func }
62    }
63
64    /// Create from a synchronous closure that can fail.
65    ///
66    /// The closure returns `Result<O, LcelError>`.
67    pub fn new_sync_fallible<F>(func: F) -> Self
68    where
69        F: Fn(I) -> Result<O, LcelError> + Send + Sync + 'static,
70    {
71        let func = Arc::new(move |input: I| {
72            let result = func(input);
73            Box::pin(async move { result })
74                as Pin<Box<dyn Future<Output = Result<O, LcelError>> + Send>>
75        });
76        Self { func }
77    }
78
79    /// Create from an async closure.
80    ///
81    /// # Example
82    ///
83    /// ```rust,ignore
84    /// let fetch = RunnableLambda::new_async(|url: String| async move {
85    ///     reqwest::get(&url).await?.text().await.map_err(|e| LcelError::Other(e.to_string()))
86    /// });
87    /// ```
88    pub fn new_async<F, Fut>(func: F) -> Self
89    where
90        F: Fn(I) -> Fut + Send + Sync + 'static,
91        Fut: Future<Output = Result<O, LcelError>> + Send + 'static,
92    {
93        let func = Arc::new(move |input: I| {
94            let fut = func(input);
95            Box::pin(fut) as Pin<Box<dyn Future<Output = Result<O, LcelError>> + Send>>
96        });
97        Self { func }
98    }
99}
100
101#[async_trait]
102impl<I: Send + Sync + 'static, O: Send + Sync + 'static> Runnable<I, O> for RunnableLambda<I, O> {
103    type Error = LcelError;
104
105    async fn invoke(&self, input: I, _config: Option<RunnableConfig>) -> Result<O, LcelError> {
106        (self.func)(input).await
107    }
108
109    // batch, stream, transform all use default implementations
110    // (sequential invoke, single-element stream, buffer-and-invoke)
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use futures_util::StreamExt;
117
118    #[tokio::test]
119    async fn sync_closure_works() {
120        let lambda = RunnableLambda::new_sync(|x: i32| x * 3);
121        let result = lambda.invoke(7, None).await.unwrap();
122        assert_eq!(result, 21);
123    }
124
125    #[tokio::test]
126    async fn sync_fallible_closure_ok() {
127        let lambda = RunnableLambda::new_sync_fallible(|x: i32| {
128            if x > 0 {
129                Ok(x * 2)
130            } else {
131                Err(LcelError::Other("must be positive".to_string()))
132            }
133        });
134        assert_eq!(lambda.invoke(5, None).await.unwrap(), 10);
135    }
136
137    #[tokio::test]
138    async fn sync_fallible_closure_err() {
139        let lambda = RunnableLambda::new_sync_fallible(|x: i32| {
140            if x > 0 {
141                Ok(x * 2)
142            } else {
143                Err(LcelError::Other("must be positive".to_string()))
144            }
145        });
146        let result = lambda.invoke(-1, None).await;
147        assert!(result.is_err());
148    }
149
150    #[tokio::test]
151    async fn async_closure_works() {
152        let lambda = RunnableLambda::new_async(|x: i32| async move {
153            tokio::task::spawn_blocking(move || x + 100)
154                .await
155                .map_err(|e| LcelError::Other(e.to_string()))
156        });
157        let result = lambda.invoke(5, None).await.unwrap();
158        assert_eq!(result, 105);
159    }
160
161    #[tokio::test]
162    async fn stream_uses_default() {
163        let lambda = RunnableLambda::new_sync(|x: i32| x + 1);
164        let mut stream = lambda.stream(9, None).await.unwrap();
165        let result = stream.next().await.unwrap().unwrap();
166        assert_eq!(result, 10);
167        assert!(stream.next().await.is_none());
168    }
169
170    #[tokio::test]
171    async fn batch_uses_default() {
172        let lambda = RunnableLambda::new_sync(|x: i32| x * 10);
173        let results = lambda.batch(vec![1, 2, 3], None).await.unwrap();
174        assert_eq!(results, vec![10, 20, 30]);
175    }
176}