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."
)]
pub type Queries<'q> = Query<'q>;
#[derive(Debug)]
#[repr(transparent)]
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]
pub const fn new() -> Self {
Self { inner: Vec::new() }
}
#[inline]
pub fn with_capacity(capacity: usize) -> Self {
Self {
inner: Vec::with_capacity(capacity),
}
}
#[inline]
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]
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]
pub fn sort(&mut self) {
self.inner.sort_unstable_by(|l, r| l.0.cmp(&r.0));
}
#[inline]
pub fn sorted(mut self) -> Self {
self.sort();
self
}
#[inline]
pub const fn inner(&self) -> &Vec<(Cow<'q, str>, Cow<'q, str>)> {
&self.inner
}
#[inline]
pub fn into_inner(self) -> Vec<(Cow<'q, str>, Cow<'q, str>)> {
self.inner
}
#[inline]
pub fn intercept<F>(mut self, f: F) -> Self
where
F: Fn(&mut Self),
{
f(&mut self);
self
}
#[inline]
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)
}
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]
pub fn build(self) -> String {
str_concat!(sep = "&"; self.inner.iter().map(|(k, v)| {
SeplessTuple::new((k, "=", urlencoding_str!(E: v)))
}))
}
#[inline]
pub fn build_signed<S: SignerT>(self, signer: S) -> Result<String, S::Error> {
signer.build_signed(self)
}
}
pub trait SignerT {
type Error;
fn build_signed(self, query: Query) -> Result<String, Self::Error>;
}
#[derive(Debug, Clone, Copy)]
pub struct Md5Signer<'s> {
pub query_key: &'s str,
pub prefix_salt: Option<&'s str>,
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), (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]
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]
pub const fn new_default() -> Self {
Self {
query_key: "sign",
prefix_salt: None,
suffix_salt: None,
}
}
#[inline]
pub const fn with_query_key(self, query_key: &'s str) -> Self {
Self { query_key, ..self }
}
#[inline]
pub const fn with_prefix_salt(self, prefix_salt: Option<&'s str>) -> Self {
Self {
prefix_salt,
..self
}
}
#[inline]
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"
)
}
}