camel_processor/
filter.rs1use std::future::Future;
2use std::pin::Pin;
3
4pub struct FilterSegment {
17 pub predicate: camel_api::PredicateSource,
18 pub body: camel_api::OutcomeSegment,
19}
20
21impl Clone for FilterSegment {
22 fn clone(&self) -> Self {
23 Self {
24 predicate: self.predicate.clone(),
25 body: self.body.clone(),
26 }
27 }
28}
29
30impl camel_api::OutcomePipeline for FilterSegment {
31 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
32 Box::new(self.clone())
33 }
34
35 fn run<'a>(
36 &'a mut self,
37 exchange: camel_api::Exchange,
38 ) -> Pin<Box<dyn Future<Output = camel_api::PipelineOutcome> + Send + 'a>> {
39 Box::pin(async move {
40 match self.predicate.matches(&exchange).await {
41 Ok(true) => self.body.run(exchange).await,
42 Ok(false) => camel_api::PipelineOutcome::Completed(exchange),
43 Err(err) => camel_api::PipelineOutcome::Failed(err),
44 }
45 })
46 }
47}
48
49#[cfg(test)]
50mod tests {
51 use super::*;
52 use crate::test_processor_segment::processor_segment;
53 use camel_api::{
54 Body, BoxProcessor, BoxProcessorExt, CamelError, Exchange, FilterPredicate, Message,
55 OutcomePipeline, PipelineOutcome, PredicateSource, Value,
56 };
57
58 fn passthrough() -> BoxProcessor {
59 BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) }))
60 }
61
62 fn uppercase_body() -> BoxProcessor {
63 BoxProcessor::from_fn(|mut ex: Exchange| {
64 Box::pin(async move {
65 if let Body::Text(s) = &ex.input.body {
66 ex.input.body = Body::Text(s.to_uppercase());
67 }
68 Ok(ex)
69 })
70 })
71 }
72
73 fn failing() -> BoxProcessor {
74 BoxProcessor::from_fn(|_ex| {
75 Box::pin(async { Err(CamelError::ProcessorError("boom".into())) })
76 })
77 }
78
79 fn predicate(f: impl Fn(&Exchange) -> bool + Send + Sync + 'static) -> PredicateSource {
80 PredicateSource::Sync(FilterPredicate::new(f))
81 }
82
83 fn segment(predicate: PredicateSource, body: BoxProcessor) -> FilterSegment {
84 FilterSegment {
85 predicate,
86 body: processor_segment(body),
87 }
88 }
89
90 fn completed_exchange(outcome: PipelineOutcome) -> Exchange {
91 match outcome {
92 PipelineOutcome::Completed(ex) => ex,
93 other => panic!("expected PipelineOutcome::Completed, got {other:?}"),
94 }
95 }
96
97 #[tokio::test]
99 async fn filter_segment_passes_matching_exchange() {
100 let mut seg = segment(
101 predicate(|ex: &Exchange| ex.input.header("active").is_some()),
102 uppercase_body(),
103 );
104 let mut ex = Exchange::new(Message::new("hello"));
105 ex.input.set_header("active", Value::Bool(true));
106 let result = completed_exchange(seg.run(ex).await);
107 assert_eq!(result.input.body.as_text(), Some("HELLO"));
108 }
109
110 #[tokio::test]
112 async fn filter_segment_blocks_non_matching_exchange() {
113 let mut seg = segment(
114 predicate(|ex: &Exchange| ex.input.header("active").is_some()),
115 uppercase_body(),
116 );
117 let ex = Exchange::new(Message::new("hello"));
118 let result = completed_exchange(seg.run(ex).await);
119 assert_eq!(result.input.body.as_text(), Some("hello"));
121 }
122
123 #[tokio::test]
125 async fn filter_segment_sub_pipeline_transforms_body() {
126 let mut seg = segment(predicate(|_: &Exchange| true), uppercase_body());
127 let ex = Exchange::new(Message::new("world"));
128 let result = completed_exchange(seg.run(ex).await);
129 assert_eq!(result.input.body.as_text(), Some("WORLD"));
130 }
131
132 #[tokio::test]
134 async fn filter_segment_sub_pipeline_error_propagates() {
135 let mut seg = segment(predicate(|_: &Exchange| true), failing());
136 let ex = Exchange::new(Message::new("x"));
137 match seg.run(ex).await {
138 PipelineOutcome::Failed(err) => {
139 assert!(err.to_string().contains("boom"), "unexpected error: {err}");
140 }
141 other => panic!("expected PipelineOutcome::Failed, got {other:?}"),
142 }
143 }
144
145 #[tokio::test]
147 async fn filter_segment_predicate_receives_original_exchange() {
148 let mut seg = segment(
149 predicate(|ex: &Exchange| ex.input.body.as_text() == Some("check")),
150 uppercase_body(),
151 );
152 let ex = Exchange::new(Message::new("check"));
153 let result = completed_exchange(seg.run(ex).await);
154 assert_eq!(result.input.body.as_text(), Some("CHECK"));
155 }
156
157 #[tokio::test]
159 async fn filter_segment_clone_is_independent() {
160 let seg = segment(predicate(|_: &Exchange| true), passthrough());
161 let mut clone = seg.clone();
162 let ex = Exchange::new(Message::new("hi"));
163 let result = completed_exchange(clone.run(ex).await);
164 assert_eq!(result.input.body.as_text(), Some("hi"));
165 }
166
167 use std::sync::Arc;
170 use std::sync::atomic::{AtomicU32, Ordering};
171
172 use camel_api::ExpressionErrorClass;
173
174 fn expression_failed() -> CamelError {
175 CamelError::ExpressionFailed {
176 language: "rhai".to_string(),
177 route_id: "r1".to_string(),
178 step_id: "step#0".to_string(),
179 verb: "filter".to_string(),
180 class: ExpressionErrorClass::Runtime,
181 position: None,
182 conversion: None,
183 cause: None,
184 }
185 }
186
187 fn async_err_predicate(err: CamelError) -> PredicateSource {
188 PredicateSource::Async(Arc::new(move |_: &Exchange| {
189 let err = err.clone();
190 Box::pin(async move { Err(err) }) as camel_api::BoxBoolFuture
191 }))
192 }
193
194 struct RecordRun(Arc<AtomicU32>);
195
196 impl camel_api::OutcomePipeline for RecordRun {
197 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
198 Box::new(RecordRun(Arc::clone(&self.0)))
199 }
200 fn run<'a>(
201 &'a mut self,
202 exchange: Exchange,
203 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
204 let c = Arc::clone(&self.0);
205 Box::pin(async move {
206 c.fetch_add(1, Ordering::SeqCst);
207 PipelineOutcome::Completed(exchange)
208 })
209 }
210 }
211
212 #[tokio::test]
213 async fn filter_segment_propagates_predicate_error() {
214 let body_calls = Arc::new(AtomicU32::new(0));
215 let mut seg = FilterSegment {
216 predicate: async_err_predicate(expression_failed()),
217 body: camel_api::OutcomeSegment::new(Box::new(RecordRun(Arc::clone(&body_calls)))),
218 };
219 let outcome = seg.run(Exchange::default()).await;
220 match outcome {
221 PipelineOutcome::Failed(err) => {
222 assert!(
223 matches!(err, CamelError::ExpressionFailed { .. }),
224 "expected ExpressionFailed, got {err:?}"
225 );
226 }
227 other => panic!("expected PipelineOutcome::Failed, got {other:?}"),
228 }
229 assert_eq!(
230 body_calls.load(Ordering::SeqCst),
231 0,
232 "filter body must not run when the predicate errors"
233 );
234 }
235}