use crate::client::encoding::{JsonEncoding, SmileEncoding};
use crate::encoding::Encoding;
use conjure_error::Error;
use http::header::CONTENT_TYPE;
use http::{HeaderMap, HeaderValue};
use mediatype::MediaType;
use std::fmt;
use std::io::Write;
#[derive(Debug)]
pub struct ConjureRuntime {
request_encoding: DebugEncoding,
accept_encodings: Vec<DebugEncoding>,
accept: HeaderValue,
}
struct DebugEncoding(Box<dyn Encoding + Sync + Send>);
impl fmt::Debug for DebugEncoding {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.0.content_type(), f)
}
}
impl ConjureRuntime {
pub fn new() -> Self {
Self::builder().build()
}
pub fn builder() -> Builder {
Builder {
request_encoding: None,
accept_encodings: vec![],
}
}
pub fn accept(&self) -> HeaderValue {
self.accept.clone()
}
pub fn request_body_encoding(&self) -> &(dyn Encoding + Sync + Send) {
&*self.request_encoding.0
}
pub fn response_body_encoding(
&self,
headers: &HeaderMap,
) -> Result<&(dyn Encoding + Sync + Send), Error> {
let content_mime = headers
.get(CONTENT_TYPE)
.ok_or_else(|| Error::internal_safe("response missing Content-Type header"))
.and_then(|h| h.to_str().map_err(Error::internal_safe))
.and_then(|s| MediaType::parse(s).map_err(Error::internal_safe))?;
for encoding in &self.accept_encodings {
let encoding_type = encoding.0.content_type();
let Some(encoding_mime) = encoding_type
.to_str()
.ok()
.and_then(|s| MediaType::parse(s).ok())
else {
continue;
};
if content_mime.essence() == encoding_mime.essence() {
return Ok(&*encoding.0);
}
}
Err(
Error::internal_safe("encoding not found for response body Content-Type")
.with_safe_param("Content-Type", content_mime.to_string()),
)
}
}
impl Default for ConjureRuntime {
fn default() -> Self {
Self::new()
}
}
pub struct Builder {
request_encoding: Option<Box<dyn Encoding + Sync + Send>>,
accept_encodings: Vec<(Box<dyn Encoding + Sync + Send>, f32)>,
}
impl Builder {
pub fn request_encoding(mut self, encoding: impl Encoding + 'static + Sync + Send) -> Self {
self.request_encoding = Some(Box::new(encoding));
self
}
pub fn accept_encoding(
mut self,
encoding: impl Encoding + 'static + Sync + Send,
weight: f32,
) -> Self {
assert!(
(0. ..=1.).contains(&weight),
"weight must be between 0 and 1",
);
self.accept_encodings.push((Box::new(encoding), weight));
self
}
pub fn build(self) -> ConjureRuntime {
let request_encoding = DebugEncoding(
self.request_encoding
.unwrap_or_else(|| Box::new(JsonEncoding)),
);
let mut accept_encodings = if self.accept_encodings.is_empty() {
vec![
(Box::new(SmileEncoding) as _, 1.),
(Box::new(JsonEncoding) as _, 0.9),
]
} else {
self.accept_encodings
};
accept_encodings.sort_by(|a, b| a.1.total_cmp(&b.1).reverse());
let mut accept = vec![];
for (i, (encoding, weight)) in accept_encodings.iter().enumerate() {
if i != 0 {
accept.extend_from_slice(b", ");
}
accept.extend_from_slice(encoding.content_type().as_bytes());
if *weight == 0. {
accept.extend_from_slice(b"; q=0");
} else if *weight != 1. {
write!(accept, "; q={weight:.3}").unwrap();
while accept.pop_if(|b| *b == b'0').is_some() {}
}
}
let accept_encodings = accept_encodings
.into_iter()
.map(|(e, _)| DebugEncoding(e))
.collect();
let accept = HeaderValue::try_from(accept).unwrap();
ConjureRuntime {
request_encoding,
accept_encodings,
accept,
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn basics() {
let runtime = ConjureRuntime::new();
assert_eq!(
runtime.accept(),
"application/x-jackson-smile, application/json; q=0.9"
);
let cases = [
(None, Err(())),
(Some("application/json"), Ok("application/json")),
(
Some("application/json; encoding=utf-8"),
Ok("application/json"),
),
(
Some("application/x-jackson-smile"),
Ok("application/x-jackson-smile"),
),
(Some("application/cbor"), Err(())),
(Some("application/*"), Err(())),
(Some("*/*"), Err(())),
];
for (content_type, result) in cases {
let mut headers = HeaderMap::new();
if let Some(content_type) = content_type {
headers.insert(CONTENT_TYPE, HeaderValue::from_str(content_type).unwrap());
}
match (result, runtime.response_body_encoding(&headers)) {
(Ok(expected), Ok(encoding)) => assert_eq!(encoding.content_type(), expected),
(Ok(expected), Err(e)) => panic!("expected Ok({expected}), got Err({e:?})"),
(Err(()), Err(_)) => {}
(Err(()), Ok(encoding)) => {
panic!("expected Err(), got Ok({:?}", encoding.content_type())
}
}
}
}
#[test]
fn q_values() {
let runtime = ConjureRuntime::builder()
.accept_encoding(SmileEncoding, 0.)
.accept_encoding(JsonEncoding, 1. / 3.)
.build();
assert_eq!(
runtime.accept(),
"application/json; q=0.333, application/x-jackson-smile; q=0"
)
}
}