const DEFAULT_PARAMS_COUNT: usize = 4;
const KV_SEPARATOR: u8 = b'=';
const PARAM_SEPARATOR: u8 = b'&';
pub(super) struct QueryArgsIter<'a> {
query: &'a str,
iter: std::slice::Iter<'a, QueryArgSpan>,
}
impl<'a> QueryArgsIter<'a> {
#[inline]
pub(super) fn new(query: &'a str, cache: &'a QueryArgsCache) -> Self {
debug_assert_eq!(cache.query_len, query.len());
Self {
query,
iter: cache.pairs.iter(),
}
}
}
impl<'a> Iterator for QueryArgsIter<'a> {
type Item = (&'a str, &'a str);
#[inline]
fn next(&mut self) -> Option<Self::Item> {
self.iter.next().map(|s| {
(
&self.query[s.key_start..s.key_end],
&self.query[s.value_start..s.value_end],
)
})
}
}
#[derive(Debug, Clone, Copy)]
struct QueryArgSpan {
key_start: usize,
key_end: usize,
value_start: usize,
value_end: usize,
}
pub(super) struct QueryArgsCache {
query_len: usize,
pairs: smallvec::SmallVec<[QueryArgSpan; DEFAULT_PARAMS_COUNT]>,
}
impl QueryArgsCache {
pub(super) fn new(query: &str) -> Self {
let mut pairs = smallvec::SmallVec::new();
let bytes = query.as_bytes();
let mut seg_start = 0usize;
let mut eq_pos: Option<usize> = None;
for i in 0..=bytes.len() {
let is_end = i == bytes.len();
let b = if is_end { PARAM_SEPARATOR } else { bytes[i] };
if !is_end && b == KV_SEPARATOR && eq_pos.is_none() {
eq_pos = Some(i);
continue;
}
if b == b'&' {
if let Some(eq) = eq_pos {
pairs.push(QueryArgSpan {
key_start: seg_start,
key_end: eq,
value_start: eq + 1,
value_end: i,
});
}
seg_start = i + 1;
eq_pos = None;
}
}
Self {
query_len: query.len(),
pairs,
}
}
#[cfg(test)]
fn collect<'a>(&'a self, query: &'a str) -> Vec<(&'a str, &'a str)> {
QueryArgsIter::new(query, self).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(query: &str) -> Vec<(String, String)> {
let cache = QueryArgsCache::new(query);
QueryArgsIter::new(query, &cache)
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
#[test]
fn it_parses_basic_pairs() {
assert_eq!(
parse("a=1&b=2"),
vec![("a".into(), "1".into()), ("b".into(), "2".into())]
);
}
#[test]
fn it_ignores_segments_without_equals() {
assert_eq!(
parse("flag&a=1&x&b=2"),
vec![("a".into(), "1".into()), ("b".into(), "2".into())]
);
}
#[test]
fn it_handles_empty_query() {
assert_eq!(parse(""), Vec::<(String, String)>::new());
}
#[test]
fn it_handles_trailing_ampersand_and_empty_segments() {
assert_eq!(parse("a=1&"), vec![("a".into(), "1".into())]);
assert_eq!(
parse("a=1&&b=2"),
vec![("a".into(), "1".into()), ("b".into(), "2".into())]
);
assert_eq!(parse("&&"), Vec::<(String, String)>::new());
}
#[test]
fn it_allows_empty_key_or_value() {
assert_eq!(
parse("=1&a="),
vec![("".into(), "1".into()), ("a".into(), "".into())]
);
}
#[test]
fn it_uses_first_equals_in_segment() {
assert_eq!(
parse("a=1=2&b==3"),
vec![("a".into(), "1=2".into()), ("b".into(), "=3".into())]
);
}
#[test]
fn it_supports_utf8_content() {
assert_eq!(
parse("name=Roman&city=London"),
vec![
("name".into(), "Roman".into()),
("city".into(), "London".into())
]
);
}
#[test]
fn cache_len_matches_query_len() {
let q = "a=1&b=2";
let cache = QueryArgsCache::new(q);
assert_eq!(cache.query_len, q.len());
}
#[test]
fn spans_are_consistent_and_sliceable() {
let q = "a=1&bb=22&ccc=333";
let cache = QueryArgsCache::new(q);
for (k, v) in cache.collect(q) {
assert!(!k.contains('&'));
assert!(!k.contains('='));
assert!(!v.contains('&'));
}
assert_eq!(
cache.collect(q),
vec![("a", "1"), ("bb", "22"), ("ccc", "333")]
);
}
}