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(v), BodyType::Text) => Ok(Body::Text(v.to_string())),
114        (Body::Json(v), BodyType::Bytes) => {
115            let b = serde_json::to_vec(&v).map_err(|e| {
116                CamelError::TypeConversionFailed(format!("cannot convert Body::Json to Bytes: {e}"))
117            })?;
118            Ok(Body::Bytes(Bytes::from(b)))
119        }
120        (Body::Json(_), BodyType::Xml) => Err(CamelError::TypeConversionFailed(
121            "cannot convert Body::Json to Xml: JSON to XML conversion is not supported".to_string(),
122        )),
123        (Body::Json(_), BodyType::Empty) => Err(CamelError::TypeConversionFailed(
124            "cannot convert Body::Json to Empty".to_string(),
125        )),
126
127        // Bytes conversions
128        (Body::Bytes(b), BodyType::Text) => {
129            let s = String::from_utf8(b.to_vec()).map_err(|e| {
130                CamelError::TypeConversionFailed(format!(
131                    "cannot convert Body::Bytes to Text: invalid UTF-8 sequence: {e}"
132                ))
133            })?;
134            Ok(Body::Text(s))
135        }
136        (Body::Bytes(b), BodyType::Json) => {
137            let s = String::from_utf8(b.to_vec()).map_err(|e| {
138                CamelError::TypeConversionFailed(format!(
139                    "cannot convert Body::Bytes to Json (UTF-8 error): {e}"
140                ))
141            })?;
142            let v = serde_json::from_str(&s).map_err(|e| {
143                CamelError::TypeConversionFailed(format!("cannot convert Body::Bytes to Json: {e}"))
144            })?;
145            Ok(Body::Json(v))
146        }
147        (Body::Bytes(b), BodyType::Xml) => {
148            let s = String::from_utf8(b.to_vec()).map_err(|e| {
149                CamelError::TypeConversionFailed(format!(
150                    "cannot convert Body::Bytes to Xml (UTF-8 error): {e}"
151                ))
152            })?;
153            validate_xml(&s)?;
154            Ok(Body::Xml(s))
155        }
156        (Body::Bytes(_), BodyType::Empty) => Err(CamelError::TypeConversionFailed(
157            "cannot convert Body::Bytes to Empty".to_string(),
158        )),
159
160        // Xml conversions
161        (Body::Xml(s), BodyType::Text) => Ok(Body::Text(s)),
162        (Body::Xml(s), BodyType::Bytes) => Ok(Body::Bytes(Bytes::from(s.into_bytes()))),
163        (Body::Xml(_), BodyType::Json) => Err(CamelError::TypeConversionFailed(
164            "cannot convert Body::Xml to Json: XML to JSON conversion is not supported".to_string(),
165        )),
166        (Body::Xml(_), BodyType::Empty) => Err(CamelError::TypeConversionFailed(
167            "cannot convert Body::Xml to Empty".to_string(),
168        )),
169
170        // Empty conversions
171        (Body::Empty, BodyType::Text) => Err(CamelError::TypeConversionFailed(
172            "cannot convert Empty body to Text".to_string(),
173        )),
174        (Body::Empty, BodyType::Json) => Err(CamelError::TypeConversionFailed(
175            "cannot convert Empty body to Json".to_string(),
176        )),
177        (Body::Empty, BodyType::Bytes) => Err(CamelError::TypeConversionFailed(
178            "cannot convert Empty body to Bytes".to_string(),
179        )),
180        (Body::Empty, BodyType::Xml) => Err(CamelError::TypeConversionFailed(
181            "cannot convert Empty body to Xml".to_string(),
182        )),
183
184        // Stream: always fails
185        (Body::Stream(_), _) => Err(CamelError::TypeConversionFailed(
186            "cannot convert Body::Stream: materialize first with into_bytes()".to_string(),
187        )),
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194    use serde_json::json;
195
196    #[test]
197    fn text_to_json_valid() {
198        let body = Body::Text(r#"{"a":1}"#.to_string());
199        let result = convert(body, BodyType::Json).unwrap();
200        assert_eq!(result, Body::Json(json!({"a": 1})));
201    }
202
203    #[test]
204    fn text_to_json_invalid() {
205        let body = Body::Text("not json".to_string());
206        let result = convert(body, BodyType::Json);
207        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
208    }
209
210    #[test]
211    fn json_to_text() {
212        let body = Body::Json(json!({"a": 1}));
213        let result = convert(body, BodyType::Text).unwrap();
214        match result {
215            Body::Text(s) => assert!(s.contains("\"a\"")),
216            _ => panic!("expected Body::Text"),
217        }
218    }
219
220    #[test]
221    fn json_to_bytes() {
222        let body = Body::Json(json!({"x": 2}));
223        let result = convert(body, BodyType::Bytes).unwrap();
224        assert!(matches!(result, Body::Bytes(_)));
225    }
226
227    #[test]
228    fn bytes_to_text_valid() {
229        let body = Body::Bytes(Bytes::from_static(b"hello"));
230        let result = convert(body, BodyType::Text).unwrap();
231        assert_eq!(result, Body::Text("hello".to_string()));
232    }
233
234    #[test]
235    fn bytes_to_text_invalid_utf8() {
236        let body = Body::Bytes(Bytes::from_static(&[0xFF, 0xFE]));
237        let result = convert(body, BodyType::Text);
238        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
239    }
240
241    #[test]
242    fn text_to_bytes() {
243        let body = Body::Text("hi".to_string());
244        let result = convert(body, BodyType::Bytes).unwrap();
245        assert_eq!(result, Body::Bytes(Bytes::from_static(b"hi")));
246    }
247
248    #[test]
249    fn empty_to_text_fails() {
250        let result = convert(Body::Empty, BodyType::Text);
251        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
252    }
253
254    #[test]
255    fn empty_to_empty_noop() {
256        let result = convert(Body::Empty, BodyType::Empty).unwrap();
257        assert!(matches!(result, Body::Empty));
258    }
259
260    #[test]
261    fn noop_same_type_text() {
262        let body = Body::Text("x".to_string());
263        let result = convert(body, BodyType::Text).unwrap();
264        assert!(matches!(result, Body::Text(_)));
265    }
266
267    #[test]
268    fn noop_same_type_json() {
269        let body = Body::Json(json!(1));
270        let result = convert(body, BodyType::Json).unwrap();
271        assert!(matches!(result, Body::Json(_)));
272    }
273
274    #[test]
275    fn noop_same_type_bytes() {
276        let body = Body::Bytes(Bytes::from_static(b"x"));
277        let result = convert(body, BodyType::Bytes).unwrap();
278        assert!(matches!(result, Body::Bytes(_)));
279    }
280
281    #[test]
282    fn stream_to_any_fails() {
283        use crate::body::{StreamBody, StreamMetadata};
284        use futures::stream;
285        use std::sync::Arc;
286        use tokio::sync::Mutex;
287
288        let stream = stream::iter(vec![Ok(Bytes::from_static(b"data"))]);
289        let body = Body::Stream(StreamBody {
290            stream: Arc::new(Mutex::new(Some(Box::pin(stream)))),
291            metadata: StreamMetadata::default(),
292        });
293        let result = convert(body, BodyType::Text);
294        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
295    }
296
297    #[test]
298    fn bytes_to_json_valid() {
299        let body = Body::Bytes(Bytes::from_static(b"{\"k\":1}"));
300        let result = convert(body, BodyType::Json).unwrap();
301        assert!(matches!(result, Body::Json(_)));
302    }
303
304    #[test]
305    fn bytes_to_json_invalid_utf8() {
306        let body = Body::Bytes(Bytes::from_static(&[0xFF, 0xFE]));
307        let result = convert(body, BodyType::Json);
308        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
309    }
310
311    #[test]
312    fn to_empty_always_fails() {
313        assert!(matches!(
314            convert(Body::Text("x".into()), BodyType::Empty),
315            Err(CamelError::TypeConversionFailed(_))
316        ));
317        assert!(matches!(
318            convert(Body::Json(serde_json::json!(1)), BodyType::Empty),
319            Err(CamelError::TypeConversionFailed(_))
320        ));
321        assert!(matches!(
322            convert(Body::Bytes(Bytes::from_static(b"x")), BodyType::Empty),
323            Err(CamelError::TypeConversionFailed(_))
324        ));
325    }
326
327    // =============================================================================
328    // XML Conversion Tests
329    // =============================================================================
330
331    #[test]
332    fn noop_same_type_xml() {
333        let body = Body::Xml("<root/>".to_string());
334        let result = convert(body, BodyType::Xml).unwrap();
335        assert!(matches!(result, Body::Xml(_)));
336    }
337
338    #[test]
339    fn test_text_to_xml() {
340        let xml = r#"<root><child>value</child></root>"#;
341        let body = Body::Text(xml.to_string());
342        let result = convert(body, BodyType::Xml).unwrap();
343        match result {
344            Body::Xml(s) => assert_eq!(s, xml),
345            _ => panic!("expected Body::Xml"),
346        }
347    }
348
349    #[test]
350    fn test_xml_to_text() {
351        let xml = r#"<root><child>value</child></root>"#;
352        let body = Body::Xml(xml.to_string());
353        let result = convert(body, BodyType::Text).unwrap();
354        match result {
355            Body::Text(s) => assert_eq!(s, xml),
356            _ => panic!("expected Body::Text"),
357        }
358    }
359
360    #[test]
361    fn test_bytes_to_xml() {
362        let xml = r#"<root><child>value</child></root>"#;
363        let body = Body::Bytes(Bytes::from(xml.as_bytes()));
364        let result = convert(body, BodyType::Xml).unwrap();
365        match result {
366            Body::Xml(s) => assert_eq!(s, xml),
367            _ => panic!("expected Body::Xml"),
368        }
369    }
370
371    #[test]
372    fn test_xml_to_bytes() {
373        let xml = r#"<root><child>value</child></root>"#;
374        let body = Body::Xml(xml.to_string());
375        let result = convert(body, BodyType::Bytes).unwrap();
376        match result {
377            Body::Bytes(b) => assert_eq!(b.as_ref(), xml.as_bytes()),
378            _ => panic!("expected Body::Bytes"),
379        }
380    }
381
382    #[test]
383    fn test_invalid_xml_rejected() {
384        let invalid_xml = "not valid xml <unclosed";
385        let body = Body::Text(invalid_xml.to_string());
386        let result = convert(body, BodyType::Xml);
387        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
388    }
389
390    #[test]
391    fn test_json_to_xml_unsupported() {
392        let body = Body::Json(json!({"key": "value"}));
393        let result = convert(body, BodyType::Xml);
394        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
395        if let Err(CamelError::TypeConversionFailed(msg)) = result {
396            assert!(
397                msg.contains("not supported"),
398                "error message should mention 'not supported', got: {}",
399                msg
400            );
401        }
402    }
403
404    #[test]
405    fn test_xml_to_json_unsupported() {
406        let body = Body::Xml("<root/>".to_string());
407        let result = convert(body, BodyType::Json);
408        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
409        if let Err(CamelError::TypeConversionFailed(msg)) = result {
410            assert!(
411                msg.contains("not supported"),
412                "error message should mention 'not supported', got: {}",
413                msg
414            );
415        }
416    }
417
418    #[test]
419    fn test_empty_to_xml_fails() {
420        let result = convert(Body::Empty, BodyType::Xml);
421        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
422    }
423
424    #[test]
425    fn test_xml_to_empty_fails() {
426        let body = Body::Xml("<root/>".to_string());
427        let result = convert(body, BodyType::Empty);
428        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
429    }
430
431    #[test]
432    fn test_bytes_to_xml_invalid_utf8() {
433        let body = Body::Bytes(Bytes::from_static(&[0xFF, 0xFE]));
434        let result = convert(body, BodyType::Xml);
435        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
436    }
437
438    #[test]
439    fn test_bytes_to_xml_invalid_xml() {
440        let invalid = b"valid utf-8 but <invalid xml";
441        let body = Body::Bytes(Bytes::from_static(invalid));
442        let result = convert(body, BodyType::Xml);
443        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
444    }
445
446    // =============================================================================
447    // XML Validation Edge Case Tests
448    // =============================================================================
449
450    #[test]
451    fn test_empty_string_rejected_as_xml() {
452        let body = Body::Text("".to_string());
453        let result = convert(body, BodyType::Xml);
454        assert!(
455            matches!(result, Err(CamelError::TypeConversionFailed(_))),
456            "empty string should be rejected as XML"
457        );
458    }
459
460    #[test]
461    fn test_whitespace_only_rejected_as_xml() {
462        let body = Body::Text("   \n\t  ".to_string());
463        let result = convert(body, BodyType::Xml);
464        assert!(
465            matches!(result, Err(CamelError::TypeConversionFailed(_))),
466            "whitespace-only string should be rejected as XML"
467        );
468    }
469
470    #[test]
471    fn test_prolog_only_rejected_as_xml() {
472        // XML declaration without any root element
473        let body = Body::Text(r#"<?xml version="1.0" encoding="UTF-8"?>"#.to_string());
474        let result = convert(body, BodyType::Xml);
475        assert!(
476            matches!(result, Err(CamelError::TypeConversionFailed(_))),
477            "XML prolog without root element should be rejected"
478        );
479    }
480
481    #[test]
482    fn test_multiple_root_elements_rejected() {
483        let body = Body::Text("<root1/><root2/>".to_string());
484        let result = convert(body, BodyType::Xml);
485        assert!(
486            matches!(result, Err(CamelError::TypeConversionFailed(_))),
487            "XML with multiple root elements should be rejected"
488        );
489    }
490
491    #[test]
492    fn test_multiple_root_elements_with_children_rejected() {
493        let body = Body::Text("<a><b/></a><c/>".to_string());
494        let result = convert(body, BodyType::Xml);
495        assert!(
496            matches!(result, Err(CamelError::TypeConversionFailed(_))),
497            "XML with multiple root elements (one with children) should be rejected"
498        );
499    }
500
501    #[test]
502    fn test_valid_xml_with_prolog_accepted() {
503        let xml = r#"<?xml version="1.0" encoding="UTF-8"?><root><child>value</child></root>"#;
504        let body = Body::Text(xml.to_string());
505        let result = convert(body, BodyType::Xml);
506        assert!(
507            result.is_ok(),
508            "XML with prolog and root element should be accepted"
509        );
510    }
511
512    #[test]
513    fn test_self_closing_root_accepted() {
514        let body = Body::Text("<root/>".to_string());
515        let result = convert(body, BodyType::Xml);
516        assert!(
517            result.is_ok(),
518            "self-closing root element should be accepted"
519        );
520    }
521}