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