use super::media_type::MediaType;
#[derive(Debug, Clone)]
pub struct ContentTypeDetector {
default_type: MediaType,
}
impl ContentTypeDetector {
pub fn new() -> Self {
Self {
default_type: MediaType::new("application", "octet-stream"),
}
}
pub fn with_default(default_type: MediaType) -> Self {
Self { default_type }
}
pub fn detect(&self, body: &[u8]) -> MediaType {
if body.is_empty() {
return self.default_type.clone();
}
let body_str = match std::str::from_utf8(body) {
Ok(s) => s.trim(),
Err(_) => return self.default_type.clone(),
};
if body_str.is_empty() {
return self.default_type.clone();
}
if self.is_json(body_str) {
return MediaType::new("application", "json");
}
if self.is_xml(body_str) {
return MediaType::new("application", "xml");
}
if self.is_yaml(body_str) {
return MediaType::new("application", "yaml");
}
if self.is_form_data(body_str) {
return MediaType::new("application", "x-www-form-urlencoded");
}
self.default_type.clone()
}
fn is_json(&self, body: &str) -> bool {
let first_char = body.chars().next();
matches!(first_char, Some('{') | Some('['))
}
fn is_xml(&self, body: &str) -> bool {
body.starts_with("<?xml") || body.starts_with('<')
}
fn is_yaml(&self, body: &str) -> bool {
if self.is_json(body) || self.is_xml(body) {
return false;
}
body.lines()
.any(|line| line.contains(':') && !line.trim_start().starts_with('#'))
}
fn is_form_data(&self, body: &str) -> bool {
if body.contains('=') {
let has_ampersand_or_single = body.contains('&') || !body.contains(' ');
let not_json_xml = !self.is_json(body) && !self.is_xml(body);
return has_ampersand_or_single && not_json_xml;
}
false
}
}
impl Default for ContentTypeDetector {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_json() {
let detector = ContentTypeDetector::new();
let json = r#"{"name": "test"}"#;
let result = detector.detect(json.as_bytes());
assert_eq!(result.subtype, "json");
}
#[test]
fn test_detect_xml() {
let detector = ContentTypeDetector::new();
let xml = r#"<?xml version="1.0"?><root/>"#;
let result = detector.detect(xml.as_bytes());
assert_eq!(result.subtype, "xml");
}
#[test]
fn test_detect_yaml() {
let detector = ContentTypeDetector::new();
let yaml = "name: test\nage: 30";
let result = detector.detect(yaml.as_bytes());
assert_eq!(result.subtype, "yaml");
}
#[test]
fn test_detect_form_data() {
let detector = ContentTypeDetector::new();
let form = "name=test&age=30";
let result = detector.detect(form.as_bytes());
assert_eq!(result.subtype, "x-www-form-urlencoded");
}
#[test]
fn test_empty_body() {
let detector = ContentTypeDetector::new();
let result = detector.detect(b"");
assert_eq!(result.subtype, "octet-stream");
}
#[test]
fn test_custom_default() {
let detector = ContentTypeDetector::with_default(MediaType::new("text", "plain"));
let result = detector.detect(b"");
assert_eq!(result.subtype, "plain");
}
#[test]
fn test_json_array() {
let detector = ContentTypeDetector::new();
let json = r#"[{"name": "test"}]"#;
let result = detector.detect(json.as_bytes());
assert_eq!(result.subtype, "json");
}
#[test]
fn test_xml_without_declaration() {
let detector = ContentTypeDetector::new();
let xml = r#"<root><child>value</child></root>"#;
let result = detector.detect(xml.as_bytes());
assert_eq!(result.subtype, "xml");
}
}