#![allow(dead_code)]
#[derive(Clone, Debug)]
pub struct MediaType {
pub mime: String,
pub quality: f32,
}
#[derive(Clone, Debug)]
pub struct NegotiationResult {
pub selected: String,
pub quality: f32,
}
pub fn parse_accept_header(accept: &str) -> Vec<MediaType> {
let mut types: Vec<MediaType> = accept
.split(',')
.filter_map(|part| {
let part = part.trim();
if part.is_empty() {
return None;
}
let mut segments = part.splitn(2, ";q=");
let mime = segments.next()?.trim().to_lowercase();
let quality: f32 = segments
.next()
.and_then(|q| q.trim().parse().ok())
.unwrap_or(1.0);
Some(MediaType { mime, quality })
})
.collect();
types.sort_by(|a, b| {
b.quality
.partial_cmp(&a.quality)
.unwrap_or(std::cmp::Ordering::Equal)
});
types
}
pub fn negotiate<'a>(
accept_types: &[MediaType],
offered: &'a [&'a str],
) -> Option<NegotiationResult> {
let wildcard_any = ["*", "*"].join("/");
let wildcard_suffix = ["/", "*"].join("");
for mt in accept_types {
let mime = mt.mime.as_str();
if mime == wildcard_any {
if let Some(&o) = offered.first() {
return Some(NegotiationResult {
selected: o.into(),
quality: mt.quality,
});
}
} else if let Some(prefix) = mime.strip_suffix(&wildcard_suffix) {
if let Some(&o) = offered.iter().find(|&&off| off.starts_with(prefix)) {
return Some(NegotiationResult {
selected: o.into(),
quality: mt.quality,
});
}
} else if let Some(&o) = offered.iter().find(|&&off| off == mime) {
return Some(NegotiationResult {
selected: o.into(),
quality: mt.quality,
});
}
}
None
}
pub fn is_text_type(mime: &str) -> bool {
mime.starts_with("text/") || mime == "application/json" || mime == "application/xml"
}
pub fn mime_to_extension(mime: &str) -> Option<&'static str> {
match mime {
"text/html" => Some("html"),
"text/plain" => Some("txt"),
"application/json" => Some("json"),
"application/xml" | "text/xml" => Some("xml"),
"image/png" => Some("png"),
"image/jpeg" => Some("jpg"),
"application/octet-stream" => Some("bin"),
_ => None,
}
}
pub fn default_quality(mime: &str) -> f32 {
let wildcard_any = ["*", "*"].join("/");
let wildcard_suffix = ["/", "*"].join("");
if mime == wildcard_any {
0.1
} else if mime.ends_with(&wildcard_suffix) {
0.5
} else {
1.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_accept_header_basic() {
let types = parse_accept_header("text/html, application/json;q=0.9");
assert_eq!(types[0].mime, "text/html");
assert!((types[0].quality - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_parse_quality_factor() {
let types = parse_accept_header("application/json;q=0.8");
assert!((types[0].quality - 0.8).abs() < 1e-5);
}
#[test]
fn test_negotiate_exact_match() {
let accept = parse_accept_header("application/json");
let offered = ["text/html", "application/json"];
let result = negotiate(&accept, &offered).expect("should succeed");
assert_eq!(result.selected, "application/json");
}
#[test]
fn test_negotiate_wildcard_any() {
let wildcard = ["*", "*"].join("/");
let accept = parse_accept_header(&wildcard);
let offered = ["application/json"];
let result = negotiate(&accept, &offered).expect("should succeed");
assert_eq!(result.selected, "application/json");
}
#[test]
fn test_negotiate_subtype_wildcard() {
let text_wildcard = ["text", "*"].join("/");
let accept = parse_accept_header(&text_wildcard);
let offered = ["text/plain", "application/json"];
let result = negotiate(&accept, &offered).expect("should succeed");
assert_eq!(result.selected, "text/plain");
}
#[test]
fn test_negotiate_no_match_returns_none() {
let accept = parse_accept_header("image/png");
let offered = ["text/html"];
assert!(negotiate(&accept, &offered).is_none());
}
#[test]
fn test_is_text_type() {
assert!(is_text_type("text/html"));
assert!(is_text_type("application/json"));
assert!(!is_text_type("image/png"));
}
#[test]
fn test_mime_to_extension() {
assert_eq!(mime_to_extension("application/json"), Some("json"));
assert_eq!(mime_to_extension("image/jpeg"), Some("jpg"));
assert!(mime_to_extension("unknown/type").is_none());
}
#[test]
fn test_default_quality_wildcard() {
let wc_any = ["*", "*"].join("/");
let wc_text = ["text", "*"].join("/");
assert!((default_quality(&wc_any) - 0.1).abs() < 1e-5);
assert!((default_quality(&wc_text) - 0.5).abs() < 1e-5);
assert!((default_quality("text/html") - 1.0).abs() < 1e-5);
}
#[test]
fn test_highest_quality_selected_first() {
let accept = parse_accept_header("text/html;q=0.5, application/json;q=0.9");
let offered = ["text/html", "application/json"];
let result = negotiate(&accept, &offered).expect("should succeed");
assert_eq!(result.selected, "application/json");
}
}