camel_processor/
filter.rs1use std::future::Future;
2use std::pin::Pin;
3use std::task::{Context, Poll};
4
5use tower::Service;
6use tower::ServiceExt;
7
8use camel_api::{BoxProcessor, CamelError, Exchange, FilterPredicate, PredicateSource};
9
10#[derive(Clone)]
17pub struct FilterService {
18 predicate: PredicateSource,
19 sub_pipeline: BoxProcessor,
20}
21
22impl FilterService {
23 pub fn new(
25 predicate: impl Fn(&Exchange) -> bool + Send + Sync + 'static,
26 sub_pipeline: BoxProcessor,
27 ) -> Self {
28 Self {
29 predicate: PredicateSource::Sync(FilterPredicate::new(predicate)),
30 sub_pipeline,
31 }
32 }
33
34 pub fn from_predicate(predicate: PredicateSource, sub_pipeline: BoxProcessor) -> Self {
36 Self {
37 predicate,
38 sub_pipeline,
39 }
40 }
41}
42
43impl Service<Exchange> for FilterService {
44 type Response = Exchange;
45 type Error = CamelError;
46 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
47
48 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
49 self.sub_pipeline.poll_ready(cx)
50 }
51
52 fn call(&mut self, exchange: Exchange) -> Self::Future {
53 let predicate = self.predicate.clone();
56 let mut sub_pipeline = self.sub_pipeline.clone();
57 Box::pin(async move {
58 match predicate.matches(&exchange).await {
59 Ok(true) => sub_pipeline.ready().await?.call(exchange).await,
60 Ok(false) => Ok(exchange),
61 Err(err) => Err(err),
62 }
63 })
64 }
65}
66
67pub struct FilterSegment {
82 pub predicate: camel_api::PredicateSource,
83 pub body: camel_api::OutcomeSegment,
84}
85
86impl Clone for FilterSegment {
87 fn clone(&self) -> Self {
88 Self {
89 predicate: self.predicate.clone(),
90 body: self.body.clone(),
91 }
92 }
93}
94
95impl camel_api::OutcomePipeline for FilterSegment {
96 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
97 Box::new(self.clone())
98 }
99
100 fn run<'a>(
101 &'a mut self,
102 exchange: camel_api::Exchange,
103 ) -> Pin<Box<dyn Future<Output = camel_api::PipelineOutcome> + Send + 'a>> {
104 Box::pin(async move {
105 match self.predicate.matches(&exchange).await {
106 Ok(true) => self.body.run(exchange).await,
107 Ok(false) => camel_api::PipelineOutcome::Completed(exchange),
108 Err(err) => camel_api::PipelineOutcome::Failed(err),
109 }
110 })
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use camel_api::{Body, BoxProcessorExt, Message, Value};
118 use tower::ServiceExt;
119
120 fn passthrough() -> BoxProcessor {
121 BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) }))
122 }
123
124 fn uppercase_body() -> BoxProcessor {
125 BoxProcessor::from_fn(|mut ex: Exchange| {
126 Box::pin(async move {
127 if let Body::Text(s) = &ex.input.body {
128 ex.input.body = Body::Text(s.to_uppercase());
129 }
130 Ok(ex)
131 })
132 })
133 }
134
135 fn failing() -> BoxProcessor {
136 BoxProcessor::from_fn(|_ex| {
137 Box::pin(async { Err(CamelError::ProcessorError("boom".into())) })
138 })
139 }
140
141 #[tokio::test]
143 async fn test_filter_passes_matching_exchange() {
144 let mut svc = FilterService::new(
145 |ex: &Exchange| ex.input.header("active").is_some(),
146 uppercase_body(),
147 );
148 let mut ex = Exchange::new(Message::new("hello"));
149 ex.input.set_header("active", Value::Bool(true));
150 let result = svc.ready().await.unwrap().call(ex).await.unwrap();
151 assert_eq!(result.input.body.as_text(), Some("HELLO"));
152 }
153
154 #[tokio::test]
156 async fn test_filter_blocks_non_matching_exchange() {
157 let mut svc = FilterService::new(
158 |ex: &Exchange| ex.input.header("active").is_some(),
159 uppercase_body(),
160 );
161 let ex = Exchange::new(Message::new("hello"));
162 let result = svc.ready().await.unwrap().call(ex).await.unwrap();
163 assert_eq!(result.input.body.as_text(), Some("hello"));
165 }
166
167 #[tokio::test]
169 async fn test_filter_sub_pipeline_transforms_body() {
170 let mut svc = FilterService::new(|_: &Exchange| true, uppercase_body());
171 let ex = Exchange::new(Message::new("world"));
172 let result = svc.ready().await.unwrap().call(ex).await.unwrap();
173 assert_eq!(result.input.body.as_text(), Some("WORLD"));
174 }
175
176 #[tokio::test]
178 async fn test_filter_sub_pipeline_error_propagates() {
179 let mut svc = FilterService::new(|_: &Exchange| true, failing());
180 let ex = Exchange::new(Message::new("x"));
181 let result = svc.ready().await.unwrap().call(ex).await;
182 assert!(result.is_err());
183 assert!(result.unwrap_err().to_string().contains("boom"));
184 }
185
186 #[tokio::test]
188 async fn test_filter_predicate_receives_original_exchange() {
189 let mut svc = FilterService::new(
190 |ex: &Exchange| ex.input.body.as_text() == Some("check"),
191 uppercase_body(),
192 );
193 let ex = Exchange::new(Message::new("check"));
194 let result = svc.ready().await.unwrap().call(ex).await.unwrap();
195 assert_eq!(result.input.body.as_text(), Some("CHECK"));
196 }
197
198 #[tokio::test]
200 async fn test_filter_clone_is_independent() {
201 let svc = FilterService::new(|_: &Exchange| true, passthrough());
202 let mut clone = svc.clone();
203 let ex = Exchange::new(Message::new("hi"));
204 let result = clone.ready().await.unwrap().call(ex).await.unwrap();
205 assert_eq!(result.input.body.as_text(), Some("hi"));
206 }
207
208 use std::sync::Arc;
211 use std::sync::atomic::{AtomicU32, Ordering};
212
213 use camel_api::outcome_pipeline::OutcomePipeline as _;
214 use camel_api::{ExpressionErrorClass, PipelineOutcome, PredicateSource};
215
216 fn expression_failed() -> CamelError {
217 CamelError::ExpressionFailed {
218 language: "rhai".to_string(),
219 route_id: "r1".to_string(),
220 step_id: "step#0".to_string(),
221 verb: "filter".to_string(),
222 class: ExpressionErrorClass::Runtime,
223 position: None,
224 conversion: None,
225 cause: None,
226 }
227 }
228
229 fn async_err_predicate(err: CamelError) -> PredicateSource {
230 PredicateSource::Async(Arc::new(move |_: &Exchange| {
231 let err = err.clone();
232 Box::pin(async move { Err(err) }) as camel_api::BoxBoolFuture
233 }))
234 }
235
236 struct RecordRun(Arc<AtomicU32>);
237
238 impl camel_api::OutcomePipeline for RecordRun {
239 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
240 Box::new(RecordRun(Arc::clone(&self.0)))
241 }
242 fn run<'a>(
243 &'a mut self,
244 exchange: Exchange,
245 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
246 let c = Arc::clone(&self.0);
247 Box::pin(async move {
248 c.fetch_add(1, Ordering::SeqCst);
249 PipelineOutcome::Completed(exchange)
250 })
251 }
252 }
253
254 #[tokio::test]
255 async fn filter_segment_propagates_predicate_error() {
256 let body_calls = Arc::new(AtomicU32::new(0));
257 let mut seg = FilterSegment {
258 predicate: async_err_predicate(expression_failed()),
259 body: camel_api::OutcomeSegment::new(Box::new(RecordRun(Arc::clone(&body_calls)))),
260 };
261 let outcome = seg.run(Exchange::default()).await;
262 match outcome {
263 PipelineOutcome::Failed(err) => {
264 assert!(
265 matches!(err, CamelError::ExpressionFailed { .. }),
266 "expected ExpressionFailed, got {err:?}"
267 );
268 }
269 other => panic!("expected PipelineOutcome::Failed, got {other:?}"),
270 }
271 assert_eq!(
272 body_calls.load(Ordering::SeqCst),
273 0,
274 "filter body must not run when the predicate errors"
275 );
276 }
277}