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)]
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
25pub 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
38pub fn convert(body: Body, target: BodyType) -> Result<Body, CamelError> {
43 match (body, target) {
44 (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 (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 (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 (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 (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 (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 (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 #[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 #[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 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 #[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}