Skip to main content

drasi_source_http/
content_parser.rs

1// Copyright 2025 The Drasi Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Content parsing for webhook payloads.
16//!
17//! Supports JSON, XML, YAML, and plain text content types with
18//! automatic detection from Content-Type header.
19
20use anyhow::{anyhow, Result};
21use serde_json::Value as JsonValue;
22
23/// Supported content types for webhook payloads
24#[derive(Debug, Clone, PartialEq)]
25pub enum ContentType {
26    Json,
27    Xml,
28    Yaml,
29    Text,
30}
31
32impl ContentType {
33    /// Parse content type from Content-Type header value
34    pub fn from_header(header: Option<&str>) -> Self {
35        match header {
36            Some(h) => {
37                let lower = h.to_lowercase();
38                if lower.contains("application/json") || lower.contains("text/json") {
39                    ContentType::Json
40                } else if lower.contains("application/xml") || lower.contains("text/xml") {
41                    ContentType::Xml
42                } else if lower.contains("application/x-yaml")
43                    || lower.contains("text/yaml")
44                    || lower.contains("application/yaml")
45                {
46                    ContentType::Yaml
47                } else if lower.contains("text/plain") {
48                    ContentType::Text
49                } else {
50                    // Default to JSON for unknown types
51                    ContentType::Json
52                }
53            }
54            None => ContentType::Json,
55        }
56    }
57}
58
59/// Parse content body into a JSON value based on content type
60///
61/// All content types are normalized to `serde_json::Value` for uniform
62/// template processing.
63pub fn parse_content(body: &[u8], content_type: ContentType) -> Result<JsonValue> {
64    match content_type {
65        ContentType::Json => parse_json(body),
66        ContentType::Xml => parse_xml(body),
67        ContentType::Yaml => parse_yaml(body),
68        ContentType::Text => parse_text(body),
69    }
70}
71
72/// Parse JSON content
73fn parse_json(body: &[u8]) -> Result<JsonValue> {
74    serde_json::from_slice(body).map_err(|e| anyhow!("Failed to parse JSON: {e}"))
75}
76
77/// Parse YAML content and convert to JSON value
78fn parse_yaml(body: &[u8]) -> Result<JsonValue> {
79    let yaml_value: serde_yaml::Value =
80        serde_yaml::from_slice(body).map_err(|e| anyhow!("Failed to parse YAML: {e}"))?;
81    yaml_to_json(yaml_value)
82}
83
84/// Convert YAML value to JSON value
85fn yaml_to_json(yaml: serde_yaml::Value) -> Result<JsonValue> {
86    match yaml {
87        serde_yaml::Value::Null => Ok(JsonValue::Null),
88        serde_yaml::Value::Bool(b) => Ok(JsonValue::Bool(b)),
89        serde_yaml::Value::Number(n) => {
90            if let Some(i) = n.as_i64() {
91                Ok(JsonValue::Number(i.into()))
92            } else if let Some(f) = n.as_f64() {
93                Ok(serde_json::Number::from_f64(f)
94                    .map(JsonValue::Number)
95                    .unwrap_or(JsonValue::Null))
96            } else {
97                Ok(JsonValue::Null)
98            }
99        }
100        serde_yaml::Value::String(s) => Ok(JsonValue::String(s)),
101        serde_yaml::Value::Sequence(seq) => {
102            let arr: Result<Vec<JsonValue>> = seq.into_iter().map(yaml_to_json).collect();
103            Ok(JsonValue::Array(arr?))
104        }
105        serde_yaml::Value::Mapping(map) => {
106            let mut obj = serde_json::Map::new();
107            for (k, v) in map {
108                let key = match k {
109                    serde_yaml::Value::String(s) => s,
110                    serde_yaml::Value::Number(n) => n.to_string(),
111                    serde_yaml::Value::Bool(b) => b.to_string(),
112                    _ => continue,
113                };
114                obj.insert(key, yaml_to_json(v)?);
115            }
116            Ok(JsonValue::Object(obj))
117        }
118        serde_yaml::Value::Tagged(tagged) => yaml_to_json(tagged.value),
119    }
120}
121
122/// Parse XML content and convert to JSON value
123fn parse_xml(body: &[u8]) -> Result<JsonValue> {
124    let xml_str = std::str::from_utf8(body).map_err(|e| anyhow!("Invalid UTF-8 in XML: {e}"))?;
125    xml_to_json(xml_str)
126}
127
128/// Convert XML string to JSON value
129///
130/// Uses a simplified conversion where:
131/// - Elements become objects
132/// - Text content goes into a "_text" field
133/// - Attributes go into "@attribute_name" fields
134/// - Repeated elements become arrays
135fn xml_to_json(xml: &str) -> Result<JsonValue> {
136    use quick_xml::events::Event;
137    use quick_xml::Reader;
138
139    let mut reader = Reader::from_str(xml);
140    reader.config_mut().trim_text(true);
141
142    let mut stack: Vec<(String, JsonValue)> = vec![(
143        "root".to_string(),
144        JsonValue::Object(serde_json::Map::new()),
145    )];
146
147    loop {
148        match reader.read_event() {
149            Ok(Event::Start(e)) => {
150                let name = String::from_utf8_lossy(e.name().as_ref()).to_string();
151                let mut obj = serde_json::Map::new();
152
153                // Add attributes
154                for attr in e.attributes().flatten() {
155                    let key = format!("@{}", String::from_utf8_lossy(attr.key.as_ref()));
156                    let value = String::from_utf8_lossy(&attr.value).to_string();
157                    obj.insert(key, JsonValue::String(value));
158                }
159
160                stack.push((name, JsonValue::Object(obj)));
161            }
162            Ok(Event::End(_)) if stack.len() > 1 => {
163                if let Some((name, value)) = stack.pop() {
164                    if let Some((_, JsonValue::Object(parent_obj))) = stack.last_mut() {
165                        // Handle repeated elements by converting to array
166                        if let Some(existing) = parent_obj.get_mut(&name) {
167                            match existing {
168                                JsonValue::Array(arr) => arr.push(value),
169                                _ => {
170                                    let prev = existing.take();
171                                    *existing = JsonValue::Array(vec![prev, value]);
172                                }
173                            }
174                        } else {
175                            parent_obj.insert(name, value);
176                        }
177                    }
178                }
179            }
180            Ok(Event::Text(e)) => {
181                let text = e
182                    .unescape()
183                    .map_err(|e| anyhow!("XML unescape error: {e}"))?;
184                let text = text.trim();
185                if !text.is_empty() {
186                    if let Some((_, current)) = stack.last_mut() {
187                        if let JsonValue::Object(obj) = current {
188                            if obj.is_empty() {
189                                // If no attributes, just use the text value directly
190                                *current = JsonValue::String(text.to_string());
191                            } else {
192                                obj.insert(
193                                    "_text".to_string(),
194                                    JsonValue::String(text.to_string()),
195                                );
196                            }
197                        }
198                    }
199                }
200            }
201            Ok(Event::Empty(e)) => {
202                let name = String::from_utf8_lossy(e.name().as_ref()).to_string();
203                let mut obj = serde_json::Map::new();
204
205                for attr in e.attributes().flatten() {
206                    let key = format!("@{}", String::from_utf8_lossy(attr.key.as_ref()));
207                    let value = String::from_utf8_lossy(&attr.value).to_string();
208                    obj.insert(key, JsonValue::String(value));
209                }
210
211                let value = if obj.is_empty() {
212                    JsonValue::Null
213                } else {
214                    JsonValue::Object(obj)
215                };
216
217                if let Some((_, JsonValue::Object(parent_obj))) = stack.last_mut() {
218                    if let Some(existing) = parent_obj.get_mut(&name) {
219                        match existing {
220                            JsonValue::Array(arr) => arr.push(value),
221                            _ => {
222                                let prev = existing.take();
223                                *existing = JsonValue::Array(vec![prev, value]);
224                            }
225                        }
226                    } else {
227                        parent_obj.insert(name, value);
228                    }
229                }
230            }
231            Ok(Event::Eof) => break,
232            Err(e) => return Err(anyhow!("XML parse error: {e}")),
233            _ => {}
234        }
235    }
236
237    // Return the root object's content
238    let Some((_, JsonValue::Object(mut root))) = stack.pop() else {
239        return Err(anyhow!("Failed to parse XML structure"));
240    };
241
242    // If there's only one child, return it directly
243    if root.len() == 1 {
244        Ok(root
245            .into_iter()
246            .next()
247            .map(|(_, v)| v)
248            .unwrap_or(JsonValue::Null))
249    } else {
250        Ok(JsonValue::Object(root))
251    }
252}
253
254/// Parse plain text content
255fn parse_text(body: &[u8]) -> Result<JsonValue> {
256    let text =
257        std::str::from_utf8(body).map_err(|e| anyhow!("Invalid UTF-8 in text content: {e}"))?;
258    Ok(JsonValue::String(text.to_string()))
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    #[test]
266    fn test_content_type_from_header() {
267        assert_eq!(
268            ContentType::from_header(Some("application/json")),
269            ContentType::Json
270        );
271        assert_eq!(
272            ContentType::from_header(Some("application/json; charset=utf-8")),
273            ContentType::Json
274        );
275        assert_eq!(
276            ContentType::from_header(Some("text/json")),
277            ContentType::Json
278        );
279        assert_eq!(
280            ContentType::from_header(Some("application/xml")),
281            ContentType::Xml
282        );
283        assert_eq!(ContentType::from_header(Some("text/xml")), ContentType::Xml);
284        assert_eq!(
285            ContentType::from_header(Some("application/x-yaml")),
286            ContentType::Yaml
287        );
288        assert_eq!(
289            ContentType::from_header(Some("text/yaml")),
290            ContentType::Yaml
291        );
292        assert_eq!(
293            ContentType::from_header(Some("text/plain")),
294            ContentType::Text
295        );
296        assert_eq!(ContentType::from_header(None), ContentType::Json);
297        assert_eq!(
298            ContentType::from_header(Some("unknown/type")),
299            ContentType::Json
300        );
301    }
302
303    #[test]
304    fn test_parse_json() {
305        let json = r#"{"name": "test", "value": 42, "nested": {"key": "value"}}"#;
306        let result = parse_content(json.as_bytes(), ContentType::Json).unwrap();
307
308        assert_eq!(result["name"], "test");
309        assert_eq!(result["value"], 42);
310        assert_eq!(result["nested"]["key"], "value");
311    }
312
313    #[test]
314    fn test_parse_json_array() {
315        let json = r#"[1, 2, 3]"#;
316        let result = parse_content(json.as_bytes(), ContentType::Json).unwrap();
317
318        assert!(result.is_array());
319        assert_eq!(result.as_array().unwrap().len(), 3);
320    }
321
322    #[test]
323    fn test_parse_yaml() {
324        let yaml = r#"
325name: test
326value: 42
327nested:
328  key: value
329items:
330  - one
331  - two
332"#;
333        let result = parse_content(yaml.as_bytes(), ContentType::Yaml).unwrap();
334
335        assert_eq!(result["name"], "test");
336        assert_eq!(result["value"], 42);
337        assert_eq!(result["nested"]["key"], "value");
338        assert_eq!(result["items"][0], "one");
339        assert_eq!(result["items"][1], "two");
340    }
341
342    #[test]
343    fn test_parse_xml_simple() {
344        let xml = r#"<root><name>test</name><value>42</value></root>"#;
345        let result = parse_content(xml.as_bytes(), ContentType::Xml).unwrap();
346
347        assert_eq!(result["name"], "test");
348        assert_eq!(result["value"], "42");
349    }
350
351    #[test]
352    fn test_parse_xml_with_attributes() {
353        let xml = r#"<item id="123" type="test">content</item>"#;
354        let result = parse_content(xml.as_bytes(), ContentType::Xml).unwrap();
355
356        assert_eq!(result["@id"], "123");
357        assert_eq!(result["@type"], "test");
358        assert_eq!(result["_text"], "content");
359    }
360
361    #[test]
362    fn test_parse_xml_repeated_elements() {
363        let xml = r#"<root><item>one</item><item>two</item><item>three</item></root>"#;
364        let result = parse_content(xml.as_bytes(), ContentType::Xml).unwrap();
365
366        assert!(result["item"].is_array());
367        let items = result["item"].as_array().unwrap();
368        assert_eq!(items.len(), 3);
369        assert_eq!(items[0], "one");
370        assert_eq!(items[1], "two");
371        assert_eq!(items[2], "three");
372    }
373
374    #[test]
375    fn test_parse_text() {
376        let text = "Hello, World!";
377        let result = parse_content(text.as_bytes(), ContentType::Text).unwrap();
378
379        assert_eq!(result, JsonValue::String("Hello, World!".to_string()));
380    }
381
382    #[test]
383    fn test_parse_invalid_json() {
384        let invalid = "not valid json";
385        let result = parse_content(invalid.as_bytes(), ContentType::Json);
386        assert!(result.is_err());
387    }
388
389    #[test]
390    fn test_parse_invalid_yaml() {
391        let invalid = "key: [unclosed";
392        let result = parse_content(invalid.as_bytes(), ContentType::Yaml);
393        assert!(result.is_err());
394    }
395
396    #[test]
397    fn test_parse_invalid_xml() {
398        // This is malformed XML with mismatched tags
399        let invalid = "<root><unclosed></root>";
400        let result = parse_content(invalid.as_bytes(), ContentType::Xml);
401        assert!(result.is_err());
402    }
403}