1use crate::body::Body;
2use crate::error::CamelError;
3use crate::xml_convert;
4use bytes::Bytes;
5use thiserror::Error;
6
7#[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
27pub 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
40pub fn convert(body: Body, target: BodyType) -> Result<Body, CamelError> {
45 match (body, target) {
46 (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 (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 (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 (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 (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 (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 (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 #[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 #[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 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 #[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}