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