1use crate::body::Body;
2use crate::error::CamelError;
3use bytes::Bytes;
4use quick_xml::Reader;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum BodyType {
9 Text,
10 Json,
11 Bytes,
12 Xml,
13 Empty,
14}
15
16fn validate_xml(s: &str) -> Result<(), CamelError> {
24 if s.trim().is_empty() {
25 return Err(CamelError::TypeConversionFailed(
26 "invalid XML: document is empty".to_string(),
27 ));
28 }
29
30 let mut reader = Reader::from_str(s);
31 let mut buf = Vec::new();
32 let mut root_element_count: u32 = 0;
33 let mut depth: u32 = 0;
34
35 loop {
36 match reader.read_event_into(&mut buf) {
37 Ok(quick_xml::events::Event::Eof) => break,
38 Ok(quick_xml::events::Event::Start(_)) => {
39 depth += 1;
40 if depth == 1 {
41 root_element_count += 1;
42 if root_element_count > 1 {
43 return Err(CamelError::TypeConversionFailed(
44 "invalid XML: document has multiple root elements".to_string(),
45 ));
46 }
47 }
48 buf.clear();
49 }
50 Ok(quick_xml::events::Event::End(_)) => {
51 depth = depth.saturating_sub(1);
52 buf.clear();
53 }
54 Ok(quick_xml::events::Event::Empty(_)) => {
55 if depth == 0 {
56 root_element_count += 1;
57 if root_element_count > 1 {
58 return Err(CamelError::TypeConversionFailed(
59 "invalid XML: document has multiple root elements".to_string(),
60 ));
61 }
62 }
63 buf.clear();
64 }
65 Ok(_) => buf.clear(),
66 Err(e) => {
67 return Err(CamelError::TypeConversionFailed(format!(
68 "invalid XML: {e}"
69 )));
70 }
71 }
72 }
73
74 if root_element_count == 0 {
75 return Err(CamelError::TypeConversionFailed(
76 "invalid XML: document has no root element".to_string(),
77 ));
78 }
79
80 Ok(())
81}
82
83pub fn convert(body: Body, target: BodyType) -> Result<Body, CamelError> {
88 match (body, target) {
89 (b @ Body::Text(_), BodyType::Text) => Ok(b),
91 (b @ Body::Json(_), BodyType::Json) => Ok(b),
92 (b @ Body::Bytes(_), BodyType::Bytes) => Ok(b),
93 (b @ Body::Xml(_), BodyType::Xml) => Ok(b),
94 (Body::Empty, BodyType::Empty) => Ok(Body::Empty),
95
96 (Body::Text(s), BodyType::Json) => {
98 let v = serde_json::from_str(&s).map_err(|e| {
99 CamelError::TypeConversionFailed(format!("cannot convert Body::Text to Json: {e}"))
100 })?;
101 Ok(Body::Json(v))
102 }
103 (Body::Text(s), BodyType::Bytes) => Ok(Body::Bytes(Bytes::from(s.into_bytes()))),
104 (Body::Text(s), BodyType::Xml) => {
105 validate_xml(&s)?;
106 Ok(Body::Xml(s))
107 }
108 (Body::Text(_), BodyType::Empty) => Err(CamelError::TypeConversionFailed(
109 "cannot convert Body::Text to Empty".to_string(),
110 )),
111
112 (Body::Json(v), BodyType::Text) => Ok(Body::Text(v.to_string())),
114 (Body::Json(v), BodyType::Bytes) => {
115 let b = serde_json::to_vec(&v).map_err(|e| {
116 CamelError::TypeConversionFailed(format!("cannot convert Body::Json to Bytes: {e}"))
117 })?;
118 Ok(Body::Bytes(Bytes::from(b)))
119 }
120 (Body::Json(_), BodyType::Xml) => Err(CamelError::TypeConversionFailed(
121 "cannot convert Body::Json to Xml: JSON to XML conversion is not supported".to_string(),
122 )),
123 (Body::Json(_), BodyType::Empty) => Err(CamelError::TypeConversionFailed(
124 "cannot convert Body::Json to Empty".to_string(),
125 )),
126
127 (Body::Bytes(b), BodyType::Text) => {
129 let s = String::from_utf8(b.to_vec()).map_err(|e| {
130 CamelError::TypeConversionFailed(format!(
131 "cannot convert Body::Bytes to Text: invalid UTF-8 sequence: {e}"
132 ))
133 })?;
134 Ok(Body::Text(s))
135 }
136 (Body::Bytes(b), BodyType::Json) => {
137 let s = String::from_utf8(b.to_vec()).map_err(|e| {
138 CamelError::TypeConversionFailed(format!(
139 "cannot convert Body::Bytes to Json (UTF-8 error): {e}"
140 ))
141 })?;
142 let v = serde_json::from_str(&s).map_err(|e| {
143 CamelError::TypeConversionFailed(format!("cannot convert Body::Bytes to Json: {e}"))
144 })?;
145 Ok(Body::Json(v))
146 }
147 (Body::Bytes(b), BodyType::Xml) => {
148 let s = String::from_utf8(b.to_vec()).map_err(|e| {
149 CamelError::TypeConversionFailed(format!(
150 "cannot convert Body::Bytes to Xml (UTF-8 error): {e}"
151 ))
152 })?;
153 validate_xml(&s)?;
154 Ok(Body::Xml(s))
155 }
156 (Body::Bytes(_), BodyType::Empty) => Err(CamelError::TypeConversionFailed(
157 "cannot convert Body::Bytes to Empty".to_string(),
158 )),
159
160 (Body::Xml(s), BodyType::Text) => Ok(Body::Text(s)),
162 (Body::Xml(s), BodyType::Bytes) => Ok(Body::Bytes(Bytes::from(s.into_bytes()))),
163 (Body::Xml(_), BodyType::Json) => Err(CamelError::TypeConversionFailed(
164 "cannot convert Body::Xml to Json: XML to JSON conversion is not supported".to_string(),
165 )),
166 (Body::Xml(_), BodyType::Empty) => Err(CamelError::TypeConversionFailed(
167 "cannot convert Body::Xml to Empty".to_string(),
168 )),
169
170 (Body::Empty, BodyType::Text) => Err(CamelError::TypeConversionFailed(
172 "cannot convert Empty body to Text".to_string(),
173 )),
174 (Body::Empty, BodyType::Json) => Err(CamelError::TypeConversionFailed(
175 "cannot convert Empty body to Json".to_string(),
176 )),
177 (Body::Empty, BodyType::Bytes) => Err(CamelError::TypeConversionFailed(
178 "cannot convert Empty body to Bytes".to_string(),
179 )),
180 (Body::Empty, BodyType::Xml) => Err(CamelError::TypeConversionFailed(
181 "cannot convert Empty body to Xml".to_string(),
182 )),
183
184 (Body::Stream(_), _) => Err(CamelError::TypeConversionFailed(
186 "cannot convert Body::Stream: materialize first with into_bytes()".to_string(),
187 )),
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194 use serde_json::json;
195
196 #[test]
197 fn text_to_json_valid() {
198 let body = Body::Text(r#"{"a":1}"#.to_string());
199 let result = convert(body, BodyType::Json).unwrap();
200 assert_eq!(result, Body::Json(json!({"a": 1})));
201 }
202
203 #[test]
204 fn text_to_json_invalid() {
205 let body = Body::Text("not json".to_string());
206 let result = convert(body, BodyType::Json);
207 assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
208 }
209
210 #[test]
211 fn json_to_text() {
212 let body = Body::Json(json!({"a": 1}));
213 let result = convert(body, BodyType::Text).unwrap();
214 match result {
215 Body::Text(s) => assert!(s.contains("\"a\"")),
216 _ => panic!("expected Body::Text"),
217 }
218 }
219
220 #[test]
221 fn json_to_bytes() {
222 let body = Body::Json(json!({"x": 2}));
223 let result = convert(body, BodyType::Bytes).unwrap();
224 assert!(matches!(result, Body::Bytes(_)));
225 }
226
227 #[test]
228 fn bytes_to_text_valid() {
229 let body = Body::Bytes(Bytes::from_static(b"hello"));
230 let result = convert(body, BodyType::Text).unwrap();
231 assert_eq!(result, Body::Text("hello".to_string()));
232 }
233
234 #[test]
235 fn bytes_to_text_invalid_utf8() {
236 let body = Body::Bytes(Bytes::from_static(&[0xFF, 0xFE]));
237 let result = convert(body, BodyType::Text);
238 assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
239 }
240
241 #[test]
242 fn text_to_bytes() {
243 let body = Body::Text("hi".to_string());
244 let result = convert(body, BodyType::Bytes).unwrap();
245 assert_eq!(result, Body::Bytes(Bytes::from_static(b"hi")));
246 }
247
248 #[test]
249 fn empty_to_text_fails() {
250 let result = convert(Body::Empty, BodyType::Text);
251 assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
252 }
253
254 #[test]
255 fn empty_to_empty_noop() {
256 let result = convert(Body::Empty, BodyType::Empty).unwrap();
257 assert!(matches!(result, Body::Empty));
258 }
259
260 #[test]
261 fn noop_same_type_text() {
262 let body = Body::Text("x".to_string());
263 let result = convert(body, BodyType::Text).unwrap();
264 assert!(matches!(result, Body::Text(_)));
265 }
266
267 #[test]
268 fn noop_same_type_json() {
269 let body = Body::Json(json!(1));
270 let result = convert(body, BodyType::Json).unwrap();
271 assert!(matches!(result, Body::Json(_)));
272 }
273
274 #[test]
275 fn noop_same_type_bytes() {
276 let body = Body::Bytes(Bytes::from_static(b"x"));
277 let result = convert(body, BodyType::Bytes).unwrap();
278 assert!(matches!(result, Body::Bytes(_)));
279 }
280
281 #[test]
282 fn stream_to_any_fails() {
283 use crate::body::{StreamBody, StreamMetadata};
284 use futures::stream;
285 use std::sync::Arc;
286 use tokio::sync::Mutex;
287
288 let stream = stream::iter(vec![Ok(Bytes::from_static(b"data"))]);
289 let body = Body::Stream(StreamBody {
290 stream: Arc::new(Mutex::new(Some(Box::pin(stream)))),
291 metadata: StreamMetadata::default(),
292 });
293 let result = convert(body, BodyType::Text);
294 assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
295 }
296
297 #[test]
298 fn bytes_to_json_valid() {
299 let body = Body::Bytes(Bytes::from_static(b"{\"k\":1}"));
300 let result = convert(body, BodyType::Json).unwrap();
301 assert!(matches!(result, Body::Json(_)));
302 }
303
304 #[test]
305 fn bytes_to_json_invalid_utf8() {
306 let body = Body::Bytes(Bytes::from_static(&[0xFF, 0xFE]));
307 let result = convert(body, BodyType::Json);
308 assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
309 }
310
311 #[test]
312 fn to_empty_always_fails() {
313 assert!(matches!(
314 convert(Body::Text("x".into()), BodyType::Empty),
315 Err(CamelError::TypeConversionFailed(_))
316 ));
317 assert!(matches!(
318 convert(Body::Json(serde_json::json!(1)), BodyType::Empty),
319 Err(CamelError::TypeConversionFailed(_))
320 ));
321 assert!(matches!(
322 convert(Body::Bytes(Bytes::from_static(b"x")), BodyType::Empty),
323 Err(CamelError::TypeConversionFailed(_))
324 ));
325 }
326
327 #[test]
332 fn noop_same_type_xml() {
333 let body = Body::Xml("<root/>".to_string());
334 let result = convert(body, BodyType::Xml).unwrap();
335 assert!(matches!(result, Body::Xml(_)));
336 }
337
338 #[test]
339 fn test_text_to_xml() {
340 let xml = r#"<root><child>value</child></root>"#;
341 let body = Body::Text(xml.to_string());
342 let result = convert(body, BodyType::Xml).unwrap();
343 match result {
344 Body::Xml(s) => assert_eq!(s, xml),
345 _ => panic!("expected Body::Xml"),
346 }
347 }
348
349 #[test]
350 fn test_xml_to_text() {
351 let xml = r#"<root><child>value</child></root>"#;
352 let body = Body::Xml(xml.to_string());
353 let result = convert(body, BodyType::Text).unwrap();
354 match result {
355 Body::Text(s) => assert_eq!(s, xml),
356 _ => panic!("expected Body::Text"),
357 }
358 }
359
360 #[test]
361 fn test_bytes_to_xml() {
362 let xml = r#"<root><child>value</child></root>"#;
363 let body = Body::Bytes(Bytes::from(xml.as_bytes()));
364 let result = convert(body, BodyType::Xml).unwrap();
365 match result {
366 Body::Xml(s) => assert_eq!(s, xml),
367 _ => panic!("expected Body::Xml"),
368 }
369 }
370
371 #[test]
372 fn test_xml_to_bytes() {
373 let xml = r#"<root><child>value</child></root>"#;
374 let body = Body::Xml(xml.to_string());
375 let result = convert(body, BodyType::Bytes).unwrap();
376 match result {
377 Body::Bytes(b) => assert_eq!(b.as_ref(), xml.as_bytes()),
378 _ => panic!("expected Body::Bytes"),
379 }
380 }
381
382 #[test]
383 fn test_invalid_xml_rejected() {
384 let invalid_xml = "not valid xml <unclosed";
385 let body = Body::Text(invalid_xml.to_string());
386 let result = convert(body, BodyType::Xml);
387 assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
388 }
389
390 #[test]
391 fn test_json_to_xml_unsupported() {
392 let body = Body::Json(json!({"key": "value"}));
393 let result = convert(body, BodyType::Xml);
394 assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
395 if let Err(CamelError::TypeConversionFailed(msg)) = result {
396 assert!(
397 msg.contains("not supported"),
398 "error message should mention 'not supported', got: {}",
399 msg
400 );
401 }
402 }
403
404 #[test]
405 fn test_xml_to_json_unsupported() {
406 let body = Body::Xml("<root/>".to_string());
407 let result = convert(body, BodyType::Json);
408 assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
409 if let Err(CamelError::TypeConversionFailed(msg)) = result {
410 assert!(
411 msg.contains("not supported"),
412 "error message should mention 'not supported', got: {}",
413 msg
414 );
415 }
416 }
417
418 #[test]
419 fn test_empty_to_xml_fails() {
420 let result = convert(Body::Empty, BodyType::Xml);
421 assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
422 }
423
424 #[test]
425 fn test_xml_to_empty_fails() {
426 let body = Body::Xml("<root/>".to_string());
427 let result = convert(body, BodyType::Empty);
428 assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
429 }
430
431 #[test]
432 fn test_bytes_to_xml_invalid_utf8() {
433 let body = Body::Bytes(Bytes::from_static(&[0xFF, 0xFE]));
434 let result = convert(body, BodyType::Xml);
435 assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
436 }
437
438 #[test]
439 fn test_bytes_to_xml_invalid_xml() {
440 let invalid = b"valid utf-8 but <invalid xml";
441 let body = Body::Bytes(Bytes::from_static(invalid));
442 let result = convert(body, BodyType::Xml);
443 assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
444 }
445
446 #[test]
451 fn test_empty_string_rejected_as_xml() {
452 let body = Body::Text("".to_string());
453 let result = convert(body, BodyType::Xml);
454 assert!(
455 matches!(result, Err(CamelError::TypeConversionFailed(_))),
456 "empty string should be rejected as XML"
457 );
458 }
459
460 #[test]
461 fn test_whitespace_only_rejected_as_xml() {
462 let body = Body::Text(" \n\t ".to_string());
463 let result = convert(body, BodyType::Xml);
464 assert!(
465 matches!(result, Err(CamelError::TypeConversionFailed(_))),
466 "whitespace-only string should be rejected as XML"
467 );
468 }
469
470 #[test]
471 fn test_prolog_only_rejected_as_xml() {
472 let body = Body::Text(r#"<?xml version="1.0" encoding="UTF-8"?>"#.to_string());
474 let result = convert(body, BodyType::Xml);
475 assert!(
476 matches!(result, Err(CamelError::TypeConversionFailed(_))),
477 "XML prolog without root element should be rejected"
478 );
479 }
480
481 #[test]
482 fn test_multiple_root_elements_rejected() {
483 let body = Body::Text("<root1/><root2/>".to_string());
484 let result = convert(body, BodyType::Xml);
485 assert!(
486 matches!(result, Err(CamelError::TypeConversionFailed(_))),
487 "XML with multiple root elements should be rejected"
488 );
489 }
490
491 #[test]
492 fn test_multiple_root_elements_with_children_rejected() {
493 let body = Body::Text("<a><b/></a><c/>".to_string());
494 let result = convert(body, BodyType::Xml);
495 assert!(
496 matches!(result, Err(CamelError::TypeConversionFailed(_))),
497 "XML with multiple root elements (one with children) should be rejected"
498 );
499 }
500
501 #[test]
502 fn test_valid_xml_with_prolog_accepted() {
503 let xml = r#"<?xml version="1.0" encoding="UTF-8"?><root><child>value</child></root>"#;
504 let body = Body::Text(xml.to_string());
505 let result = convert(body, BodyType::Xml);
506 assert!(
507 result.is_ok(),
508 "XML with prolog and root element should be accepted"
509 );
510 }
511
512 #[test]
513 fn test_self_closing_root_accepted() {
514 let body = Body::Text("<root/>".to_string());
515 let result = convert(body, BodyType::Xml);
516 assert!(
517 result.is_ok(),
518 "self-closing root element should be accepted"
519 );
520 }
521}