From 9e4b09d96d4e310f0202ce6f7b401c29e7e9ac20 Mon Sep 17 00:00:00 2001 From: Laurent Pelecq Date: Sun, 27 Mar 2022 18:51:48 +0200 Subject: [PATCH] Implement history get client list. --- src/client.rs | 122 +++++++++++++++++++++------------------ src/constants.rs | 2 +- src/protocol.rs | 21 ++++--- src/types.rs | 108 +++++++++++++++++++++++++++++++++- tests/fifo_sync_tests.rs | 26 +++++++++ 5 files changed, 211 insertions(+), 68 deletions(-) diff --git a/src/client.rs b/src/client.rs index d7c9945..59c10e3 100644 --- a/src/client.rs +++ b/src/client.rs @@ -11,7 +11,7 @@ use std::io::{self, Read, Write}; use crate::constants::*; use crate::protocol::{ - flush_lines, parse_event_id, parse_single_value, parse_synthesis_voices, write_lines, + flush_lines, parse_event_id, parse_single_value, parse_typed_lines, write_lines, }; use crate::types::*; @@ -96,57 +96,57 @@ pub enum Request { #[derive(Debug)] /// Response from SSIP server. pub enum Response { - LanguageSet, // 201 - PrioritySet, // 202 - RateSet, // 203 - PitchSet, // 204 - PunctuationSet, // 205 - CapLetRecognSet, // 206 - SpellingSet, // 207 - ClientNameSet, // 208 - VoiceSet, // 209 - Stopped, // 210 - Paused, // 211 - Resumed, // 212 - Canceled, // 213 - TableSet, // 215 - OutputModuleSet, // 216 - PauseContextSet, // 217 - VolumeSet, // 218 - SsmlModeSet, // 219 - NotificationSet, // 220 - PitchRangeSet, // 263 - DebugSet, // 262 - HistoryCurSetFirst, // 220 - HistoryCurSetLast, // 221 - HistoryCurSetPos, // 222 - HistoryCurMoveFor, // 223 - HistoryCurMoveBack, // 224 - MessageQueued, // 225, - SoundIconQueued, // 226 - MessageCanceled, // 227 - ReceivingData, // 230 - Bye, // 231 - HistoryClientListSent(Vec), // 240 - HistoryMsgsListSent(Vec), // 241 - HistoryLastMsg(String), // 242 - HistoryCurPosRet(String), // 243 - TableListSent(Vec), // 244 - HistoryClientIdSent(String), // 245 - MessageTextSent, // 246 - HelpSent(Vec), // 248 - VoicesListSent(Vec), // 249 - OutputModulesListSent(Vec), // 250 - Get(String), // 251 - InsideBlock, // 260 - OutsideBlock, // 261 - NotImplemented, // 299 - EventIndexMark(EventId, String), // 700 - EventBegin(EventId), // 701 - EventEnd(EventId), // 702 - EventCanceled(EventId), // 703 - EventPaused(EventId), // 704 - EventResumed(EventId), // 705 + LanguageSet, // 201 + PrioritySet, // 202 + RateSet, // 203 + PitchSet, // 204 + PunctuationSet, // 205 + CapLetRecognSet, // 206 + SpellingSet, // 207 + ClientNameSet, // 208 + VoiceSet, // 209 + Stopped, // 210 + Paused, // 211 + Resumed, // 212 + Canceled, // 213 + TableSet, // 215 + OutputModuleSet, // 216 + PauseContextSet, // 217 + VolumeSet, // 218 + SsmlModeSet, // 219 + NotificationSet, // 220 + PitchRangeSet, // 263 + DebugSet, // 262 + HistoryCurSetFirst, // 220 + HistoryCurSetLast, // 221 + HistoryCurSetPos, // 222 + HistoryCurMoveFor, // 223 + HistoryCurMoveBack, // 224 + MessageQueued, // 225, + SoundIconQueued, // 226 + MessageCanceled, // 227 + ReceivingData, // 230 + Bye, // 231 + HistoryClientListSent(Vec), // 240 + HistoryMsgsListSent(Vec), // 241 + HistoryLastMsg(String), // 242 + HistoryCurPosRet(String), // 243 + TableListSent(Vec), // 244 + HistoryClientIdSent(String), // 245 + MessageTextSent, // 246 + HelpSent(Vec), // 248 + VoicesListSent(Vec), // 249 + OutputModulesListSent(Vec), // 250 + Get(String), // 251 + InsideBlock, // 260 + OutsideBlock, // 261 + NotImplemented, // 299 + EventIndexMark(EventId, String), // 700 + EventBegin(EventId), // 701 + EventEnd(EventId), // 702 + EventCanceled(EventId), // 703 + EventPaused(EventId), // 704 + EventResumed(EventId), // 705 } macro_rules! send_one_line { @@ -297,7 +297,7 @@ impl Client { } Request::Begin => send_one_line!(self, "BLOCK BEGIN"), Request::End => send_one_line!(self, "BLOCK END"), - Request::HistoryGetClients => panic!("not implemented"), + Request::HistoryGetClients => send_one_line!(self, "HISTORY GET CLIENT_LIST"), Request::HistoryGetClientId => panic!("not implemented"), Request::HistoryGetClientMsgs(_scope, _start, _number) => panic!("not implemented"), Request::HistoryGetLastMsgId => panic!("not implemented"), @@ -646,7 +646,9 @@ impl Client { OK_MSG_CANCELED => Ok(Response::MessageCanceled), OK_RECEIVING_DATA => Ok(Response::ReceivingData), OK_BYE => Ok(Response::Bye), - OK_CLIENT_LIST_SENT => Ok(Response::HistoryClientListSent(lines)), + OK_CLIENTS_LIST_SENT => Ok(Response::HistoryClientListSent(parse_typed_lines::< + HistoryClientStatus, + >(&lines)?)), OK_MSGS_LIST_SENT => Ok(Response::HistoryMsgsListSent(lines)), OK_LAST_MSG => Ok(Response::HistoryLastMsg(parse_single_value(&lines)?)), OK_CUR_POS_RET => Ok(Response::HistoryCurPosRet(parse_single_value(&lines)?)), @@ -654,7 +656,9 @@ impl Client { OK_CLIENT_ID_SENT => Ok(Response::HistoryClientIdSent(parse_single_value(&lines)?)), OK_MSG_TEXT_SENT => Ok(Response::MessageTextSent), OK_HELP_SENT => Ok(Response::HelpSent(lines)), - OK_VOICES_LIST_SENT => Ok(Response::VoicesListSent(parse_synthesis_voices(&lines)?)), + OK_VOICES_LIST_SENT => Ok(Response::VoicesListSent( + parse_typed_lines::(&lines)?, + )), OK_OUTPUT_MODULES_LIST_SENT => Ok(Response::OutputModulesListSent(lines)), OK_GET => Ok(Response::Get(parse_single_value(&lines)?)), OK_INSIDE_BLOCK => Ok(Response::InsideBlock), @@ -726,7 +730,7 @@ impl Client { /// Receive a list of synthesis voices pub fn receive_synthesis_voices(&mut self) -> ClientResult> { self.receive_lines(OK_VOICES_LIST_SENT) - .and_then(|lines| parse_synthesis_voices(&lines)) + .and_then(|lines| parse_typed_lines::(&lines)) } /// Receive a notification @@ -758,6 +762,12 @@ impl Client { }) } + /// Receive a list of client status from history. + pub fn receive_history_clients(&mut self) -> ClientResult> { + self.receive_lines(OK_CLIENTS_LIST_SENT) + .and_then(|lines| parse_typed_lines::(&lines)) + } + /// Check the result of `set_client_name`. pub fn check_client_name_set(&mut self) -> ClientResult<&mut Self> { self.check_status(OK_CLIENT_NAME_SET) diff --git a/src/constants.rs b/src/constants.rs index 4c24f18..1f68ac8 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -97,7 +97,7 @@ pub const OK_RECEIVING_DATA: ReturnCode = 230; pub const OK_BYE: ReturnCode = 231; /// Successful completion: OK CLIENTS LIST SENT -pub const OK_CLIENT_LIST_SENT: ReturnCode = 240; +pub const OK_CLIENTS_LIST_SENT: ReturnCode = 240; /// Successful completion: OK MSGS LIST SENT pub const OK_MSGS_LIST_SENT: ReturnCode = 241; diff --git a/src/protocol.rs b/src/protocol.rs index f1f7cc1..920486b 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -11,7 +11,7 @@ use log::debug; use std::io::{self, BufRead, Write}; use std::str::FromStr; -use crate::types::{ClientError, ClientResult, ClientStatus, EventId, StatusLine, SynthesisVoice}; +use crate::types::{ClientError, ClientResult, ClientStatus, EventId, StatusLine}; macro_rules! invalid_input { ($msg:expr) => { @@ -40,13 +40,14 @@ pub(crate) fn parse_event_id(lines: &[String]) -> ClientResult { } } -pub(crate) fn parse_synthesis_voices(lines: &[String]) -> ClientResult> { - let mut voices = Vec::new(); - for name in lines.iter() { - let voice = SynthesisVoice::from_str(name.as_str())?; - voices.push(voice); - } - Ok(voices) +pub(crate) fn parse_typed_lines(lines: &[String]) -> ClientResult> +where + T: FromStr, +{ + lines + .iter() + .map(|line| T::from_str(line.as_str()).map_err(|err| ClientError::from(err))) + .collect::>>() } /// Write lines separated by CRLF. @@ -120,6 +121,8 @@ mod tests { use super::{receive_answer, ClientError, ClientResult}; + use crate::types::SynthesisVoice; + #[test] fn single_ok_status_line() { let mut input = BufReader::new("208 OK CLIENT NAME SET\r\n".as_bytes()); @@ -225,7 +228,7 @@ mod tests { .iter() .map(|s| s.to_string()) .collect::>(); - let voices = super::parse_synthesis_voices(&lines)?; + let voices = super::parse_typed_lines::(&lines)?; assert_eq!(3, voices.len()); assert_eq!("en", voices[0].name.as_str()); assert_eq!(Some(String::from("af")), voices[1].language); diff --git a/src/types.rs b/src/types.rs index a7944f0..fb51fc7 100644 --- a/src/types.rs +++ b/src/types.rs @@ -21,7 +21,7 @@ pub type ReturnCode = u16; pub type MessageId = u32; /// Client identifier -pub type ClientId = String; +pub type ClientId = u32; /// Message identifiers #[derive(Debug, Clone)] @@ -522,12 +522,72 @@ impl fmt::Display for HistoryPosition { } } +/// History client status +#[derive(Debug, PartialEq)] +pub struct HistoryClientStatus { + pub id: ClientId, + pub name: String, + pub connected: bool, +} + +impl HistoryClientStatus { + pub fn new(id: ClientId, name: &str, connected: bool) -> Self { + Self { + id, + name: name.to_string(), + connected, + } + } + + fn invalid_data(msg: &str) -> io::Error { + io::Error::new(io::ErrorKind::InvalidData, msg) + } + + fn unexpected_eof(msg: &str) -> io::Error { + io::Error::new(io::ErrorKind::UnexpectedEof, msg) + } +} + +impl FromStr for HistoryClientStatus { + type Err = io::Error; + + fn from_str(s: &str) -> Result { + let mut iter = s.splitn(3, ' '); + match iter.next() { + Some("") => Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "expecting client id", + )), + Some(client_id) => match client_id.parse::() { + Ok(id) => match iter.next() { + Some(name) => match iter.next() { + Some(status) if status == "0" => { + Ok(HistoryClientStatus::new(id, name, false)) + } + Some(status) if status == "1" => { + Ok(HistoryClientStatus::new(id, name, true)) + } + Some(_) => Err(HistoryClientStatus::invalid_data("invalid client status")), + None => Err(HistoryClientStatus::unexpected_eof( + "expecting client status", + )), + }, + None => Err(HistoryClientStatus::unexpected_eof("expecting client name")), + }, + Err(_) => Err(HistoryClientStatus::invalid_data("invalid client id")), + }, + None => Err(HistoryClientStatus::unexpected_eof("expecting client id")), + } + } +} + #[cfg(test)] mod tests { + use std::io; use std::str::FromStr; - use super::{MessageScope, SynthesisVoice}; + use super::{HistoryClientStatus, HistoryPosition, MessageScope, SynthesisVoice}; #[test] fn parse_synthesis_voice() { @@ -551,4 +611,48 @@ mod tests { assert_eq!("all", format!("{}", MessageScope::All).as_str()); assert_eq!("123", format!("{}", MessageScope::Message(123)).as_str()); } + + #[test] + fn format_history_position() { + assert_eq!("first", format!("{}", HistoryPosition::First).as_str()); + assert_eq!("last", format!("{}", HistoryPosition::Last).as_str()); + assert_eq!("pos 15", format!("{}", HistoryPosition::Pos(15)).as_str()); + } + + #[test] + fn parse_history_client_status() { + assert_eq!( + HistoryClientStatus::new(10, "joe:speechd_client:main", false), + HistoryClientStatus::from_str("10 joe:speechd_client:main 0").unwrap() + ); + assert_eq!( + HistoryClientStatus::new(11, "joe:speechd_client:main", true), + HistoryClientStatus::from_str("11 joe:speechd_client:main 1").unwrap() + ); + for line in &[ + "9 joe:speechd_client:main xxx", + "xxx joe:speechd_client:main 1", + ] { + match HistoryClientStatus::from_str(line) { + Ok(_) => panic!("parsing should have failed"), + Err(err) => assert_eq!( + io::ErrorKind::InvalidData, + err.kind(), + "expecting error 'invalid data' parsing \"{}\"", + line + ), + } + } + for line in &["8 joe:speechd_client:main", "8", ""] { + match HistoryClientStatus::from_str(line) { + Ok(_) => panic!("parsing should have failed"), + Err(err) => assert_eq!( + io::ErrorKind::UnexpectedEof, + err.kind(), + "expecting error 'unexpected EOF' parsing \"{}\"", + line + ), + } + } + } } diff --git a/tests/fifo_sync_tests.rs b/tests/fifo_sync_tests.rs index 7e7df14..0aab359 100644 --- a/tests/fifo_sync_tests.rs +++ b/tests/fifo_sync_tests.rs @@ -396,3 +396,29 @@ fn receive_notification() -> ClientResult<()> { }, ) } + +#[test] +#[cfg(not(feature = "async-mio"))] +fn history_clients_list() -> ClientResult<()> { + test_client( + &[ + SET_CLIENT_COMMUNICATION, + ( + "HISTORY GET CLIENT_LIST\r\n", + "240-0 joe:speechd_client:main 0\r\n240-1 joe:speechd_client:status 0\r\n240-2 unknown:unknown:unknown 1\r\n240 OK CLIENTS LIST SENT\r\n" + ), + ], + |client| { + let statuses = client.history_get_clients().unwrap().receive_history_clients().unwrap(); + let expected_statuses: [HistoryClientStatus; 3] = [ HistoryClientStatus::new(0, "joe:speechd_client:main", false), + HistoryClientStatus::new(1, "joe:speechd_client:status", false), + HistoryClientStatus::new(2, "unknown:unknown:unknown", true), + ]; + assert_eq!(expected_statuses.len(), statuses.len()); + for (expected, found) in expected_statuses.iter().zip(statuses.iter()) { + assert_eq!(*expected, *found); + } + Ok(()) + }, + ) +}