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