etwin_mt_querystring 0.12.3

Motion Twin querystring format support
Documentation
use percent_encoding::{percent_decode_str, utf8_percent_encode, NON_ALPHANUMERIC};
use std::borrow::Cow;

#[cfg(feature = "url")]
pub trait UrlExt {
  /// Parse a Motion Twin querystring. Motion Twin uses `;` as the parameter
  /// separator.
  ///
  /// Pairs are separated by `;` instead of `&`.
  /// Keys and values are URL-encoded.
  ///
  /// See <https://www.w3.org/TR/html401/appendix/notes.html#h-B.2.2>
  /// > We recommend that HTTP server implementors, and in particular, CGI
  /// > implementors support the use of ";" in place of "&" to save authors the
  /// > trouble of escaping "&" characters in this manner.
  fn mt_query_pairs(&self) -> MtQueryPairsParse;

  fn mt_query_pairs_mut(&mut self) -> UrlMtQuery;
}

#[cfg(feature = "url")]
impl UrlExt for url::Url {
  fn mt_query_pairs(&self) -> MtQueryPairsParse {
    MtQueryPairsParse::new(self.query().unwrap_or(""))
  }

  fn mt_query_pairs_mut(&mut self) -> UrlMtQuery {
    let input = self.query().unwrap_or("").to_string();
    let pairs = MtQueryPairsSerializer { input };
    UrlMtQuery { url: self, pairs }
  }
}

#[cfg(feature = "url")]
#[derive(Debug)]
pub struct UrlMtQuery<'url> {
  url: &'url mut url::Url,
  pairs: MtQueryPairsSerializer,
}

#[cfg(feature = "url")]
impl<'url> UrlMtQuery<'url> {
  pub fn clear(&mut self) -> &mut Self {
    self.pairs.clear();
    self
  }

  pub fn append_pair(&mut self, name: &str, value: &str) -> &mut Self {
    self.pairs.append_pair(name, value);
    self
  }
}

#[cfg(feature = "url")]
impl<'url> Drop for UrlMtQuery<'url> {
  fn drop(&mut self) {
    self.url.set_query(Some(&self.pairs.input));
  }
}

#[derive(Debug, Copy, Clone)]
pub struct MtQueryPairsParse<'input> {
  input: &'input str,
}

impl<'input> MtQueryPairsParse<'input> {
  pub fn new(input: &'input str) -> Self {
    Self { input }
  }
}

impl<'input> Iterator for MtQueryPairsParse<'input> {
  type Item = (Cow<'input, str>, Cow<'input, str>);

  fn next(&mut self) -> Option<Self::Item> {
    loop {
      if self.input.is_empty() {
        return None;
      }
      let (pair, tail) = self.input.split_once(';').unwrap_or((self.input, ""));
      self.input = tail;
      if pair.is_empty() {
        continue;
      }
      let (key, value) = pair.split_once('=').unwrap_or((pair, ""));
      return Some((decode(key), decode(value)));
    }
  }
}

fn decode(raw: &str) -> Cow<str> {
  percent_decode_str(raw).decode_utf8_lossy()
}

#[derive(Debug, Clone)]
pub struct MtQueryPairsSerializer {
  input: String,
}

impl MtQueryPairsSerializer {
  pub fn clear(&mut self) -> &mut Self {
    self.input.clear();
    self
  }

  pub fn append_pair(&mut self, name: &str, value: &str) -> &mut Self {
    if !self.input.is_empty() {
      self.input.push(';');
    }
    self.input.extend(utf8_percent_encode(name, NON_ALPHANUMERIC));
    self.input.push('=');
    self.input.extend(utf8_percent_encode(value, NON_ALPHANUMERIC));
    self
  }
}