Skip to main content

camel_processor/
sort.rs

1use std::cmp::Ordering;
2use std::future::Future;
3use std::pin::Pin;
4use std::sync::Arc;
5use std::task::{Context, Poll};
6
7use tower::Service;
8
9use camel_api::{CamelError, Exchange};
10
11/// Total order over a JSON value used as a sort key.
12/// Tier: Null < Bool < Number < String.
13/// Array/Object keys are REJECTED at extraction.
14#[derive(Debug, Clone)]
15pub struct SortKey(pub serde_json::Value);
16
17impl Ord for SortKey {
18    fn cmp(&self, other: &Self) -> Ordering {
19        use serde_json::Value::*;
20        match (&self.0, &other.0) {
21            (Null, Null) => Ordering::Equal,
22            (Null, _) => Ordering::Less,
23            (_, Null) => Ordering::Greater,
24            (Bool(a), Bool(b)) => a.cmp(b),
25            (Bool(_), Number(_)) | (Bool(_), String(_)) => Ordering::Less,
26            (Number(_), Bool(_)) | (String(_), Bool(_)) => Ordering::Greater,
27            // ponytail: serde_json::Number cannot represent NaN/Infinity; this branch
28            // is defensive and will never fire for valid JSON input.
29            (Number(a), Number(b)) => {
30                let af = a.as_f64().unwrap_or(f64::INFINITY);
31                let bf = b.as_f64().unwrap_or(f64::INFINITY);
32                af.partial_cmp(&bf)
33                    .unwrap_or_else(|| af.is_nan().cmp(&bf.is_nan()))
34            }
35            (Number(_), String(_)) => Ordering::Less,
36            (String(_), Number(_)) => Ordering::Greater,
37            (String(a), String(b)) => a.cmp(b),
38            _ => Ordering::Equal, // defensive fallback for Array/Object (shouldn't reach here)
39        }
40    }
41}
42
43impl PartialOrd for SortKey {
44    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
45        Some(self.cmp(other))
46    }
47}
48
49impl PartialEq for SortKey {
50    fn eq(&self, other: &Self) -> bool {
51        self.cmp(other) == Ordering::Equal
52    }
53}
54
55impl Eq for SortKey {}
56
57/// Extracts a sort key from each element of the body array.
58///
59/// The sync arm keeps the historical contract (the closure decides what is a
60/// valid key). The async arm rejects non-scalar results: Array/Object keys
61/// are a user error.
62// The closure shapes are part of the published contract (mirroring
63// camel-api's source enums); keep the signatures literal.
64#[allow(clippy::type_complexity)]
65#[derive(Clone)]
66pub enum SortKeySource {
67    /// Programmatic synchronous key extractor.
68    Sync(Arc<dyn Fn(&serde_json::Value) -> Result<SortKey, CamelError> + Send + Sync>),
69    /// Language-backed asynchronous expression.
70    Async(Arc<dyn Fn(&serde_json::Value) -> camel_api::BoxValueFuture + Send + Sync>),
71}
72
73impl SortKeySource {
74    /// Extract the sort key for `value`, propagating failures.
75    pub async fn key(&self, value: &serde_json::Value) -> Result<SortKey, CamelError> {
76        match self {
77            Self::Sync(f) => f(value),
78            Self::Async(f) => {
79                let evaluated = f(value).await?;
80                match evaluated {
81                    serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
82                        Err(CamelError::ProcessorError(
83                            "sort expression returned a non-scalar value (array/object); expected null/bool/number/string".into(),
84                        ))
85                    }
86                    scalar => Ok(SortKey(scalar)),
87                }
88            }
89        }
90    }
91}
92
93/// SortService: order body collection by expression.
94///
95/// Process-mode leaf processor (no child pipeline). Requires `Body::Json(Value::Array(_))`.
96/// Non-array/non-Json body → Err. Array/Object keys → Err.
97#[derive(Clone)]
98pub struct SortService {
99    expression: SortKeySource,
100    reverse: bool,
101}
102
103impl SortService {
104    pub fn new(expression: SortKeySource, reverse: bool) -> Self {
105        Self {
106            expression,
107            reverse,
108        }
109    }
110}
111
112impl Service<Exchange> for SortService {
113    type Response = Exchange;
114    type Error = CamelError;
115    type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
116
117    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
118        Poll::Ready(Ok(()))
119    }
120
121    fn call(&mut self, mut exchange: Exchange) -> Self::Future {
122        let expression = self.expression.clone();
123        let reverse = self.reverse;
124        Box::pin(async move {
125            let array = match std::mem::take(&mut exchange.input.body) {
126                camel_api::body::Body::Json(serde_json::Value::Array(arr)) => arr,
127                _ => {
128                    return Err(CamelError::ProcessorError(
129                        "sort requires an array body (Body::Json(Value::Array))".into(),
130                    ));
131                }
132            };
133
134            let mut indexed: Vec<(SortKey, serde_json::Value)> = Vec::with_capacity(array.len());
135            for element in array {
136                let key = expression.key(&element).await?;
137                indexed.push((key, element));
138            }
139
140            if reverse {
141                indexed.sort_by(|a, b| b.0.cmp(&a.0));
142            } else {
143                indexed.sort_by(|a, b| a.0.cmp(&b.0));
144            }
145
146            let sorted: Vec<serde_json::Value> = indexed.into_iter().map(|(_, v)| v).collect();
147            exchange.input.body = camel_api::body::Body::Json(serde_json::Value::Array(sorted));
148            Ok(exchange)
149        })
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156    use camel_api::{Exchange, Message, body::Body};
157    use serde_json::json;
158    use tower::ServiceExt;
159
160    #[tokio::test]
161    async fn ascending_numeric_sort() {
162        let exchange = Exchange::new(Message::new(Body::Json(json!([3, 1, 2]))));
163        let expr = SortKeySource::Sync(Arc::new(|v| Ok(SortKey(v.clone()))));
164        let svc = SortService::new(expr, false);
165        let result = svc.oneshot(exchange).await.unwrap();
166        assert_eq!(result.input.body, Body::Json(json!([1, 2, 3])));
167    }
168
169    #[tokio::test]
170    async fn descending_numeric_sort() {
171        let exchange = Exchange::new(Message::new(Body::Json(json!([3, 1, 2]))));
172        let expr = SortKeySource::Sync(Arc::new(|v| Ok(SortKey(v.clone()))));
173        let svc = SortService::new(expr, true);
174        let result = svc.oneshot(exchange).await.unwrap();
175        assert_eq!(result.input.body, Body::Json(json!([3, 2, 1])));
176    }
177
178    #[tokio::test]
179    async fn string_sort() {
180        let exchange = Exchange::new(Message::new(Body::Json(json!([
181            "banana", "apple", "cherry"
182        ]))));
183        let expr = SortKeySource::Sync(Arc::new(|v| Ok(SortKey(v.clone()))));
184        let svc = SortService::new(expr, false);
185        let result = svc.oneshot(exchange).await.unwrap();
186        assert_eq!(
187            result.input.body,
188            Body::Json(json!(["apple", "banana", "cherry"]))
189        );
190    }
191
192    #[tokio::test]
193    async fn empty_array_passthrough() {
194        let exchange = Exchange::new(Message::new(Body::Json(json!([]))));
195        let expr = SortKeySource::Sync(Arc::new(|v| Ok(SortKey(v.clone()))));
196        let svc = SortService::new(expr, false);
197        let result = svc.oneshot(exchange).await.unwrap();
198        assert_eq!(result.input.body, Body::Json(json!([])));
199    }
200
201    #[tokio::test]
202    async fn non_array_body_errors() {
203        let exchange = Exchange::new(Message::new(Body::Text("hello".to_string())));
204        let expr = SortKeySource::Sync(Arc::new(|v| Ok(SortKey(v.clone()))));
205        let svc = SortService::new(expr, false);
206        let result = svc.oneshot(exchange).await;
207        assert!(matches!(result, Err(CamelError::ProcessorError(_))));
208    }
209
210    #[tokio::test]
211    async fn array_key_errors() {
212        let exchange = Exchange::new(Message::new(Body::Json(json!([[1, 2], 3]))));
213        let expr = SortKeySource::Sync(Arc::new(|v| {
214            if v.is_array() {
215                Err(CamelError::ProcessorError("array key rejected".into()))
216            } else {
217                Ok(SortKey(v.clone()))
218            }
219        }));
220        let svc = SortService::new(expr, false);
221        let result = svc.oneshot(exchange).await;
222        assert!(result.is_err());
223    }
224
225    #[tokio::test]
226    async fn mixed_type_sort_key_order() {
227        // Null < Bool < Number < String
228        let exchange = Exchange::new(Message::new(Body::Json(json!([
229            "str", null, false, 42, true, 1
230        ]))));
231        let expr = SortKeySource::Sync(Arc::new(|v| Ok(SortKey(v.clone()))));
232        let svc = SortService::new(expr, false);
233        let result = svc.oneshot(exchange).await.unwrap();
234        // Expected: null, false, true, 1, 42, "str"
235        assert_eq!(
236            result.input.body,
237            Body::Json(json!([null, false, true, 1, 42, "str"]))
238        );
239    }
240
241    #[tokio::test]
242    async fn sort_async_error_propagates() {
243        use camel_api::BoxValueFuture;
244
245        let source = SortKeySource::Async(Arc::new(|_: &serde_json::Value| {
246            Box::pin(async { Err(CamelError::ProcessorError("sort boom".into())) })
247                as BoxValueFuture
248        }));
249        let exchange = Exchange::new(Message::new(Body::Json(json!([3, 1, 2]))));
250        let svc = SortService::new(source, false);
251
252        let result = svc.oneshot(exchange).await;
253        assert!(
254            matches!(result, Err(CamelError::ProcessorError(ref e)) if e.contains("sort boom")),
255            "an async key-expression failure must propagate"
256        );
257    }
258}