conjure_http/client/
runtime.rs1use crate::client::encoding::{JsonEncoding, SmileEncoding};
17use crate::encoding::Encoding;
18use conjure_error::Error;
19use http::header::CONTENT_TYPE;
20use http::{HeaderMap, HeaderValue};
21use mediatype::MediaType;
22use std::fmt;
23use std::io::Write;
24
25#[derive(Debug)]
27pub struct ConjureRuntime {
28 request_encoding: DebugEncoding,
29 accept_encodings: Vec<DebugEncoding>,
30 accept: HeaderValue,
31}
32
33struct DebugEncoding(Box<dyn Encoding + Sync + Send>);
34
35impl fmt::Debug for DebugEncoding {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 fmt::Debug::fmt(&self.0.content_type(), f)
38 }
39}
40
41impl ConjureRuntime {
42 pub fn new() -> Self {
44 Self::builder().build()
45 }
46
47 pub fn builder() -> Builder {
49 Builder {
50 request_encoding: None,
51 accept_encodings: vec![],
52 }
53 }
54
55 pub fn accept(&self) -> HeaderValue {
57 self.accept.clone()
58 }
59
60 pub fn request_body_encoding(&self) -> &(dyn Encoding + Sync + Send) {
62 &*self.request_encoding.0
63 }
64
65 pub fn response_body_encoding(
70 &self,
71 headers: &HeaderMap,
72 ) -> Result<&(dyn Encoding + Sync + Send), Error> {
73 let content_mime = headers
74 .get(CONTENT_TYPE)
75 .ok_or_else(|| Error::internal_safe("response missing Content-Type header"))
76 .and_then(|h| h.to_str().map_err(Error::internal_safe))
77 .and_then(|s| MediaType::parse(s).map_err(Error::internal_safe))?;
78
79 for encoding in &self.accept_encodings {
80 let encoding_type = encoding.0.content_type();
81 let Some(encoding_mime) = encoding_type
82 .to_str()
83 .ok()
84 .and_then(|s| MediaType::parse(s).ok())
85 else {
86 continue;
87 };
88
89 if content_mime.essence() == encoding_mime.essence() {
91 return Ok(&*encoding.0);
92 }
93 }
94
95 Err(
96 Error::internal_safe("encoding not found for response body Content-Type")
97 .with_safe_param("Content-Type", content_mime.to_string()),
98 )
99 }
100}
101
102impl Default for ConjureRuntime {
103 fn default() -> Self {
104 Self::new()
105 }
106}
107
108pub struct Builder {
110 request_encoding: Option<Box<dyn Encoding + Sync + Send>>,
111 accept_encodings: Vec<(Box<dyn Encoding + Sync + Send>, f32)>,
112}
113
114impl Builder {
115 pub fn request_encoding(mut self, encoding: impl Encoding + 'static + Sync + Send) -> Self {
119 self.request_encoding = Some(Box::new(encoding));
120 self
121 }
122
123 pub fn accept_encoding(
132 mut self,
133 encoding: impl Encoding + 'static + Sync + Send,
134 weight: f32,
135 ) -> Self {
136 assert!(
137 (0. ..=1.).contains(&weight),
138 "weight must be between 0 and 1",
139 );
140 self.accept_encodings.push((Box::new(encoding), weight));
141 self
142 }
143
144 pub fn build(self) -> ConjureRuntime {
146 let request_encoding = DebugEncoding(
147 self.request_encoding
148 .unwrap_or_else(|| Box::new(JsonEncoding)),
149 );
150
151 let mut accept_encodings = if self.accept_encodings.is_empty() {
152 vec![
153 (Box::new(SmileEncoding) as _, 1.),
154 (Box::new(JsonEncoding) as _, 0.9),
155 ]
156 } else {
157 self.accept_encodings
158 };
159
160 accept_encodings.sort_by(|a, b| a.1.total_cmp(&b.1).reverse());
162
163 let mut accept = vec![];
164 for (i, (encoding, weight)) in accept_encodings.iter().enumerate() {
165 if i != 0 {
166 accept.extend_from_slice(b", ");
167 }
168
169 accept.extend_from_slice(encoding.content_type().as_bytes());
170
171 if *weight == 0. {
172 accept.extend_from_slice(b"; q=0");
173 } else if *weight != 1. {
174 write!(accept, "; q={weight:.3}").unwrap();
175
176 while accept.pop_if(|b| *b == b'0').is_some() {}
178 }
179 }
180
181 let accept_encodings = accept_encodings
182 .into_iter()
183 .map(|(e, _)| DebugEncoding(e))
184 .collect();
185 let accept = HeaderValue::try_from(accept).unwrap();
186
187 ConjureRuntime {
188 request_encoding,
189 accept_encodings,
190 accept,
191 }
192 }
193}
194
195#[cfg(test)]
196mod test {
197 use super::*;
198
199 #[test]
200 fn basics() {
201 let runtime = ConjureRuntime::new();
202
203 assert_eq!(
204 runtime.accept(),
205 "application/x-jackson-smile, application/json; q=0.9"
206 );
207
208 let cases = [
209 (None, Err(())),
210 (Some("application/json"), Ok("application/json")),
211 (
212 Some("application/json; encoding=utf-8"),
213 Ok("application/json"),
214 ),
215 (
216 Some("application/x-jackson-smile"),
217 Ok("application/x-jackson-smile"),
218 ),
219 (Some("application/cbor"), Err(())),
220 (Some("application/*"), Err(())),
221 (Some("*/*"), Err(())),
222 ];
223
224 for (content_type, result) in cases {
225 let mut headers = HeaderMap::new();
226 if let Some(content_type) = content_type {
227 headers.insert(CONTENT_TYPE, HeaderValue::from_str(content_type).unwrap());
228 }
229
230 match (result, runtime.response_body_encoding(&headers)) {
231 (Ok(expected), Ok(encoding)) => assert_eq!(encoding.content_type(), expected),
232 (Ok(expected), Err(e)) => panic!("expected Ok({expected}), got Err({e:?})"),
233 (Err(()), Err(_)) => {}
234 (Err(()), Ok(encoding)) => {
235 panic!("expected Err(), got Ok({:?}", encoding.content_type())
236 }
237 }
238 }
239 }
240
241 #[test]
242 fn q_values() {
243 let runtime = ConjureRuntime::builder()
244 .accept_encoding(SmileEncoding, 0.)
245 .accept_encoding(JsonEncoding, 1. / 3.)
246 .build();
247
248 assert_eq!(
249 runtime.accept(),
250 "application/json; q=0.333, application/x-jackson-smile; q=0"
251 )
252 }
253}