diff --git a/Cargo.toml b/Cargo.toml index 3c2098c..7723dbe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ssip-client" -version = "0.4.1" +version = "0.5.0" authors = ["Laurent Pelecq "] edition = "2018" description = "Client API for Speech Dispatcher" diff --git a/src/async_mio.rs b/src/async_mio.rs index cf6f2ab..35c00dd 100644 --- a/src/async_mio.rs +++ b/src/async_mio.rs @@ -12,128 +12,18 @@ use std::collections::VecDeque; use std::io::{self, Read, Write}; use crate::{ - client::{Client, ClientError, ClientName, ClientResult}, - constants::*, + client::{Client, Request, Response}, types::*, }; -#[derive(Debug, Clone)] -/// Request for SSIP server. -pub enum Request { - SetName(ClientName), - // Speech related requests - Speak, - SendLine(String), - SendLines(Vec), - SendChar(char), - // Flow control - Stop(MessageScope), - Cancel(MessageScope), - Pause(MessageScope), - Resume(MessageScope), - // Setter and getter - SetPriority(Priority), - SetDebug(bool), - SetOutputModule(ClientScope, String), - GetOutputModule, - ListOutputModule, - SetLanguage(ClientScope, String), - GetLanguage, - SetSsmlMode(bool), - SetPunctuationMode(ClientScope, PunctuationMode), - SetSpelling(ClientScope, bool), - SetCapitalLettersRecognitionMode(ClientScope, CapitalLettersRecognitionMode), - SetVoiceType(ClientScope, String), - GetVoiceType, - ListVoiceTypes, - SetSynthesisVoice(ClientScope, String), - ListSynthesisVoices, - SetRate(ClientScope, i8), - GetRate, - SetPitch(ClientScope, i8), - GetPitch, - SetVolume(ClientScope, i8), - GetVolume, - SetPauseContext(ClientScope, u8), - SetHistory(ClientScope, bool), - Begin, - End, - Quit, - EnableNotification(NotificationType), - DisableNotification(NotificationType), -} - -#[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 - GetString(String), // 251 - GetInteger(i8), // 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 -} - const INITIAL_REQUEST_QUEUE_CAPACITY: usize = 4; -enum GetType { - StringType, - IntegerType, -} - /// Asynchronous client based on `mio`. /// /// pub struct AsyncClient { client: Client, requests: VecDeque, - get_types: VecDeque, } impl AsyncClient { @@ -142,16 +32,6 @@ impl AsyncClient { Self { client, requests: VecDeque::with_capacity(INITIAL_REQUEST_QUEUE_CAPACITY), - get_types: VecDeque::with_capacity(INITIAL_REQUEST_QUEUE_CAPACITY), - } - } - - /// Convert two lines of the response in an event id - fn parse_event_id(lines: &[String]) -> ClientResult { - match lines.len() { - 0 | 1 => Err(ClientError::TooFewLines), - 2 => Ok(EventId::new(&lines[0], &lines[1])), - _ => Err(ClientError::TooManyLines), } } @@ -175,199 +55,21 @@ impl AsyncClient { !self.requests.is_empty() } - /// Next get is a string. - fn push_get_string(&mut self) { - self.get_types.push_back(GetType::StringType); - } - - /// Next get is an integer. - fn push_get_int(&mut self) { - self.get_types.push_back(GetType::IntegerType); - } - /// Write one pending request if any. /// /// Instance of `mio::Poll` generates a writable event only once until the socket returns `WouldBlock`. /// This error is mapped to `ClientError::NotReady`. pub fn send_next(&mut self) -> ClientResult<()> { - match self.requests.pop_front() { - Some(request) => match request { - Request::SetName(client_name) => self.client.set_client_name(client_name), - Request::Speak => self.client.speak(), - Request::SendLine(line) => self.client.send_line(&line), - Request::SendLines(lines) => self.client.send_lines( - lines - .iter() - .map(|s| s.as_str()) - .collect::>() - .as_slice(), - ), - Request::SendChar(ch) => self.client.send_char(ch), - Request::Stop(scope) => self.client.stop(scope), - Request::Cancel(scope) => self.client.cancel(scope), - Request::Pause(scope) => self.client.pause(scope), - Request::Resume(scope) => self.client.resume(scope), - Request::SetPriority(prio) => self.client.set_priority(prio), - Request::SetDebug(value) => self.client.set_debug(value), - Request::SetOutputModule(scope, value) => { - self.client.set_output_module(scope, &value) - } - Request::GetOutputModule => { - self.push_get_string(); - self.client.get_output_module() - } - Request::ListOutputModule => self.client.list_output_modules(), - Request::SetLanguage(scope, lang) => self.client.set_language(scope, &lang), - Request::GetLanguage => { - self.push_get_string(); - self.client.get_language() - } - Request::SetSsmlMode(value) => self.client.set_ssml_mode(value), - Request::SetPunctuationMode(scope, mode) => { - self.client.set_punctuation_mode(scope, mode) - } - Request::SetSpelling(scope, value) => self.client.set_spelling(scope, value), - Request::SetCapitalLettersRecognitionMode(scope, mode) => { - self.client.set_capital_letter_recogn(scope, mode) - } - Request::SetVoiceType(scope, value) => self.client.set_voice_type(scope, &value), - Request::GetVoiceType => { - self.push_get_string(); - self.client.get_voice_type() - } - Request::ListVoiceTypes => self.client.list_voice_types(), - Request::SetSynthesisVoice(scope, value) => { - self.client.set_synthesis_voice(scope, &value) - } - Request::ListSynthesisVoices => self.client.list_synthesis_voices(), - Request::SetRate(scope, value) => self.client.set_rate(scope, value), - Request::GetRate => { - self.push_get_int(); - self.client.get_rate() - } - Request::SetPitch(scope, value) => self.client.set_pitch(scope, value), - Request::GetPitch => { - self.push_get_int(); - self.client.get_pitch() - } - Request::SetVolume(scope, value) => self.client.set_volume(scope, value), - Request::GetVolume => { - self.push_get_int(); - self.client.get_volume() - } - Request::SetPauseContext(scope, value) => { - self.client.set_pause_context(scope, value) - } - Request::SetHistory(scope, value) => self.client.set_history(scope, value), - Request::Quit => self.client.quit(), - Request::Begin => self.client.block_begin(), - Request::End => self.client.block_end(), - Request::EnableNotification(ntype) => self.client.enable_notification(ntype), - Request::DisableNotification(ntype) => self.client.disable_notification(ntype), - } - .map(|_| ()), - None => Ok(()), + if let Some(request) = self.requests.pop_front() { + self.client.send(request)?; } + Ok(()) } /// Receive one response. /// /// Must be called each time a readable event is returned by `mio::Poll`. pub fn receive_next(&mut self) -> ClientResult { - const MSG_CURSOR_SET_FIRST: &str = "OK CURSOR SET FIRST"; - let mut lines = Vec::new(); - let status = self.client.receive(&mut lines)?; - match status.code { - OK_LANGUAGE_SET => Ok(Response::LanguageSet), - OK_PRIORITY_SET => Ok(Response::PrioritySet), - OK_RATE_SET => Ok(Response::RateSet), - OK_PITCH_SET => Ok(Response::PitchSet), - OK_PUNCTUATION_SET => Ok(Response::PunctuationSet), - OK_CAP_LET_RECOGN_SET => Ok(Response::CapLetRecognSet), - OK_SPELLING_SET => Ok(Response::SpellingSet), - OK_CLIENT_NAME_SET => Ok(Response::ClientNameSet), - OK_VOICE_SET => Ok(Response::VoiceSet), - OK_STOPPED => Ok(Response::Stopped), - OK_PAUSED => Ok(Response::Paused), - OK_RESUMED => Ok(Response::Resumed), - OK_CANCELED => Ok(Response::Canceled), - OK_TABLE_SET => Ok(Response::TableSet), - OK_OUTPUT_MODULE_SET => Ok(Response::OutputModuleSet), - OK_PAUSE_CONTEXT_SET => Ok(Response::PauseContextSet), - OK_VOLUME_SET => Ok(Response::VolumeSet), - OK_SSML_MODE_SET => Ok(Response::SsmlModeSet), - // Warning OK_CUR_SET_FIRST == OK_NOTIFICATION_SET == 220. Matching message to make the difference - OK_NOTIFICATION_SET => { - if status.message == MSG_CURSOR_SET_FIRST { - //OK_CUR_SET_FIRST => Ok(Response::HistoryCurSetFirst) - Ok(Response::HistoryCurSetFirst) - } else { - Ok(Response::NotificationSet) - } - } - - OK_CUR_SET_LAST => Ok(Response::HistoryCurSetLast), - OK_CUR_SET_POS => Ok(Response::HistoryCurSetPos), - OK_PITCH_RANGE_SET => Ok(Response::PitchRangeSet), - OK_DEBUG_SET => Ok(Response::DebugSet), - OK_CUR_MOV_FOR => Ok(Response::HistoryCurMoveFor), - OK_CUR_MOV_BACK => Ok(Response::HistoryCurMoveBack), - OK_MESSAGE_QUEUED => Ok(Response::MessageQueued), - OK_SND_ICON_QUEUED => Ok(Response::SoundIconQueued), - 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_MSGS_LIST_SENT => Ok(Response::HistoryMsgsListSent(lines)), - OK_LAST_MSG => Ok(Response::HistoryLastMsg(Client::::parse_single_value( - &lines, - )?)), - OK_CUR_POS_RET => Ok(Response::HistoryCurPosRet(Client::::parse_single_value( - &lines, - )?)), - OK_TABLE_LIST_SENT => Ok(Response::TableListSent(lines)), - OK_CLIENT_ID_SENT => Ok(Response::HistoryClientIdSent( - Client::::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( - Client::::parse_synthesis_voices(&lines)?, - )), - OK_OUTPUT_MODULES_LIST_SENT => Ok(Response::OutputModulesListSent(lines)), - OK_GET => { - let sval = Client::::parse_single_value(&lines)?; - match self - .get_types - .pop_front() - .expect("internal error: get_types is empty") - { - GetType::StringType => Ok(Response::GetString(sval)), - GetType::IntegerType => sval - .parse::() - .map(|uval| Response::GetInteger(uval)) - .map_err(|_| ClientError::InvalidType), - } - } - OK_INSIDE_BLOCK => Ok(Response::InsideBlock), - OK_OUTSIDE_BLOCK => Ok(Response::OutsideBlock), - OK_NOT_IMPLEMENTED => Ok(Response::NotImplemented), - EVENT_INDEX_MARK => { - if lines.len() == 3 { - Ok(Response::EventIndexMark( - Self::parse_event_id(&lines)?, - lines[2].to_owned(), - )) - } else { - Err(ClientError::TooFewLines) - } - } - EVENT_BEGIN => Ok(Response::EventBegin(Self::parse_event_id(&lines)?)), - EVENT_END => Ok(Response::EventEnd(Self::parse_event_id(&lines)?)), - EVENT_CANCELED => Ok(Response::EventCanceled(Self::parse_event_id(&lines)?)), - EVENT_PAUSED => Ok(Response::EventPaused(Self::parse_event_id(&lines)?)), - EVENT_RESUMED => Ok(Response::EventResumed(Self::parse_event_id(&lines)?)), - _ => panic!("error should have been caught earlier"), - } + self.client.receive() } } diff --git a/src/client.rs b/src/client.rs index 5a65e79..fdf6938 100644 --- a/src/client.rs +++ b/src/client.rs @@ -8,15 +8,12 @@ // modified, or distributed except according to those terms. use std::io::{self, Read, Write}; -use std::str::FromStr; -use thiserror::Error as ThisError; use crate::constants::*; -use crate::protocol::{send_lines, write_lines}; -use crate::types::{ - CapitalLettersRecognitionMode, ClientScope, Event, KeyName, MessageId, MessageScope, - NotificationType, Priority, PunctuationMode, ReturnCode, StatusLine, SynthesisVoice, +use crate::protocol::{ + flush_lines, parse_event_id, parse_single_value, parse_synthesis_voices, write_lines, }; +use crate::types::*; // Trick to have common implementation for std and mio streams.. #[cfg(not(feature = "async-mio"))] @@ -25,65 +22,6 @@ use std::fmt::Debug as Source; #[cfg(feature = "async-mio")] use mio::event::Source; -/// Client error, either I/O error or SSIP error. -#[derive(ThisError, Debug)] -pub enum ClientError { - #[error("Invalid type")] - InvalidType, - #[error("I/O: {0}")] - Io(io::Error), - #[error("Not ready")] - NotReady, - #[error("SSIP: {0}")] - Ssip(StatusLine), - #[error("Too few lines")] - TooFewLines, - #[error("Too many lines")] - TooManyLines, - #[error("Truncated message")] - TruncatedMessage, - #[error("Unexpected status: {0}")] - UnexpectedStatus(ReturnCode), -} - -impl From for ClientError { - fn from(err: io::Error) -> Self { - if err.kind() == io::ErrorKind::WouldBlock { - ClientError::NotReady - } else { - ClientError::Io(err) - } - } -} - -/// Client result. -pub type ClientResult = Result; - -/// Client result consisting in a single status line -pub type ClientStatus = ClientResult; - -/// Client name -#[derive(Debug, Clone)] -pub struct ClientName { - pub user: String, - pub application: String, - pub component: String, -} - -impl ClientName { - pub fn new(user: &str, application: &str) -> Self { - ClientName::with_component(user, application, "main") - } - - pub fn with_component(user: &str, application: &str, component: &str) -> Self { - ClientName { - user: user.to_string(), - application: application.to_string(), - component: component.to_string(), - } - } -} - /// Convert boolean to ON or OFF fn on_off(value: bool) -> &'static str { if value { @@ -93,83 +31,142 @@ fn on_off(value: bool) -> &'static str { } } -macro_rules! client_send { - ($name:ident, $doc:expr, $scope:ident, $value_name:ident as $value_type:ty, $fmt:expr, $value:expr) => { - #[doc=$doc] - pub fn $name( - &mut self, - $scope: ClientScope, - $value_name: $value_type, - ) -> ClientResult<&mut Client> { - let line = match $scope { - ClientScope::Current => format!($fmt, "self", $value), - ClientScope::All => format!($fmt, "all", $value), - ClientScope::Client(id) => format!($fmt, id, $value), - }; - - send_lines(&mut self.output, &[line.as_str()])?; - Ok(self) - } - }; - ($name:ident, $doc:expr, $scope:ident, $value_name:ident as $value_type:ty, $fmt:expr) => { - client_send!( - $name, - $doc, - $scope, - $value_name as $value_type, - $fmt, - $value_name - ); - }; - ($name:ident, $doc:expr, $value_name:ident as $value_type:ty, $fmt:expr, $value:expr) => { - #[doc=$doc] - pub fn $name(&mut self, $value_name: $value_type) -> ClientResult<&mut Client> { - send_lines(&mut self.output, &[format!($fmt, $value).as_str()])?; - Ok(self) - } - }; - ($name:ident, $doc:expr, $value_name:ident as $value_type:ty, $fmt:expr) => { - client_send!($name, $doc, $value_name as $value_type, $fmt, $value_name); - }; - ($name:ident, $doc:expr, $line:expr) => { - #[doc=$doc] - pub fn $name(&mut self) -> ClientResult<&mut Client> { - send_lines(&mut self.output, &[$line])?; - Ok(self) - } +#[derive(Debug, Clone)] +/// Request for SSIP server. +pub enum Request { + SetName(ClientName), + // Speech related requests + Speak, + SendLine(String), + SendLines(Vec), + SpeakChar(char), + SpeakKey(KeyName), + // Flow control + Stop(MessageScope), + Cancel(MessageScope), + Pause(MessageScope), + Resume(MessageScope), + // Setter and getter + SetPriority(Priority), + SetDebug(bool), + SetOutputModule(ClientScope, String), + GetOutputModule, + ListOutputModules, + SetLanguage(ClientScope, String), + GetLanguage, + SetSsmlMode(bool), + SetPunctuationMode(ClientScope, PunctuationMode), + SetSpelling(ClientScope, bool), + SetCapitalLettersRecognitionMode(ClientScope, CapitalLettersRecognitionMode), + SetVoiceType(ClientScope, String), + GetVoiceType, + ListVoiceTypes, + SetSynthesisVoice(ClientScope, String), + ListSynthesisVoices, + SetRate(ClientScope, i8), + GetRate, + SetPitch(ClientScope, i8), + GetPitch, + SetVolume(ClientScope, i8), + GetVolume, + SetPauseContext(ClientScope, u8), + SetHistory(ClientScope, bool), + SetNotification(NotificationType, bool), + Begin, + End, + Quit, +} + +#[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 +} + +macro_rules! send_one_line { + ($self:expr, $fmt:expr, $( $arg:expr ),+) => { + flush_lines(&mut $self.output, &[format!($fmt, $( $arg ),+).as_str()]) }; + ($self:expr, $fmt:expr) => { + flush_lines(&mut $self.output, &[$fmt]) + } } -macro_rules! client_send_boolean { - ($name:ident, $doc:expr, $scope:ident, $value_name:ident, $fmt:expr) => { - client_send!( - $name, - $doc, - $scope, - $value_name as bool, - $fmt, - on_off($value_name) - ); +macro_rules! send_toggle { + ($output:expr, $fmt:expr, $val:expr) => { + send_one_line!($output, $fmt, on_off($val)) }; - ($name:ident, $doc:expr, $value_name:ident, $fmt:expr) => { - client_send!($name, $doc, $value_name as bool, $fmt, on_off($value_name)); + ($output:expr, $fmt:expr, $arg:expr, $val:expr) => { + send_one_line!($output, $fmt, $arg, on_off($val)) }; } -macro_rules! client_send_range { - ($name:ident, $doc:expr, $scope:ident, $value_name:ident, $fmt:expr) => { - client_send!( - $name, - $doc, - $scope, - $value_name as i8, +macro_rules! send_range { + ($output:expr, $fmt:expr, $scope:expr, $val:expr) => { + send_one_line!( + $output, $fmt, - std::cmp::max(-100, std::cmp::min(100, $value_name)) - ); + $scope, + std::cmp::max(-100, std::cmp::min(100, $val)) + ) }; } /// SSIP client on generic stream +/// +/// There are two ways to send requests and receive responses: +/// * Either with the generic [`send`] and [`receive`] +/// * Or with the specific methods such as [`set_rate`], ..., [`get_rate`], ... pub struct Client { input: io::BufReader, output: io::BufWriter, @@ -182,284 +179,381 @@ impl Client { Self { input, output } } - /// Return the only string in the list or an error if there is no line or too many. - pub(crate) fn parse_single_value(lines: &[String]) -> ClientResult { - match lines.len() { - 0 => Err(ClientError::TooFewLines), - 1 => Ok(lines[0].to_string()), - _ => Err(ClientError::TooManyLines), - } - } - - 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) - } - - /// Set the client name. It must be the first call on startup. - pub fn set_client_name(&mut self, client_name: ClientName) -> ClientResult<&mut Self> { - send_lines( + /// Send lines of text (terminated by a single dot). + pub fn send_lines(&mut self, lines: &[String]) -> ClientResult<&mut Self> { + const END_OF_DATA: [&str; 1] = ["."]; + write_lines( &mut self.output, - &[format!( - "SET self CLIENT_NAME {}:{}:{}", - client_name.user, client_name.application, client_name.component - ) - .as_str()], + lines + .iter() + .map(|s| s.as_str()) + .collect::>() + .as_slice(), )?; + flush_lines(&mut self.output, &END_OF_DATA)?; Ok(self) } - /// Initiate communitation to send text to speak - pub fn speak(&mut self) -> ClientResult<&mut Self> { - send_lines(&mut self.output, &["SPEAK"])?; + /// Send one line of text (terminated by a single dot). + pub fn send_line(&mut self, line: &str) -> ClientResult<&mut Self> { + const END_OF_DATA: &str = "."; + flush_lines(&mut self.output, &[line, END_OF_DATA])?; Ok(self) } - /// Send lines - pub fn send_lines(&mut self, lines: &[&str]) -> ClientResult<&mut Self> { - const END_OF_DATA: [&str; 1] = ["."]; - write_lines(&mut self.output, lines)?; - send_lines(&mut self.output, &END_OF_DATA)?; + /// Send a request + pub fn send(&mut self, request: Request) -> ClientResult<&mut Self> { + match request { + Request::SetName(client_name) => send_one_line!( + self, + "SET self CLIENT_NAME {}:{}:{}", + client_name.user, + client_name.application, + client_name.component + ), + Request::Speak => send_one_line!(self, "SPEAK"), + Request::SendLine(line) => self.send_line(&line).map(|_| ()), + Request::SendLines(lines) => self.send_lines(&lines).map(|_| ()), + Request::SpeakChar(ch) => send_one_line!(self, "CHAR {}", ch), + Request::SpeakKey(key) => send_one_line!(self, "KEY {}", key), + Request::Stop(scope) => send_one_line!(self, "STOP {}", scope), + Request::Cancel(scope) => send_one_line!(self, "CANCEL {}", scope), + Request::Pause(scope) => send_one_line!(self, "PAUSE {}", scope), + Request::Resume(scope) => send_one_line!(self, "RESUME {}", scope), + Request::SetPriority(prio) => send_one_line!(self, "SET self PRIORITY {}", prio), + Request::SetDebug(value) => send_toggle!(self, "SET all DEBUG {}", value), + Request::SetOutputModule(scope, value) => { + send_one_line!(self, "SET {} OUTPUT_MODULE {}", scope, value) + } + Request::GetOutputModule => send_one_line!(self, "GET OUTPUT_MODULE"), + Request::ListOutputModules => send_one_line!(self, "LIST OUTPUT_MODULES"), + Request::SetLanguage(scope, lang) => { + send_one_line!(self, "SET {} LANGUAGE {}", scope, lang) + } + Request::GetLanguage => send_one_line!(self, "GET LANGUAGE"), + Request::SetSsmlMode(value) => send_toggle!(self, "SET self SSML_MODE {}", value), + Request::SetPunctuationMode(scope, mode) => { + send_one_line!(self, "SET {} PUNCTUATION {}", scope, mode) + } + Request::SetSpelling(scope, value) => { + send_toggle!(self, "SET {} SPELLING {}", scope, value) + } + Request::SetCapitalLettersRecognitionMode(scope, mode) => { + send_one_line!(self, "SET {} CAP_LET_RECOGN {}", scope, mode) + } + Request::SetVoiceType(scope, value) => { + send_one_line!(self, "SET {} VOICE_TYPE {}", scope, value) + } + Request::GetVoiceType => send_one_line!(self, "GET VOICE_TYPE"), + Request::ListVoiceTypes => send_one_line!(self, "LIST VOICES"), + Request::SetSynthesisVoice(scope, value) => { + send_one_line!(self, "SET {} SYNTHESIS_VOICE {}", scope, value) + } + Request::ListSynthesisVoices => send_one_line!(self, "LIST SYNTHESIS_VOICES"), + Request::SetRate(scope, value) => send_range!(self, "SET {} RATE {}", scope, value), + Request::GetRate => send_one_line!(self, "GET RATE"), + Request::SetPitch(scope, value) => send_range!(self, "SET {} PITCH {}", scope, value), + Request::GetPitch => send_one_line!(self, "GET PITCH"), + Request::SetVolume(scope, value) => { + send_range!(self, "SET {} VOLUME {}", scope, value) + } + Request::GetVolume => send_one_line!(self, "GET VOLUME"), + Request::SetPauseContext(scope, value) => { + send_one_line!(self, "SET {} PAUSE_CONTEXT {}", scope, value) + } + Request::SetHistory(scope, value) => { + send_toggle!(self, "SET {} HISTORY {}", scope, value) + } + Request::SetNotification(ntype, value) => { + send_toggle!(self, "SET self NOTIFICATION {} {}", ntype, value) + } + Request::Begin => send_one_line!(self, "BLOCK BEGIN"), + Request::End => send_one_line!(self, "BLOCK END"), + Request::Quit => send_one_line!(self, "QUIT"), + }?; Ok(self) } - /// Send a line - pub fn send_line(&mut self, line: &str) -> ClientResult<&mut Self> { - const END_OF_DATA: &str = "."; - send_lines(&mut self.output, &[line, END_OF_DATA])?; - Ok(self) + /// Set the client name. It must be the first call on startup. + pub fn set_client_name(&mut self, client_name: ClientName) -> ClientResult<&mut Self> { + self.send(Request::SetName(client_name)) } - /// Send a char - pub fn send_char(&mut self, ch: char) -> ClientResult<&mut Self> { - send_lines(&mut self.output, &[format!("CHAR {}", ch).as_str()])?; - Ok(self) + /// Initiate communitation to send text to speak + pub fn speak(&mut self) -> ClientResult<&mut Self> { + self.send(Request::Speak) } - /// Send a symbolic key name - pub fn say_key_name(&mut self, keyname: KeyName) -> ClientResult<&mut Self> { - self.send_lines(&[format!("KEY {}", keyname).as_str()]) + /// Speak a char + pub fn speak_char(&mut self, ch: char) -> ClientResult<&mut Self> { + self.send(Request::SpeakChar(ch)) } - /// Action on a message or a group of messages - fn send_message_command( - &mut self, - command: &str, - scope: MessageScope, - ) -> ClientResult<&mut Self> { - let line = match scope { - MessageScope::Last => format!("{} self", command), - MessageScope::All => format!("{} all", command), - MessageScope::Message(id) => format!("{} {}", command, id), - }; - send_lines(&mut self.output, &[line.as_str()])?; - Ok(self) + /// Speak a symbolic key name + pub fn speak_key(&mut self, key_name: KeyName) -> ClientResult<&mut Self> { + self.send(Request::SpeakKey(key_name)) } /// Stop current message pub fn stop(&mut self, scope: MessageScope) -> ClientResult<&mut Self> { - self.send_message_command("STOP", scope) + self.send(Request::Stop(scope)) } /// Cancel current message pub fn cancel(&mut self, scope: MessageScope) -> ClientResult<&mut Self> { - self.send_message_command("CANCEL", scope) + self.send(Request::Cancel(scope)) } /// Pause current message pub fn pause(&mut self, scope: MessageScope) -> ClientResult<&mut Self> { - self.send_message_command("PAUSE", scope) + self.send(Request::Pause(scope)) } /// Resume current message pub fn resume(&mut self, scope: MessageScope) -> ClientResult<&mut Self> { - self.send_message_command("RESUME", scope) - } - - client_send!( - set_priority, - "Set message priority", - priority as Priority, - "SET self PRIORITY {}" - ); - - client_send_boolean!( - set_debug, - "Set debug mode. Return the log location", - value, - "SET all DEBUG {}" - ); - - client_send!( - set_output_module, - "Set output module", - scope, - value as &str, - "SET {} OUTPUT_MODULE {}" - ); - - client_send!( - get_output_module, - "Get the current output module", - "GET OUTPUT_MODULE" - ); - - client_send!( - list_output_modules, - "List the available output modules", - "LIST OUTPUT_MODULES" - ); - - client_send!( - set_language, - "Set language code", - scope, - value as &str, - "SET {} LANGUAGE {}" - ); - - client_send!(get_language, "Get the current language", "GET LANGUAGE"); - - client_send_boolean!( - set_ssml_mode, - "Set SSML mode (Speech Synthesis Markup Language)", - value, - "SET self SSML_MODE {}" - ); - - client_send!( - set_punctuation_mode, - "Set punctuation mode", - scope, - value as PunctuationMode, - "SET {} PUNCTUATION {}" - ); - - client_send_boolean!( - set_spelling, - "Set spelling on or off", - scope, - value, - "SET {} SPELLING {}" - ); - - client_send!( - set_capital_letter_recogn, - "Set capital letters recognition mode", - scope, - value as CapitalLettersRecognitionMode, - "SET {} CAP_LET_RECOGN {}" - ); - - client_send!( - set_voice_type, - "Set the voice type (MALE1, FEMALE1, …)", - scope, - value as &str, - "SET {} VOICE_TYPE {}" - ); - - client_send!( - get_voice_type, - "Get the current pre-defined voice", - "GET VOICE_TYPE" - ); - - client_send!( - list_voice_types, - "List the available symbolic voice names", - "LIST VOICES" - ); - - client_send!( - set_synthesis_voice, - "Set the voice", - scope, - value as &str, - "SET {} SYNTHESIS_VOICE {}" - ); - - client_send!( - list_synthesis_voices, - "Lists the available voices for the current synthesizer", - "LIST SYNTHESIS_VOICES" - ); - - client_send_range!( - set_rate, - "Set the rate of speech. n is an integer value within the range from -100 to 100, lower values meaning slower speech.", - scope, - value, - "SET {} RATE {}" - ); - - client_send!(get_rate, "Get the current rate of speech.", "GET RATE"); - - client_send_range!( - set_pitch, - "Set the pitch of speech. n is an integer value within the range from -100 to 100.", - scope, - value, - "SET {} PITCH {}" - ); - - client_send!(get_pitch, "Get the current pitch value.", "GET PITCH"); - - client_send_range!( - set_volume, - "Set the volume of speech. n is an integer value within the range from -100 to 100.", - scope, - value, - "SET {} VOLUME {}" - ); - - client_send!(get_volume, "Get the current volume.", "GET VOLUME"); - - client_send!( - set_pause_context, - "Set the number of (more or less) sentences that should be repeated after a previously paused text is resumed.", - scope, - value as u8, - "SET {} PAUSE_CONTEXT {}" - ); - - client_send_boolean!( - set_history, - "Enable or disable history of received messages.", - scope, - value, - "SET {} HISTORY {}" - ); - - client_send!(block_begin, "Open a block", "BLOCK BEGIN"); - - client_send!(block_end, "End a block", "BLOCK END"); - - client_send!(quit, "Close the connection", "QUIT"); - - client_send!( - enable_notification, - "Enable notification events", - value as NotificationType, - "SET self NOTIFICATION {} on" - ); - - client_send!( - disable_notification, - "Disable notification events", - value as NotificationType, - "SET self NOTIFICATION {} off" - ); + self.send(Request::Resume(scope)) + } + + /// Set message priority + pub fn set_priority(&mut self, prio: Priority) -> ClientResult<&mut Self> { + self.send(Request::SetPriority(prio)) + } + + /// Set debug mode. Return the log location + pub fn set_debug(&mut self, value: bool) -> ClientResult<&mut Self> { + self.send(Request::SetDebug(value)) + } + + /// Set output module + pub fn set_output_module( + &mut self, + scope: ClientScope, + value: &str, + ) -> ClientResult<&mut Self> { + self.send(Request::SetOutputModule(scope, value.to_string())) + } + + /// Get the current output module + pub fn get_output_module(&mut self) -> ClientResult<&mut Self> { + self.send(Request::GetOutputModule) + } + + /// List the available output modules + pub fn list_output_modules(&mut self) -> ClientResult<&mut Self> { + self.send(Request::ListOutputModules) + } + + /// Set language code + pub fn set_language(&mut self, scope: ClientScope, value: &str) -> ClientResult<&mut Self> { + self.send(Request::SetLanguage(scope, value.to_string())) + } + + /// Get the current language + pub fn get_language(&mut self) -> ClientResult<&mut Self> { + self.send(Request::GetLanguage) + } + + /// Set SSML mode (Speech Synthesis Markup Language) + pub fn set_ssml_mode(&mut self, mode: bool) -> ClientResult<&mut Self> { + self.send(Request::SetSsmlMode(mode)) + } + + /// Set punctuation mode + pub fn set_punctuation_mode( + &mut self, + scope: ClientScope, + mode: PunctuationMode, + ) -> ClientResult<&mut Self> { + self.send(Request::SetPunctuationMode(scope, mode)) + } + + /// Set spelling on or off + pub fn set_spelling(&mut self, scope: ClientScope, value: bool) -> ClientResult<&mut Self> { + self.send(Request::SetSpelling(scope, value)) + } + + /// Set capital letters recognition mode + pub fn set_capital_letter_recogn( + &mut self, + scope: ClientScope, + mode: CapitalLettersRecognitionMode, + ) -> ClientResult<&mut Self> { + self.send(Request::SetCapitalLettersRecognitionMode(scope, mode)) + } + + /// Set the voice type (MALE1, FEMALE1, …) + pub fn set_voice_type(&mut self, scope: ClientScope, value: &str) -> ClientResult<&mut Self> { + self.send(Request::SetVoiceType(scope, value.to_string())) + } + + /// Get the current pre-defined voice + pub fn get_voice_type(&mut self) -> ClientResult<&mut Self> { + self.send(Request::GetVoiceType) + } + + /// List the available symbolic voice names + pub fn list_voice_types(&mut self) -> ClientResult<&mut Self> { + self.send(Request::ListVoiceTypes) + } + + /// Set the voice + pub fn set_synthesis_voice( + &mut self, + scope: ClientScope, + value: &str, + ) -> ClientResult<&mut Self> { + self.send(Request::SetSynthesisVoice(scope, value.to_string())) + } + + /// Lists the available voices for the current synthesizer + pub fn list_synthesis_voices(&mut self) -> ClientResult<&mut Self> { + self.send(Request::ListSynthesisVoices) + } + + /// Set the rate of speech. n is an integer value within the range from -100 to 100, lower values meaning slower speech. + pub fn set_rate(&mut self, scope: ClientScope, value: i8) -> ClientResult<&mut Self> { + self.send(Request::SetRate(scope, value)) + } + + /// Get the current rate of speech. + pub fn get_rate(&mut self) -> ClientResult<&mut Self> { + self.send(Request::GetRate) + } + + /// Set the pitch of speech. n is an integer value within the range from -100 to 100. + pub fn set_pitch(&mut self, scope: ClientScope, value: i8) -> ClientResult<&mut Self> { + self.send(Request::SetPitch(scope, value)) + } + + /// Get the current pitch value. + pub fn get_pitch(&mut self) -> ClientResult<&mut Self> { + self.send(Request::GetPitch) + } + + /// Set the volume of speech. n is an integer value within the range from -100 to 100. + pub fn set_volume(&mut self, scope: ClientScope, value: i8) -> ClientResult<&mut Self> { + self.send(Request::SetVolume(scope, value)) + } + + /// Get the current volume. + pub fn get_volume(&mut self) -> ClientResult<&mut Self> { + self.send(Request::GetVolume) + } + + /// Set the number of (more or less) sentences that should be repeated after a previously paused text is resumed. + pub fn set_pause_context(&mut self, scope: ClientScope, value: u8) -> ClientResult<&mut Self> { + self.send(Request::SetPauseContext(scope, value)) + } + + /// Enable or disable history of received messages. + pub fn set_history(&mut self, scope: ClientScope, value: bool) -> ClientResult<&mut Self> { + self.send(Request::SetHistory(scope, value)) + } + + /// Enable notification events + pub fn set_notification( + &mut self, + ntype: NotificationType, + value: bool, + ) -> ClientResult<&mut Self> { + self.send(Request::SetNotification(ntype, value)) + } + + /// Open a block + pub fn block_begin(&mut self) -> ClientResult<&mut Self> { + self.send(Request::Begin) + } + + /// End a block + pub fn block_end(&mut self) -> ClientResult<&mut Self> { + self.send(Request::End) + } + + /// Close the connection + pub fn quit(&mut self) -> ClientResult<&mut Self> { + self.send(Request::Quit) + } /// Receive answer from server - pub fn receive(&mut self, lines: &mut Vec) -> ClientStatus { + fn receive_answer(&mut self, lines: &mut Vec) -> ClientStatus { crate::protocol::receive_answer(&mut self.input, Some(lines)) } + /// Receive one response. + pub fn receive(&mut self) -> ClientResult { + const MSG_CURSOR_SET_FIRST: &str = "OK CURSOR SET FIRST"; + let mut lines = Vec::new(); + let status = self.receive_answer(&mut lines)?; + match status.code { + OK_LANGUAGE_SET => Ok(Response::LanguageSet), + OK_PRIORITY_SET => Ok(Response::PrioritySet), + OK_RATE_SET => Ok(Response::RateSet), + OK_PITCH_SET => Ok(Response::PitchSet), + OK_PUNCTUATION_SET => Ok(Response::PunctuationSet), + OK_CAP_LET_RECOGN_SET => Ok(Response::CapLetRecognSet), + OK_SPELLING_SET => Ok(Response::SpellingSet), + OK_CLIENT_NAME_SET => Ok(Response::ClientNameSet), + OK_VOICE_SET => Ok(Response::VoiceSet), + OK_STOPPED => Ok(Response::Stopped), + OK_PAUSED => Ok(Response::Paused), + OK_RESUMED => Ok(Response::Resumed), + OK_CANCELED => Ok(Response::Canceled), + OK_TABLE_SET => Ok(Response::TableSet), + OK_OUTPUT_MODULE_SET => Ok(Response::OutputModuleSet), + OK_PAUSE_CONTEXT_SET => Ok(Response::PauseContextSet), + OK_VOLUME_SET => Ok(Response::VolumeSet), + OK_SSML_MODE_SET => Ok(Response::SsmlModeSet), + // Warning OK_CUR_SET_FIRST == OK_NOTIFICATION_SET == 220. Matching message to make the difference + OK_NOTIFICATION_SET => { + if status.message == MSG_CURSOR_SET_FIRST { + //OK_CUR_SET_FIRST => Ok(Response::HistoryCurSetFirst) + Ok(Response::HistoryCurSetFirst) + } else { + Ok(Response::NotificationSet) + } + } + OK_CUR_SET_LAST => Ok(Response::HistoryCurSetLast), + OK_CUR_SET_POS => Ok(Response::HistoryCurSetPos), + OK_PITCH_RANGE_SET => Ok(Response::PitchRangeSet), + OK_DEBUG_SET => Ok(Response::DebugSet), + OK_CUR_MOV_FOR => Ok(Response::HistoryCurMoveFor), + OK_CUR_MOV_BACK => Ok(Response::HistoryCurMoveBack), + OK_MESSAGE_QUEUED => Ok(Response::MessageQueued), + OK_SND_ICON_QUEUED => Ok(Response::SoundIconQueued), + 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_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)?)), + OK_TABLE_LIST_SENT => Ok(Response::TableListSent(lines)), + 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_OUTPUT_MODULES_LIST_SENT => Ok(Response::OutputModulesListSent(lines)), + OK_GET => Ok(Response::Get(parse_single_value(&lines)?)), + OK_INSIDE_BLOCK => Ok(Response::InsideBlock), + OK_OUTSIDE_BLOCK => Ok(Response::OutsideBlock), + OK_NOT_IMPLEMENTED => Ok(Response::NotImplemented), + EVENT_INDEX_MARK => match lines.len() { + 0 | 1 | 2 => Err(ClientError::TooFewLines), + 3 => Ok(Response::EventIndexMark( + parse_event_id(&lines)?, + lines[2].to_owned(), + )), + _ => Err(ClientError::TooManyLines), + }, + EVENT_BEGIN => Ok(Response::EventBegin(parse_event_id(&lines)?)), + EVENT_END => Ok(Response::EventEnd(parse_event_id(&lines)?)), + EVENT_CANCELED => Ok(Response::EventCanceled(parse_event_id(&lines)?)), + EVENT_PAUSED => Ok(Response::EventPaused(parse_event_id(&lines)?)), + EVENT_RESUMED => Ok(Response::EventResumed(parse_event_id(&lines)?)), + _ => panic!("error should have been caught earlier"), + } + } + /// Check status of answer, discard lines. pub fn check_status(&mut self, expected_code: ReturnCode) -> ClientResult<&mut Self> { crate::protocol::receive_answer(&mut self.input, None).and_then(|status| { @@ -474,7 +568,7 @@ impl Client { /// Receive lines pub fn receive_lines(&mut self, expected_code: ReturnCode) -> ClientResult> { let mut lines = Vec::new(); - let status = self.receive(&mut lines)?; + let status = self.receive_answer(&mut lines)?; if status.code == expected_code { Ok(lines) } else { @@ -485,7 +579,7 @@ impl Client { /// Receive a single string pub fn receive_string(&mut self, expected_code: ReturnCode) -> ClientResult { self.receive_lines(expected_code) - .and_then(|lines| Self::parse_single_value(&lines)) + .and_then(|lines| parse_single_value(&lines)) } /// Receive integer @@ -503,7 +597,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| Self::parse_synthesis_voices(&lines)) + .and_then(|lines| parse_synthesis_voices(&lines)) } /// Receive a notification @@ -560,26 +654,3 @@ impl Client { Ok(()) } } - -#[cfg(test)] -mod tests { - - #[cfg(not(feature = "async-mio"))] - use std::net::TcpStream; - - #[cfg(feature = "async-mio")] - use mio::net::TcpStream; - - use super::{Client, ClientError}; - - #[test] - fn parse_single_value() { - let result = Client::::parse_single_value(&[String::from("one")]).unwrap(); - assert_eq!("one", result); - let err_empty = Client::::parse_single_value(&[]); - assert!(matches!(err_empty, Err(ClientError::TooFewLines))); - let err_too_many = - Client::::parse_single_value(&[String::from("one"), String::from("two")]); - assert!(matches!(err_too_many, Err(ClientError::TooManyLines))); - } -} diff --git a/src/lib.rs b/src/lib.rs index 2fcd24f..3fa3117 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,7 +38,7 @@ pub mod fifo; #[cfg(any(not(feature = "async-mio"), doc))] pub use client::Client; -pub use client::{ClientError, ClientName, ClientResult, ClientStatus}; +pub use client::{Request, Response}; pub use constants::*; pub use types::*; @@ -46,4 +46,4 @@ pub use types::*; mod async_mio; #[cfg(feature = "async-mio")] -pub use async_mio::{AsyncClient, Request, Response}; +pub use async_mio::AsyncClient; diff --git a/src/protocol.rs b/src/protocol.rs index 5b7611a..80f475d 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -9,9 +9,9 @@ use log::debug; use std::io::{self, BufRead, Write}; +use std::str::FromStr; -use crate::client::{ClientError, ClientResult, ClientStatus}; -use crate::types::StatusLine; +use crate::types::{ClientError, ClientResult, ClientStatus, EventId, StatusLine, SynthesisVoice}; macro_rules! invalid_input { ($msg:expr) => { @@ -22,6 +22,33 @@ macro_rules! invalid_input { }; } +/// Return the only string in the list or an error if there is no line or too many. +pub(crate) fn parse_single_value(lines: &[String]) -> ClientResult { + match lines.len() { + 0 => Err(ClientError::TooFewLines), + 1 => Ok(lines[0].to_string()), + _ => Err(ClientError::TooManyLines), + } +} + +/// Convert two lines of the response in an event id +pub(crate) fn parse_event_id(lines: &[String]) -> ClientResult { + match lines.len() { + 0 | 1 => Err(ClientError::TooFewLines), + 2 => Ok(EventId::new(&lines[0], &lines[1])), + _ => Err(ClientError::TooManyLines), + } +} + +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) +} + /// Write lines separated by CRLF. pub(crate) fn write_lines(output: &mut dyn Write, lines: &[&str]) -> ClientResult<()> { for line in lines.iter() { @@ -33,7 +60,7 @@ pub(crate) fn write_lines(output: &mut dyn Write, lines: &[&str]) -> ClientResul } /// Write lines separated by CRLF and flush the output. -pub(crate) fn send_lines(output: &mut dyn Write, lines: &[&str]) -> ClientResult<()> { +pub(crate) fn flush_lines(output: &mut dyn Write, lines: &[&str]) -> ClientResult<()> { write_lines(output, lines)?; output.flush()?; Ok(()) @@ -91,7 +118,7 @@ mod tests { use std::io::BufReader; - use super::{receive_answer, ClientError}; + use super::{receive_answer, ClientError, ClientResult}; #[test] fn single_ok_status_line() { @@ -136,4 +163,73 @@ mod tests { lines.as_slice() ); } + + #[test] + fn parse_single_value() -> ClientResult<()> { + let no_lines = Vec::new(); + assert!(matches!( + super::parse_single_value(&no_lines), + Err(ClientError::TooFewLines) + )); + + let one = String::from("one"); + let one_line = vec![one.to_owned()]; + assert_eq!(one, super::parse_single_value(&one_line)?); + + let two_lines = vec![one.to_owned(), String::from("two")]; + assert!(matches!( + super::parse_single_value(&two_lines), + Err(ClientError::TooManyLines) + )); + + Ok(()) + } + + #[test] + fn parse_event_id() -> ClientResult<()> { + let no_lines = Vec::new(); + assert!(matches!( + super::parse_event_id(&no_lines), + Err(ClientError::TooFewLines) + )); + + let one_line = vec![String::from("one")]; + assert!(matches!( + super::parse_event_id(&one_line), + Err(ClientError::TooFewLines) + )); + + let mid = String::from("message"); + let cid = String::from("client"); + let two_lines = vec![mid.to_owned(), cid.to_owned()]; + let event_id = super::parse_event_id(&two_lines)?; + assert_eq!(mid, event_id.message); + assert_eq!(cid, event_id.client); + + let three_lines = vec![ + String::from("one"), + String::from("two"), + String::from("three"), + ]; + assert!(matches!( + super::parse_event_id(&three_lines), + Err(ClientError::TooManyLines) + )); + + Ok(()) + } + + #[test] + fn parse_synthesis_voices() -> ClientResult<()> { + let lines = ["en", "afrikaans\taf", "lancashire\ten\tuk-north"] + .iter() + .map(|s| s.to_string()) + .collect::>(); + let voices = super::parse_synthesis_voices(&lines)?; + assert_eq!(3, voices.len()); + assert_eq!("en", voices[0].name.as_str()); + assert_eq!(Some(String::from("af")), voices[1].language); + assert_eq!(Some(String::from("uk-north")), voices[2].dialect); + Ok(()) + } } diff --git a/src/types.rs b/src/types.rs index ebcddf2..375761a 100644 --- a/src/types.rs +++ b/src/types.rs @@ -8,7 +8,9 @@ // modified, or distributed except according to those terms. use std::fmt; +use std::io; use std::str::FromStr; +use thiserror::Error as ThisError; use strum_macros::Display as StrumDisplay; @@ -32,6 +34,16 @@ pub enum MessageScope { Message(MessageId), } +impl fmt::Display for MessageScope { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + MessageScope::Last => write!(f, "self"), + MessageScope::All => write!(f, "all"), + MessageScope::Message(id) => write!(f, "{}", id), + } + } +} + /// Client identifiers #[derive(Debug, Clone)] pub enum ClientScope { @@ -43,6 +55,16 @@ pub enum ClientScope { Client(ClientId), } +impl fmt::Display for ClientScope { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ClientScope::Current => write!(f, "self"), + ClientScope::All => write!(f, "all"), + ClientScope::Client(id) => write!(f, "{}", id), + } + } +} + /// Priority #[derive(StrumDisplay, Debug, Clone)] pub enum Priority { @@ -347,7 +369,7 @@ impl SynthesisVoice { } impl FromStr for SynthesisVoice { - type Err = std::io::Error; + type Err = io::Error; fn from_str(s: &str) -> Result { let mut iter = s.split('\t'); @@ -377,13 +399,71 @@ impl fmt::Display for StatusLine { write!(f, "{} {}", self.code, self.message) } } +/// Client error, either I/O error or SSIP error. +#[derive(ThisError, Debug)] +pub enum ClientError { + #[error("Invalid type")] + InvalidType, + #[error("I/O: {0}")] + Io(io::Error), + #[error("Not ready")] + NotReady, + #[error("SSIP: {0}")] + Ssip(StatusLine), + #[error("Too few lines")] + TooFewLines, + #[error("Too many lines")] + TooManyLines, + #[error("Truncated message")] + TruncatedMessage, + #[error("Unexpected status: {0}")] + UnexpectedStatus(ReturnCode), +} + +impl From for ClientError { + fn from(err: io::Error) -> Self { + if err.kind() == io::ErrorKind::WouldBlock { + ClientError::NotReady + } else { + ClientError::Io(err) + } + } +} + +/// Client result. +pub type ClientResult = Result; + +/// Client result consisting in a single status line +pub type ClientStatus = ClientResult; + +/// Client name +#[derive(Debug, Clone)] +pub struct ClientName { + pub user: String, + pub application: String, + pub component: String, +} + +impl ClientName { + pub fn new(user: &str, application: &str) -> Self { + ClientName::with_component(user, application, "main") + } + + pub fn with_component(user: &str, application: &str, component: &str) -> Self { + ClientName { + user: user.to_string(), + application: application.to_string(), + component: component.to_string(), + } + } +} #[cfg(test)] mod tests { use std::str::FromStr; - use super::SynthesisVoice; + use super::{MessageScope, SynthesisVoice}; #[test] fn parse_synthesis_voice() { @@ -400,4 +480,14 @@ mod tests { assert_eq!("eo", v2.language.unwrap()); assert!(matches!(v2.dialect, None)); } + + #[test] + fn format_message_scope() { + assert_eq!("self", format!("{}", MessageScope::Last).as_str()); + assert_eq!("all", format!("{}", MessageScope::All).as_str()); + assert_eq!( + "123", + format!("{}", MessageScope::Message("123".to_string())).as_str() + ); + } } diff --git a/tests/fifo_async_tests.rs b/tests/fifo_async_tests.rs index 86cfb71..3c90f48 100644 --- a/tests/fifo_async_tests.rs +++ b/tests/fifo_async_tests.rs @@ -20,12 +20,6 @@ mod server; #[cfg(feature = "async-mio")] use server::Server; -#[cfg(feature = "async-mio")] -enum Answer { - Str(&'static str), - Int(i8), -} - #[cfg(feature = "async-mio")] struct State<'a, 'b> { pub done: bool, @@ -33,12 +27,12 @@ struct State<'a, 'b> { pub writable: bool, pub start_get: bool, pub iter_requests: Iter<'a, Request>, - pub iter_answers: Iter<'b, Answer>, + pub iter_answers: Iter<'b, &'static str>, } #[cfg(feature = "async-mio")] impl<'a, 'b> State<'a, 'b> { - fn new(iter_requests: Iter<'a, Request>, iter_answers: Iter<'b, Answer>) -> Self { + fn new(iter_requests: Iter<'a, Request>, iter_answers: Iter<'b, &'static str>) -> Self { State { done: false, countdown: 50, @@ -65,24 +59,9 @@ impl<'a, 'b> State<'a, 'b> { } } - fn assert_string(&mut self, val: &str) { - match self.iter_answers.next() { - Some(Answer::Str(expected_val)) => assert_eq!(expected_val, &val), - Some(Answer::Int(expected_val)) => panic!( - "expecting integer {} instead of string '{}'", - expected_val, val - ), - None => panic!("no more answers"), - } - } - - fn assert_integer(&mut self, val: i8) { + fn assert_answer(&mut self, val: &str) { match self.iter_answers.next() { - Some(Answer::Int(expected_val)) => assert_eq!(expected_val, &val), - Some(Answer::Str(expected_val)) => panic!( - "expecting string '{}' instead of integer {}", - expected_val, val - ), + Some(expected_val) => assert_eq!(expected_val, &val), None => panic!("no more answers"), } } @@ -106,7 +85,7 @@ fn basic_async_communication() -> ClientResult<()> { ]; let get_requests = vec![Request::GetOutputModule, Request::GetRate]; - let get_answers = vec![Answer::Str("espeak"), Answer::Int(10)]; + let get_answers = vec!["espeak", "10"]; let mut state = State::new(get_requests.iter(), get_answers.iter()); let socket_dir = tempfile::tempdir()?; @@ -135,8 +114,7 @@ fn basic_async_communication() -> ClientResult<()> { } Response::LanguageSet => client.push(Request::Stop(MessageScope::Last)), Response::Stopped => state.start_get = true, - Response::GetString(val) => state.assert_string(&val), - Response::GetInteger(val) => state.assert_integer(val), + Response::Get(val) => state.assert_answer(&val), result => panic!("Unexpected response: {:?}", result), } if let Some(request) = state.next_request() {