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
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::str::FromStr;
|
||||
mod parsers;
|
||||
mod types;
|
||||
|
||||
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";
|
||||
|
||||
#[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",
|
||||
(),
|
||||
)
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
pub use parsers::parse_spec;
|
||||
pub use types::{Spec, Action, Client, Metadata};
|
||||
|
|
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