Skip to main content

camel_api/
body_converter.rs

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