pub const LINO_CONTENT_TYPE: &str = "text/lino";
pub const LINO_LINE_CONTENT_TYPE: &str = "text/lino-line";
pub const LINO_COMPACT_CONTENT_TYPE: &str = "text/lino-compact";
pub const JSON_CONTENT_TYPE: &str = "application/json";
pub const LINO_PROBLEM_CONTENT_TYPE: &str = "application/problem+lino";
pub const JSON_PROBLEM_CONTENT_TYPE: &str = "application/problem+json";
pub const SUPPORTED_MEDIA_TYPES: [&str; 4] = [
LINO_CONTENT_TYPE,
LINO_LINE_CONTENT_TYPE,
LINO_COMPACT_CONTENT_TYPE,
JSON_CONTENT_TYPE,
];
#[derive(Debug, Clone, PartialEq)]
pub struct AcceptRange {
pub media_type: String,
pub quality: f64,
pub specificity: u8,
}
pub fn with_charset(media_type: &str) -> String {
format!("{media_type}; charset=utf-8")
}
pub fn parse_content_type(header_value: Option<&str>) -> String {
match header_value {
None => String::new(),
Some(value) => value
.split(';')
.next()
.unwrap_or("")
.trim()
.to_ascii_lowercase(),
}
}
pub fn parse_accept(header_value: Option<&str>) -> Vec<AcceptRange> {
let header = header_value.unwrap_or("").trim();
if header.is_empty() {
return vec![AcceptRange {
media_type: "*/*".to_string(),
quality: 1.0,
specificity: 0,
}];
}
let mut ranges: Vec<(usize, AcceptRange)> = Vec::new();
for (index, part) in header.split(',').enumerate() {
let mut pieces = part.split(';');
let media_type = pieces.next().unwrap_or("").trim().to_ascii_lowercase();
if media_type.is_empty() {
continue;
}
let mut quality = 1.0;
for parameter in pieces {
let (name, value) = match parameter.split_once('=') {
Some((name, value)) => (name, value),
None => (parameter, ""),
};
if name.trim().eq_ignore_ascii_case("q") {
quality = value.trim().parse::<f64>().unwrap_or(0.0);
}
}
let specificity = if media_type == "*/*" {
0
} else if media_type.ends_with("/*") {
1
} else {
2
};
ranges.push((
index,
AcceptRange {
media_type,
quality,
specificity,
},
));
}
ranges.sort_by(|left, right| {
right
.1
.quality
.partial_cmp(&left.1.quality)
.unwrap_or(std::cmp::Ordering::Equal)
.then(right.1.specificity.cmp(&left.1.specificity))
.then(left.0.cmp(&right.0))
});
ranges.into_iter().map(|entry| entry.1).collect()
}
fn range_matches(range_type: &str, media_type: &str) -> bool {
if range_type == "*/*" || range_type == media_type {
return true;
}
match range_type.strip_suffix('*') {
Some(prefix) => media_type.starts_with(prefix),
None => false,
}
}
pub fn negotiate_media_type(accept_header: Option<&str>, supported: &[String]) -> Option<String> {
for accepted in parse_accept(accept_header) {
if accepted.quality <= 0.0 {
continue;
}
for media_type in supported {
if range_matches(&accepted.media_type, media_type) {
return Some(media_type.clone());
}
}
}
None
}
pub fn supported_media_types() -> Vec<String> {
SUPPORTED_MEDIA_TYPES
.iter()
.map(|entry| entry.to_string())
.collect()
}
pub fn normalize_media_type(media_type: &str) -> &str {
match media_type {
LINO_PROBLEM_CONTENT_TYPE => LINO_CONTENT_TYPE,
JSON_PROBLEM_CONTENT_TYPE => JSON_CONTENT_TYPE,
other => other,
}
}
pub fn is_decodable_media_type(media_type: &str) -> bool {
SUPPORTED_MEDIA_TYPES.contains(&normalize_media_type(media_type))
}
pub fn problem_media_type(media_type: &str) -> &str {
match media_type {
LINO_CONTENT_TYPE => LINO_PROBLEM_CONTENT_TYPE,
JSON_CONTENT_TYPE => JSON_PROBLEM_CONTENT_TYPE,
other => other,
}
}