timvw_rust_runtime/
request_options.rs1use http::header::{HeaderName, HeaderValue};
2use http::HeaderMap;
3use serde::Serialize;
4
5use crate::Error;
6
7#[derive(Clone, Debug, Default)]
27pub struct RequestOptions {
28 headers: HeaderMap,
29 query: Vec<(String, String)>,
30}
31
32impl RequestOptions {
33 pub fn new() -> Self {
35 Self::default()
36 }
37
38 pub fn header<K, V>(mut self, name: K, value: V) -> Result<Self, Error>
41 where
42 HeaderName: TryFrom<K>,
43 <HeaderName as TryFrom<K>>::Error: Into<http::Error>,
44 HeaderValue: TryFrom<V>,
45 <HeaderValue as TryFrom<V>>::Error: Into<http::Error>,
46 {
47 let name = HeaderName::try_from(name).map_err(|e| Error::builder(e.into()))?;
48 let value = HeaderValue::try_from(value).map_err(|e| Error::builder(e.into()))?;
49 self.headers.insert(name, value);
50 Ok(self)
51 }
52
53 pub fn headers(mut self, headers: HeaderMap) -> Self {
56 self.headers.extend(headers);
57 self
58 }
59
60 pub fn query<T: Serialize + ?Sized>(mut self, query: &T) -> Result<Self, Error> {
64 let encoded = serde_urlencoded::to_string(query).map_err(Error::builder)?;
65 self.query
66 .extend(url::form_urlencoded::parse(encoded.as_bytes()).into_owned());
67 Ok(self)
68 }
69
70 pub fn header_map(&self) -> &HeaderMap {
72 &self.headers
73 }
74
75 pub fn query_pairs(&self) -> &[(String, String)] {
77 &self.query
78 }
79
80 pub fn is_empty(&self) -> bool {
82 self.headers.is_empty() && self.query.is_empty()
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89 use http::header::{AUTHORIZATION, CONTENT_TYPE};
90
91 #[test]
92 fn header_replaces_and_headers_keeps_every_value() {
93 let mut extra = HeaderMap::new();
94 extra.append("x-tag", HeaderValue::from_static("a"));
95 extra.append("x-tag", HeaderValue::from_static("b"));
96 let options = RequestOptions::new()
97 .header("x-one", "1")
98 .unwrap()
99 .header("x-one", "2")
100 .unwrap()
101 .header(CONTENT_TYPE, HeaderValue::from_static("text/plain"))
102 .unwrap()
103 .headers(extra);
104 let headers = options.header_map();
105 assert_eq!(headers.get_all("x-one").iter().collect::<Vec<_>>(), ["2"]);
106 assert_eq!(
107 headers.get_all("x-tag").iter().collect::<Vec<_>>(),
108 ["a", "b"]
109 );
110 assert_eq!(headers[CONTENT_TYPE], "text/plain");
111 }
112
113 #[test]
114 fn query_keeps_order_and_decodes() {
115 let options = RequestOptions::new()
116 .query(&[("a", "x y"), ("b", "&")])
117 .unwrap()
118 .query(&[("a", 2)])
119 .unwrap();
120 let pairs: Vec<(&str, &str)> = options
121 .query_pairs()
122 .iter()
123 .map(|(k, v)| (k.as_str(), v.as_str()))
124 .collect();
125 assert_eq!(pairs, [("a", "x y"), ("b", "&"), ("a", "2")]);
126 }
127
128 #[test]
129 fn invalid_input_is_a_builder_error() {
130 assert!(RequestOptions::new()
131 .header("x bad", "v")
132 .unwrap_err()
133 .is_builder());
134 assert!(RequestOptions::new()
135 .header("x-bad", "line\nbreak")
136 .unwrap_err()
137 .is_builder());
138 assert!(RequestOptions::new()
140 .query(&[("a", [1, 2])])
141 .unwrap_err()
142 .is_builder());
143 }
144
145 #[test]
146 fn empty() {
147 assert!(RequestOptions::new().is_empty());
148 assert!(RequestOptions::default().is_empty());
149 assert!(!RequestOptions::new()
150 .header(AUTHORIZATION, "Bearer x")
151 .unwrap()
152 .is_empty());
153 assert!(!RequestOptions::new()
154 .query(&[("a", "")])
155 .unwrap()
156 .is_empty());
157 }
158}