1use crate::body::Body;
2use crate::error::CamelError;
3use bytes::Bytes;
4use sxd_document::{Package, parser};
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<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
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(_), 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 (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 (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 (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 (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 #[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 #[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 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}