use std::borrow::Cow;
use nom::{
branch::alt,
bytes::complete::tag,
character::complete::{anychar, char as nchar, one_of, space0, tab},
combinator::{recognize, verify},
multi::{many0, many1, separated_list1},
sequence::{delimited, pair, preceded, separated_pair},
IResult, Parser as _,
};
#[inline(always)]
fn ows(input: &str) -> IResult<&str, &str> {
space0(input)
}
#[inline]
fn vchar(input: &str) -> IResult<&str, char> {
verify(anychar, |c| c.is_ascii_graphic())(input)
}
#[inline]
fn quoted_string(input: &str) -> IResult<&str, String> {
let (input, chars) = delimited(tag("\""), many0(alt((qdtext, quoted_pair))), tag("\""))(input)?;
Ok((input, chars.into_iter().collect()))
}
#[inline]
fn qdtext(input: &str) -> IResult<&str, char> {
alt((
tab,
nchar(' '),
verify(anychar, |c| {
let c = *c as u32;
c == 0x21 || (c >= 0x23 && c <= 0x5B) || (c >= 0x5D && c <= 0x7E)
}),
obs_text,
))(input)
}
#[inline]
fn quoted_pair(input: &str) -> IResult<&str, char> {
preceded(nchar('\\'), alt((tab, nchar(' '), vchar, obs_text)))(input)
}
#[inline]
fn obs_text(input: &str) -> IResult<&str, char> {
verify(anychar, |c| {
let c = *c as u32;
c >= 0x80 && c <= 0xFF
})(input)
}
#[inline]
fn tchar(input: &str) -> IResult<&str, char> {
alt((
(one_of("!#$'*+-.^_`|~")),
verify(anychar, |c| c.is_ascii_alphanumeric()),
))(input)
}
#[inline]
fn token(input: &str) -> IResult<&str, &str> {
recognize(many1(tchar))(input)
}
fn server_timing_param(input: &str) -> IResult<&str, (&str, Cow<str>)> {
let server_timing_param_name = token;
let server_timing_param_value = alt((token.map(Cow::Borrowed), quoted_string.map(Cow::Owned)));
separated_pair(
server_timing_param_name,
delimited(ows, tag("="), ows),
server_timing_param_value,
)(input)
}
pub type Metric<'a> = (&'a str, Vec<(&'a str, Cow<'a, str>)>);
fn server_timing_metric(input: &str) -> IResult<&str, Metric> {
let metric_name = token;
let params = many0(preceded(delimited(ows, tag(";"), ows), server_timing_param));
pair(metric_name, params)(input)
}
pub fn server_timing(input: &str) -> IResult<&str, Vec<Metric>> {
separated_list1(delimited(ows, tag(","), ows), server_timing_metric)(input)
}
#[cfg(test)]
mod test {
use super::{server_timing, server_timing_metric, server_timing_param};
#[test]
fn test_server_timing_metric_param_order() {
let input = "foo;dur=12.3;desc=bar";
let (rest, (name, params)) = server_timing_metric(input).unwrap();
assert_eq!(rest, "");
assert_eq!(name, "foo");
assert_eq!(params, vec![("dur", "12.3".into()), ("desc", "bar".into())]);
}
#[test]
fn test_server_timing_param_bare_string() {
let input = "desc=bar;dur=12.3";
let (rest, (key, value)) = server_timing_param(input).unwrap();
assert_eq!(rest, ";dur=12.3");
assert_eq!(key, "desc");
assert_eq!(value, "bar");
}
#[test]
fn test_server_timing_param_bare_string_spaces() {
let input = "desc=bar baz;dur=12.3";
let (rest, (key, value)) = server_timing_param(input).unwrap();
assert_eq!(rest, " baz;dur=12.3");
assert_eq!(key, "desc");
assert_eq!(value, "bar");
}
#[test]
fn test_server_timing_param_quoted_string() {
let input = "desc=\"hello=world;description\";dur=12.3";
let (rest, (key, value)) = server_timing_param(input).unwrap();
assert_eq!(rest, ";dur=12.3");
assert_eq!(key, "desc");
assert_eq!(value, "hello=world;description");
}
#[test]
fn test_server_timing_param_quoted_string_empty() {
let input = "desc=\"\";dur=12.3";
let (rest, (key, value)) = server_timing_param(input).unwrap();
assert_eq!(rest, ";dur=12.3");
assert_eq!(key, "desc");
assert_eq!(value, "");
}
#[test]
fn test_server_timing_param_quoted_string_spaces() {
let input = "desc=\"bar baz\";dur=12.3";
let (rest, (key, value)) = server_timing_param(input).unwrap();
assert_eq!(rest, ";dur=12.3");
assert_eq!(key, "desc");
assert_eq!(value, "bar baz");
}
#[test]
fn test_server_timing_param_quoted_string_escaped() {
let input = "desc=\"\\\"\";dur=12.3";
let (rest, (key, value)) = server_timing_param(input).unwrap();
assert_eq!(rest, ";dur=12.3");
assert_eq!(key, "desc");
assert_eq!(value, "\"");
}
#[test]
fn test_server_timing_metric() {
let input = "foo;desc=bar;dur=12.3";
let (rest, (name, params)) = server_timing_metric(input).unwrap();
assert_eq!(rest, "");
assert_eq!(name, "foo");
assert_eq!(params, vec![("desc", "bar".into()), ("dur", "12.3".into())]);
}
#[test]
fn test_server_timing_metric_param_unknown() {
let input = "foo;bar=baz";
let (rest, (name, params)) = server_timing_metric(input).unwrap();
assert_eq!(rest, "");
assert_eq!(name, "foo");
assert_eq!(params, vec![("bar", "baz".into())]);
}
#[test]
fn test_server_timing() {
let input = "foo;bar=baz, hello, db;dur=12.3;desc=mysql";
let (rest, metrics) = server_timing(input).unwrap();
let expected = vec![
("foo", vec![("bar", "baz".into())]),
("hello", vec![]),
("db", vec![("dur", "12.3".into()), ("desc", "mysql".into())]),
];
assert_eq!(rest, "");
assert_eq!(metrics, expected);
}
#[test]
fn test_comma_in_quoted_string() {
let input = "foo;desc=\"bar, baz\", hello";
let (rest, metrics) = server_timing(input).unwrap();
assert_eq!(rest, "");
assert_eq!(
metrics,
vec![
("foo", vec![("desc", "bar, baz".into())]),
("hello", vec![])
]
);
}
}