Skip to main content

camel_processor/data_format/
xml.rs

1use camel_api::body::Body;
2use camel_api::data_format::DataFormat;
3use camel_api::error::CamelError;
4use camel_api::xml_convert::{json_to_xml, xml_to_json_with_depth_limit};
5use serde::Deserialize;
6
7fn default_max_depth() -> usize {
8    camel_api::xml_convert::DEFAULT_MAX_XML_DEPTH
9}
10
11/// Configuration for [`XmlDataFormat`]. All fields have hardened defaults;
12/// setting a non-default value is the per-item explicit choice per ADR-0033.
13#[derive(Debug, Clone, Deserialize)]
14#[serde(deny_unknown_fields)]
15pub struct XmlConfig {
16    /// Maximum XML nesting depth accepted by `unmarshal` (DoS cap).
17    /// Default 100. Inert during `marshal`.
18    #[serde(default = "default_max_depth")]
19    pub max_depth: usize,
20}
21
22impl Default for XmlConfig {
23    fn default() -> Self {
24        Self {
25            max_depth: default_max_depth(),
26        }
27    }
28}
29
30#[derive(Debug, Clone, Default)]
31pub struct XmlDataFormat {
32    config: XmlConfig,
33}
34
35impl XmlDataFormat {
36    pub fn new(config: XmlConfig) -> Self {
37        Self { config }
38    }
39}
40
41impl DataFormat for XmlDataFormat {
42    fn name(&self) -> &str {
43        "xml"
44    }
45
46    fn marshal(&self, body: Body) -> Result<Body, CamelError> {
47        match body {
48            Body::Json(v) => {
49                let xml = json_to_xml(&v)?;
50                Ok(Body::Text(xml))
51            }
52            Body::Text(_) => Ok(body),
53            Body::Stream(_) => Err(CamelError::TypeConversionFailed(
54                "cannot marshal Body::Stream — add 'stream_cache' or 'convert_body_to' before this step".to_string(),
55            )),
56            Body::Empty | Body::Bytes(_) => Err(CamelError::TypeConversionFailed(
57                "XmlDataFormat::marshal only supports Body::Json and Body::Text".to_string(),
58            )),
59            Body::Xml(_) => Err(CamelError::TypeConversionFailed(
60                "XmlDataFormat::marshal does not accept Body::Xml — use unmarshal to convert XML to JSON"
61                    .to_string(),
62            )),
63            _ => Err(CamelError::TypeConversionFailed(
64                "XmlDataFormat::marshal does not support this body type".to_string(),
65            )),
66        }
67    }
68
69    fn unmarshal(&self, body: Body) -> Result<Body, CamelError> {
70        match body {
71            Body::Json(_) => Ok(body),
72            Body::Text(s) => {
73                let v = xml_to_json_with_depth_limit(&s, self.config.max_depth)?;
74                Ok(Body::Json(v))
75            }
76            Body::Bytes(b) => {
77                let s = String::from_utf8(b.to_vec()).map_err(|e| {
78                    CamelError::TypeConversionFailed(format!(
79                        "cannot unmarshal Body::Bytes as XML: {e}"
80                    ))
81                })?;
82                let v = xml_to_json_with_depth_limit(&s, self.config.max_depth)?;
83                Ok(Body::Json(v))
84            }
85            Body::Xml(s) => {
86                let v = xml_to_json_with_depth_limit(&s, self.config.max_depth)?;
87                Ok(Body::Json(v))
88            }
89            Body::Stream(_) => Err(CamelError::TypeConversionFailed(
90                "cannot unmarshal Body::Stream directly — use UnmarshalService which auto-materializes"
91                    .to_string(),
92            )),
93            Body::Empty => Err(CamelError::TypeConversionFailed(
94                "XmlDataFormat::unmarshal only supports Body::Json, Body::Text, Body::Bytes, and Body::Xml"
95                    .to_string(),
96            )),
97            _ => Err(CamelError::TypeConversionFailed(
98                "XmlDataFormat::unmarshal does not support this body type".to_string(),
99            )),
100        }
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use bytes::Bytes;
108    use serde_json::json;
109
110    #[test]
111    fn test_name() {
112        assert_eq!(XmlDataFormat::default().name(), "xml");
113    }
114
115    #[test]
116    fn test_unmarshal_text_to_json() {
117        let body = Body::Text("<root><child>value</child></root>".to_string());
118        let result = XmlDataFormat::default().unmarshal(body).unwrap();
119        match result {
120            Body::Json(v) => assert_eq!(v["root"]["child"], json!("value")),
121            _ => panic!("expected Body::Json"),
122        }
123    }
124
125    #[test]
126    fn test_unmarshal_bytes_to_json() {
127        let body = Body::Bytes(Bytes::from_static(b"<root/>"));
128        let result = XmlDataFormat::default().unmarshal(body).unwrap();
129        match result {
130            Body::Json(v) => assert_eq!(v["root"], serde_json::Value::Null),
131            _ => panic!("expected Body::Json"),
132        }
133    }
134
135    #[test]
136    fn test_unmarshal_xml_body_to_json() {
137        let body = Body::Xml("<root><a>1</a></root>".to_string());
138        let result = XmlDataFormat::default().unmarshal(body).unwrap();
139        match result {
140            Body::Json(v) => assert_eq!(v["root"]["a"], json!("1")),
141            _ => panic!("expected Body::Json"),
142        }
143    }
144
145    #[test]
146    fn test_unmarshal_invalid_xml() {
147        let body = Body::Text("not xml".to_string());
148        let result = XmlDataFormat::default().unmarshal(body);
149        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
150    }
151
152    #[test]
153    fn test_unmarshal_json_noop() {
154        let body = Body::Json(json!({"x": 1}));
155        let result = XmlDataFormat::default().unmarshal(body).unwrap();
156        assert!(matches!(result, Body::Json(_)));
157    }
158
159    #[test]
160    fn test_unmarshal_empty_returns_error() {
161        let body = Body::Empty;
162        let result = XmlDataFormat::default().unmarshal(body);
163        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
164    }
165
166    #[test]
167    fn test_marshal_json_to_text() {
168        let body = Body::Json(json!({"root": {"name": "Alice"}}));
169        let result = XmlDataFormat::default().marshal(body).unwrap();
170        match result {
171            Body::Text(s) => {
172                assert!(s.contains("<root>"));
173                assert!(s.contains("<name>Alice</name>"));
174            }
175            _ => panic!("expected Body::Text"),
176        }
177    }
178
179    #[test]
180    fn test_marshal_text_noop() {
181        let body = Body::Text("already text".to_string());
182        let result = XmlDataFormat::default().marshal(body).unwrap();
183        assert_eq!(result, Body::Text("already text".to_string()));
184    }
185
186    #[test]
187    fn test_marshal_empty_returns_error() {
188        let body = Body::Empty;
189        let result = XmlDataFormat::default().marshal(body);
190        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
191    }
192
193    #[test]
194    fn test_marshal_xml_body_rejected() {
195        let body = Body::Xml("<root/>".to_string());
196        let result = XmlDataFormat::default().marshal(body);
197        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
198    }
199
200    #[test]
201    fn test_unmarshal_with_raised_max_depth() {
202        let mut xml = String::new();
203        for _ in 0..150 {
204            xml.push_str("<a>");
205        }
206        xml.push('1');
207        for _ in 0..150 {
208            xml.push_str("</a>");
209        }
210        // Default depth (100) rejects 150 nesting; raised cap accepts it.
211        let body = Body::Text(xml);
212        let df = XmlDataFormat::new(XmlConfig { max_depth: 200 });
213        let result = df.unmarshal(body);
214        assert!(
215            result.is_ok(),
216            "should accept depth 150 under raised cap 200"
217        );
218    }
219}