camel_processor/
loop_eip.rs1use std::future::Future;
2use std::pin::Pin;
3use std::task::{Context, Poll};
4
5use tower::{Service, ServiceExt};
6
7use camel_api::loop_eip::{LoopConfig, LoopMode, MAX_LOOP_ITERATIONS};
8use camel_api::{BoxProcessor, CamelError, Exchange, Value};
9
10pub const CAMEL_LOOP_INDEX: &str = "CamelLoopIndex";
11pub const CAMEL_LOOP_SIZE: &str = "CamelLoopSize";
12
13#[derive(Clone)]
14pub struct LoopService {
15 config: LoopConfig,
16 sub_pipeline: BoxProcessor,
17}
18
19impl LoopService {
20 pub fn new(config: LoopConfig, sub_pipeline: BoxProcessor) -> Self {
21 Self {
22 config,
23 sub_pipeline,
24 }
25 }
26}
27
28impl Service<Exchange> for LoopService {
29 type Response = Exchange;
30 type Error = CamelError;
31 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
32
33 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
34 Poll::Ready(Ok(()))
35 }
36
37 fn call(&mut self, mut exchange: Exchange) -> Self::Future {
38 let config = self.config.clone();
39 let mut pipeline = self.sub_pipeline.clone();
40
41 Box::pin(async move {
42 match config.mode {
43 LoopMode::Count(n) => {
44 let n_clamped = n.min(MAX_LOOP_ITERATIONS);
51 if n > MAX_LOOP_ITERATIONS {
52 tracing::warn!(
53 requested = n,
54 clamped_to = MAX_LOOP_ITERATIONS,
55 "LoopMode::Count exceeded MAX_LOOP_ITERATIONS; clamping"
56 );
57 }
58 exchange.set_property(CAMEL_LOOP_SIZE, Value::from(n_clamped as u64));
59 for i in 0..n_clamped {
60 exchange.set_property(CAMEL_LOOP_INDEX, Value::from(i as u64));
61 exchange = pipeline.ready().await?.call(exchange).await?;
62 }
63 }
64 LoopMode::While(ref predicate) => {
65 exchange.set_property(CAMEL_LOOP_SIZE, Value::from(0u64));
66 for i in 0..MAX_LOOP_ITERATIONS {
67 if !predicate(&exchange) {
68 break;
69 }
70 exchange.set_property(CAMEL_LOOP_INDEX, Value::from(i as u64));
71 exchange = pipeline.ready().await?.call(exchange).await?;
72 }
73 if predicate(&exchange) {
74 tracing::warn!(
75 "Loop while-mode hit MAX_LOOP_ITERATIONS ({}) safety guard. Predicate still true.",
76 MAX_LOOP_ITERATIONS
77 );
78 }
79 }
80 }
81 Ok(exchange)
82 })
83 }
84}
85
86pub struct LoopSegment {
98 pub config: camel_api::loop_eip::LoopConfig,
99 pub body: camel_api::OutcomeSegment,
100}
101
102impl Clone for LoopSegment {
103 fn clone(&self) -> Self {
104 Self {
105 config: self.config.clone(),
106 body: self.body.clone(),
107 }
108 }
109}
110
111impl camel_api::OutcomePipeline for LoopSegment {
112 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
113 Box::new(self.clone())
114 }
115
116 fn run<'a>(
117 &'a mut self,
118 exchange: camel_api::Exchange,
119 ) -> Pin<Box<dyn Future<Output = camel_api::PipelineOutcome> + Send + 'a>> {
120 use camel_api::loop_eip::MAX_LOOP_ITERATIONS;
121 use camel_api::{PipelineOutcome, Value};
122
123 let config = self.config.clone();
124 let body = &mut self.body;
125
126 Box::pin(async move {
127 match config.mode {
128 camel_api::loop_eip::LoopMode::Count(n) => {
129 let n_clamped = n.min(MAX_LOOP_ITERATIONS);
130 if n > MAX_LOOP_ITERATIONS {
131 tracing::warn!(
132 requested = n,
133 clamped_to = MAX_LOOP_ITERATIONS,
134 "LoopMode::Count exceeded MAX_LOOP_ITERATIONS; clamping"
135 );
136 }
137 let mut ex = exchange;
138 ex.set_property(CAMEL_LOOP_SIZE, Value::from(n_clamped as u64));
139 for i in 0..n_clamped {
140 ex.set_property(CAMEL_LOOP_INDEX, Value::from(i as u64));
141 match body.run(ex).await {
142 PipelineOutcome::Completed(next) => {
143 ex = next;
144 }
145 other => return other,
146 }
147 }
148 PipelineOutcome::Completed(ex)
149 }
150 camel_api::loop_eip::LoopMode::While(ref predicate) => {
151 let mut ex = exchange;
152 ex.set_property(CAMEL_LOOP_SIZE, Value::from(0u64));
153 let mut i = 0u64;
154 while i < MAX_LOOP_ITERATIONS as u64 {
155 if !predicate(&ex) {
156 break;
157 }
158 ex.set_property(CAMEL_LOOP_INDEX, Value::from(i));
159 match body.run(ex).await {
160 PipelineOutcome::Completed(next) => {
161 ex = next;
162 }
163 other => return other,
164 }
165 i += 1;
166 }
167 if predicate(&ex) {
168 tracing::warn!(
169 "Loop while-mode hit MAX_LOOP_ITERATIONS ({}) safety guard. Predicate still true.",
170 MAX_LOOP_ITERATIONS
171 );
172 }
173 PipelineOutcome::Completed(ex)
174 }
175 }
176 })
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use std::sync::atomic::{AtomicUsize, Ordering};
183 use std::sync::{Arc, Mutex};
184
185 use camel_api::loop_eip::{LoopConfig, LoopMode, MAX_LOOP_ITERATIONS};
186 use camel_api::{
187 Body, BoxProcessor, BoxProcessorExt, CamelError, Exchange, IdentityProcessor, Message,
188 };
189 use tower::{Service, ServiceExt};
190
191 use super::{CAMEL_LOOP_INDEX, CAMEL_LOOP_SIZE, LoopService};
192
193 fn identity_pipeline() -> BoxProcessor {
194 BoxProcessor::new(IdentityProcessor)
195 }
196
197 fn counter_pipeline(counter: Arc<AtomicUsize>) -> BoxProcessor {
198 BoxProcessor::from_fn(move |exchange: Exchange| {
199 let counter = Arc::clone(&counter);
200 Box::pin(async move {
201 counter.fetch_add(1, Ordering::SeqCst);
202 Ok(exchange)
203 })
204 })
205 }
206
207 #[tokio::test]
208 async fn test_loop_count_iterates_n_times() {
209 let counter = Arc::new(AtomicUsize::new(0));
210 let config = LoopConfig::new(LoopMode::Count(3));
211 let mut service = LoopService::new(config, counter_pipeline(Arc::clone(&counter)));
212
213 let exchange = Exchange::new(Message::new("test"));
214 let result = service.ready().await.unwrap().call(exchange).await;
215
216 assert!(result.is_ok());
217 assert_eq!(counter.load(Ordering::SeqCst), 3);
218 }
219
220 #[tokio::test]
221 async fn test_loop_count_sets_properties() {
222 let seen_indices = Arc::new(Mutex::new(Vec::<u64>::new()));
223 let seen_indices_for_pipeline = Arc::clone(&seen_indices);
224
225 let pipeline = BoxProcessor::from_fn(move |exchange: Exchange| {
226 let seen_indices = Arc::clone(&seen_indices_for_pipeline);
227 Box::pin(async move {
228 if let Some(index) = exchange.property(CAMEL_LOOP_INDEX).and_then(|v| v.as_u64()) {
229 seen_indices.lock().unwrap().push(index);
230 }
231 Ok(exchange)
232 })
233 });
234
235 let config = LoopConfig::new(LoopMode::Count(3));
236 let mut service = LoopService::new(config, pipeline);
237
238 let exchange = Exchange::new(Message::new("test"));
239 let result = service.ready().await.unwrap().call(exchange).await.unwrap();
240
241 assert_eq!(*seen_indices.lock().unwrap(), vec![0, 1, 2]);
242 assert_eq!(
243 result.property(CAMEL_LOOP_SIZE).and_then(|v| v.as_u64()),
244 Some(3)
245 );
246 }
247
248 #[tokio::test]
249 async fn test_loop_count_zero_is_noop() {
250 let config = LoopConfig::new(LoopMode::Count(0));
251 let mut service = LoopService::new(config, identity_pipeline());
252
253 let exchange = Exchange::new(Message::new("test"));
254 let result = service.ready().await.unwrap().call(exchange).await.unwrap();
255
256 assert_eq!(result.input.body.as_text(), Some("test"));
257 assert_eq!(
258 result.property(CAMEL_LOOP_SIZE).and_then(|v| v.as_u64()),
259 Some(0)
260 );
261 assert!(result.property(CAMEL_LOOP_INDEX).is_none());
262 }
263
264 #[tokio::test]
265 async fn test_loop_while_stops_when_predicate_false() {
266 let counter = Arc::new(AtomicUsize::new(0));
267
268 let predicate = Arc::new(|exchange: &Exchange| {
269 exchange
270 .property("iterations")
271 .and_then(|v| v.as_u64())
272 .unwrap_or(0)
273 < 2
274 });
275
276 let counter_for_pipeline = Arc::clone(&counter);
277 let pipeline = BoxProcessor::from_fn(move |mut exchange: Exchange| {
278 let counter = Arc::clone(&counter_for_pipeline);
279 Box::pin(async move {
280 let current = exchange
281 .property("iterations")
282 .and_then(|v| v.as_u64())
283 .unwrap_or(0);
284 exchange.set_property("iterations", current + 1);
285 counter.fetch_add(1, Ordering::SeqCst);
286 Ok(exchange)
287 })
288 });
289
290 let config = LoopConfig::new(LoopMode::While(predicate));
291 let mut service = LoopService::new(config, pipeline);
292
293 let exchange = Exchange::new(Message::new("test"));
294 let result = service.ready().await.unwrap().call(exchange).await.unwrap();
295
296 assert_eq!(counter.load(Ordering::SeqCst), 2);
297 assert_eq!(
298 result.property("iterations").and_then(|v| v.as_u64()),
299 Some(2)
300 );
301 assert_eq!(
302 result.property(CAMEL_LOOP_INDEX).and_then(|v| v.as_u64()),
303 Some(1)
304 );
305 assert_eq!(
306 result.property(CAMEL_LOOP_SIZE).and_then(|v| v.as_u64()),
307 Some(0)
308 );
309 }
310
311 #[tokio::test]
312 async fn test_loop_while_respects_max_iterations() {
313 let counter = Arc::new(AtomicUsize::new(0));
314 let predicate = Arc::new(|_exchange: &Exchange| true);
315 let config = LoopConfig::new(LoopMode::While(predicate));
316 let mut service = LoopService::new(config, counter_pipeline(Arc::clone(&counter)));
317
318 let exchange = Exchange::new(Message::new("test"));
319 let result = service.ready().await.unwrap().call(exchange).await;
320
321 assert!(result.is_ok());
322 assert_eq!(counter.load(Ordering::SeqCst), MAX_LOOP_ITERATIONS);
323 }
324
325 #[tokio::test]
326 async fn test_loop_error_propagation() {
327 let pipeline = BoxProcessor::from_fn(|_exchange: Exchange| {
328 Box::pin(async { Err(CamelError::ProcessorError("boom".into())) })
329 });
330
331 let config = LoopConfig::new(LoopMode::Count(3));
332 let mut service = LoopService::new(config, pipeline);
333
334 let exchange = Exchange::new(Message::new("test"));
335 let result = service.ready().await.unwrap().call(exchange).await;
336
337 assert!(matches!(result, Err(CamelError::ProcessorError(msg)) if msg == "boom"));
338 }
339
340 #[tokio::test]
348 async fn test_loop_count_clamped_to_max_iterations() {
349 let counter = Arc::new(AtomicUsize::new(0));
350 let config = LoopConfig::new(LoopMode::Count(usize::MAX));
351 let mut service = LoopService::new(config, counter_pipeline(Arc::clone(&counter)));
352
353 let exchange = Exchange::new(Message::new("test"));
354 let result = service.ready().await.unwrap().call(exchange).await;
355
356 assert!(result.is_ok());
357 assert_eq!(counter.load(Ordering::SeqCst), MAX_LOOP_ITERATIONS);
359 assert_eq!(
361 result
362 .unwrap()
363 .property(CAMEL_LOOP_SIZE)
364 .and_then(|v| v.as_u64()),
365 Some(MAX_LOOP_ITERATIONS as u64)
366 );
367 }
368
369 #[tokio::test]
370 async fn test_loop_pipeline_chaining() {
371 let pipeline = BoxProcessor::from_fn(|mut exchange: Exchange| {
372 Box::pin(async move {
373 if let Body::Text(s) = &exchange.input.body {
374 exchange.input.body = Body::Text(format!("{s}x"));
375 }
376 Ok(exchange)
377 })
378 });
379
380 let config = LoopConfig::new(LoopMode::Count(3));
381 let mut service = LoopService::new(config, pipeline);
382
383 let exchange = Exchange::new(Message::new("start"));
384 let result = service.ready().await.unwrap().call(exchange).await.unwrap();
385
386 assert_eq!(result.input.body.as_text(), Some("startxxx"));
387 }
388}