use axum::http::header::ACCEPT;
use axum::http::HeaderMap;
use sbol_db_core::SerializationFormat;
use crate::error::ApiError;
use crate::v2::error::V2Error;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Negotiated {
Json,
Rdf(SerializationFormat),
}
fn media_to_negotiated(media: &str) -> Option<Negotiated> {
match media {
"*/*" | "application/*" | "application/json" => Some(Negotiated::Json),
"text/turtle" | "application/x-turtle" => {
Some(Negotiated::Rdf(SerializationFormat::Turtle))
}
"application/rdf+xml" => Some(Negotiated::Rdf(SerializationFormat::RdfXml)),
"application/ld+json" => Some(Negotiated::Rdf(SerializationFormat::JsonLd)),
"application/n-triples" => Some(Negotiated::Rdf(SerializationFormat::NTriples)),
_ => None,
}
}
pub fn content_type_for(negotiated: Negotiated) -> &'static str {
match negotiated {
Negotiated::Json => "application/json",
Negotiated::Rdf(SerializationFormat::Turtle) => "text/turtle",
Negotiated::Rdf(SerializationFormat::RdfXml) => "application/rdf+xml",
Negotiated::Rdf(SerializationFormat::JsonLd) => "application/ld+json",
Negotiated::Rdf(SerializationFormat::NTriples) => "application/n-triples",
Negotiated::Rdf(_) => "application/octet-stream",
}
}
pub fn negotiate(headers: &HeaderMap) -> Result<Negotiated, V2Error> {
let accept = headers
.get(ACCEPT)
.and_then(|v| v.to_str().ok())
.unwrap_or("")
.trim();
if accept.is_empty() {
return Ok(Negotiated::Json);
}
let mut ranges: Vec<(usize, &str, f32)> = accept
.split(',')
.enumerate()
.filter_map(|(index, raw)| {
let mut parts = raw.split(';');
let media = parts.next()?.trim();
if media.is_empty() {
return None;
}
let q = parts
.find_map(|p| {
let p = p.trim();
p.strip_prefix("q=")
.and_then(|v| v.trim().parse::<f32>().ok())
})
.unwrap_or(1.0);
Some((index, media, q))
})
.collect();
ranges.sort_by(|a, b| b.2.total_cmp(&a.2).then(a.0.cmp(&b.0)));
for (_, media, _) in &ranges {
if let Some(negotiated) = media_to_negotiated(media) {
return Ok(negotiated);
}
}
Err(
ApiError::Sparql(sbol_db_sparql::SparqlError::UnsupportedFormat(
accept.to_owned(),
))
.into(),
)
}
#[cfg(test)]
mod tests {
use super::*;
use axum::http::header::HeaderValue;
fn headers(accept: &str) -> HeaderMap {
let mut h = HeaderMap::new();
h.insert(ACCEPT, HeaderValue::from_str(accept).unwrap());
h
}
#[test]
fn json_default_when_absent() {
assert_eq!(negotiate(&HeaderMap::new()).unwrap(), Negotiated::Json);
}
#[test]
fn wildcard_is_json() {
assert_eq!(negotiate(&headers("*/*")).unwrap(), Negotiated::Json);
}
#[test]
fn turtle_and_rdfxml() {
assert_eq!(
negotiate(&headers("text/turtle")).unwrap(),
Negotiated::Rdf(SerializationFormat::Turtle)
);
assert_eq!(
negotiate(&headers("application/rdf+xml")).unwrap(),
Negotiated::Rdf(SerializationFormat::RdfXml)
);
}
#[test]
fn qvalue_prefers_higher_weight() {
let chosen = negotiate(&headers("text/turtle;q=0.5, application/json;q=0.9")).unwrap();
assert_eq!(chosen, Negotiated::Json);
}
#[test]
fn unsupported_is_406() {
use axum::response::IntoResponse;
let err = negotiate(&headers("image/png")).unwrap_err();
assert_eq!(
err.into_response().status(),
axum::http::StatusCode::NOT_ACCEPTABLE
);
}
#[test]
fn content_type_round_trips() {
assert_eq!(content_type_for(Negotiated::Json), "application/json");
assert_eq!(
content_type_for(Negotiated::Rdf(SerializationFormat::Turtle)),
"text/turtle"
);
}
}