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    #[inline]
28    fn deref(&self) -> &Self::Target {
29        &self.inner
30    }
31}
32
33impl<'q> Query<'q> {
34    #[inline]
35    /// Create a new empty query string builder.
36    pub fn with_capacity(capacity: usize) -> Self {
37        Self {
38            inner: Vec::with_capacity(capacity),
39        }
40    }
41
42    #[inline]
43    /// Push a new key-value pair into the query string builder.
44    pub fn push(mut self, key: impl Into<Cow<'q, str>>, value: impl Into<Cow<'q, str>>) -> Self {
45        self.inner.push((key.into(), value.into()));
46        self
47    }
48
49    #[inline]
50    /// Push a new key-value pair into the query string builder.
51    pub fn push_any(mut self, key: impl Into<Cow<'q, str>>, value: impl StringExtT) -> Self {
52        self.inner.push((key.into(), value.to_string_ext().into()));
53        self
54    }
55
56    #[inline]
57    /// Sort the query pairs by key.
58    pub fn sorted(mut self) -> Self {
59        self.inner.sort_unstable_by(|l, r| l.0.cmp(&r.0));
60        self
61    }
62
63    #[inline]
64    /// Get inner query pairs.
65    pub const fn inner(&self) -> &Vec<(Cow<'q, str>, Cow<'q, str>)> {
66        &self.inner
67    }
68
69    #[inline]
70    /// Get inner query pairs.
71    pub fn into_inner(self) -> Vec<(Cow<'q, str>, Cow<'q, str>)> {
72        self.inner
73    }
74
75    #[inline]
76    /// Build the query string, unsigned.
77    pub fn build(self) -> String {
78        str_concat!(sep = "&"; self.inner.iter().map(|(k, v)| {
79            (k, "=", urlencoding_str!(E: v))
80        }))
81    }
82
83    #[inline]
84    /// Build the query string with given signer.
85    pub fn build_signed<S: SignerT>(self, signer: S) -> Result<String, S::Error> {
86        signer.build_signed(self)
87    }
88}
89
90/// Helper trait for query string signing.
91pub trait SignerT {
92    /// The error type.
93    type Error;
94
95    /// Sign the query string and return the final query string.
96    fn build_signed(self, query: Query) -> Result<String, Self::Error>;
97}
98
99#[derive(Debug, Clone, Copy)]
100/// Helper for query string signing: MD5.
101pub struct Md5Signer<'s> {
102    /// The query param key.
103    ///
104    /// The default is `"sign"`.
105    pub query_key: &'s str,
106
107    /// The salt to be used for signing (prefix).
108    pub prefix_salt: Option<&'s str>,
109
110    /// The salt to be used for signing (suffix).
111    pub suffix_salt: Option<&'s str>,
112}
113
114impl Default for Md5Signer<'_> {
115    fn default() -> Self {
116        Self {
117            query_key: "sign",
118            prefix_salt: None,
119            suffix_salt: None,
120        }
121    }
122}
123
124impl SignerT for Md5Signer<'_> {
125    type Error = Infallible;
126
127    fn build_signed(self, query: Query) -> Result<String, Self::Error> {
128        let query = query.sorted();
129
130        let mut final_string_buf = String::with_capacity(64);
131
132        final_string_buf.push_any_with_separator(
133            query
134                .inner
135                .iter()
136                .map(|(k, v)| SeplessTuple::new((k, "=", urlencoding_str!(E: v)))),
137            "&",
138        );
139
140        let signed = match (self.prefix_salt, self.suffix_salt) {
141            (None, Some(suffix_salt)) => md5!(final_string_buf, suffix_salt), // most frequent
142            (None, None) => md5!(final_string_buf),
143            (Some(prefix_salt), Some(suffix_salt)) => {
144                md5!(prefix_salt, final_string_buf, suffix_salt)
145            }
146            (Some(prefix_salt), None) => md5!(prefix_salt, final_string_buf),
147        };
148
149        if final_string_buf.is_empty() {
150            final_string_buf.push_any((self.query_key, "=", signed.as_str()));
151        } else {
152            final_string_buf.push_any(("&", self.query_key, "=", signed.as_str()));
153        }
154
155        Ok(final_string_buf)
156    }
157}
158
159impl<'s> Md5Signer<'s> {
160    #[inline]
161    /// Create a new MD5 signer.
162    pub const fn new(
163        query_key: &'s str,
164        prefix_salt: Option<&'s str>,
165        suffix_salt: Option<&'s str>,
166    ) -> Self {
167        Self {
168            query_key,
169            prefix_salt,
170            suffix_salt,
171        }
172    }
173
174    #[inline]
175    /// Create a new MD5 signer with the default query key.
176    pub const fn new_default() -> Self {
177        Self {
178            query_key: "sign",
179            prefix_salt: None,
180            suffix_salt: None,
181        }
182    }
183
184    #[inline]
185    /// Set the query key.
186    pub const fn with_query_key(self, query_key: &'s str) -> Self {
187        Self { query_key, ..self }
188    }
189
190    #[inline]
191    /// Add a prefix salt to the signer.
192    pub const fn with_prefix_salt(self, prefix_salt: Option<&'s str>) -> Self {
193        Self {
194            prefix_salt,
195            ..self
196        }
197    }
198
199    #[inline]
200    /// Add a suffix salt to the signer.
201    pub const fn with_suffix_salt(self, suffix_salt: Option<&'s str>) -> Self {
202        Self {
203            suffix_salt,
204            ..self
205        }
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    #[test]
214    fn test_general() {
215        let query = Query::with_capacity(16)
216            .push_any("test1", 1)
217            .push_any("test2", "2")
218            .build_signed(Md5Signer::new_default().with_suffix_salt(Some("0123456789abcdef")))
219            .unwrap();
220
221        assert_eq!(
222            query,
223            "test1=1&test2=2&sign=cc4f5844a6a1893a88d648cebba5462f"
224        )
225    }
226}