Add nom-locate, ground for more detailed errors
Some checks are pending
ci/woodpecker/push/woodpecker Pipeline is pending

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
This commit is contained in:
Maxime “pep” Buquet 2023-01-11 23:22:31 +01:00
parent e626352e87
commit b985906d6d
Signed by: pep
GPG key ID: DEDA74AEECA9D0F2
2 changed files with 155 additions and 68 deletions

View file

@ -10,6 +10,7 @@ description = "Parser for the Scansion DSL"
nom = "7.1"
jid = "0.9"
minidom = "0.15"
nom_locate = "4.0.0"
[dev-dependencies]
pretty_assertions = "1.3"

View file

@ -18,8 +18,27 @@ use nom::{
error::{ErrorKind, ParseError},
multi::{many0, many1},
sequence::{delimited, tuple},
FindSubstring, IResult, InputLength, InputTake,
Err as NomErr, FindSubstring, IResult, InputLength, InputTake,
};
use nom_locate::{position, LocatedSpan};
pub type Span<'a> = LocatedSpan<&'a str>;
#[derive(Debug, PartialEq)]
pub struct Token<'a> {
pub position: Span<'a>,
}
impl<'a> From<NomErr<nom::error::Error<LocatedSpan<&'a str>>>> for Token<'a> {
fn from(err: NomErr<nom::error::Error<LocatedSpan<&'a str>>>) -> Token<'a> {
let span = match err {
NomErr::Incomplete(_) => unreachable!(),
NomErr::Error(error) | NomErr::Failure(error) => error.input,
};
Token { position: span }
}
}
pub static DEFAULT_NS: &'static str = "jabber:client";
@ -46,25 +65,29 @@ pub struct Spec {
pub actions: Vec<Action>,
}
fn allspaces(i: &str) -> IResult<&str, &str> {
let (i, (_, comments)) = tuple((multispace0, opt(comment)))(i)?;
fn allspaces(s: Span) -> IResult<Span, Token> {
let (s, (pos, _, comments)) = tuple((position, multispace0, opt(comment)))(s)?;
let mut i = i;
let mut s = s;
if let Some(_) = comments {
let (j, _) = multispace0(i)?;
i = j;
let (j, _) = multispace0(s)?;
s = j;
}
Ok((i, ""))
Ok((s, Token { position: pos }))
}
fn comment(i: &str) -> IResult<&str, &str> {
let (i, _) = many1(delimited(
alt((tag("#"), tag("//"))),
take_until("\n"),
tag("\n"),
))(i)?;
Ok((i, ""))
fn comment(s: Span) -> IResult<Span, Token> {
let (s, (pos, _)) = tuple((
position,
many1(delimited(
alt((tag("#"), tag("//"))),
take_until("\n"),
tag("\n"),
)),
))(s)?;
Ok((s, Token { position: pos }))
}
fn take_until_tags<T, I, E: ParseError<I>, List>(list: List) -> impl Fn(I) -> IResult<I, I, E>
@ -93,15 +116,15 @@ where
}
}
fn parse_account(i: &str) -> IResult<&str, (AccountName, Account)> {
let (i, (_, _, _, name, _)) =
tuple((allspaces, tag("[Client]"), space0, take_until("\n"), space0))(i)?;
fn parse_account(s: Span) -> IResult<Span, (AccountName, Account)> {
let (s, (_, _, _, name, _)) =
tuple((allspaces, tag("[Client]"), space0, take_until("\n"), space0))(s)?;
let (i, lines) = many1(tuple((
let (s, lines) = many1(tuple((
tag("\n"),
take_while1(|c| c == '\t'),
take_until("\n"),
)))(i)?;
)))(s)?;
let name = name.trim();
let mut jid: Option<BareJid> = None;
@ -125,40 +148,40 @@ fn parse_account(i: &str) -> IResult<&str, (AccountName, Account)> {
if jid.is_none() || password.is_none() {
return Err(nom::Err::Error(nom::error::Error::from_error_kind(
i,
s,
ErrorKind::Tag,
)));
}
// Skip comments and empty newlines
let (i, _) = allspaces(i)?;
let (s, _) = allspaces(s)?;
let account = Account {
jid: jid.unwrap(),
password: password.unwrap(),
resource,
};
Ok((i, (String::from(name), account)))
Ok((s, (String::from(name), account)))
}
fn parse_accounts(i: &str) -> IResult<&str, HashMap<AccountName, Account>> {
let (i, accounts) = many0(parse_account)(i)?;
fn parse_accounts(s: Span) -> IResult<Span, HashMap<AccountName, Account>> {
let (s, accounts) = many0(parse_account)(s)?;
let mut map: HashMap<AccountName, Account> = HashMap::new();
for (name, account) in accounts {
map.insert(name, account);
}
Ok((i, map))
Ok((s, map))
}
fn parse_sep(i: &str) -> IResult<&str, &str> {
let (i, _) = delimited(allspaces, many1(tag("-")), allspaces)(i)?;
Ok((i, ""))
fn parse_sep(s: Span) -> IResult<Span, Token> {
let (s, (pos, _)) = delimited(allspaces, tuple((position, many1(tag("-")))), allspaces)(s)?;
Ok((s, Token { position: pos }))
}
fn parse_action(i: &str) -> IResult<&str, Action> {
let (i, name) =
take_until_tags(vec!["disconnects", "connects", "sends:", "receives:"].into_iter())(i)?;
let (i, (tagname, _, _)) = tuple((
fn parse_action(s: Span) -> IResult<Span, Action> {
let (s, name) =
take_until_tags(vec!["disconnects", "connects", "sends:", "receives:"].into_iter())(s)?;
let (s, (tagname, _, _)) = tuple((
alt((
tag("connects"),
tag("disconnects"),
@ -167,49 +190,50 @@ fn parse_action(i: &str) -> IResult<&str, Action> {
)),
space0,
tag("\n"),
))(i)?;
))(s)?;
let name = String::from(name.trim());
let (i, action) = match tagname {
"connects" => (i, Action::Connect(name)),
"disconnects" => (i, Action::Disconnect(name)),
tagname @ "sends:" | tagname @ "receives:" => parse_send_receive(tagname, name, i)?,
let (s, action) = match *tagname.fragment() {
"connects" => (s, Action::Connect(name)),
"disconnects" => (s, Action::Disconnect(name)),
tagname @ "sends:" | tagname @ "receives:" => parse_send_receive(tagname, name, s)?,
_ => unreachable!(),
};
Ok((i, action))
Ok((s, action))
}
fn parse_action_subline(i: &str) -> IResult<&str, &str> {
let (i, (_, line, _)) = tuple((many1(tag("\t")), take_until1("\n"), tag("\n")))(i)?;
Ok((i, line))
fn parse_action_subline(s: Span) -> IResult<Span, &str> {
let (s, (_, line, _)) = tuple((many1(tag("\t")), take_until1("\n"), tag("\n")))(s)?;
Ok((s, line.fragment()))
}
fn parse_send_receive<'a>(tagname: &str, name: String, i: &'a str) -> IResult<&'a str, Action> {
let (i, (_, lines)) = tuple((
fn parse_send_receive<'a>(tagname: &str, name: String, s: Span<'a>) -> IResult<Span<'a>, Action> {
let (s, (_, lines)) = tuple((
take_while(|c| c == ' ' || c == '\r' || c == '\n'), // Spaces but \t
recognize(many1(parse_action_subline)),
))(i)?;
))(s)?;
let lines = lines.trim();
let elem: Element =
Element::from_reader_with_prefixes(&lines.as_bytes()[..], String::from(DEFAULT_NS))
.unwrap();
Ok(match tagname {
"sends:" => (i, Action::Send(name, elem)),
"receives:" => (i, Action::Receive(name, elem)),
"sends:" => (s, Action::Send(name, elem)),
"receives:" => (s, Action::Receive(name, elem)),
_ => unreachable!(),
})
}
fn parse_actions(i: &str) -> IResult<&str, Vec<Action>> {
let (i, actions) = many1(delimited(allspaces, parse_action, allspaces))(i)?;
Ok((i, actions))
fn parse_actions(s: Span) -> IResult<Span, Vec<Action>> {
let (s, actions) = many1(delimited(allspaces, parse_action, allspaces))(s)?;
Ok((s, actions))
}
pub fn parse_spec(i: &str) -> Result<Spec, nom::Err<nom::error::Error<&str>>> {
let (i, accounts) = parse_accounts(i)?;
let (i, _) = parse_sep(i)?;
let (_i, actions) = parse_actions(i)?;
pub fn parse_spec(i: &str) -> Result<Spec, Token> {
let s: Span = i.into();
let (s, accounts) = parse_accounts(s)?;
let (s, _) = parse_sep(s)?;
let (_, actions) = parse_actions(s)?;
Ok(Spec { accounts, actions })
}
@ -230,17 +254,41 @@ mod tests {
fn test_comment() {
let buf1 = "#\n";
let buf2 = "# Foo\n";
assert_eq!(comment(buf1), Ok(("", "")));
assert_eq!(comment(buf2), Ok(("", "")));
assert_eq!(
comment(buf1.into()),
Ok((
unsafe { LocatedSpan::new_from_raw_offset(2, 2, "", ()) },
Token {
position: LocatedSpan::new("")
}
))
);
assert_eq!(
comment(buf2.into()),
Ok((
unsafe { LocatedSpan::new_from_raw_offset(6, 2, "", ()) },
Token {
position: LocatedSpan::new("")
}
))
);
let buf3 = " # Foo\n";
match comment(buf3) {
match comment(buf3.into()) {
Err(_) => (),
err => panic!("Unexpected result: {:?}", err),
}
let buf4 = "// Foo\n";
assert_eq!(comment(buf4), Ok(("", "")));
assert_eq!(
comment(buf4.into()),
Ok((
unsafe { LocatedSpan::new_from_raw_offset(7, 2, "", ()) },
Token {
position: LocatedSpan::new("")
}
))
);
}
#[test]
@ -256,8 +304,11 @@ mod tests {
resource: None,
};
assert_eq!(
parse_account(buf1),
Ok(("", (name.clone(), account.clone())))
parse_account(buf1.into()),
Ok((
unsafe { LocatedSpan::new_from_raw_offset(59, 4, "", ()) },
(name.clone(), account.clone())
))
);
let name = String::from("louise's phone");
@ -267,12 +318,15 @@ mod tests {
resource: Some(String::from("resource1")),
};
assert_eq!(
parse_account(buf2),
Ok(("", (name.clone(), account.clone())))
parse_account(buf2.into()),
Ok((
unsafe { LocatedSpan::new_from_raw_offset(90, 5, "", ()) },
(name.clone(), account.clone())
))
);
// Missing tab
match parse_account(buf3) {
match parse_account(buf3.into()) {
Err(_) => (),
err => panic!("Unexpected result: {:?}", err),
}
@ -308,7 +362,13 @@ mod tests {
resource: Some(String::from("resource1")),
},
);
assert_eq!(parse_accounts(buf1), Ok(("", accounts)));
assert_eq!(
parse_accounts(buf1.into()),
Ok((
unsafe { LocatedSpan::new_from_raw_offset(144, 10, "", ()) },
accounts
))
);
}
#[test]
@ -316,12 +376,12 @@ mod tests {
let buf1 = "louise connects\n";
let action1 = Action::Connect(String::from("louise"));
assert_eq!(parse_action(buf1).unwrap().1, action1);
assert_eq!(parse_action(buf1.into()).unwrap().1, action1);
let buf2 = "louise's phone connects\n";
let action2 = Action::Connect(String::from("louise's phone"));
assert_eq!(parse_action(buf2).unwrap().1, action2);
assert_eq!(parse_action(buf2.into()).unwrap().1, action2);
}
#[test]
@ -329,7 +389,7 @@ mod tests {
let buf1 = "louise disconnects\n";
let action = Action::Disconnect(String::from("louise"));
assert_eq!(parse_action(buf1).unwrap().1, action);
assert_eq!(parse_action(buf1.into()).unwrap().1, action);
}
#[test]
@ -347,7 +407,7 @@ mod tests {
String::from("rosa"),
Element::from_reader_with_prefixes(&xml[..], String::from(DEFAULT_NS)).unwrap(),
);
assert_eq!(parse_action(buf).unwrap().1, send);
assert_eq!(parse_action(buf.into()).unwrap().1, send);
}
#[test]
@ -365,7 +425,7 @@ mod tests {
String::from("rosa"),
Element::from_reader_with_prefixes(&xml[..], String::from(DEFAULT_NS)).unwrap(),
);
assert_eq!(parse_action(buf).unwrap().1, receive);
assert_eq!(parse_action(buf.into()).unwrap().1, receive);
}
#[test]
@ -410,4 +470,30 @@ louise receives:
assert_eq!(parse_spec(buf), Ok(spec));
}
#[test]
fn test_parse_errors() {
let buf1: Span = "[Foo] bar\n".into();
assert_eq!(
parse_spec(&buf1),
Err(Token {
position: LocatedSpan::new("[Foo] bar\n")
})
);
let buf2: Span = "[Client] louise\n\tjid: jid@localhost\npassword: password\n".into();
assert_eq!(
parse_spec(&buf2),
Err(Token {
position: unsafe {
LocatedSpan::new_from_raw_offset(
0,
1,
"[Client] louise\n\tjid: jid@localhost\npassword: password\n",
(),
)
}
})
);
}
}