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(serde_json::Value::String(s)), BodyType::Text) => Ok(Body::Text(s)),
114 (Body::Json(v), BodyType::Text) => Ok(Body::Text(v.to_string())),
115 (Body::Json(v), BodyType::Bytes) => {
116 let b = serde_json::to_vec(&v).map_err(|e| {
117 CamelError::TypeConversionFailed(format!("cannot convert Body::Json to Bytes: {e}"))
118 })?;
119 Ok(Body::Bytes(Bytes::from(b)))
120 }
121 (Body::Json(_), BodyType::Xml) => Err(CamelError::TypeConversionFailed(
122 "cannot convert Body::Json to Xml: JSON to XML conversion is not supported".to_string(),
123 )),
124 (Body::Json(_), BodyType::Empty) => Err(CamelError::TypeConversionFailed(
125 "cannot convert Body::Json to Empty".to_string(),
126 )),
127
128 (Body::Bytes(b), BodyType::Text) => {
130 let s = String::from_utf8(b.to_vec()).map_err(|e| {
131 CamelError::TypeConversionFailed(format!(
132 "cannot convert Body::Bytes to Text: invalid UTF-8 sequence: {e}"
133 ))
134 })?;
135 Ok(Body::Text(s))
136 }
137 (Body::Bytes(b), BodyType::Json) => {
138 let s = String::from_utf8(b.to_vec()).map_err(|e| {
139 CamelError::TypeConversionFailed(format!(
140 "cannot convert Body::Bytes to Json (UTF-8 error): {e}"
141 ))
142 })?;
143 let v = serde_json::from_str(&s).map_err(|e| {
144 CamelError::TypeConversionFailed(format!("cannot convert Body::Bytes to Json: {e}"))
145 })?;
146 Ok(Body::Json(v))
147 }
148 (Body::Bytes(b), BodyType::Xml) => {
149 let s = String::from_utf8(b.to_vec()).map_err(|e| {
150 CamelError::TypeConversionFailed(format!(
151 "cannot convert Body::Bytes to Xml (UTF-8 error): {e}"
152 ))
153 })?;
154 validate_xml(&s)?;
155 Ok(Body::Xml(s))
156 }
157 (Body::Bytes(_), BodyType::Empty) => Err(CamelError::TypeConversionFailed(
158 "cannot convert Body::Bytes to Empty".to_string(),
159 )),
160
161 (Body::Xml(s), BodyType::Text) => Ok(Body::Text(s)),
163 (Body::Xml(s), BodyType::Bytes) => Ok(Body::Bytes(Bytes::from(s.into_bytes()))),
164 (Body::Xml(_), BodyType::Json) => Err(CamelError::TypeConversionFailed(
165 "cannot convert Body::Xml to Json: XML to JSON conversion is not supported".to_string(),
166 )),
167 (Body::Xml(_), BodyType::Empty) => Err(CamelError::TypeConversionFailed(
168 "cannot convert Body::Xml to Empty".to_string(),
169 )),
170
171 (Body::Empty, BodyType::Text) => Err(CamelError::TypeConversionFailed(
173 "cannot convert Empty body to Text".to_string(),
174 )),
175 (Body::Empty, BodyType::Json) => Err(CamelError::TypeConversionFailed(
176 "cannot convert Empty body to Json".to_string(),
177 )),
178 (Body::Empty, BodyType::Bytes) => Err(CamelError::TypeConversionFailed(
179 "cannot convert Empty body to Bytes".to_string(),
180 )),
181 (Body::Empty, BodyType::Xml) => Err(CamelError::TypeConversionFailed(
182 "cannot convert Empty body to Xml".to_string(),
183 )),
184
185 (Body::Stream(_), _) => Err(CamelError::TypeConversionFailed(
187 "cannot convert Body::Stream: materialize first with into_bytes()".to_string(),
188 )),
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use serde_json::json;
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_unsupported() {
393 let body = Body::Json(json!({"key": "value"}));
394 let result = convert(body, BodyType::Xml);
395 assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
396 if let Err(CamelError::TypeConversionFailed(msg)) = result {
397 assert!(
398 msg.contains("not supported"),
399 "error message should mention 'not supported', got: {}",
400 msg
401 );
402 }
403 }
404
405 #[test]
406 fn test_xml_to_json_unsupported() {
407 let body = Body::Xml("<root/>".to_string());
408 let result = convert(body, BodyType::Json);
409 assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
410 if let Err(CamelError::TypeConversionFailed(msg)) = result {
411 assert!(
412 msg.contains("not supported"),
413 "error message should mention 'not supported', got: {}",
414 msg
415 );
416 }
417 }
418
419 #[test]
420 fn test_empty_to_xml_fails() {
421 let result = convert(Body::Empty, BodyType::Xml);
422 assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
423 }
424
425 #[test]
426 fn test_xml_to_empty_fails() {
427 let body = Body::Xml("<root/>".to_string());
428 let result = convert(body, BodyType::Empty);
429 assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
430 }
431
432 #[test]
433 fn test_bytes_to_xml_invalid_utf8() {
434 let body = Body::Bytes(Bytes::from_static(&[0xFF, 0xFE]));
435 let result = convert(body, BodyType::Xml);
436 assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
437 }
438
439 #[test]
440 fn test_bytes_to_xml_invalid_xml() {
441 let invalid = b"valid utf-8 but <invalid xml";
442 let body = Body::Bytes(Bytes::from_static(invalid));
443 let result = convert(body, BodyType::Xml);
444 assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
445 }
446
447 #[test]
452 fn test_empty_string_rejected_as_xml() {
453 let body = Body::Text("".to_string());
454 let result = convert(body, BodyType::Xml);
455 assert!(
456 matches!(result, Err(CamelError::TypeConversionFailed(_))),
457 "empty string should be rejected as XML"
458 );
459 }
460
461 #[test]
462 fn test_whitespace_only_rejected_as_xml() {
463 let body = Body::Text(" \n\t ".to_string());
464 let result = convert(body, BodyType::Xml);
465 assert!(
466 matches!(result, Err(CamelError::TypeConversionFailed(_))),
467 "whitespace-only string should be rejected as XML"
468 );
469 }
470
471 #[test]
472 fn test_prolog_only_rejected_as_xml() {
473 let body = Body::Text(r#"<?xml version="1.0" encoding="UTF-8"?>"#.to_string());
475 let result = convert(body, BodyType::Xml);
476 assert!(
477 matches!(result, Err(CamelError::TypeConversionFailed(_))),
478 "XML prolog without root element should be rejected"
479 );
480 }
481
482 #[test]
483 fn test_multiple_root_elements_rejected() {
484 let body = Body::Text("<root1/><root2/>".to_string());
485 let result = convert(body, BodyType::Xml);
486 assert!(
487 matches!(result, Err(CamelError::TypeConversionFailed(_))),
488 "XML with multiple root elements should be rejected"
489 );
490 }
491
492 #[test]
493 fn test_multiple_root_elements_with_children_rejected() {
494 let body = Body::Text("<a><b/></a><c/>".to_string());
495 let result = convert(body, BodyType::Xml);
496 assert!(
497 matches!(result, Err(CamelError::TypeConversionFailed(_))),
498 "XML with multiple root elements (one with children) should be rejected"
499 );
500 }
501
502 #[test]
503 fn test_valid_xml_with_prolog_accepted() {
504 let xml = r#"<?xml version="1.0" encoding="UTF-8"?><root><child>value</child></root>"#;
505 let body = Body::Text(xml.to_string());
506 let result = convert(body, BodyType::Xml);
507 assert!(
508 result.is_ok(),
509 "XML with prolog and root element should be accepted"
510 );
511 }
512
513 #[test]
514 fn test_self_closing_root_accepted() {
515 let body = Body::Text("<root/>".to_string());
516 let result = convert(body, BodyType::Xml);
517 assert!(
518 result.is_ok(),
519 "self-closing root element should be accepted"
520 );
521 }
522}