Skip to main content

camel_api/
body_converter.rs

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