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