use std::sync::Arc;
use smallvec::SmallVec;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Token<'a> {
pub text: &'a str,
pub leading_delim: Option<char>,
pub trailing_delim: Option<char>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OwnedToken {
pub text: Arc<str>,
pub leading_delim: Option<char>,
pub trailing_delim: Option<char>,
}
impl From<&Token<'_>> for OwnedToken {
fn from(t: &Token<'_>) -> Self {
OwnedToken {
text: Arc::from(t.text),
leading_delim: t.leading_delim,
trailing_delim: t.trailing_delim,
}
}
}
pub type Tokens<'a> = SmallVec<[Token<'a>; 16]>;
#[cfg(test)]
pub(crate) fn tokenize(line: &str) -> Tokens<'_> {
tokenize_with(line, &[])
}
pub(crate) fn tokenize_with<'a>(line: &'a str, delims: &[char]) -> Tokens<'a> {
let mut out: Tokens<'a> = SmallVec::new();
for piece in line.split_whitespace() {
push_path_tokens(piece, delims, &mut out);
}
out
}
fn push_path_tokens<'a>(piece: &'a str, delims: &[char], out: &mut Tokens<'a>) {
let before = out.len();
let mut cur_leading: Option<char> = None;
let mut seg_start = 0usize;
for (i, ch) in piece.char_indices() {
if delims.contains(&ch) {
let seg = &piece[seg_start..i];
if !seg.is_empty() {
out.push(Token {
text: seg,
leading_delim: cur_leading,
trailing_delim: Some(ch),
});
}
cur_leading = Some(ch);
seg_start = i + ch.len_utf8();
}
}
let seg = &piece[seg_start..];
if !seg.is_empty() {
out.push(Token {
text: seg,
leading_delim: cur_leading,
trailing_delim: None,
});
}
if out.len() == before && !piece.is_empty() {
out.push(Token {
text: piece,
leading_delim: None,
trailing_delim: None,
});
}
}
pub(crate) fn split_first_line(line: &str) -> (&str, Option<&str>) {
match line.find('\n') {
Some(i) => (&line[..i], Some(&line[i + 1..])),
None => (line, None),
}
}
pub fn is_numeric_token(s: &str) -> bool {
!s.is_empty() && s.bytes().all(|b| b.is_ascii_digit())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn splits_on_whitespace() {
let toks = tokenize("GET /api/v1 200");
let texts: Vec<&str> = toks.iter().map(|t| t.text).collect();
assert_eq!(texts, vec!["GET", "/api/v1", "200"]);
}
#[test]
fn collapses_runs_of_whitespace() {
let toks = tokenize(" a b\tc ");
let texts: Vec<&str> = toks.iter().map(|t| t.text).collect();
assert_eq!(texts, vec!["a", "b", "c"]);
}
#[test]
fn empty_input_yields_no_tokens() {
assert_eq!(tokenize("").len(), 0);
assert_eq!(tokenize(" ").len(), 0);
}
#[test]
fn numeric_detection() {
assert!(is_numeric_token("12345"));
assert!(is_numeric_token("0"));
assert!(!is_numeric_token(""));
assert!(!is_numeric_token("12a"));
assert!(!is_numeric_token("-3"));
assert!(!is_numeric_token("1.5"));
}
fn flags<'a>(t: &Token<'a>) -> (&'a str, Option<char>, Option<char>) {
(t.text, t.leading_delim, t.trailing_delim)
}
#[test]
fn path_splits_with_delim_flags() {
let toks = tokenize_with("/servers/409/foo", &['/']);
assert_eq!(toks.len(), 3);
assert_eq!(flags(&toks[0]), ("servers", Some('/'), Some('/')));
assert_eq!(flags(&toks[1]), ("409", Some('/'), Some('/')));
assert_eq!(flags(&toks[2]), ("foo", Some('/'), None));
}
#[test]
fn mixed_whitespace_and_path() {
let toks = tokenize_with("GET /servers/409 ok", &['/']);
assert_eq!(toks.len(), 4);
assert_eq!(flags(&toks[0]), ("GET", None, None));
assert_eq!(flags(&toks[1]), ("servers", Some('/'), Some('/')));
assert_eq!(flags(&toks[2]), ("409", Some('/'), None));
assert_eq!(flags(&toks[3]), ("ok", None, None));
}
#[test]
fn leading_and_trailing_delims() {
let a = tokenize_with("a/b", &['/']);
assert_eq!(flags(&a[0]), ("a", None, Some('/')));
assert_eq!(flags(&a[1]), ("b", Some('/'), None));
let lead = tokenize_with("/a", &['/']);
assert_eq!(flags(&lead[0]), ("a", Some('/'), None));
let trail = tokenize_with("a/", &['/']);
assert_eq!(trail.len(), 1);
assert_eq!(flags(&trail[0]), ("a", None, Some('/')));
}
#[test]
fn consecutive_delims_collapse() {
let toks = tokenize_with("a//b", &['/']);
assert_eq!(toks.len(), 2);
assert_eq!(flags(&toks[0]), ("a", None, Some('/')));
assert_eq!(flags(&toks[1]), ("b", Some('/'), None));
}
#[test]
fn lone_delimiter_is_preserved() {
let toks = tokenize_with("GET / HTTP", &['/']);
let texts: Vec<&str> = toks.iter().map(|t| t.text).collect();
assert_eq!(texts, vec!["GET", "/", "HTTP"]);
}
#[test]
fn empty_delims_behaves_like_v0_1() {
let toks = tokenize_with("/servers/409/foo", &[]);
assert_eq!(toks.len(), 1);
assert_eq!(flags(&toks[0]), ("/servers/409/foo", None, None));
}
#[test]
fn split_first_line_works() {
assert_eq!(split_first_line("a b\nc\nd"), ("a b", Some("c\nd")));
assert_eq!(split_first_line("single"), ("single", None));
assert_eq!(split_first_line("first\n"), ("first", Some("")));
}
}