miku-http-util 0.6.2

Utilities for parsing or building parts of HTTP requests and responses.
Documentation
//! HTTP request utilities: builder related.

use std::{borrow::Cow, convert::Infallible, ops};

use macro_toolset::{
    md5, str_concat,
    string::{general::tuple::SeplessTuple, PushAnyT, StringExtT},
    urlencoding_str,
};

#[deprecated(
    since = "0.6.0",
    note = "Renamed and deprecated, use [`Query`] instead."
)]
/// Renamed and deprecated, use [`Query`] instead.
pub type Queries<'q> = Query<'q>;

#[derive(Debug)]
#[repr(transparent)]
/// Helper for query string building.
pub struct Query<'q> {
    inner: Vec<(Cow<'q, str>, Cow<'q, str>)>,
}

impl<'q> ops::Deref for Query<'q> {
    type Target = Vec<(Cow<'q, str>, Cow<'q, str>)>;

    #[inline]
    fn deref(&self) -> &Self::Target {
        &self.inner
    }
}

impl Default for Query<'_> {
    fn default() -> Self {
        Self::with_capacity(4)
    }
}

impl<'q> Query<'q> {
    #[inline]
    /// Create a new empty query string builder.
    ///
    /// This is not recommended for general use unless const is needed.
    /// [`Query::with_capacity`] is recommended.
    pub const fn new() -> Self {
        Self { inner: Vec::new() }
    }

    #[inline]
    /// Create a new empty query string builder.
    pub fn with_capacity(capacity: usize) -> Self {
        Self {
            inner: Vec::with_capacity(capacity),
        }
    }

    #[inline]
    /// Push a new key-value pair into the query string builder.
    pub fn push(mut self, key: impl Into<Cow<'q, str>>, value: impl Into<Cow<'q, str>>) -> Self {
        self.inner.push((key.into(), value.into()));
        self
    }

    #[inline]
    /// Push a new key-value pair into the query string builder.
    ///
    /// This accepts any value that can be converted into a string. See
    /// [`StringExtT`] for more details.
    pub fn push_any(mut self, key: impl Into<Cow<'q, str>>, value: impl StringExtT) -> Self {
        self.inner.push((key.into(), value.to_string_ext().into()));
        self
    }

    #[inline]
    /// Sort the inner query pairs by key.
    ///
    /// See [`sort_unstable_by`](https://doc.rust-lang.org/std/primitive.slice.html#method.sort_unstable_by) for more details about the time complexity.
    pub fn sort(&mut self) {
        self.inner.sort_unstable_by(|l, r| l.0.cmp(&r.0));
    }

    #[inline]
    /// Sort the query pairs by key.
    pub fn sorted(mut self) -> Self {
        self.sort();
        self
    }

    #[inline]
    /// Get inner query pairs.
    pub const fn inner(&self) -> &Vec<(Cow<'q, str>, Cow<'q, str>)> {
        &self.inner
    }

    #[inline]
    /// Consume the builder and get the inner query pairs.
    pub fn into_inner(self) -> Vec<(Cow<'q, str>, Cow<'q, str>)> {
        self.inner
    }

    #[inline]
    /// Apply an infallible interceptor to the query string builder.
    pub fn intercept<F>(mut self, f: F) -> Self
    where
        F: Fn(&mut Self),
    {
        f(&mut self);

        self
    }

    #[inline]
    /// Apply a fallible interceptor to the query string builder.
    pub fn intercept_fallible<F, E>(mut self, f: F) -> Result<Self, E>
    where
        F: Fn(&mut Self) -> Result<(), E>,
    {
        f(&mut self)?;

        Ok(self)
    }

    /// Apply a batch of fallible interceptors to the query string builder.
    ///
    /// Will stop immediately if any interceptor returns an error.
    pub fn batch_intercept_fallible<I, E>(mut self, interceptors: I) -> Result<Self, E>
    where
        I: Iterator,
        I::Item: Fn(&mut Self) -> Result<(), E>,
    {
        for f in interceptors {
            f(&mut self)?;
        }

        Ok(self)
    }

    #[inline]
    /// Simply build the query string.
    pub fn build(self) -> String {
        str_concat!(sep = "&"; self.inner.iter().map(|(k, v)| {
            SeplessTuple::new((k, "=", urlencoding_str!(E: v)))
        }))
    }

    #[inline]
    /// Build the query string with given signer.
    pub fn build_signed<S: SignerT>(self, signer: S) -> Result<String, S::Error> {
        signer.build_signed(self)
    }
}

/// Helper trait for query string signing.
pub trait SignerT {
    /// The error type.
    type Error;

    /// Sign the query string and return the final query string.
    fn build_signed(self, query: Query) -> Result<String, Self::Error>;
}

#[derive(Debug, Clone, Copy)]
/// Helper for query string signing: MD5.
pub struct Md5Signer<'s> {
    /// The query param key.
    ///
    /// The default is `"sign"`.
    pub query_key: &'s str,

    /// The salt to be used for signing (prefix).
    pub prefix_salt: Option<&'s str>,

    /// The salt to be used for signing (suffix).
    pub suffix_salt: Option<&'s str>,
}

impl Default for Md5Signer<'_> {
    fn default() -> Self {
        Self {
            query_key: "sign",
            prefix_salt: None,
            suffix_salt: None,
        }
    }
}

impl SignerT for Md5Signer<'_> {
    type Error = Infallible;

    fn build_signed(self, query: Query) -> Result<String, Self::Error> {
        let query = query.sorted();

        let mut final_string_buf = String::with_capacity(64);

        final_string_buf.push_any_with_separator(
            query
                .inner
                .iter()
                .map(|(k, v)| SeplessTuple::new((k, "=", urlencoding_str!(E: v)))),
            "&",
        );

        let signed = match (self.prefix_salt, self.suffix_salt) {
            (None, Some(suffix_salt)) => md5!(final_string_buf, suffix_salt), // most frequent
            (None, None) => md5!(final_string_buf),
            (Some(prefix_salt), Some(suffix_salt)) => {
                md5!(prefix_salt, final_string_buf, suffix_salt)
            }
            (Some(prefix_salt), None) => md5!(prefix_salt, final_string_buf),
        };

        if final_string_buf.is_empty() {
            final_string_buf.push_any((self.query_key, "=", signed.as_str()));
        } else {
            final_string_buf.push_any(("&", self.query_key, "=", signed.as_str()));
        }

        Ok(final_string_buf)
    }
}

impl<'s> Md5Signer<'s> {
    #[inline]
    /// Create a new MD5 signer.
    pub const fn new(
        query_key: &'s str,
        prefix_salt: Option<&'s str>,
        suffix_salt: Option<&'s str>,
    ) -> Self {
        Self {
            query_key,
            prefix_salt,
            suffix_salt,
        }
    }

    #[inline]
    /// Create a new MD5 signer with the default query key.
    pub const fn new_default() -> Self {
        Self {
            query_key: "sign",
            prefix_salt: None,
            suffix_salt: None,
        }
    }

    #[inline]
    /// Set the query key.
    pub const fn with_query_key(self, query_key: &'s str) -> Self {
        Self { query_key, ..self }
    }

    #[inline]
    /// Add a prefix salt to the signer.
    pub const fn with_prefix_salt(self, prefix_salt: Option<&'s str>) -> Self {
        Self {
            prefix_salt,
            ..self
        }
    }

    #[inline]
    /// Add a suffix salt to the signer.
    pub const fn with_suffix_salt(self, suffix_salt: Option<&'s str>) -> Self {
        Self {
            suffix_salt,
            ..self
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_general() {
        let query = Query::with_capacity(16)
            .push_any("test1", 1)
            .push_any("test2", "2")
            .build_signed(Md5Signer::new_default().with_suffix_salt(Some("0123456789abcdef")))
            .unwrap();

        assert_eq!(
            query,
            "test1=1&test2=2&sign=cc4f5844a6a1893a88d648cebba5462f"
        )
    }
}