use std::{
fmt::Display,
ops::{RangeFrom, RangeTo},
};
use itertools::Itertools;
use nom::{
branch::alt,
character::complete::{char, none_of, satisfy, space0},
combinator::{all_consuming, opt, recognize},
error::{ParseError, VerboseError},
multi::{fold_many0, many0, many0_count, many1_count, many_m_n, separated_list1},
sequence::{delimited, pair, preceded, tuple},
AsChar, IResult, InputIter, InputLength, InputTake, InputTakeAtPosition, Offset, Parser, Slice,
};
use thiserror::Error;
use crate::{is_qdtext, is_quoted_pair, is_tchar, optional_parser};
pub fn tchar<I, E>(input: I) -> IResult<I, char, E>
where
I: InputIter + Slice<RangeFrom<usize>>,
<I as InputIter>::Item: AsChar,
E: ParseError<I>,
{
satisfy(is_tchar)(input)
}
pub fn token<I, E>(input: I) -> IResult<I, I, E>
where
I: InputIter + Slice<RangeFrom<usize>> + Slice<RangeTo<usize>> + Copy + InputLength + Offset,
<I as InputIter>::Item: AsChar,
E: ParseError<I>,
{
recognize(many1_count(tchar))(input)
}
pub fn qdtext<I, E>(input: I) -> IResult<I, char, E>
where
I: InputIter + Slice<RangeFrom<usize>> + Copy,
<I as InputIter>::Item: AsChar,
E: ParseError<I>,
{
satisfy(is_qdtext)(input)
}
fn quoted_pair<I, E>(input: I) -> IResult<I, char, E>
where
I: InputIter + Slice<RangeFrom<usize>> + Copy,
<I as InputIter>::Item: AsChar,
E: ParseError<I>,
{
preceded(char('\\'), satisfy(is_quoted_pair))(input)
}
fn quoted_string<I, E>(input: I) -> IResult<I, I, E>
where
I: InputIter + Slice<RangeFrom<usize>> + Slice<RangeTo<usize>> + Copy + InputLength + Offset,
<I as InputIter>::Item: AsChar,
E: ParseError<I>,
{
recognize(delimited(
char('"'),
recognize(many0_count(alt((quoted_pair, qdtext)))),
char('"'),
))(input)
}
fn quoted_string_alloca<I, E>(input: I) -> IResult<I, String, E>
where
I: InputIter + Slice<RangeFrom<usize>> + Copy + InputLength,
<I as InputIter>::Item: AsChar,
E: ParseError<I>,
{
all_consuming(delimited(
char('"'),
fold_many0(alt((quoted_pair, qdtext)), String::new, |mut acc, item| {
acc.push(item);
acc
}),
char('"'),
))(input)
}
pub fn list<I, O, E, L>(
reasonable_count: usize,
element: L,
input: I,
) -> IResult<I, Vec<Option<O>>, E>
where
L: Parser<I, O, E>,
E: ParseError<I>,
I: InputLength
+ Clone
+ Copy
+ InputTakeAtPosition
+ InputIter
+ InputTake
+ Slice<RangeFrom<usize>>,
<I as InputIter>::Item: Clone + AsChar,
<I as InputTakeAtPosition>::Item: AsChar + Clone,
{
let allow_empty_elements = reasonable_count != 0;
separated_list1(
many_m_n(1, reasonable_count + 1, tuple((space0, char(','), space0))),
optional_parser(allow_empty_elements, element),
)(input)
}
#[derive(PartialEq, Debug)]
pub struct LinkParam<'a> {
pub key: &'a str,
pub val: Option<String>,
}
#[derive(PartialEq, Debug)]
pub struct LinkData<'a> {
pub url: &'a str,
pub params: Vec<LinkParam<'a>>,
}
#[derive(Error, Debug)]
pub enum LinkParseError {
#[error("left over data could not be parsed: `{0}`")]
IncompleteParse(String),
#[error("the data for key `{0}` is not available")]
FailedToParse(String),
}
impl<E> From<nom::Err<VerboseError<E>>> for LinkParseError
where
E: Display,
{
fn from(err: nom::Err<VerboseError<E>>) -> Self {
match err {
nom::Err::Incomplete(_) => Self::FailedToParse("Incomplete input".into()),
nom::Err::Error(err) | nom::Err::Failure(err) => Self::FailedToParse(err.to_string()),
}
}
}
const NUM_EMPTY_ELEMENTS: usize = 2;
pub fn link<'a, E>(input: &'a str) -> Result<Vec<Option<LinkData<'a>>>, LinkParseError>
where
E: ParseError<&'a str>,
nom::Err<VerboseError<&'a str>>: From<nom::Err<E>>,
{
type ParserOutput<'s> = (
&'s str,
Vec<Option<(&'s str, Vec<(&'s str, Option<&'s str>)>)>>,
);
let parsed = list::<_, _, VerboseError<&str>, _>(
NUM_EMPTY_ELEMENTS,
tuple((
delimited(char('<'), recognize(many0_count(none_of(">"))), char('>')),
many0(preceded(
tuple((space0, char(';'), space0)),
pair(
token::<&str, VerboseError<&str>>,
opt(preceded(
pair(char('='), space0),
alt((quoted_string, token)),
)),
),
)),
)),
input,
);
let (remainder, mut output): ParserOutput<'a> = parsed?;
if !remainder.is_empty() {
return Err(LinkParseError::IncompleteParse(remainder.to_owned()));
}
let res = output
.drain(..)
.map(|parsed_link| {
let mut parsed_link = match parsed_link {
Some(l) => l,
None => return Ok(None),
};
let link_params = parsed_link
.1
.drain(..)
.map(|link_param| {
let parsed_link_param_val = match link_param.1 {
None => None,
Some(link_param_val) if link_param_val.starts_with('"') => {
match quoted_string_alloca::<&str, VerboseError<&str>>(link_param_val) {
Ok(s) => Some(s.1),
Err(e) => return Err(e),
}
}
Some(link_param_val) => Some(link_param_val.to_owned()),
};
Ok(LinkParam {
key: link_param.0,
val: parsed_link_param_val,
})
})
.fold_ok(Vec::new(), |mut acc, item| {
acc.push(item);
acc
})?;
Ok(Some(LinkData {
url: parsed_link.0,
params: link_params,
}))
})
.fold_ok(Vec::new(), |mut acc, item| {
acc.push(item);
acc
});
res
}
#[cfg(test)]
mod tests {
use nom::{error::VerboseError, Err as OutCome};
use crate::complete::{
quoted_string, quoted_string_alloca, tchar, token, LinkData, LinkParam, LinkParseError,
};
use super::{link, list, quoted_pair};
#[test]
fn test_tchar() {
assert_eq!(tchar::<_, VerboseError<&str>>("mbbb"), Ok(("bbb", 'm')));
assert_eq!(tchar::<_, VerboseError<&str>>("!aa"), Ok(("aa", '!')));
assert!(tchar::<_, VerboseError<&str>>(",").is_err());
}
#[test]
fn test_token() {
assert!(matches!(
token::<_, VerboseError<&str>>(""),
Err(OutCome::Error(_))
));
assert_eq!(token::<_, VerboseError<&str>>("mbbb"), Ok(("", "mbbb")));
assert_eq!(token::<_, VerboseError<&str>>("a,"), Ok((",", "a")));
assert!(matches!(
token::<_, VerboseError<&str>>(","),
Err(OutCome::Error(_))
));
}
#[test]
fn test_quoted_string() {
assert_eq!(
quoted_string::<_, VerboseError<&str>>(r#""""#),
Ok(("", r#""""#))
);
assert_eq!(
quoted_string::<_, VerboseError<&str>>(r#""hello""#),
Ok(("", r#""hello""#))
);
assert_eq!(
quoted_string::<_, VerboseError<&str>>(r#""\"hello""#),
Ok(("", r#""\"hello""#))
);
assert!(matches!(
quoted_string::<_, VerboseError<&str>>(r#""awd"#),
Err(OutCome::Error(_))
));
assert!(matches!(
quoted_string::<_, VerboseError<&str>>(r#" "text""#),
Err(OutCome::Error(_))
));
assert_eq!(
quoted_string::<_, VerboseError<&str>>(r#""awd"trailing"#),
Ok(("trailing", r#""awd""#))
);
}
#[test]
fn test_list_rule() {
assert_eq!(
list::<_, _, VerboseError<&str>, _>(0, token, "a,b,c"),
Ok(("", vec![Some("a"), Some("b"), Some("c")]))
);
assert_eq!(
list::<_, _, VerboseError<&str>, _>(0, token, "a , b , c"),
Ok(("", vec![Some("a"), Some("b"), Some("c")]))
);
assert_eq!(
list::<_, _, VerboseError<&str>, _>(0, token, "a , b , "),
Ok((" , ", vec![Some("a"), Some("b")]))
);
assert_eq!(
list::<_, _, VerboseError<&str>, _>(0, token, "a , b , ,"),
Ok((" , ,", vec![Some("a"), Some("b")]))
);
}
#[test]
fn test_link() {
let input = r##"</terms>; rel="copyright"; anchor="#foo""##;
let res = link::<VerboseError<&str>>(input).unwrap();
assert_eq!(
res,
vec![Some(LinkData {
url: "/terms",
params: vec![
LinkParam {
key: "rel",
val: Some("copyright".into())
},
LinkParam {
key: "anchor",
val: Some("#foo".into())
}
]
})]
);
}
#[test]
fn test_quoted_pair() {
let input = r#"\a"#;
let res = quoted_pair::<_, VerboseError<&str>>(input).unwrap();
assert_eq!(res.1, 'a');
}
#[test]
fn test_quoted_string_alloca() {
let input = r#""aaaa""#;
let res = quoted_string_alloca::<_, VerboseError<&str>>(input).unwrap();
assert_eq!(res.1, "aaaa".to_owned());
}
#[test]
fn test_quoted_string_alloca_quotes() {
let input = r#""aa\"aa""#;
let res = quoted_string_alloca::<_, VerboseError<&str>>(input).unwrap();
assert_eq!(res.1, "aa\"aa".to_owned());
}
#[test]
fn test_link_quoted_link_param() {
let input = r##"</terms>; rel="copy\"right"; anchor=#foo"##;
let res = link::<VerboseError<&str>>(input).unwrap();
assert_eq!(
res,
vec![Some(LinkData {
url: "/terms",
params: vec![
LinkParam {
key: "rel",
val: Some("copy\"right".into())
},
LinkParam {
key: "anchor",
val: Some("#foo".into())
}
]
})]
);
}
#[test]
fn test_error_return_ergonomics() -> Result<(), LinkParseError> {
fn function_with_return_val<'a>() -> Result<Vec<Option<LinkData<'a>>>, LinkParseError> {
let input = r##"</terms>; rel="copy\"right"; anchor=#foo"##;
link::<VerboseError<&str>>(input)
}
function_with_return_val()?;
Ok(())
}
}