Skip to main content

camel_api/
body_converter.rs

1use crate::body::Body;
2use crate::error::CamelError;
3use bytes::Bytes;
4use quick_xml::Reader;
5
6/// Target type for body conversion.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum BodyType {
9    Text,
10    Json,
11    Bytes,
12    Xml,
13    Empty,
14}
15
16/// Validate that a string is well-formed XML.
17///
18/// Requirements:
19/// - Must not be empty or whitespace-only.
20/// - Must contain at least one root element (not just a prolog/declaration).
21/// - Must not contain multiple root elements.
22/// - Must be parseable by quick_xml without errors.
23fn validate_xml(s: &str) -> Result<(), CamelError> {
24    if s.trim().is_empty() {
25        return Err(CamelError::TypeConversionFailed(
26            "invalid XML: document is empty".to_string(),
27        ));
28    }
29
30    let mut reader = Reader::from_str(s);
31    let mut buf = Vec::new();
32    let mut root_element_count: u32 = 0;
33    let mut depth: u32 = 0;
34
35    loop {
36        match reader.read_event_into(&mut buf) {
37            Ok(quick_xml::events::Event::Eof) => break,
38            Ok(quick_xml::events::Event::Start(_)) => {
39                depth += 1;
40                if depth == 1 {
41                    root_element_count += 1;
42                    if root_element_count > 1 {
43                        return Err(CamelError::TypeConversionFailed(
44                            "invalid XML: document has multiple root elements".to_string(),
45                        ));
46                    }
47                }
48                buf.clear();
49            }
50            Ok(quick_xml::events::Event::End(_)) => {
51                depth = depth.saturating_sub(1);
52                buf.clear();
53            }
54            Ok(quick_xml::events::Event::Empty(_)) => {
55                if depth == 0 {
56                    root_element_count += 1;
57                    if root_element_count > 1 {
58                        return Err(CamelError::TypeConversionFailed(
59                            "invalid XML: document has multiple root elements".to_string(),
60                        ));
61                    }
62                }
63                buf.clear();
64            }
65            Ok(_) => buf.clear(),
66            Err(e) => {
67                return Err(CamelError::TypeConversionFailed(format!(
68                    "invalid XML: {e}"
69                )));
70            }
71        }
72    }
73
74    if root_element_count == 0 {
75        return Err(CamelError::TypeConversionFailed(
76            "invalid XML: document has no root element".to_string(),
77        ));
78    }
79
80    Ok(())
81}
82
83/// Convert a `Body` to the target `BodyType`.
84///
85/// `Body::Stream` is always an error — materialize with `into_bytes()` first.
86/// Returns `CamelError::TypeConversionFailed` on any incompatible conversion.
87pub fn convert(body: Body, target: BodyType) -> Result<Body, CamelError> {
88    match (body, target) {
89        // noop: same variant
90        (b @ Body::Text(_), BodyType::Text) => Ok(b),
91        (b @ Body::Json(_), BodyType::Json) => Ok(b),
92        (b @ Body::Bytes(_), BodyType::Bytes) => Ok(b),
93        (b @ Body::Xml(_), BodyType::Xml) => Ok(b),
94        (Body::Empty, BodyType::Empty) => Ok(Body::Empty),
95
96        // Text conversions
97        (Body::Text(s), BodyType::Json) => {
98            let v = serde_json::from_str(&s).map_err(|e| {
99                CamelError::TypeConversionFailed(format!("cannot convert Body::Text to Json: {e}"))
100            })?;
101            Ok(Body::Json(v))
102        }
103        (Body::Text(s), BodyType::Bytes) => Ok(Body::Bytes(Bytes::from(s.into_bytes()))),
104        (Body::Text(s), BodyType::Xml) => {
105            validate_xml(&s)?;
106            Ok(Body::Xml(s))
107        }
108        (Body::Text(_), BodyType::Empty) => Err(CamelError::TypeConversionFailed(
109            "cannot convert Body::Text to Empty".to_string(),
110        )),
111
112        // Json conversions
113        (Body::Json(serde_json::Value::String(s)), BodyType::Text) => Ok(Body::Text(s)),
114        (Body::Json(v), BodyType::Text) => Ok(Body::Text(v.to_string())),
115        (Body::Json(v), BodyType::Bytes) => {
116            let b = serde_json::to_vec(&v).map_err(|e| {
117                CamelError::TypeConversionFailed(format!("cannot convert Body::Json to Bytes: {e}"))
118            })?;
119            Ok(Body::Bytes(Bytes::from(b)))
120        }
121        (Body::Json(_), BodyType::Xml) => Err(CamelError::TypeConversionFailed(
122            "cannot convert Body::Json to Xml: JSON to XML conversion is not supported".to_string(),
123        )),
124        (Body::Json(_), BodyType::Empty) => Err(CamelError::TypeConversionFailed(
125            "cannot convert Body::Json to Empty".to_string(),
126        )),
127
128        // Bytes conversions
129        (Body::Bytes(b), BodyType::Text) => {
130            let s = String::from_utf8(b.to_vec()).map_err(|e| {
131                CamelError::TypeConversionFailed(format!(
132                    "cannot convert Body::Bytes to Text: invalid UTF-8 sequence: {e}"
133                ))
134            })?;
135            Ok(Body::Text(s))
136        }
137        (Body::Bytes(b), BodyType::Json) => {
138            let s = String::from_utf8(b.to_vec()).map_err(|e| {
139                CamelError::TypeConversionFailed(format!(
140                    "cannot convert Body::Bytes to Json (UTF-8 error): {e}"
141                ))
142            })?;
143            let v = serde_json::from_str(&s).map_err(|e| {
144                CamelError::TypeConversionFailed(format!("cannot convert Body::Bytes to Json: {e}"))
145            })?;
146            Ok(Body::Json(v))
147        }
148        (Body::Bytes(b), BodyType::Xml) => {
149            let s = String::from_utf8(b.to_vec()).map_err(|e| {
150                CamelError::TypeConversionFailed(format!(
151                    "cannot convert Body::Bytes to Xml (UTF-8 error): {e}"
152                ))
153            })?;
154            validate_xml(&s)?;
155            Ok(Body::Xml(s))
156        }
157        (Body::Bytes(_), BodyType::Empty) => Err(CamelError::TypeConversionFailed(
158            "cannot convert Body::Bytes to Empty".to_string(),
159        )),
160
161        // Xml conversions
162        (Body::Xml(s), BodyType::Text) => Ok(Body::Text(s)),
163        (Body::Xml(s), BodyType::Bytes) => Ok(Body::Bytes(Bytes::from(s.into_bytes()))),
164        (Body::Xml(_), BodyType::Json) => Err(CamelError::TypeConversionFailed(
165            "cannot convert Body::Xml to Json: XML to JSON conversion is not supported".to_string(),
166        )),
167        (Body::Xml(_), BodyType::Empty) => Err(CamelError::TypeConversionFailed(
168            "cannot convert Body::Xml to Empty".to_string(),
169        )),
170
171        // Empty conversions
172        (Body::Empty, BodyType::Text) => Err(CamelError::TypeConversionFailed(
173            "cannot convert Empty body to Text".to_string(),
174        )),
175        (Body::Empty, BodyType::Json) => Err(CamelError::TypeConversionFailed(
176            "cannot convert Empty body to Json".to_string(),
177        )),
178        (Body::Empty, BodyType::Bytes) => Err(CamelError::TypeConversionFailed(
179            "cannot convert Empty body to Bytes".to_string(),
180        )),
181        (Body::Empty, BodyType::Xml) => Err(CamelError::TypeConversionFailed(
182            "cannot convert Empty body to Xml".to_string(),
183        )),
184
185        // Stream: always fails
186        (Body::Stream(_), _) => Err(CamelError::TypeConversionFailed(
187            "cannot convert Body::Stream: materialize first with into_bytes()".to_string(),
188        )),
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195    use serde_json::json;
196
197    #[test]
198    fn text_to_json_valid() {
199        let body = Body::Text(r#"{"a":1}"#.to_string());
200        let result = convert(body, BodyType::Json).unwrap();
201        assert_eq!(result, Body::Json(json!({"a": 1})));
202    }
203
204    #[test]
205    fn text_to_json_invalid() {
206        let body = Body::Text("not json".to_string());
207        let result = convert(body, BodyType::Json);
208        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
209    }
210
211    #[test]
212    fn json_to_text() {
213        let body = Body::Json(json!({"a": 1}));
214        let result = convert(body, BodyType::Text).unwrap();
215        match result {
216            Body::Text(s) => assert!(s.contains("\"a\"")),
217            _ => panic!("expected Body::Text"),
218        }
219    }
220
221    #[test]
222    fn json_to_bytes() {
223        let body = Body::Json(json!({"x": 2}));
224        let result = convert(body, BodyType::Bytes).unwrap();
225        assert!(matches!(result, Body::Bytes(_)));
226    }
227
228    #[test]
229    fn bytes_to_text_valid() {
230        let body = Body::Bytes(Bytes::from_static(b"hello"));
231        let result = convert(body, BodyType::Text).unwrap();
232        assert_eq!(result, Body::Text("hello".to_string()));
233    }
234
235    #[test]
236    fn bytes_to_text_invalid_utf8() {
237        let body = Body::Bytes(Bytes::from_static(&[0xFF, 0xFE]));
238        let result = convert(body, BodyType::Text);
239        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
240    }
241
242    #[test]
243    fn text_to_bytes() {
244        let body = Body::Text("hi".to_string());
245        let result = convert(body, BodyType::Bytes).unwrap();
246        assert_eq!(result, Body::Bytes(Bytes::from_static(b"hi")));
247    }
248
249    #[test]
250    fn empty_to_text_fails() {
251        let result = convert(Body::Empty, BodyType::Text);
252        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
253    }
254
255    #[test]
256    fn empty_to_empty_noop() {
257        let result = convert(Body::Empty, BodyType::Empty).unwrap();
258        assert!(matches!(result, Body::Empty));
259    }
260
261    #[test]
262    fn noop_same_type_text() {
263        let body = Body::Text("x".to_string());
264        let result = convert(body, BodyType::Text).unwrap();
265        assert!(matches!(result, Body::Text(_)));
266    }
267
268    #[test]
269    fn noop_same_type_json() {
270        let body = Body::Json(json!(1));
271        let result = convert(body, BodyType::Json).unwrap();
272        assert!(matches!(result, Body::Json(_)));
273    }
274
275    #[test]
276    fn noop_same_type_bytes() {
277        let body = Body::Bytes(Bytes::from_static(b"x"));
278        let result = convert(body, BodyType::Bytes).unwrap();
279        assert!(matches!(result, Body::Bytes(_)));
280    }
281
282    #[test]
283    fn stream_to_any_fails() {
284        use crate::body::{StreamBody, StreamMetadata};
285        use futures::stream;
286        use std::sync::Arc;
287        use tokio::sync::Mutex;
288
289        let stream = stream::iter(vec![Ok(Bytes::from_static(b"data"))]);
290        let body = Body::Stream(StreamBody {
291            stream: Arc::new(Mutex::new(Some(Box::pin(stream)))),
292            metadata: StreamMetadata::default(),
293        });
294        let result = convert(body, BodyType::Text);
295        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
296    }
297
298    #[test]
299    fn bytes_to_json_valid() {
300        let body = Body::Bytes(Bytes::from_static(b"{\"k\":1}"));
301        let result = convert(body, BodyType::Json).unwrap();
302        assert!(matches!(result, Body::Json(_)));
303    }
304
305    #[test]
306    fn bytes_to_json_invalid_utf8() {
307        let body = Body::Bytes(Bytes::from_static(&[0xFF, 0xFE]));
308        let result = convert(body, BodyType::Json);
309        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
310    }
311
312    #[test]
313    fn to_empty_always_fails() {
314        assert!(matches!(
315            convert(Body::Text("x".into()), BodyType::Empty),
316            Err(CamelError::TypeConversionFailed(_))
317        ));
318        assert!(matches!(
319            convert(Body::Json(serde_json::json!(1)), BodyType::Empty),
320            Err(CamelError::TypeConversionFailed(_))
321        ));
322        assert!(matches!(
323            convert(Body::Bytes(Bytes::from_static(b"x")), BodyType::Empty),
324            Err(CamelError::TypeConversionFailed(_))
325        ));
326    }
327
328    // =============================================================================
329    // XML Conversion Tests
330    // =============================================================================
331
332    #[test]
333    fn noop_same_type_xml() {
334        let body = Body::Xml("<root/>".to_string());
335        let result = convert(body, BodyType::Xml).unwrap();
336        assert!(matches!(result, Body::Xml(_)));
337    }
338
339    #[test]
340    fn test_text_to_xml() {
341        let xml = r#"<root><child>value</child></root>"#;
342        let body = Body::Text(xml.to_string());
343        let result = convert(body, BodyType::Xml).unwrap();
344        match result {
345            Body::Xml(s) => assert_eq!(s, xml),
346            _ => panic!("expected Body::Xml"),
347        }
348    }
349
350    #[test]
351    fn test_xml_to_text() {
352        let xml = r#"<root><child>value</child></root>"#;
353        let body = Body::Xml(xml.to_string());
354        let result = convert(body, BodyType::Text).unwrap();
355        match result {
356            Body::Text(s) => assert_eq!(s, xml),
357            _ => panic!("expected Body::Text"),
358        }
359    }
360
361    #[test]
362    fn test_bytes_to_xml() {
363        let xml = r#"<root><child>value</child></root>"#;
364        let body = Body::Bytes(Bytes::from(xml.as_bytes()));
365        let result = convert(body, BodyType::Xml).unwrap();
366        match result {
367            Body::Xml(s) => assert_eq!(s, xml),
368            _ => panic!("expected Body::Xml"),
369        }
370    }
371
372    #[test]
373    fn test_xml_to_bytes() {
374        let xml = r#"<root><child>value</child></root>"#;
375        let body = Body::Xml(xml.to_string());
376        let result = convert(body, BodyType::Bytes).unwrap();
377        match result {
378            Body::Bytes(b) => assert_eq!(b.as_ref(), xml.as_bytes()),
379            _ => panic!("expected Body::Bytes"),
380        }
381    }
382
383    #[test]
384    fn test_invalid_xml_rejected() {
385        let invalid_xml = "not valid xml <unclosed";
386        let body = Body::Text(invalid_xml.to_string());
387        let result = convert(body, BodyType::Xml);
388        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
389    }
390
391    #[test]
392    fn test_json_to_xml_unsupported() {
393        let body = Body::Json(json!({"key": "value"}));
394        let result = convert(body, BodyType::Xml);
395        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
396        if let Err(CamelError::TypeConversionFailed(msg)) = result {
397            assert!(
398                msg.contains("not supported"),
399                "error message should mention 'not supported', got: {}",
400                msg
401            );
402        }
403    }
404
405    #[test]
406    fn test_xml_to_json_unsupported() {
407        let body = Body::Xml("<root/>".to_string());
408        let result = convert(body, BodyType::Json);
409        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
410        if let Err(CamelError::TypeConversionFailed(msg)) = result {
411            assert!(
412                msg.contains("not supported"),
413                "error message should mention 'not supported', got: {}",
414                msg
415            );
416        }
417    }
418
419    #[test]
420    fn test_empty_to_xml_fails() {
421        let result = convert(Body::Empty, BodyType::Xml);
422        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
423    }
424
425    #[test]
426    fn test_xml_to_empty_fails() {
427        let body = Body::Xml("<root/>".to_string());
428        let result = convert(body, BodyType::Empty);
429        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
430    }
431
432    #[test]
433    fn test_bytes_to_xml_invalid_utf8() {
434        let body = Body::Bytes(Bytes::from_static(&[0xFF, 0xFE]));
435        let result = convert(body, BodyType::Xml);
436        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
437    }
438
439    #[test]
440    fn test_bytes_to_xml_invalid_xml() {
441        let invalid = b"valid utf-8 but <invalid xml";
442        let body = Body::Bytes(Bytes::from_static(invalid));
443        let result = convert(body, BodyType::Xml);
444        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
445    }
446
447    // =============================================================================
448    // XML Validation Edge Case Tests
449    // =============================================================================
450
451    #[test]
452    fn test_empty_string_rejected_as_xml() {
453        let body = Body::Text("".to_string());
454        let result = convert(body, BodyType::Xml);
455        assert!(
456            matches!(result, Err(CamelError::TypeConversionFailed(_))),
457            "empty string should be rejected as XML"
458        );
459    }
460
461    #[test]
462    fn test_whitespace_only_rejected_as_xml() {
463        let body = Body::Text("   \n\t  ".to_string());
464        let result = convert(body, BodyType::Xml);
465        assert!(
466            matches!(result, Err(CamelError::TypeConversionFailed(_))),
467            "whitespace-only string should be rejected as XML"
468        );
469    }
470
471    #[test]
472    fn test_prolog_only_rejected_as_xml() {
473        // XML declaration without any root element
474        let body = Body::Text(r#"<?xml version="1.0" encoding="UTF-8"?>"#.to_string());
475        let result = convert(body, BodyType::Xml);
476        assert!(
477            matches!(result, Err(CamelError::TypeConversionFailed(_))),
478            "XML prolog without root element should be rejected"
479        );
480    }
481
482    #[test]
483    fn test_multiple_root_elements_rejected() {
484        let body = Body::Text("<root1/><root2/>".to_string());
485        let result = convert(body, BodyType::Xml);
486        assert!(
487            matches!(result, Err(CamelError::TypeConversionFailed(_))),
488            "XML with multiple root elements should be rejected"
489        );
490    }
491
492    #[test]
493    fn test_multiple_root_elements_with_children_rejected() {
494        let body = Body::Text("<a><b/></a><c/>".to_string());
495        let result = convert(body, BodyType::Xml);
496        assert!(
497            matches!(result, Err(CamelError::TypeConversionFailed(_))),
498            "XML with multiple root elements (one with children) should be rejected"
499        );
500    }
501
502    #[test]
503    fn test_valid_xml_with_prolog_accepted() {
504        let xml = r#"<?xml version="1.0" encoding="UTF-8"?><root><child>value</child></root>"#;
505        let body = Body::Text(xml.to_string());
506        let result = convert(body, BodyType::Xml);
507        assert!(
508            result.is_ok(),
509            "XML with prolog and root element should be accepted"
510        );
511    }
512
513    #[test]
514    fn test_self_closing_root_accepted() {
515        let body = Body::Text("<root/>".to_string());
516        let result = convert(body, BodyType::Xml);
517        assert!(
518            result.is_ok(),
519            "self-closing root element should be accepted"
520        );
521    }
522}