Split into modules

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
This commit is contained in:
Maxime “pep” Buquet 2023-01-17 18:32:51 +01:00
parent 7314127763
commit 309be31d45
Signed by: pep
GPG key ID: DEDA74AEECA9D0F2
3 changed files with 710 additions and 688 deletions

View file

@ -4,692 +4,8 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this // License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/. // file, You can obtain one at http://mozilla.org/MPL/2.0/.
use std::collections::{BTreeMap, HashMap}; mod parsers;
use std::str::FromStr; mod types;
use jid::Jid; pub use parsers::parse_spec;
use minidom::Element; pub use types::{Spec, Action, Client, Metadata};
use nom::{
self,
branch::alt,
bytes::complete::{tag, take_until, take_until1, take_while, take_while1},
character::complete::{digit1, multispace0, space0},
combinator::{map_res, opt, recognize},
error::{ErrorKind, ParseError},
multi::{many0, many1},
sequence::{delimited, tuple},
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: &str = "jabber:client";
pub static SCANSION_NS: &str = "https://matthewwild.co.uk/projects/scansion";
#[derive(Debug, Clone, PartialEq)]
pub struct Metadata {
pub title: String,
pub description: Option<String>,
pub tags: Vec<String>,
}
impl Metadata {
pub fn new<S: Into<String>, T: Into<String>>(title: S, desc: Option<T>) -> Metadata {
Metadata {
title: title.into(),
description: desc.map(|t| t.into()),
tags: vec![],
}
}
pub fn with_tags(mut self, tags: Vec<String>) -> Metadata {
self.tags = tags;
self
}
}
pub type ClientName = String;
#[derive(Debug, Clone, PartialEq)]
pub struct Client {
pub jid: Jid,
pub password: String,
pub custom_host: Option<String>,
pub custom_port: Option<u16>,
}
impl Client {
pub fn new<S: Into<String>>(jid: Jid, password: S) -> Client {
Client {
jid,
password: password.into(),
custom_host: None,
custom_port: None,
}
}
pub fn with_custom_host<S: Into<String>>(mut self, custom_host: S) -> Client {
self.custom_host = Some(custom_host.into());
self
}
pub fn with_custom_port(mut self, custom_port: u16) -> Client {
self.custom_port = Some(custom_port);
self
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Action {
Connect(ClientName),
Send(ClientName, Element),
Receive(ClientName, Element),
ReceiveNone(ClientName),
Disconnect(ClientName),
}
#[derive(Debug, Clone, PartialEq)]
pub struct Spec {
pub metadata: Option<Metadata>,
pub clients: HashMap<ClientName, Client>,
pub actions: Vec<Action>,
}
fn allspaces(s: Span) -> IResult<Span, Token> {
let (s, (pos, _, comments)) = tuple((position, multispace0, opt(comment)))(s)?;
let mut s = s;
#[allow(clippy::redundant_pattern_matching)]
if let Some(_) = comments {
let (j, _) = multispace0(s)?;
s = j;
}
Ok((s, Token { position: pos }))
}
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,
endmark: T,
) -> impl Fn(I) -> IResult<I, I, E>
where
I: InputTake + FindSubstring<T>,
T: InputLength + Clone,
List: Iterator<Item = T> + Clone,
{
move |i: I| {
let mut l = list.clone();
let endmark = endmark.clone();
let endindex = match i.find_substring(endmark) {
None | Some(0) => {
return Err(nom::Err::Error(E::from_error_kind(i, ErrorKind::TakeUntil)))
}
Some(index) => index,
};
let res: Option<usize> = loop {
if let Some(item) = l.next() {
match i.find_substring(item) {
None | Some(0) => continue,
Some(index) if index >= endindex => continue,
valid => break valid,
}
} else {
break None;
}
};
let res: IResult<_, _, E> = match res {
None => Err(nom::Err::Error(E::from_error_kind(i, ErrorKind::TakeUntil))),
Some(index) => Ok(i.take_split(index)),
};
res
}
}
fn parse_meta_tags(s: Span) -> IResult<Span, Vec<String>> {
let (s, tags) = many0(map_res(
delimited(tuple((position, tag("##"))), take_until("\n"), tag("\n")),
|tag: Span| Ok::<String, ()>(tag.trim().to_string()),
))(s)?;
Ok((s, tags))
}
fn parse_meta(s: Span) -> IResult<Span, Metadata> {
let (s, (_pos, title)) = tuple((
position,
delimited(tag("#"), take_while1(|c| c != '#' && c != '\n'), tag("\n")),
))(s)?;
let (s, optdesc) = opt(tuple((
position,
delimited(tag("#"), take_while1(|c| c != '#' && c != '\n'), tag("\n")),
)))(s)?;
let description = optdesc.map(|(_pos, d)| String::from(d.trim()));
let (s, tags) = parse_meta_tags(s)?;
let meta = Metadata::new(title.trim(), description).with_tags(tags);
Ok((s, meta))
}
fn parse_client(s: Span) -> IResult<Span, (ClientName, Client)> {
let (s, (_, _, _, name, _)) =
tuple((allspaces, tag("[Client]"), space0, take_until("\n"), space0))(s)?;
let (s, lines) = many1(tuple((
tag("\n"),
take_while1(|c| c == '\t'),
take_until("\n"),
)))(s)?;
let name = name.trim();
let mut jid: Option<Jid> = None;
let mut password: Option<&str> = None;
let mut custom_host: Option<&str> = None;
let mut custom_port: Option<u16> = None;
for line in lines {
let (_, _, attr) = line;
if let Some((key, val)) = attr.split_once(':') {
let val = val.trim();
match key.trim() {
"jid" => jid = Some(Jid::from_str(val).unwrap()),
"password" => password = Some(val),
"custom_host" => custom_host = Some(val),
"custom_port" => {
let val: Span = val.into();
let (j, digits): (Span, Span) = digit1(val)?;
let val = digits.parse::<u16>().or(Err(nom::Err::Error(
nom::error::Error::from_error_kind(j, ErrorKind::Digit),
)))?;
custom_port = Some(val);
}
_ => (),
}
} else {
return Err(nom::Err::Error(nom::error::Error::from_error_kind(
s,
ErrorKind::Tag,
)));
}
}
if jid.is_none() || password.is_none() {
return Err(nom::Err::Error(nom::error::Error::from_error_kind(
s,
ErrorKind::Tag,
)));
}
// Skip comments and empty newlines
let (s, _) = allspaces(s)?;
let mut client = Client::new(jid.unwrap(), password.unwrap());
client.custom_host = custom_host.map(String::from);
client.custom_port = custom_port;
Ok((s, (String::from(name), client)))
}
fn parse_clients(s: Span) -> IResult<Span, HashMap<ClientName, Client>> {
let (s, clients) = many0(parse_client)(s)?;
let mut map: HashMap<ClientName, Client> = HashMap::new();
for (name, client) in clients {
map.insert(name, client);
}
Ok((s, map))
}
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(s: Span) -> IResult<Span, Action> {
let (s, name) = take_until_tags(
vec![
"disconnects",
"connects",
"receives: nothing",
"sends:",
"receives:",
]
.into_iter(),
"\n",
)(s)?;
let (s, (tagname, _, _)) = tuple((
alt((
tag("connects"),
tag("disconnects"),
tag("receives: nothing"),
tag("sends:"),
tag("receives:"),
)),
space0,
tag("\n"),
))(s)?;
let name = String::from(name.trim());
let (s, action) = match *tagname.fragment() {
"connects" => (s, Action::Connect(name)),
"disconnects" => (s, Action::Disconnect(name)),
"receives: nothing" => (s, Action::ReceiveNone(name)),
tagname @ "sends:" | tagname @ "receives:" => parse_send_receive(tagname, name, s)?,
_ => unreachable!(),
};
Ok((s, action))
}
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, 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)),
))(s)?;
let lines = lines.trim();
// Namespaces
let mut prefixes = BTreeMap::new();
prefixes.insert(None, String::from(DEFAULT_NS));
prefixes.insert(Some(String::from("scansion")), String::from(SCANSION_NS));
let elem: Element = Element::from_reader_with_prefixes(lines.as_bytes(), prefixes).unwrap();
Ok(match tagname {
"sends:" => (s, Action::Send(name, elem)),
"receives:" => (s, Action::Receive(name, elem)),
_ => unreachable!(),
})
}
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, Token> {
let s: Span = i.into();
let (s, metadata) = opt(parse_meta)(s)?;
let (s, clients) = parse_clients(s)?;
let (s, _) = parse_sep(s)?;
let (_, actions) = parse_actions(s)?;
Ok(Spec {
metadata,
clients,
actions,
})
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
fn get_client(name: &str) -> Client {
Client::new(
Jid::from_str(format!("{}@localhost", name).as_str()).unwrap(),
"password",
)
}
#[test]
fn test_comment() {
let buf1 = "#\n";
let buf2 = "# Foo\n";
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.into()) {
Err(_) => (),
err => panic!("Unexpected result: {:?}", err),
}
let buf4 = "// Foo\n";
assert_eq!(
comment(buf4.into()),
Ok((
unsafe { LocatedSpan::new_from_raw_offset(7, 2, "", ()) },
Token {
position: LocatedSpan::new("")
}
))
);
}
#[test]
fn test_meta_success() {
let buf1 = "# Title.\n";
let buf2 = "#Foo\n# Desc\n";
let buf3 = "#Foo\n# Desc\n## tag1\n";
let buf4 = "#Foo\n## tag1\n";
assert_eq!(
parse_meta(buf1.into()).unwrap().1,
Metadata::new("Title.", None::<String>)
);
assert_eq!(
parse_meta(buf2.into()).unwrap().1,
Metadata::new("Foo", Some("Desc"))
);
assert_eq!(
parse_meta(buf3.into()).unwrap().1,
Metadata::new("Foo", Some("Desc")).with_tags(vec![String::from("tag1")])
);
assert_eq!(
parse_meta(buf4.into()).unwrap().1,
Metadata::new("Foo", None::<String>).with_tags(vec![String::from("tag1")])
);
}
#[test]
fn test_meta_failure() {
let buf1 = "# Title.";
let buf2 = "// Foo Title\n# Desc\n";
let buf3 = "##Foo\n# Desc\n## tag1\n";
// Missing newline
match parse_meta(buf1.into()) {
Err(nom::Err::Error(nom::error::Error { input, .. })) => {
assert_eq!(input.location_offset(), 8);
assert_eq!(input.location_line(), 1);
}
err => panic!("Expected Err, found: {err:?}"),
}
// Invalid starting tag (// not #)
match parse_meta(buf2.into()) {
Err(nom::Err::Error(nom::error::Error { input, .. })) => {
assert_eq!(input.location_offset(), 0);
assert_eq!(input.location_line(), 1);
}
err => panic!("Expected Err, found: {err:?}"),
}
// Only taga, missing title
match parse_meta(buf3.into()) {
Err(nom::Err::Error(nom::error::Error { input, .. })) => {
assert_eq!(input.location_offset(), 1);
assert_eq!(input.location_line(), 1);
}
err => panic!("Expected Err, found: {err:?}"),
}
}
#[test]
fn test_client() {
let buf1 = "[Client] louise\n\tjid: louise@localhost\n\tpassword: password\n";
let buf2 = "[Client] louise's phone \n\tjid: louise2@localhost\n\tpassword: password\n\tcustom_port: 5234\n";
let buf3 = "[Client] louise\njid: louise@localhost\n\tpassword: password\n";
let name = String::from("louise");
let client = Client::new(Jid::from_str("louise@localhost").unwrap(), "password");
assert_eq!(
parse_client(buf1.into()),
Ok((
unsafe { LocatedSpan::new_from_raw_offset(59, 4, "", ()) },
(name.clone(), client.clone())
))
);
let name = String::from("louise's phone");
let client = Client::new(Jid::from_str("louise2@localhost").unwrap(), "password")
.with_custom_port(5234);
assert_eq!(
parse_client(buf2.into()),
Ok((
unsafe { LocatedSpan::new_from_raw_offset(88, 5, "", ()) },
(name.clone(), client.clone())
))
);
// Missing tab
match parse_client(buf3.into()) {
Err(_) => (),
err => panic!("Unexpected result: {:?}", err),
}
}
#[test]
fn test_clients() {
let buf1 = r#"
[Client] louise
jid: louise@localhost
password: password
[Client]
jid: sugako@localhost
password: password
"#;
let mut clients: HashMap<ClientName, Client> = HashMap::new();
clients.insert(
String::from("louise"),
Client::new(Jid::from_str("louise@localhost").unwrap(), "password"),
);
clients.insert(
String::from("須賀子"),
Client::new(Jid::from_str("sugako@localhost").unwrap(), "password"),
);
assert_eq!(
parse_clients(buf1.into()),
Ok((
unsafe { LocatedSpan::new_from_raw_offset(123, 9, "", ()) },
clients
))
);
}
#[test]
fn test_action_connect() {
let buf1 = "louise connects\n";
let action1 = Action::Connect(String::from("louise"));
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.into()).unwrap().1, action2);
}
#[test]
fn test_action_disconnect() {
let buf1 = "louise disconnects\n";
let action = Action::Disconnect(String::from("louise"));
assert_eq!(parse_action(buf1.into()).unwrap().1, action);
}
#[test]
fn test_action_send() {
let buf = r#"rosa sends:
<presence
type="unavailable"
/>
"#;
let xml = b"<presence\n\t\ttype=\"unavailable\"\t\n/>";
let send = Action::Send(
String::from("rosa"),
Element::from_reader_with_prefixes(&xml[..], String::from(DEFAULT_NS)).unwrap(),
);
assert_eq!(parse_action(buf.into()).unwrap().1, send);
}
#[test]
fn test_action_receive() {
let buf = r#"rosa receives:
<presence
type="unavailable"
/>
"#;
let xml = b"<presence\n\t\ttype=\"unavailable\"\t\n/>";
let receive = Action::Receive(
String::from("rosa"),
Element::from_reader_with_prefixes(&xml[..], String::from(DEFAULT_NS)).unwrap(),
);
assert_eq!(parse_action(buf.into()).unwrap().1, receive);
}
#[test]
fn test_actions_take_until() {
let buf = "Rosa receives:\n\t<presence/>\n\n\n# Comment\n\nPeter sends:\n\t<presence/>\n";
let xml = b"<presence/>";
let actions = vec![
Action::Receive(
String::from("Rosa"),
Element::from_reader_with_prefixes(&xml[..], String::from(DEFAULT_NS)).unwrap(),
),
Action::Send(
String::from("Peter"),
Element::from_reader_with_prefixes(&xml[..], String::from(DEFAULT_NS)).unwrap(),
),
];
assert_eq!(parse_actions(buf.into()).unwrap().1, actions);
}
#[test]
fn test_action_receive_none() {
let buf = "rosa receives: nothing\n";
let receive = Action::ReceiveNone(String::from("rosa"));
assert_eq!(parse_action(buf.into()).unwrap().1, receive);
}
#[test]
fn test_parse_spec() {
let buf = r#"# Test title
# Description Foo
## tag1
## tag2
[Client] louise
jid: louise@localhost
password: password
-----
louise connects
louise sends:
<presence to="some@room" />
louise receives:
<message from="louise@localhost"
/>
"#;
let metadata = Some(
Metadata::new("Test title", Some("Description Foo"))
.with_tags(vec![String::from("tag1"), String::from("tag2")]),
);
let mut clients: HashMap<ClientName, Client> = HashMap::new();
clients.insert(String::from("louise"), get_client("louise"));
let xml1 = b"<presence to=\"some@room\" />\n\n";
let xml2 = b"<message from=\"louise@localhost\"\n\t\t/>\n\n";
let actions = vec![
Action::Connect(String::from("louise")),
Action::Send(
String::from("louise"),
Element::from_reader_with_prefixes(&xml1[..], String::from(DEFAULT_NS)).unwrap(),
),
Action::Receive(
String::from("louise"),
Element::from_reader_with_prefixes(&xml2[..], String::from(DEFAULT_NS)).unwrap(),
),
];
let spec = Spec {
metadata,
clients,
actions,
};
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",
(),
)
}
})
);
}
}

628
src/parsers.rs Normal file
View file

@ -0,0 +1,628 @@
// Copyright (c) 2023-2099 Crate Authors
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
use crate::types::{Spec, Action, Client, ClientName, Metadata};
use std::collections::{BTreeMap, HashMap};
use std::str::FromStr;
use jid::Jid;
use minidom::Element;
use nom::{
self,
branch::alt,
bytes::complete::{tag, take_until, take_until1, take_while, take_while1},
character::complete::{digit1, multispace0, space0},
combinator::{map_res, opt, recognize},
error::{ErrorKind, ParseError},
multi::{many0, many1},
sequence::{delimited, tuple},
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: &str = "jabber:client";
pub static SCANSION_NS: &str = "https://matthewwild.co.uk/projects/scansion";
fn allspaces(s: Span) -> IResult<Span, Token> {
let (s, (pos, _, comments)) = tuple((position, multispace0, opt(comment)))(s)?;
let mut s = s;
#[allow(clippy::redundant_pattern_matching)]
if let Some(_) = comments {
let (j, _) = multispace0(s)?;
s = j;
}
Ok((s, Token { position: pos }))
}
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,
endmark: T,
) -> impl Fn(I) -> IResult<I, I, E>
where
I: InputTake + FindSubstring<T>,
T: InputLength + Clone,
List: Iterator<Item = T> + Clone,
{
move |i: I| {
let mut l = list.clone();
let endmark = endmark.clone();
let endindex = match i.find_substring(endmark) {
None | Some(0) => {
return Err(nom::Err::Error(E::from_error_kind(i, ErrorKind::TakeUntil)))
}
Some(index) => index,
};
let res: Option<usize> = loop {
if let Some(item) = l.next() {
match i.find_substring(item) {
None | Some(0) => continue,
Some(index) if index >= endindex => continue,
valid => break valid,
}
} else {
break None;
}
};
let res: IResult<_, _, E> = match res {
None => Err(nom::Err::Error(E::from_error_kind(i, ErrorKind::TakeUntil))),
Some(index) => Ok(i.take_split(index)),
};
res
}
}
fn parse_meta_tags(s: Span) -> IResult<Span, Vec<String>> {
let (s, tags) = many0(map_res(
delimited(tuple((position, tag("##"))), take_until("\n"), tag("\n")),
|tag: Span| Ok::<String, ()>(tag.trim().to_string()),
))(s)?;
Ok((s, tags))
}
fn parse_meta(s: Span) -> IResult<Span, Metadata> {
let (s, (_pos, title)) = tuple((
position,
delimited(tag("#"), take_while1(|c| c != '#' && c != '\n'), tag("\n")),
))(s)?;
let (s, optdesc) = opt(tuple((
position,
delimited(tag("#"), take_while1(|c| c != '#' && c != '\n'), tag("\n")),
)))(s)?;
let description = optdesc.map(|(_pos, d)| String::from(d.trim()));
let (s, tags) = parse_meta_tags(s)?;
let meta = Metadata::new(title.trim(), description).with_tags(tags);
Ok((s, meta))
}
fn parse_client(s: Span) -> IResult<Span, (ClientName, Client)> {
let (s, (_, _, _, name, _)) =
tuple((allspaces, tag("[Client]"), space0, take_until("\n"), space0))(s)?;
let (s, lines) = many1(tuple((
tag("\n"),
take_while1(|c| c == '\t'),
take_until("\n"),
)))(s)?;
let name = name.trim();
let mut jid: Option<Jid> = None;
let mut password: Option<&str> = None;
let mut custom_host: Option<&str> = None;
let mut custom_port: Option<u16> = None;
for line in lines {
let (_, _, attr) = line;
if let Some((key, val)) = attr.split_once(':') {
let val = val.trim();
match key.trim() {
"jid" => jid = Some(Jid::from_str(val).unwrap()),
"password" => password = Some(val),
"custom_host" => custom_host = Some(val),
"custom_port" => {
let val: Span = val.into();
let (j, digits): (Span, Span) = digit1(val)?;
let val = digits.parse::<u16>().or(Err(nom::Err::Error(
nom::error::Error::from_error_kind(j, ErrorKind::Digit),
)))?;
custom_port = Some(val);
}
_ => (),
}
} else {
return Err(nom::Err::Error(nom::error::Error::from_error_kind(
s,
ErrorKind::Tag,
)));
}
}
if jid.is_none() || password.is_none() {
return Err(nom::Err::Error(nom::error::Error::from_error_kind(
s,
ErrorKind::Tag,
)));
}
// Skip comments and empty newlines
let (s, _) = allspaces(s)?;
let mut client = Client::new(jid.unwrap(), password.unwrap());
client.custom_host = custom_host.map(String::from);
client.custom_port = custom_port;
Ok((s, (String::from(name), client)))
}
fn parse_clients(s: Span) -> IResult<Span, HashMap<ClientName, Client>> {
let (s, clients) = many0(parse_client)(s)?;
let mut map: HashMap<ClientName, Client> = HashMap::new();
for (name, client) in clients {
map.insert(name, client);
}
Ok((s, map))
}
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(s: Span) -> IResult<Span, Action> {
let (s, name) = take_until_tags(
vec![
"disconnects",
"connects",
"receives: nothing",
"sends:",
"receives:",
]
.into_iter(),
"\n",
)(s)?;
let (s, (tagname, _, _)) = tuple((
alt((
tag("connects"),
tag("disconnects"),
tag("receives: nothing"),
tag("sends:"),
tag("receives:"),
)),
space0,
tag("\n"),
))(s)?;
let name = String::from(name.trim());
let (s, action) = match *tagname.fragment() {
"connects" => (s, Action::Connect(name)),
"disconnects" => (s, Action::Disconnect(name)),
"receives: nothing" => (s, Action::ReceiveNone(name)),
tagname @ "sends:" | tagname @ "receives:" => parse_send_receive(tagname, name, s)?,
_ => unreachable!(),
};
Ok((s, action))
}
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, 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)),
))(s)?;
let lines = lines.trim();
// Namespaces
let mut prefixes = BTreeMap::new();
prefixes.insert(None, String::from(DEFAULT_NS));
prefixes.insert(Some(String::from("scansion")), String::from(SCANSION_NS));
let elem: Element = Element::from_reader_with_prefixes(lines.as_bytes(), prefixes).unwrap();
Ok(match tagname {
"sends:" => (s, Action::Send(name, elem)),
"receives:" => (s, Action::Receive(name, elem)),
_ => unreachable!(),
})
}
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, Token> {
let s: Span = i.into();
let (s, metadata) = opt(parse_meta)(s)?;
let (s, clients) = parse_clients(s)?;
let (s, _) = parse_sep(s)?;
let (_, actions) = parse_actions(s)?;
Ok(Spec {
metadata,
clients,
actions,
})
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
fn get_client(name: &str) -> Client {
Client::new(
Jid::from_str(format!("{}@localhost", name).as_str()).unwrap(),
"password",
)
}
#[test]
fn test_comment() {
let buf1 = "#\n";
let buf2 = "# Foo\n";
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.into()) {
Err(_) => (),
err => panic!("Unexpected result: {:?}", err),
}
let buf4 = "// Foo\n";
assert_eq!(
comment(buf4.into()),
Ok((
unsafe { LocatedSpan::new_from_raw_offset(7, 2, "", ()) },
Token {
position: LocatedSpan::new("")
}
))
);
}
#[test]
fn test_meta_success() {
let buf1 = "# Title.\n";
let buf2 = "#Foo\n# Desc\n";
let buf3 = "#Foo\n# Desc\n## tag1\n";
let buf4 = "#Foo\n## tag1\n";
assert_eq!(
parse_meta(buf1.into()).unwrap().1,
Metadata::new("Title.", None::<String>)
);
assert_eq!(
parse_meta(buf2.into()).unwrap().1,
Metadata::new("Foo", Some("Desc"))
);
assert_eq!(
parse_meta(buf3.into()).unwrap().1,
Metadata::new("Foo", Some("Desc")).with_tags(vec![String::from("tag1")])
);
assert_eq!(
parse_meta(buf4.into()).unwrap().1,
Metadata::new("Foo", None::<String>).with_tags(vec![String::from("tag1")])
);
}
#[test]
fn test_meta_failure() {
let buf1 = "# Title.";
let buf2 = "// Foo Title\n# Desc\n";
let buf3 = "##Foo\n# Desc\n## tag1\n";
// Missing newline
match parse_meta(buf1.into()) {
Err(nom::Err::Error(nom::error::Error { input, .. })) => {
assert_eq!(input.location_offset(), 8);
assert_eq!(input.location_line(), 1);
}
err => panic!("Expected Err, found: {err:?}"),
}
// Invalid starting tag (// not #)
match parse_meta(buf2.into()) {
Err(nom::Err::Error(nom::error::Error { input, .. })) => {
assert_eq!(input.location_offset(), 0);
assert_eq!(input.location_line(), 1);
}
err => panic!("Expected Err, found: {err:?}"),
}
// Only taga, missing title
match parse_meta(buf3.into()) {
Err(nom::Err::Error(nom::error::Error { input, .. })) => {
assert_eq!(input.location_offset(), 1);
assert_eq!(input.location_line(), 1);
}
err => panic!("Expected Err, found: {err:?}"),
}
}
#[test]
fn test_client() {
let buf1 = "[Client] louise\n\tjid: louise@localhost\n\tpassword: password\n";
let buf2 = "[Client] louise's phone \n\tjid: louise2@localhost\n\tpassword: password\n\tcustom_port: 5234\n";
let buf3 = "[Client] louise\njid: louise@localhost\n\tpassword: password\n";
let name = String::from("louise");
let client = Client::new(Jid::from_str("louise@localhost").unwrap(), "password");
assert_eq!(
parse_client(buf1.into()),
Ok((
unsafe { LocatedSpan::new_from_raw_offset(59, 4, "", ()) },
(name.clone(), client.clone())
))
);
let name = String::from("louise's phone");
let client = Client::new(Jid::from_str("louise2@localhost").unwrap(), "password")
.with_custom_port(5234);
assert_eq!(
parse_client(buf2.into()),
Ok((
unsafe { LocatedSpan::new_from_raw_offset(88, 5, "", ()) },
(name.clone(), client.clone())
))
);
// Missing tab
match parse_client(buf3.into()) {
Err(_) => (),
err => panic!("Unexpected result: {:?}", err),
}
}
#[test]
fn test_clients() {
let buf1 = r#"
[Client] louise
jid: louise@localhost
password: password
[Client]
jid: sugako@localhost
password: password
"#;
let mut clients: HashMap<ClientName, Client> = HashMap::new();
clients.insert(
String::from("louise"),
Client::new(Jid::from_str("louise@localhost").unwrap(), "password"),
);
clients.insert(
String::from("須賀子"),
Client::new(Jid::from_str("sugako@localhost").unwrap(), "password"),
);
assert_eq!(
parse_clients(buf1.into()),
Ok((
unsafe { LocatedSpan::new_from_raw_offset(123, 9, "", ()) },
clients
))
);
}
#[test]
fn test_action_connect() {
let buf1 = "louise connects\n";
let action1 = Action::Connect(String::from("louise"));
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.into()).unwrap().1, action2);
}
#[test]
fn test_action_disconnect() {
let buf1 = "louise disconnects\n";
let action = Action::Disconnect(String::from("louise"));
assert_eq!(parse_action(buf1.into()).unwrap().1, action);
}
#[test]
fn test_action_send() {
let buf = r#"rosa sends:
<presence
type="unavailable"
/>
"#;
let xml = b"<presence\n\t\ttype=\"unavailable\"\t\n/>";
let send = Action::Send(
String::from("rosa"),
Element::from_reader_with_prefixes(&xml[..], String::from(DEFAULT_NS)).unwrap(),
);
assert_eq!(parse_action(buf.into()).unwrap().1, send);
}
#[test]
fn test_action_receive() {
let buf = r#"rosa receives:
<presence
type="unavailable"
/>
"#;
let xml = b"<presence\n\t\ttype=\"unavailable\"\t\n/>";
let receive = Action::Receive(
String::from("rosa"),
Element::from_reader_with_prefixes(&xml[..], String::from(DEFAULT_NS)).unwrap(),
);
assert_eq!(parse_action(buf.into()).unwrap().1, receive);
}
#[test]
fn test_actions_take_until() {
let buf = "Rosa receives:\n\t<presence/>\n\n\n# Comment\n\nPeter sends:\n\t<presence/>\n";
let xml = b"<presence/>";
let actions = vec![
Action::Receive(
String::from("Rosa"),
Element::from_reader_with_prefixes(&xml[..], String::from(DEFAULT_NS)).unwrap(),
),
Action::Send(
String::from("Peter"),
Element::from_reader_with_prefixes(&xml[..], String::from(DEFAULT_NS)).unwrap(),
),
];
assert_eq!(parse_actions(buf.into()).unwrap().1, actions);
}
#[test]
fn test_action_receive_none() {
let buf = "rosa receives: nothing\n";
let receive = Action::ReceiveNone(String::from("rosa"));
assert_eq!(parse_action(buf.into()).unwrap().1, receive);
}
#[test]
fn test_parse_spec() {
let buf = r#"# Test title
# Description Foo
## tag1
## tag2
[Client] louise
jid: louise@localhost
password: password
-----
louise connects
louise sends:
<presence to="some@room" />
louise receives:
<message from="louise@localhost"
/>
"#;
let metadata = Some(
Metadata::new("Test title", Some("Description Foo"))
.with_tags(vec![String::from("tag1"), String::from("tag2")]),
);
let mut clients: HashMap<ClientName, Client> = HashMap::new();
clients.insert(String::from("louise"), get_client("louise"));
let xml1 = b"<presence to=\"some@room\" />\n\n";
let xml2 = b"<message from=\"louise@localhost\"\n\t\t/>\n\n";
let actions = vec![
Action::Connect(String::from("louise")),
Action::Send(
String::from("louise"),
Element::from_reader_with_prefixes(&xml1[..], String::from(DEFAULT_NS)).unwrap(),
),
Action::Receive(
String::from("louise"),
Element::from_reader_with_prefixes(&xml2[..], String::from(DEFAULT_NS)).unwrap(),
),
];
let spec = Spec {
metadata,
clients,
actions,
};
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",
(),
)
}
})
);
}
}

78
src/types.rs Normal file
View file

@ -0,0 +1,78 @@
// Copyright (c) 2023-2099 Crate Authors
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
use std::collections::HashMap;
use jid::Jid;
use minidom::Element;
#[derive(Debug, Clone, PartialEq)]
pub struct Metadata {
pub title: String,
pub description: Option<String>,
pub tags: Vec<String>,
}
impl Metadata {
pub fn new<S: Into<String>, T: Into<String>>(title: S, desc: Option<T>) -> Metadata {
Metadata {
title: title.into(),
description: desc.map(|t| t.into()),
tags: vec![],
}
}
pub fn with_tags(mut self, tags: Vec<String>) -> Metadata {
self.tags = tags;
self
}
}
pub type ClientName = String;
#[derive(Debug, Clone, PartialEq)]
pub struct Client {
pub jid: Jid,
pub password: String,
pub custom_host: Option<String>,
pub custom_port: Option<u16>,
}
impl Client {
pub fn new<S: Into<String>>(jid: Jid, password: S) -> Client {
Client {
jid,
password: password.into(),
custom_host: None,
custom_port: None,
}
}
pub fn with_custom_host<S: Into<String>>(mut self, custom_host: S) -> Client {
self.custom_host = Some(custom_host.into());
self
}
pub fn with_custom_port(mut self, custom_port: u16) -> Client {
self.custom_port = Some(custom_port);
self
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Action {
Connect(ClientName),
Send(ClientName, Element),
Receive(ClientName, Element),
ReceiveNone(ClientName),
Disconnect(ClientName),
}
#[derive(Debug, Clone, PartialEq)]
pub struct Spec {
pub metadata: Option<Metadata>,
pub clients: HashMap<ClientName, Client>,
pub actions: Vec<Action>,
}