diff --git a/src/lib.rs b/src/lib.rs index 09e4fb3..7b3d579 100644 --- a/src/lib.rs +++ b/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>>> for Token<'a> { - fn from(err: NomErr>>) -> 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, - pub tags: Vec, -} - -impl Metadata { - pub fn new, T: Into>(title: S, desc: Option) -> Metadata { - Metadata { - title: title.into(), - description: desc.map(|t| t.into()), - tags: vec![], - } - } - - pub fn with_tags(mut self, tags: Vec) -> 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, - pub custom_port: Option, -} - -impl Client { - pub fn new>(jid: Jid, password: S) -> Client { - Client { - jid, - password: password.into(), - custom_host: None, - custom_port: None, - } - } - - pub fn with_custom_host>(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, - pub clients: HashMap, - pub actions: Vec, -} - -fn allspaces(s: Span) -> IResult { - 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 { - 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, List>( - list: List, - endmark: T, -) -> impl Fn(I) -> IResult -where - I: InputTake + FindSubstring, - T: InputLength + Clone, - List: Iterator + 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 = 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> { - let (s, tags) = many0(map_res( - delimited(tuple((position, tag("##"))), take_until("\n"), tag("\n")), - |tag: Span| Ok::(tag.trim().to_string()), - ))(s)?; - Ok((s, tags)) -} - -fn parse_meta(s: Span) -> IResult { - 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 { - 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 = None; - let mut password: Option<&str> = None; - let mut custom_host: Option<&str> = None; - let mut custom_port: Option = 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::().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> { - let (s, clients) = many0(parse_client)(s)?; - let mut map: HashMap = HashMap::new(); - for (name, client) in clients { - map.insert(name, client); - } - Ok((s, map)) -} - -fn parse_sep(s: Span) -> IResult { - let (s, (pos, _)) = delimited(allspaces, tuple((position, many1(tag("-")))), allspaces)(s)?; - Ok((s, Token { position: pos })) -} - -fn parse_action(s: Span) -> IResult { - 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 { - 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, 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> { - let (s, actions) = many1(delimited(allspaces, parse_action, allspaces))(s)?; - Ok((s, actions)) -} - -pub fn parse_spec(i: &str) -> Result { - 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::) - ); - - 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::).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 = 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: - - - -"#; - let xml = b""; - - 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: - - - -"#; - let xml = b""; - - 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\n\n\n# Comment\n\nPeter sends:\n\t\n"; - let xml = b""; - 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: - - -louise receives: - - -"#; - - let metadata = Some( - Metadata::new("Test title", Some("Description Foo")) - .with_tags(vec![String::from("tag1"), String::from("tag2")]), - ); - - let mut clients: HashMap = HashMap::new(); - clients.insert(String::from("louise"), get_client("louise")); - - let xml1 = b"\n\n"; - let xml2 = b"\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}; diff --git a/src/parsers.rs b/src/parsers.rs new file mode 100644 index 0000000..5a3821b --- /dev/null +++ b/src/parsers.rs @@ -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>>> for Token<'a> { + fn from(err: NomErr>>) -> 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 { + 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 { + 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, List>( + list: List, + endmark: T, +) -> impl Fn(I) -> IResult +where + I: InputTake + FindSubstring, + T: InputLength + Clone, + List: Iterator + 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 = 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> { + let (s, tags) = many0(map_res( + delimited(tuple((position, tag("##"))), take_until("\n"), tag("\n")), + |tag: Span| Ok::(tag.trim().to_string()), + ))(s)?; + Ok((s, tags)) +} + +fn parse_meta(s: Span) -> IResult { + 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 { + 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 = None; + let mut password: Option<&str> = None; + let mut custom_host: Option<&str> = None; + let mut custom_port: Option = 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::().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> { + let (s, clients) = many0(parse_client)(s)?; + let mut map: HashMap = HashMap::new(); + for (name, client) in clients { + map.insert(name, client); + } + Ok((s, map)) +} + +fn parse_sep(s: Span) -> IResult { + let (s, (pos, _)) = delimited(allspaces, tuple((position, many1(tag("-")))), allspaces)(s)?; + Ok((s, Token { position: pos })) +} + +fn parse_action(s: Span) -> IResult { + 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 { + 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, 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> { + let (s, actions) = many1(delimited(allspaces, parse_action, allspaces))(s)?; + Ok((s, actions)) +} + +pub fn parse_spec(i: &str) -> Result { + 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::) + ); + + 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::).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 = 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: + + + +"#; + let xml = b""; + + 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: + + + +"#; + let xml = b""; + + 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\n\n\n# Comment\n\nPeter sends:\n\t\n"; + let xml = b""; + 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: + + +louise receives: + + +"#; + + let metadata = Some( + Metadata::new("Test title", Some("Description Foo")) + .with_tags(vec![String::from("tag1"), String::from("tag2")]), + ); + + let mut clients: HashMap = HashMap::new(); + clients.insert(String::from("louise"), get_client("louise")); + + let xml1 = b"\n\n"; + let xml2 = b"\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", + (), + ) + } + }) + ); + } +} diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..0bf8e0f --- /dev/null +++ b/src/types.rs @@ -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, + pub tags: Vec, +} + +impl Metadata { + pub fn new, T: Into>(title: S, desc: Option) -> Metadata { + Metadata { + title: title.into(), + description: desc.map(|t| t.into()), + tags: vec![], + } + } + + pub fn with_tags(mut self, tags: Vec) -> 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, + pub custom_port: Option, +} + +impl Client { + pub fn new>(jid: Jid, password: S) -> Client { + Client { + jid, + password: password.into(), + custom_host: None, + custom_port: None, + } + } + + pub fn with_custom_host>(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, + pub clients: HashMap, + pub actions: Vec, +}