miku_http_util/request/
builder.rs

1//! HTTP request utilities: builder related.
2
3use std::{borrow::Cow, convert::Infallible, ops};
4
5use macro_toolset::{
6    md5, str_concat,
7    string::{general::tuple::SeplessTuple, PushAnyT, StringExtT},
8    urlencoding_str,
9};
10
11#[deprecated(
12    since = "0.6.0",
13    note = "Renamed and deprecated, use [`Query`] instead."
14)]
15/// Renamed and deprecated, use [`Query`] instead.
16pub type Queries<'q> = Query<'q>;
17
18#[derive(Debug)]
19#[repr(transparent)]
20/// Helper for query string building.
21pub struct Query<'q> {
22    inner: Vec<(Cow<'q, str>, Cow<'q, str>)>,
23}
24
25impl<'q> ops::Deref for Query<'q> {
26    type Target = Vec<(Cow<'q, str>, Cow<'q, str>)>;
27
28    #[inline]
29    fn deref(&self) -> &Self::Target {
30        &self.inner
31    }
32}
33
34impl Default for Query<'_> {
35    fn default() -> Self {
36        Self::with_capacity(4)
37    }
38}
39
40impl<'q> Query<'q> {
41    #[inline]
42    /// Create a new empty query string builder.
43    ///
44    /// This is not recommended for general use unless const is needed.
45    /// [`Query::with_capacity`] is recommended.
46    pub const fn new() -> Self {
47        Self { inner: Vec::new() }
48    }
49
50    #[inline]
51    /// Create a new empty query string builder.
52    pub fn with_capacity(capacity: usize) -> Self {
53        Self {
54            inner: Vec::with_capacity(capacity),
55        }
56    }
57
58    #[inline]
59    /// Push a new key-value pair into the query string builder.
60    pub fn push(mut self, key: impl Into<Cow<'q, str>>, value: impl Into<Cow<'q, str>>) -> Self {
61        self.inner.push((key.into(), value.into()));
62        self
63    }
64
65    #[inline]
66    /// Push a new key-value pair into the query string builder.
67    ///
68    /// This accepts any value that can be converted into a string. See
69    /// [`StringExtT`] for more details.
70    pub fn push_any(mut self, key: impl Into<Cow<'q, str>>, value: impl StringExtT) -> Self {
71        self.inner.push((key.into(), value.to_string_ext().into()));
72        self
73    }
74
75    #[inline]
76    /// Sort the inner query pairs by key.
77    ///
78    /// See [`sort_unstable_by`](https://doc.rust-lang.org/std/primitive.slice.html#method.sort_unstable_by) for more details about the time complexity.
79    pub fn sort(&mut self) {
80        self.inner.sort_unstable_by(|l, r| l.0.cmp(&r.0));
81    }
82
83    #[inline]
84    /// Sort the query pairs by key.
85    pub fn sorted(mut self) -> Self {
86        self.sort();
87        self
88    }
89
90    #[inline]
91    /// Get inner query pairs.
92    pub const fn inner(&self) -> &Vec<(Cow<'q, str>, Cow<'q, str>)> {
93        &self.inner
94    }
95
96    #[inline]
97    /// Consume the builder and get the inner query pairs.
98    pub fn into_inner(self) -> Vec<(Cow<'q, str>, Cow<'q, str>)> {
99        self.inner
100    }
101
102    #[inline]
103    /// Apply an infallible interceptor to the query string builder.
104    pub fn intercept<F>(mut self, f: F) -> Self
105    where
106        F: Fn(&mut Self),
107    {
108        f(&mut self);
109
110        self
111    }
112
113    #[inline]
114    /// Apply a fallible interceptor to the query string builder.
115    pub fn intercept_fallible<F, E>(mut self, f: F) -> Result<Self, E>
116    where
117        F: Fn(&mut Self) -> Result<(), E>,
118    {
119        f(&mut self)?;
120
121        Ok(self)
122    }
123
124    /// Apply a batch of fallible interceptors to the query string builder.
125    ///
126    /// Will stop immediately if any interceptor returns an error.
127    pub fn batch_intercept_fallible<I, E>(mut self, interceptors: I) -> Result<Self, E>
128    where
129        I: Iterator,
130        I::Item: Fn(&mut Self) -> Result<(), E>,
131    {
132        for f in interceptors {
133            f(&mut self)?;
134        }
135
136        Ok(self)
137    }
138
139    #[inline]
140    /// Simply build the query string.
141    pub fn build(self) -> String {
142        str_concat!(sep = "&"; self.inner.iter().map(|(k, v)| {
143            SeplessTuple::new((k, "=", urlencoding_str!(E: v)))
144        }))
145    }
146
147    #[inline]
148    /// Build the query string with given signer.
149    pub fn build_signed<S: SignerT>(self, signer: S) -> Result<String, S::Error> {
150        signer.build_signed(self)
151    }
152}
153
154/// Helper trait for query string signing.
155pub trait SignerT {
156    /// The error type.
157    type Error;
158
159    /// Sign the query string and return the final query string.
160    fn build_signed(self, query: Query) -> Result<String, Self::Error>;
161}
162
163#[derive(Debug, Clone, Copy)]
164/// Helper for query string signing: MD5.
165pub struct Md5Signer<'s> {
166    /// The query param key.
167    ///
168    /// The default is `"sign"`.
169    pub query_key: &'s str,
170
171    /// The salt to be used for signing (prefix).
172    pub prefix_salt: Option<&'s str>,
173
174    /// The salt to be used for signing (suffix).
175    pub suffix_salt: Option<&'s str>,
176}
177
178impl Default for Md5Signer<'_> {
179    fn default() -> Self {
180        Self {
181            query_key: "sign",
182            prefix_salt: None,
183            suffix_salt: None,
184        }
185    }
186}
187
188impl SignerT for Md5Signer<'_> {
189    type Error = Infallible;
190
191    fn build_signed(self, query: Query) -> Result<String, Self::Error> {
192        let query = query.sorted();
193
194        let mut final_string_buf = String::with_capacity(64);
195
196        final_string_buf.push_any_with_separator(
197            query
198                .inner
199                .iter()
200                .map(|(k, v)| SeplessTuple::new((k, "=", urlencoding_str!(E: v)))),
201            "&",
202        );
203
204        let signed = match (self.prefix_salt, self.suffix_salt) {
205            (None, Some(suffix_salt)) => md5!(final_string_buf, suffix_salt), // most frequent
206            (None, None) => md5!(final_string_buf),
207            (Some(prefix_salt), Some(suffix_salt)) => {
208                md5!(prefix_salt, final_string_buf, suffix_salt)
209            }
210            (Some(prefix_salt), None) => md5!(prefix_salt, final_string_buf),
211        };
212
213        if final_string_buf.is_empty() {
214            final_string_buf.push_any((self.query_key, "=", signed.as_str()));
215        } else {
216            final_string_buf.push_any(("&", self.query_key, "=", signed.as_str()));
217        }
218
219        Ok(final_string_buf)
220    }
221}
222
223impl<'s> Md5Signer<'s> {
224    #[inline]
225    /// Create a new MD5 signer.
226    pub const fn new(
227        query_key: &'s str,
228        prefix_salt: Option<&'s str>,
229        suffix_salt: Option<&'s str>,
230    ) -> Self {
231        Self {
232            query_key,
233            prefix_salt,
234            suffix_salt,
235        }
236    }
237
238    #[inline]
239    /// Create a new MD5 signer with the default query key.
240    pub const fn new_default() -> Self {
241        Self {
242            query_key: "sign",
243            prefix_salt: None,
244            suffix_salt: None,
245        }
246    }
247
248    #[inline]
249    /// Set the query key.
250    pub const fn with_query_key(self, query_key: &'s str) -> Self {
251        Self { query_key, ..self }
252    }
253
254    #[inline]
255    /// Add a prefix salt to the signer.
256    pub const fn with_prefix_salt(self, prefix_salt: Option<&'s str>) -> Self {
257        Self {
258            prefix_salt,
259            ..self
260        }
261    }
262
263    #[inline]
264    /// Add a suffix salt to the signer.
265    pub const fn with_suffix_salt(self, suffix_salt: Option<&'s str>) -> Self {
266        Self {
267            suffix_salt,
268            ..self
269        }
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276
277    #[test]
278    fn test_general() {
279        let query = Query::with_capacity(16)
280            .push_any("test1", 1)
281            .push_any("test2", "2")
282            .build_signed(Md5Signer::new_default().with_suffix_salt(Some("0123456789abcdef")))
283            .unwrap();
284
285        assert_eq!(
286            query,
287            "test1=1&test2=2&sign=cc4f5844a6a1893a88d648cebba5462f"
288        )
289    }
290}