Split into modules
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
This commit is contained in:
parent
7314127763
commit
309be31d45
3 changed files with 710 additions and 688 deletions
692
src/lib.rs
692
src/lib.rs
|
@ -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
628
src/parsers.rs
Normal 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
78
src/types.rs
Normal 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>,
|
||||||
|
}
|
Loading…
Reference in a new issue