lc_core/runnables/
runnable_trait.rs1use super::RunnableConfig;
8use async_trait::async_trait;
9use futures_util::Stream;
10use std::pin::Pin;
11
12#[async_trait]
39pub trait Runnable<Input: Send + Sync + 'static, Output: Send + Sync + 'static>:
40 Send + Sync
41{
42 type Error: std::error::Error + Send + Sync + 'static;
44
45 async fn invoke(
56 &self,
57 input: Input,
58 config: Option<RunnableConfig>,
59 ) -> Result<Output, Self::Error>;
60
61 async fn batch(
76 &self,
77 inputs: Vec<Input>,
78 config: Option<RunnableConfig>,
79 ) -> Result<Vec<Output>, Self::Error> {
80 use futures_util::StreamExt;
81
82 let limit = config
85 .as_ref()
86 .and_then(|c| c.max_concurrency)
87 .unwrap_or(inputs.len())
88 .max(1);
89
90 let results = futures_util::stream::iter(inputs)
91 .map(|input| {
92 let config = config.clone();
93 async move { self.invoke(input, config).await }
94 })
95 .buffered(limit)
96 .collect::<Vec<Result<_, _>>>()
97 .await;
98
99 results.into_iter().collect()
101 }
102
103 async fn stream(
119 &self,
120 input: Input,
121 config: Option<RunnableConfig>,
122 ) -> Result<Pin<Box<dyn Stream<Item = Result<Output, Self::Error>> + Send>>, Self::Error> {
123 let result = self.invoke(input, config).await?;
127 let stream = futures_util::stream::once(async move { Ok(result) });
128 Ok(Box::pin(stream))
129 }
130
131 async fn transform(
151 &self,
152 input: Pin<Box<dyn Stream<Item = Result<Input, Self::Error>> + Send>>,
153 config: Option<RunnableConfig>,
154 ) -> Result<Pin<Box<dyn Stream<Item = Result<Output, Self::Error>> + Send>>, Self::Error> {
155 use futures_util::StreamExt;
156
157 let mut items = Vec::new();
160 let mut input = input;
161 while let Some(item) = input.next().await {
162 items.push(item?);
163 }
164
165 let mut per_item_streams = Vec::with_capacity(items.len());
166 for item in items {
167 let stream = self.stream(item, config.clone()).await?;
168 per_item_streams.push(stream);
169 }
170
171 let flattened = futures_util::stream::iter(per_item_streams).flatten();
172 Ok(Box::pin(flattened))
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use futures_util::StreamExt;
180
181 struct TestRunnable;
182
183 #[async_trait]
184 impl Runnable<String, String> for TestRunnable {
185 type Error = std::convert::Infallible;
186
187 async fn invoke(
188 &self,
189 input: String,
190 _config: Option<RunnableConfig>,
191 ) -> Result<String, Self::Error> {
192 Ok(format!("processed: {}", input))
193 }
194 }
195
196 #[tokio::test]
197 async fn test_default_stream_returns_single_element() {
198 let runnable = TestRunnable;
199 let mut stream = runnable.stream("test".to_string(), None).await.unwrap();
200
201 let first = stream.next().await;
202 assert!(first.is_some());
203 assert_eq!(first.unwrap().unwrap(), "processed: test");
204
205 let second = stream.next().await;
206 assert!(second.is_none());
207 }
208
209 #[tokio::test]
210 async fn test_invoke_matches_stream_result() {
211 let runnable = TestRunnable;
212
213 let invoke_result = runnable.invoke("hello".to_string(), None).await.unwrap();
214 let mut stream = runnable.stream("hello".to_string(), None).await.unwrap();
215 let stream_result = stream.next().await.unwrap().unwrap();
216
217 assert_eq!(invoke_result, stream_result);
218 }
219
220 #[tokio::test]
221 async fn test_default_transform_maps_elementwise() {
222 let runnable = TestRunnable;
223 let input_stream = Box::pin(futures_util::stream::iter(vec![
224 Ok("first".to_string()),
225 Ok("second".to_string()),
226 Ok("third".to_string()),
227 ]))
228 as Pin<Box<dyn Stream<Item = Result<String, std::convert::Infallible>> + Send>>;
229
230 let mut output_stream = runnable.transform(input_stream, None).await.unwrap();
231
232 let mut results = Vec::new();
234 while let Some(item) = output_stream.next().await {
235 results.push(item.unwrap());
236 }
237 assert_eq!(
238 results,
239 vec![
240 "processed: first".to_string(),
241 "processed: second".to_string(),
242 "processed: third".to_string(),
243 ]
244 );
245 }
246
247 #[tokio::test]
248 async fn test_default_transform_empty_input() {
249 let runnable = TestRunnable;
250 let input_stream = Box::pin(futures_util::stream::empty::<
251 Result<String, std::convert::Infallible>,
252 >())
253 as Pin<Box<dyn Stream<Item = Result<String, std::convert::Infallible>> + Send>>;
254
255 let mut output_stream = runnable.transform(input_stream, None).await.unwrap();
256
257 assert!(output_stream.next().await.is_none());
259 }
260
261 #[tokio::test]
262 async fn test_default_batch_preserves_order() {
263 let runnable = TestRunnable;
264 let results = runnable
265 .batch(
266 vec!["a".to_string(), "b".to_string(), "c".to_string()],
267 None,
268 )
269 .await
270 .unwrap();
271 assert_eq!(
272 results,
273 vec![
274 "processed: a".to_string(),
275 "processed: b".to_string(),
276 "processed: c".to_string(),
277 ]
278 );
279 }
280
281 #[tokio::test]
282 async fn test_default_batch_respects_max_concurrency() {
283 let runnable = TestRunnable;
284 let config = RunnableConfig::new().with_max_concurrency(1);
285 let results = runnable
286 .batch(
287 vec!["x".to_string(), "y".to_string(), "z".to_string()],
288 Some(config),
289 )
290 .await
291 .unwrap();
292 assert_eq!(
293 results,
294 vec![
295 "processed: x".to_string(),
296 "processed: y".to_string(),
297 "processed: z".to_string(),
298 ]
299 );
300 }
301
302 #[tokio::test]
303 async fn test_default_batch_empty_input() {
304 let runnable = TestRunnable;
305 let results = runnable.batch(vec![], None).await.unwrap();
306 assert!(results.is_empty());
307 }
308}