Skip to main content

camel_api/
body_converter.rs

1use crate::body::Body;
2use crate::error::CamelError;
3use crate::xml_convert;
4use bytes::Bytes;
5use thiserror::Error;
6
7/// Target type for body conversion.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9#[non_exhaustive]
10pub enum BodyType {
11    Text,
12    Json,
13    Bytes,
14    Xml,
15    Empty,
16}
17
18#[derive(Debug, Clone, Error)]
19#[non_exhaustive]
20pub enum BodyConverterError {
21    #[error("invalid UTF-8 input: {0}")]
22    InvalidUtf8(String),
23    #[error("XML parse error: {0}")]
24    Parse(String),
25}
26
27/// Parse and validate XML bytes.
28///
29/// XSD validation moved to `camel-validator` component (xsd mode via xml-bridge) in v0.7+.
30pub fn parse_xml(input: &[u8]) -> Result<(), BodyConverterError> {
31    let s =
32        std::str::from_utf8(input).map_err(|e| BodyConverterError::InvalidUtf8(e.to_string()))?;
33    xml_convert::validate_xml(s).map_err(|e| BodyConverterError::Parse(e.to_string()))
34}
35
36pub fn is_well_formed_xml(input: &[u8]) -> bool {
37    parse_xml(input).is_ok()
38}
39
40/// Convert a `Body` to the target `BodyType`.
41///
42/// `Body::Stream` is always an error — materialize with `into_bytes()` first.
43/// Returns `CamelError::TypeConversionFailed` on any incompatible conversion.
44pub fn convert(body: Body, target: BodyType) -> Result<Body, CamelError> {
45    match (body, target) {
46        // noop: same variant
47        (b @ Body::Text(_), BodyType::Text) => Ok(b),
48        (b @ Body::Json(_), BodyType::Json) => Ok(b),
49        (b @ Body::Bytes(_), BodyType::Bytes) => Ok(b),
50        (b @ Body::Xml(_), BodyType::Xml) => Ok(b),
51        (Body::Empty, BodyType::Empty) => Ok(Body::Empty),
52
53        // Text conversions
54        (Body::Text(s), BodyType::Json) => {
55            let v = serde_json::from_str(&s).map_err(|e| {
56                CamelError::TypeConversionFailed(format!("cannot convert Body::Text to Json: {e}"))
57            })?;
58            Ok(Body::Json(v))
59        }
60        (Body::Text(s), BodyType::Bytes) => Ok(Body::Bytes(Bytes::from(s.into_bytes()))),
61        (Body::Text(s), BodyType::Xml) => {
62            parse_xml(s.as_bytes())
63                .map_err(|e| CamelError::TypeConversionFailed(format!("invalid XML: {e}")))?;
64            Ok(Body::Xml(s))
65        }
66        (Body::Text(_), BodyType::Empty) => Err(CamelError::TypeConversionFailed(
67            "cannot convert Body::Text to Empty".to_string(),
68        )),
69
70        // Json conversions
71        (Body::Json(serde_json::Value::String(s)), BodyType::Text) => Ok(Body::Text(s)),
72        (Body::Json(v), BodyType::Text) => Ok(Body::Text(v.to_string())),
73        (Body::Json(v), BodyType::Bytes) => {
74            let b = serde_json::to_vec(&v).map_err(|e| {
75                CamelError::TypeConversionFailed(format!("cannot convert Body::Json to Bytes: {e}"))
76            })?;
77            Ok(Body::Bytes(Bytes::from(b)))
78        }
79        (Body::Json(v), BodyType::Xml) => {
80            let s = xml_convert::json_to_xml(&v)?;
81            Ok(Body::Xml(s))
82        }
83        (Body::Json(_), BodyType::Empty) => Err(CamelError::TypeConversionFailed(
84            "cannot convert Body::Json to Empty".to_string(),
85        )),
86
87        // Bytes conversions
88        (Body::Bytes(b), BodyType::Text) => {
89            let s = String::from_utf8(b.to_vec()).map_err(|e| {
90                CamelError::TypeConversionFailed(format!(
91                    "cannot convert Body::Bytes to Text: invalid UTF-8 sequence: {e}"
92                ))
93            })?;
94            Ok(Body::Text(s))
95        }
96        (Body::Bytes(b), BodyType::Json) => {
97            let s = String::from_utf8(b.to_vec()).map_err(|e| {
98                CamelError::TypeConversionFailed(format!(
99                    "cannot convert Body::Bytes to Json (UTF-8 error): {e}"
100                ))
101            })?;
102            let v = serde_json::from_str(&s).map_err(|e| {
103                CamelError::TypeConversionFailed(format!("cannot convert Body::Bytes to Json: {e}"))
104            })?;
105            Ok(Body::Json(v))
106        }
107        (Body::Bytes(b), BodyType::Xml) => {
108            let s = String::from_utf8(b.to_vec()).map_err(|e| {
109                CamelError::TypeConversionFailed(format!(
110                    "cannot convert Body::Bytes to Xml (UTF-8 error): {e}"
111                ))
112            })?;
113            parse_xml(s.as_bytes())
114                .map_err(|e| CamelError::TypeConversionFailed(format!("invalid XML: {e}")))?;
115            Ok(Body::Xml(s))
116        }
117        (Body::Bytes(_), BodyType::Empty) => Err(CamelError::TypeConversionFailed(
118            "cannot convert Body::Bytes to Empty".to_string(),
119        )),
120
121        // Xml conversions
122        (Body::Xml(s), BodyType::Text) => Ok(Body::Text(s)),
123        (Body::Xml(s), BodyType::Bytes) => Ok(Body::Bytes(Bytes::from(s.into_bytes()))),
124        (Body::Xml(s), BodyType::Json) => {
125            let v = xml_convert::xml_to_json(&s)?;
126            Ok(Body::Json(v))
127        }
128        (Body::Xml(_), BodyType::Empty) => Err(CamelError::TypeConversionFailed(
129            "cannot convert Body::Xml to Empty".to_string(),
130        )),
131
132        // Empty conversions
133        (Body::Empty, BodyType::Text) => Err(CamelError::TypeConversionFailed(
134            "cannot convert Empty body to Text".to_string(),
135        )),
136        (Body::Empty, BodyType::Json) => Err(CamelError::TypeConversionFailed(
137            "cannot convert Empty body to Json".to_string(),
138        )),
139        (Body::Empty, BodyType::Bytes) => Err(CamelError::TypeConversionFailed(
140            "cannot convert Empty body to Bytes".to_string(),
141        )),
142        (Body::Empty, BodyType::Xml) => Err(CamelError::TypeConversionFailed(
143            "cannot convert Empty body to Xml".to_string(),
144        )),
145
146        // Stream: always fails
147        (Body::Stream(_), _) => Err(CamelError::TypeConversionFailed(
148            "cannot convert Body::Stream: materialize first with into_bytes()".to_string(),
149        )),
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156    use serde_json::json;
157    use std::time::Instant;
158
159    #[test]
160    fn parse_xml_valid_returns_ok() {
161        let xml = b"<root><child/></root>";
162        let parsed = parse_xml(xml);
163        assert!(parsed.is_ok());
164    }
165
166    #[test]
167    fn parse_xml_malformed_returns_error() {
168        let xml = b"<root><child></root>";
169        let parsed = parse_xml(xml);
170        assert!(parsed.is_err());
171    }
172
173    #[test]
174    fn parse_xml_xxe_entity_not_expanded() {
175        let xml = b"<!DOCTYPE x [<!ENTITY e SYSTEM \"file:///etc/passwd\">]><x>&e;</x>";
176        let parsed = parse_xml(xml);
177        assert!(parsed.is_err());
178    }
179
180    #[test]
181    fn parse_xml_large_1mib_ok() {
182        let content = "a".repeat(1024 * 1024);
183        let xml = format!("<root>{content}</root>");
184
185        let start = Instant::now();
186        let parsed = parse_xml(xml.as_bytes());
187        let elapsed = start.elapsed();
188
189        assert!(parsed.is_ok());
190        assert!(
191            elapsed.as_millis() < 500,
192            "expected parse to complete in <500ms, got {:?}",
193            elapsed
194        );
195    }
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_supported() {
393        let body = Body::Json(json!({"key": "value"}));
394        let result = convert(body, BodyType::Xml).unwrap();
395        assert!(matches!(result, Body::Xml(_)));
396    }
397
398    #[test]
399    fn test_xml_to_json_supported() {
400        let body = Body::Xml("<root/>".to_string());
401        let result = convert(body, BodyType::Json).unwrap();
402        assert!(matches!(result, Body::Json(_)));
403    }
404
405    #[test]
406    fn test_empty_to_xml_fails() {
407        let result = convert(Body::Empty, BodyType::Xml);
408        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
409    }
410
411    #[test]
412    fn test_xml_to_empty_fails() {
413        let body = Body::Xml("<root/>".to_string());
414        let result = convert(body, BodyType::Empty);
415        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
416    }
417
418    #[test]
419    fn test_bytes_to_xml_invalid_utf8() {
420        let body = Body::Bytes(Bytes::from_static(&[0xFF, 0xFE]));
421        let result = convert(body, BodyType::Xml);
422        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
423    }
424
425    #[test]
426    fn test_bytes_to_xml_invalid_xml() {
427        let invalid = b"valid utf-8 but <invalid xml";
428        let body = Body::Bytes(Bytes::from_static(invalid));
429        let result = convert(body, BodyType::Xml);
430        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
431    }
432
433    // =============================================================================
434    // XML Validation Edge Case Tests
435    // =============================================================================
436
437    #[test]
438    fn test_empty_string_rejected_as_xml() {
439        let body = Body::Text("".to_string());
440        let result = convert(body, BodyType::Xml);
441        assert!(
442            matches!(result, Err(CamelError::TypeConversionFailed(_))),
443            "empty string should be rejected as XML"
444        );
445    }
446
447    #[test]
448    fn test_whitespace_only_rejected_as_xml() {
449        let body = Body::Text("   \n\t  ".to_string());
450        let result = convert(body, BodyType::Xml);
451        assert!(
452            matches!(result, Err(CamelError::TypeConversionFailed(_))),
453            "whitespace-only string should be rejected as XML"
454        );
455    }
456
457    #[test]
458    fn test_prolog_only_rejected_as_xml() {
459        // XML declaration without any root element
460        let body = Body::Text(r#"<?xml version="1.0" encoding="UTF-8"?>"#.to_string());
461        let result = convert(body, BodyType::Xml);
462        assert!(
463            matches!(result, Err(CamelError::TypeConversionFailed(_))),
464            "XML prolog without root element should be rejected"
465        );
466    }
467
468    #[test]
469    fn test_multiple_root_elements_rejected() {
470        let body = Body::Text("<root1/><root2/>".to_string());
471        let result = convert(body, BodyType::Xml);
472        assert!(
473            matches!(result, Err(CamelError::TypeConversionFailed(_))),
474            "XML with multiple root elements should be rejected"
475        );
476    }
477
478    #[test]
479    fn test_multiple_root_elements_with_children_rejected() {
480        let body = Body::Text("<a><b/></a><c/>".to_string());
481        let result = convert(body, BodyType::Xml);
482        assert!(
483            matches!(result, Err(CamelError::TypeConversionFailed(_))),
484            "XML with multiple root elements (one with children) should be rejected"
485        );
486    }
487
488    #[test]
489    fn test_valid_xml_with_prolog_accepted() {
490        let xml = r#"<?xml version="1.0" encoding="UTF-8"?><root><child>value</child></root>"#;
491        let body = Body::Text(xml.to_string());
492        let result = convert(body, BodyType::Xml);
493        assert!(
494            result.is_ok(),
495            "XML with prolog and root element should be accepted"
496        );
497    }
498
499    #[test]
500    fn test_self_closing_root_accepted() {
501        let body = Body::Text("<root/>".to_string());
502        let result = convert(body, BodyType::Xml);
503        assert!(
504            result.is_ok(),
505            "self-closing root element should be accepted"
506        );
507    }
508
509    // =============================================================================
510    // Xml↔Json Conversion Tests (Task 5)
511    // =============================================================================
512
513    #[test]
514    fn xml_to_json_valid() {
515        let body = Body::Xml("<root><a>1</a></root>".to_string());
516        let result = convert(body, BodyType::Json).unwrap();
517        match result {
518            Body::Json(v) => assert_eq!(v["root"]["a"], json!("1")),
519            _ => panic!("expected Body::Json"),
520        }
521    }
522
523    #[test]
524    fn xml_to_json_invalid() {
525        let body = Body::Xml("not xml".to_string());
526        let result = convert(body, BodyType::Json);
527        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
528    }
529
530    #[test]
531    fn json_to_xml_valid() {
532        let body = Body::Json(json!({"root": {"a": "1"}}));
533        let result = convert(body, BodyType::Xml).unwrap();
534        match result {
535            Body::Xml(s) => {
536                assert!(s.contains("<root>"));
537                assert!(s.contains("<a>1</a>"));
538            }
539            _ => panic!("expected Body::Xml"),
540        }
541    }
542
543    #[test]
544    fn json_to_xml_non_object_fails() {
545        let body = Body::Json(json!("just a string"));
546        let result = convert(body, BodyType::Xml);
547        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
548    }
549}