Skip to main content

drasi_bootstrap_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 HTTP bootstrap responses.
16//!
17//! Supports JSON, XML, YAML content types with automatic detection
18//! from Content-Type header.
19
20use anyhow::{Context, Result};
21use serde_json::Value as JsonValue;
22
23use crate::config::ContentTypeOverride;
24
25/// Detected content type.
26#[derive(Debug, Clone, PartialEq)]
27pub enum ContentType {
28    Json,
29    Xml,
30    Yaml,
31}
32
33impl ContentType {
34    /// Detect content type from Content-Type header value.
35    pub fn from_header(header: Option<&str>) -> Self {
36        match header {
37            Some(h) => {
38                let lower = h.to_lowercase();
39                if lower.contains("application/json") || lower.contains("text/json") {
40                    ContentType::Json
41                } else if lower.contains("application/xml") || lower.contains("text/xml") {
42                    ContentType::Xml
43                } else if lower.contains("application/x-yaml")
44                    || lower.contains("text/yaml")
45                    || lower.contains("application/yaml")
46                {
47                    ContentType::Yaml
48                } else {
49                    // Default to JSON for unknown types
50                    ContentType::Json
51                }
52            }
53            None => ContentType::Json,
54        }
55    }
56
57    /// Create from override configuration.
58    pub fn from_override(override_type: &ContentTypeOverride) -> Self {
59        match override_type {
60            ContentTypeOverride::Json => ContentType::Json,
61            ContentTypeOverride::Xml => ContentType::Xml,
62            ContentTypeOverride::Yaml => ContentType::Yaml,
63        }
64    }
65}
66
67/// Parse response body into a JSON value based on content type.
68pub fn parse_body(body: &str, content_type: &ContentType) -> Result<JsonValue> {
69    match content_type {
70        ContentType::Json => {
71            serde_json::from_str(body).context("Failed to parse response body as JSON")
72        }
73        ContentType::Xml => parse_xml(body),
74        ContentType::Yaml => {
75            serde_yaml::from_str(body).context("Failed to parse response body as YAML")
76        }
77    }
78}
79
80/// Parse XML body into a JSON value.
81fn parse_xml(body: &str) -> Result<JsonValue> {
82    // Use quick-xml to parse into a simple JSON structure
83    let value: JsonValue =
84        quick_xml::de::from_str(body).context("Failed to parse response body as XML")?;
85    Ok(value)
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn test_content_type_detection() {
94        assert_eq!(
95            ContentType::from_header(Some("application/json")),
96            ContentType::Json
97        );
98        assert_eq!(
99            ContentType::from_header(Some("application/json; charset=utf-8")),
100            ContentType::Json
101        );
102        assert_eq!(
103            ContentType::from_header(Some("application/xml")),
104            ContentType::Xml
105        );
106        assert_eq!(
107            ContentType::from_header(Some("text/yaml")),
108            ContentType::Yaml
109        );
110        assert_eq!(ContentType::from_header(None), ContentType::Json);
111        assert_eq!(
112            ContentType::from_header(Some("text/plain")),
113            ContentType::Json
114        );
115    }
116
117    #[test]
118    fn test_parse_json() {
119        let body = r#"{"name": "test", "value": 42}"#;
120        let result = parse_body(body, &ContentType::Json).unwrap();
121        assert_eq!(result["name"], "test");
122        assert_eq!(result["value"], 42);
123    }
124
125    #[test]
126    fn test_parse_yaml() {
127        let body = "name: test\nvalue: 42\n";
128        let result = parse_body(body, &ContentType::Yaml).unwrap();
129        assert_eq!(result["name"], "test");
130        assert_eq!(result["value"], 42);
131    }
132
133    #[test]
134    fn test_parse_xml_simple() {
135        let body = "<root><name>test</name><value>42</value></root>";
136        let result = parse_body(body, &ContentType::Xml).unwrap();
137        assert!(result.get("name").is_some());
138        assert!(result.get("value").is_some());
139    }
140
141    #[test]
142    fn test_parse_xml_with_text_fields() {
143        // quick-xml wraps text content in $text for nested elements
144        let body = "<root><item><id>x1</id><name>Alice</name></item></root>";
145        let result = parse_body(body, &ContentType::Xml).unwrap();
146        let item = &result["item"];
147        // quick-xml produces {"$text": "x1"} for <id>x1</id>
148        assert!(item["id"].is_object() || item["id"].is_string());
149    }
150}