diff --git a/Cargo.toml b/Cargo.toml index 42af7d7..434a837 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ssip-client" -version = "0.2.0" +version = "0.3.0" authors = ["Laurent Pelecq "] edition = "2018" description = "Client API for Speech Dispatcher" @@ -11,9 +11,17 @@ repository = "https://gitlab.com/lp-accessibility/ssip-client" [dependencies] dirs = "4" +mio = { version = "0", optional = true } thiserror = "1" strum = "0.24" strum_macros = "0.24" [dev-dependencies] libc = "0" + +[features] +metal-io = ["mio/net", "mio/os-poll"] + +[[example]] +name = "async-mio" +required-features = ["metal-io"] diff --git a/README.md b/README.md index f962f5f..d54439d 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ Rust SSIP Client Speech Dispatcher [SSIP client library](http://htmlpreview.github.io/?https://github.com/brailcom/speechd/blob/master/doc/ssip.html) in pure rust. +- [x] Unix socket. +- [ ] TCP socket. - [x] Stop, cancel, pause and resume. - [x] List, set voices. - [x] Set rate, pitch, volume. diff --git a/examples/async-mio.rs b/examples/async-mio.rs new file mode 100644 index 0000000..b2790e5 --- /dev/null +++ b/examples/async-mio.rs @@ -0,0 +1,51 @@ +use mio::{Events, Poll, Token}; + +use ssip_client::{ClientName, ClientResult}; + +fn main() -> ClientResult<()> { + let mut poll = Poll::new()?; + let mut events = Events::with_capacity(128); + let mut client = ssip_client::new_default_fifo_client()?; + let token = Token(0); + client.register(&poll, token)?; + + poll.poll(&mut events, None)?; + let mut is_opened = false; + while !is_opened { + for event in &events { + if event.token() == token && event.is_writable() { + println!("opening client"); + match client.open(ClientName::new("joe", "hello")) { + Ok(()) => { + is_opened = true; + break; + } + Err(err) if err.kind() == io::ErrorKing::WouldBlock => {} + Err(err) => panic!("Error opening client: {:?}", err), + } + break; + } + } + } + + poll.poll(&mut events, None)?; + for event in &events { + if event.token() == token && event.is_writable() { + println!("sending message"); + let msg_id = client.say_line("hello")?; + println!("message: {}", msg_id); + + break; + } + } + + poll.poll(&mut events, None)?; + for event in &events { + if event.token() == token && event.is_writable() { + println!("quitting"); + client.quit()?; + break; + } + } + Ok(()) +} diff --git a/examples/hello.rs b/examples/hello.rs index 5c3d3e1..b0a50f7 100644 --- a/examples/hello.rs +++ b/examples/hello.rs @@ -1,8 +1,11 @@ -use ssip_client::{ClientName, ClientResult}; +use ssip_client::{new_default_fifo_client, ClientName, ClientResult, OK_CLIENT_NAME_SET}; fn main() -> ClientResult<()> { - let mut client = ssip_client::new_default_fifo_client(&ClientName::new("joe", "hello"), None)?; - let msg_id = client.say_line("hello")?; + let mut client = new_default_fifo_client(None)?; + client + .open(ClientName::new("joe", "hello"))? + .check_status(OK_CLIENT_NAME_SET)?; + let msg_id = client.speak()?.send_line("hello")?.receive_message_id()?; println!("message: {}", msg_id); client.quit()?; Ok(()) diff --git a/examples/list.rs b/examples/list.rs index 3c94f9a..55beeda 100644 --- a/examples/list.rs +++ b/examples/list.rs @@ -1,4 +1,7 @@ -use ssip_client::{ClientName, ClientResult, SynthesisVoice}; +use ssip_client::{ + ClientName, ClientResult, SynthesisVoice, OK_CLIENT_NAME_SET, OK_OUTPUT_MODULES_LIST_SENT, + OK_VOICES_LIST_SENT, +}; fn voice_to_string(voice: &SynthesisVoice) -> String { match &voice.language { @@ -18,28 +21,32 @@ fn print_list(title: &str, values: &[String]) { } fn main() -> ClientResult<()> { - let mut client = ssip_client::new_default_fifo_client(&ClientName::new("joe", "list"), None)?; + let mut client = ssip_client::new_default_fifo_client(None)?; + client + .open(ClientName::new("joe", "list"))? + .check_status(OK_CLIENT_NAME_SET)?; + const OUTPUT_MODULE_TITLE: &str = "output modules"; - match client.list_output_modules() { - Ok(values) => print_list(OUTPUT_MODULE_TITLE, &values), - Err(err) => eprintln!("{}: {}", OUTPUT_MODULE_TITLE, err), - }; + let modules = client + .list_output_modules()? + .receive_lines(OK_OUTPUT_MODULES_LIST_SENT)?; + print_list(OUTPUT_MODULE_TITLE, &modules); + const VOICE_TYPES_TITLE: &str = "voice types"; - match client.list_voice_types() { - Ok(values) => print_list(VOICE_TYPES_TITLE, &values), - Err(err) => eprintln!("{}: {}", VOICE_TYPES_TITLE, err), - }; + let vtypes = client + .list_voice_types()? + .receive_lines(OK_VOICES_LIST_SENT)?; + print_list(VOICE_TYPES_TITLE, &vtypes); + const SYNTHESIS_VOICES_TITLE: &str = "synthesis voices"; - match client.list_synthesis_voices() { - Ok(values) => print_list( - SYNTHESIS_VOICES_TITLE, - &values - .iter() - .map(|ref v| voice_to_string(v)) - .collect::>(), - ), - Err(err) => eprintln!("{}: {}", VOICE_TYPES_TITLE, err), - }; + let voices = client.list_synthesis_voices()?.receive_synthesis_voices()?; + print_list( + SYNTHESIS_VOICES_TITLE, + &voices + .iter() + .map(|ref v| voice_to_string(v)) + .collect::>(), + ); client.quit().unwrap(); Ok(()) diff --git a/examples/notifications.rs b/examples/notifications.rs index 828d987..cce9ff7 100644 --- a/examples/notifications.rs +++ b/examples/notifications.rs @@ -1,10 +1,12 @@ -use ssip_client::{ClientName, ClientResult, EventType, NotificationType}; +use ssip_client::{ClientName, ClientResult, EventType, NotificationType, OK_CLIENT_NAME_SET}; fn main() -> ClientResult<()> { - let mut client = - ssip_client::new_default_fifo_client(&ClientName::new("joe", "notifications"), None)?; + let mut client = ssip_client::new_default_fifo_client(None)?; + client + .open(ClientName::new("joe", "notifications"))? + .check_status(OK_CLIENT_NAME_SET)?; client.enable_notification(NotificationType::All).unwrap(); - let msg_id = client.say_line("hello")?; + let msg_id = client.speak()?.send_line("hello")?.receive_message_id()?; println!("message: {}", msg_id); loop { match client.receive_event() { diff --git a/src/client.rs b/src/client.rs index 8bc3f29..7cacea4 100644 --- a/src/client.rs +++ b/src/client.rs @@ -11,10 +11,11 @@ use std::io::{self, Read, Write}; use std::str::FromStr; use thiserror::Error as ThisError; -use crate::constants::OK_RECEIVING_DATA; +use crate::constants::{OK_MESSAGE_QUEUED, OK_VOICES_LIST_SENT}; +use crate::protocol::{send_lines, write_lines}; use crate::types::{ CapitalLettersRecognitionMode, ClientScope, Event, KeyName, MessageId, MessageScope, - NotificationType, Priority, PunctuationMode, StatusLine, SynthesisVoice, + NotificationType, Priority, PunctuationMode, ReturnCode, Source, StatusLine, SynthesisVoice, }; /// Client error, either I/O error or SSIP error. @@ -32,6 +33,8 @@ pub enum ClientError { TooManyLines, #[error("Truncated message")] TruncatedMessage, + #[error("Unexpected status: {0}")] + UnexpectedStatus(ReturnCode), } impl From for ClientError { @@ -76,20 +79,26 @@ fn on_off(value: bool) -> &'static str { } } -macro_rules! client_setter { +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) -> ClientStatus { + 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.input, &mut self.output, &[line.as_str()]) + + 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_setter!( + client_send!( $name, $doc, $scope, @@ -100,24 +109,26 @@ macro_rules! client_setter { }; ($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) -> ClientStatus { - send_line!(&mut self.input, &mut self.output, $fmt, $value) + 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_setter!($name, $doc, $value_name as $value_type, $fmt, $value_name); + 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) -> ClientStatus { - send_line!(&mut self.input, &mut self.output, $line) + pub fn $name(&mut self) -> ClientResult<&mut Client> { + send_lines(&mut self.output, &[$line])?; + Ok(self) } }; } -macro_rules! client_boolean_setter { +macro_rules! client_send_boolean { ($name:ident, $doc:expr, $scope:ident, $value_name:ident, $fmt:expr) => { - client_setter!( + client_send!( $name, $doc, $scope, @@ -127,13 +138,13 @@ macro_rules! client_boolean_setter { ); }; ($name:ident, $doc:expr, $value_name:ident, $fmt:expr) => { - client_setter!($name, $doc, $value_name as bool, $fmt, on_off($value_name)); + client_send!($name, $doc, $value_name as bool, $fmt, on_off($value_name)); }; } -macro_rules! client_range_setter { +macro_rules! client_send_range { ($name:ident, $doc:expr, $scope:ident, $value_name:ident, $fmt:expr) => { - client_setter!( + client_send!( $name, $doc, $scope, @@ -144,63 +155,40 @@ macro_rules! client_range_setter { }; } -macro_rules! client_getter { - ($name:ident, $doc:expr, $line:expr) => { - #[doc=$doc] - pub fn $name(&mut self) -> ClientResult> { - let mut result = Vec::new(); - send_lines!(&mut self.input, &mut self.output, &[&$line], &mut result)?; - Ok(result) - } - }; -} - -macro_rules! client_single_getter { - ($name:ident, $doc:expr, $value_type:ty, $line:expr) => { - #[doc=$doc] - pub fn $name(&mut self) -> ClientResult<$value_type> { - let mut lines = Vec::new(); - send_lines!(&mut self.input, &mut self.output, &[&$line], &mut lines)?; - let result = Client::::parse_single_value(&lines)? - .parse() - .map_err(|_| ClientError::InvalidType)?; - Ok(result) - } - }; - ($name:ident, $doc:expr, $line:expr) => { - #[doc=$doc] - pub fn $name(&mut self) -> ClientResult { - let mut lines = Vec::new(); - send_lines!(&mut self.input, &mut self.output, &[&$line], &mut lines)?; - Client::::parse_single_value(&lines) - } - }; -} - /// SSIP client on generic stream +#[cfg(not(feature = "metal-io"))] pub struct Client { input: io::BufReader, output: io::BufWriter, } -impl Client { - pub(crate) fn new( - mut input: io::BufReader, - mut output: io::BufWriter, - client_name: &ClientName, - ) -> ClientResult { +#[cfg(feature = "metal-io")] +pub struct Client { + input: io::BufReader, + output: io::BufWriter, + socket: S, +} + +impl Client { + #[cfg(not(feature = "metal-io"))] + pub(crate) fn new(input: io::BufReader, output: io::BufWriter) -> ClientResult { // https://stackoverflow.com/questions/58467659/how-to-store-tcpstream-with-bufreader-and-bufwriter-in-a-data-structure - send_line!( - &mut input, - &mut output, - "SET self CLIENT_NAME {}:{}:{}", - client_name.user, - client_name.application, - client_name.component - )?; Ok(Self { input, output }) } + #[cfg(feature = "metal-io")] + pub(crate) fn new( + input: io::BufReader, + output: io::BufWriter, + socket: S, + ) -> ClientResult { + Ok(Self { + socket, + input, + output, + }) + } + fn parse_single_value(lines: &[String]) -> ClientResult { match lines.len() { 0 => Err(ClientError::NoLine), @@ -209,88 +197,98 @@ impl Client { } } - /// Send text to server - pub fn say_text(&mut self, lines: &[&str]) -> ClientResult { - let status = send_line!(&mut self.input, &mut self.output, "SPEAK")?; - if status.code == OK_RECEIVING_DATA { - const END_OF_DATA: [&str; 1] = ["."]; - crate::protocol::write_lines(&mut self.output, lines)?; - let mut answer = Vec::new(); - send_lines!(&mut self.input, &mut self.output, &END_OF_DATA, &mut answer)?; - Client::::parse_single_value(&answer) - } else { - Err(ClientError::Ssip(status)) - } + pub fn open(&mut self, client_name: ClientName) -> ClientResult<&mut Client> { + send_lines( + &mut self.output, + &[format!( + "SET self CLIENT_NAME {}:{}:{}", + client_name.user, client_name.application, client_name.component + ) + .as_str()], + )?; + Ok(self) + } + + /// Initiate communitation to send text to speak + pub fn speak(&mut self) -> ClientResult<&mut Client> { + send_lines(&mut self.output, &["SPEAK"])?; + Ok(self) + } + + /// Send lines + pub fn send_lines(&mut self, lines: &[&str]) -> ClientResult<&mut Client> { + const END_OF_DATA: [&str; 1] = ["."]; + write_lines(&mut self.output, lines)?; + send_lines(&mut self.output, &END_OF_DATA)?; + Ok(self) } - /// Send a single line to the server - pub fn say_line(&mut self, line: &str) -> ClientResult { - let lines: [&str; 1] = [line]; - self.say_text(&lines) + /// Send a line + pub fn send_line(&mut self, line: &str) -> ClientResult<&mut Client> { + const END_OF_DATA: &str = "."; + self.send_lines(&[line, END_OF_DATA]) } - /// Send a char to the server - pub fn say_char(&mut self, ch: char) -> ClientResult { - let line = format!("CHAR {}", ch); - let mut answer = Vec::new(); - send_lines!(&mut self.input, &mut self.output, &[&line], &mut answer)?; - Client::::parse_single_value(&answer) + /// Send a char + pub fn send_char(&mut self, ch: char) -> ClientResult<&mut Client> { + self.send_lines(&[format!("CHAR {}", ch).as_str()]) } /// Send a symbolic key name - pub fn say_key_name(&mut self, keyname: KeyName) -> ClientResult { - let line = format!("KEY {}", keyname); - let mut answer = Vec::new(); - send_lines!(&mut self.input, &mut self.output, &[&line], &mut answer)?; - Client::::parse_single_value(&answer) + pub fn say_key_name(&mut self, keyname: KeyName) -> ClientResult<&mut Client> { + self.send_lines(&[format!("KEY {}", keyname).as_str()]) } /// Action on a message or a group of messages - fn send_message_command(&mut self, command: &str, scope: MessageScope) -> ClientStatus { + fn send_message_command( + &mut self, + command: &str, + scope: MessageScope, + ) -> ClientResult<&mut Client> { let line = match scope { MessageScope::Last => format!("{} self", command), MessageScope::All => format!("{} all", command), MessageScope::Message(id) => format!("{} {}", command, id), }; - send_lines!(&mut self.input, &mut self.output, &[line.as_str()]) + send_lines(&mut self.output, &[line.as_str()])?; + Ok(self) } /// Stop current message - pub fn stop(&mut self, scope: MessageScope) -> ClientStatus { + pub fn stop(&mut self, scope: MessageScope) -> ClientResult<&mut Client> { self.send_message_command("STOP", scope) } /// Cancel current message - pub fn cancel(&mut self, scope: MessageScope) -> ClientStatus { + pub fn cancel(&mut self, scope: MessageScope) -> ClientResult<&mut Client> { self.send_message_command("CANCEL", scope) } /// Pause current message - pub fn pause(&mut self, scope: MessageScope) -> ClientStatus { + pub fn pause(&mut self, scope: MessageScope) -> ClientResult<&mut Client> { self.send_message_command("PAUSE", scope) } /// Resume current message - pub fn resume(&mut self, scope: MessageScope) -> ClientStatus { + pub fn resume(&mut self, scope: MessageScope) -> ClientResult<&mut Client> { self.send_message_command("RESUME", scope) } - client_setter!( + client_send!( set_priority, "Set message priority", priority as Priority, "SET self PRIORITY {}" ); - /// Set debug mode. Return the log location. - pub fn set_debug(&mut self, value: bool) -> ClientResult { - let line = format!("SET all DEBUG {}", on_off(value)); - let mut answer = Vec::new(); - send_lines!(&mut self.input, &mut self.output, &[&line], &mut answer)?; - Client::::parse_single_value(&answer) - } + client_send_boolean!( + set_debug, + "Set debug mode. Return the log location", + value, + "SET all DEBUG {}" + ); - client_setter!( + client_send!( set_output_module, "Set output module", scope, @@ -298,19 +296,19 @@ impl Client { "SET {} OUTPUT_MODULE {}" ); - client_single_getter!( + client_send!( get_output_module, "Get the current output module", "GET OUTPUT_MODULE" ); - client_getter!( + client_send!( list_output_modules, "List the available output modules", "LIST OUTPUT_MODULES" ); - client_setter!( + client_send!( set_language, "Set language code", scope, @@ -318,16 +316,16 @@ impl Client { "SET {} LANGUAGE {}" ); - client_single_getter!(get_language, "Get the current language", "GET LANGUAGE"); + client_send!(get_language, "Get the current language", "GET LANGUAGE"); - client_boolean_setter!( + client_send_boolean!( set_ssml_mode, "Set SSML mode (Speech Synthesis Markup Language)", value, "SET self SSML_MODE {}" ); - client_setter!( + client_send!( set_punctuation_mode, "Set punctuation mode", scope, @@ -335,7 +333,7 @@ impl Client { "SET {} PUNCTUATION {}" ); - client_boolean_setter!( + client_send_boolean!( set_spelling, "Set spelling on or off", scope, @@ -343,7 +341,7 @@ impl Client { "SET {} SPELLING {}" ); - client_setter!( + client_send!( set_capital_letter_recogn, "Set capital letters recognition mode", scope, @@ -351,7 +349,7 @@ impl Client { "SET {} CAP_LET_RECOGN {}" ); - client_setter!( + client_send!( set_voice_type, "Set the voice type (MALE1, FEMALE1, …)", scope, @@ -359,19 +357,19 @@ impl Client { "SET {} VOICE_TYPE {}" ); - client_single_getter!( + client_send!( get_voice_type, "Get the current pre-defined voice", "GET VOICE_TYPE" ); - client_getter!( + client_send!( list_voice_types, "List the available symbolic voice names", "LIST VOICES" ); - client_setter!( + client_send!( set_synthesis_voice, "Set the voice", scope, @@ -379,24 +377,13 @@ impl Client { "SET {} SYNTHESIS_VOICE {}" ); - /// Lists the available voices for the current synthesizer. - pub fn list_synthesis_voices(&mut self) -> ClientResult> { - let mut result = Vec::new(); - send_lines!( - &mut self.input, - &mut self.output, - &["LIST SYNTHESIS_VOICES"], - &mut result - )?; - let mut voices = Vec::new(); - for name in result.iter() { - let voice = SynthesisVoice::from_str(name.as_str())?; - voices.push(voice); - } - Ok(voices) - } + client_send!( + list_synthesis_voices, + "Lists the available voices for the current synthesizer", + "LIST SYNTHESIS_VOICES" + ); - client_range_setter!( + 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, @@ -404,9 +391,9 @@ impl Client { "SET {} RATE {}" ); - client_single_getter!(get_rate, "Get the current rate of speech.", u8, "GET RATE"); + client_send!(get_rate, "Get the current rate of speech.", "GET RATE"); - client_range_setter!( + client_send_range!( set_pitch, "Set the pitch of speech. n is an integer value within the range from -100 to 100.", scope, @@ -414,9 +401,9 @@ impl Client { "SET {} PITCH {}" ); - client_single_getter!(get_pitch, "Get the current pitch value.", u8, "GET PITCH"); + client_send!(get_pitch, "Get the current pitch value.", "GET PITCH"); - client_range_setter!( + client_send_range!( set_volume, "Set the volume of speech. n is an integer value within the range from -100 to 100.", scope, @@ -424,7 +411,7 @@ impl Client { "SET {} VOLUME {}" ); - client_setter!( + 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, @@ -432,7 +419,7 @@ impl Client { "SET {} PAUSE_CONTEXT {}" ); - client_boolean_setter!( + client_send_boolean!( set_history, "Enable or disable history of received messages.", scope, @@ -440,28 +427,85 @@ impl Client { "SET {} HISTORY {}" ); - client_single_getter!(get_volume, "Get the current volume.", u8, "GET VOLUME"); + client_send!(get_volume, "Get the current volume.", "GET VOLUME"); - client_setter!(block_begin, "Open a block", "BLOCK BEGIN"); + client_send!(block_begin, "Open a block", "BLOCK BEGIN"); - client_setter!(block_end, "End a block", "BLOCK END"); + client_send!(block_end, "End a block", "BLOCK END"); - client_setter!(quit, "Close the connection", "QUIT"); + client_send!(quit, "Close the connection", "QUIT"); - client_setter!( + client_send!( enable_notification, "Enable notification events", value as NotificationType, "SET self NOTIFICATION {} on" ); - client_setter!( + client_send!( disable_notification, "Disable notification events", value as NotificationType, "SET self NOTIFICATION {} off" ); + /// Receive answer from server + pub fn receive(&mut self, lines: &mut Vec) -> ClientStatus { + crate::protocol::receive_answer(&mut self.input, Some(lines)) + } + + /// Check status of answer, discard lines. + pub fn check_status(&mut self, expected_code: ReturnCode) -> ClientResult<&mut Client> { + crate::protocol::receive_answer(&mut self.input, None).and_then(|status| { + if status.code == expected_code { + Ok(self) + } else { + Err(ClientError::UnexpectedStatus(status.code)) + } + }) + } + + /// Receive lines + pub fn receive_lines(&mut self, expected_code: ReturnCode) -> ClientResult> { + let mut lines = Vec::new(); + let status = self.receive(&mut lines)?; + if status.code == expected_code { + Ok(lines) + } else { + Err(ClientError::UnexpectedStatus(status.code)) + } + } + + /// Receive a single string + pub fn receive_string(&mut self, expected_code: ReturnCode) -> ClientResult { + self.receive_lines(expected_code) + .and_then(|lines| Client::::parse_single_value(&lines)) + } + + /// Receive integer + pub fn receive_u8(&mut self, expected_code: ReturnCode) -> ClientResult { + self.receive_string(expected_code) + .and_then(|s| s.parse().map_err(|_| ClientError::InvalidType)) + } + + /// Receive message id + pub fn receive_message_id(&mut self) -> ClientResult { + self.receive_string(OK_MESSAGE_QUEUED) + .and_then(|s| s.parse().map_err(|_| ClientError::InvalidType)) + } + + /// Receive a list of synthesis voices + pub fn receive_synthesis_voices(&mut self) -> ClientResult> { + self.receive_lines(OK_VOICES_LIST_SENT).and_then(|lines| { + let mut voices = Vec::new(); + for name in lines.iter() { + let voice = SynthesisVoice::from_str(name.as_str())?; + voices.push(voice); + } + Ok(voices) + }) + } + /// Receive a notification pub fn receive_event(&mut self) -> ClientResult { let mut lines = Vec::new(); @@ -490,13 +534,27 @@ impl Client { } }) } + + #[cfg(feature = "metal-io")] + pub fn register(&mut self, poll: &mio::Poll, token: mio::Token) -> ClientResult<()> { + poll.registry().register( + &mut self.socket, + token, + mio::Interest::READABLE | mio::Interest::WRITABLE, + )?; + Ok(()) + } } #[cfg(test)] mod tests { + #[cfg(not(feature = "metal-io"))] use std::net::TcpStream; + #[cfg(feature = "metal-io")] + use mio::net::TcpStream; + use super::{Client, ClientError}; #[test] diff --git a/src/fifo.rs b/src/fifo.rs index 1f1962f..6c4ec74 100644 --- a/src/fifo.rs +++ b/src/fifo.rs @@ -7,12 +7,8 @@ // option. All files in the project carrying such notice may not be copied, // modified, or distributed except according to those terms. -use std::io::{self, BufReader, BufWriter}; -use std::os::unix::net::UnixStream; -use std::path::{Path, PathBuf}; -use std::time::Duration; - -use crate::client::{Client, ClientName, ClientResult}; +use std::io; +use std::path::PathBuf; const SPEECHD_APPLICATION_NAME: &str = "speech-dispatcher"; const SPEECHD_SOCKET_NAME: &str = "speechd.sock"; @@ -30,41 +26,73 @@ fn speech_dispatcher_socket() -> io::Result { } } -/// Create a pair of FIFO reader and writer -fn new_pair

( - socket_path: P, - read_timeout: Option, -) -> io::Result<(BufReader, BufWriter)> -where - P: AsRef, -{ - let stream = UnixStream::connect(socket_path.as_ref())?; - stream.set_read_timeout(read_timeout)?; - Ok((BufReader::new(stream.try_clone()?), BufWriter::new(stream))) -} +#[cfg(not(feature = "metal-io"))] +mod synchronous { + use std::io::{BufReader, BufWriter}; + pub use std::os::unix::net::UnixStream; + use std::path::Path; + use std::time::Duration; -/// New FIFO client -pub fn new_client

( - socket_path: P, - client_name: &ClientName, - read_timeout: Option, -) -> ClientResult> -where - P: AsRef, -{ - let (input, output) = new_pair(&socket_path, read_timeout)?; - Client::new(input, output, client_name) + use crate::client::{Client, ClientResult}; + + /// New FIFO client + pub fn new_fifo_client

( + socket_path: P, + read_timeout: Option, + ) -> ClientResult> + where + P: AsRef, + { + let stream = UnixStream::connect(socket_path.as_ref())?; + stream.set_read_timeout(read_timeout)?; + Client::new(BufReader::new(stream.try_clone()?), BufWriter::new(stream)) + } + + /// New FIFO client on the standard socket `${XDG_RUNTIME_DIR}/speech-dispatcher/speechd.sock` + pub fn new_default_fifo_client( + read_timeout: Option, + ) -> ClientResult> { + let socket_path = super::speech_dispatcher_socket()?; + new_fifo_client(socket_path.as_path(), read_timeout) + } } -/// New FIFO client on the standard socket `${XDG_RUNTIME_DIR}/speech-dispatcher/speechd.sock` -pub fn new_default_client( - client_name: &ClientName, - read_timeout: Option, -) -> ClientResult> { - let socket_path = speech_dispatcher_socket()?; - new_client(socket_path.as_path(), client_name, read_timeout) +#[cfg(not(feature = "metal-io"))] +pub use synchronous::{new_default_fifo_client, new_fifo_client, UnixStream}; + +#[cfg(feature = "metal-io")] +mod asynchronous { + pub use mio::net::UnixStream; + use std::io::{BufReader, BufWriter}; + use std::os::unix::net::UnixStream as StdUnixStream; + use std::path::Path; + + use crate::client::{Client, ClientResult}; + + /// New FIFO client + pub fn new_fifo_client

(socket_path: P) -> ClientResult> + where + P: AsRef, + { + let stream = StdUnixStream::connect(socket_path.as_ref())?; + stream.set_nonblocking(true)?; + Client::new( + BufReader::new(UnixStream::from_std(stream.try_clone()?)), + BufWriter::new(UnixStream::from_std(stream.try_clone()?)), + UnixStream::from_std(stream), + ) + } + + /// New FIFO client on the standard socket `${XDG_RUNTIME_DIR}/speech-dispatcher/speechd.sock` + pub fn new_default_fifo_client() -> ClientResult> { + let socket_path = super::speech_dispatcher_socket()?; + new_fifo_client(socket_path.as_path()) + } } +#[cfg(feature = "metal-io")] +pub use asynchronous::{new_default_fifo_client, new_fifo_client, UnixStream}; + #[cfg(test)] mod tests { diff --git a/src/lib.rs b/src/lib.rs index 90f5c25..b193490 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,9 +16,12 @@ //! //! Example //! ```no_run -//! use ssip_client::{new_default_fifo_client, ClientName}; -//! let mut client = new_default_fifo_client(&ClientName::new("joe", "hello"), None)?; -//! let msg_id = client.say_line("hello")?; +//! use ssip_client::{new_default_fifo_client, ClientName, OK_CLIENT_NAME_SET}; +//! let mut client = new_default_fifo_client(None)?; +//! client +//! .open(ClientName::new("joe", "hello"))? +//! .check_status(OK_CLIENT_NAME_SET)?; +//! let msg_id = client.speak()?.send_line("hello")?.receive_message_id()?; //! client.quit()?; //! # Ok::<(), ssip_client::ClientError>(()) //! ``` @@ -33,7 +36,6 @@ mod types; pub use client::{Client, ClientError, ClientName, ClientResult, ClientStatus}; pub use constants::*; -pub use fifo::new_client as new_fifo_client; -pub use fifo::new_default_client as new_default_fifo_client; +pub use fifo::{new_default_fifo_client, new_fifo_client}; pub use types::StatusLine; pub use types::*; diff --git a/src/protocol.rs b/src/protocol.rs index a0793a1..306e268 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -21,26 +21,6 @@ macro_rules! invalid_input { }; } -macro_rules! send_lines { - ($input:expr, $output:expr, $lines:expr) => { - crate::protocol::send_lines($output, $lines) - .and_then(|()| crate::protocol::receive_answer($input, None)) - }; - ($input:expr, $output:expr, $lines:expr, $out:expr) => { - crate::protocol::send_lines($output, $lines) - .and_then(|()| crate::protocol::receive_answer($input, Some($out))) - }; -} - -macro_rules! send_line { - ($input:expr, $output:expr, $line:expr) => { - send_lines!($input, $output, &[$line]) - }; - ($input:expr, $output:expr, $fmt:expr, $($arg:tt)*) => { - send_line!($input, $output, format!($fmt, $($arg)*).as_str()) - }; -} - pub(crate) fn write_lines(output: &mut dyn Write, lines: &[&str]) -> ClientResult<()> { for line in lines.iter() { output.write_all(line.as_bytes())?; diff --git a/src/types.rs b/src/types.rs index 85b1b3f..d3a4237 100644 --- a/src/types.rs +++ b/src/types.rs @@ -361,6 +361,12 @@ impl fmt::Display for StatusLine { } } +#[cfg(not(feature = "metal-io"))] +pub use std::fmt::Debug as Source; // Trick to have common implementation for sync and async. + +#[cfg(feature = "metal-io")] +pub use mio::event::Source; + #[cfg(test)] mod tests { diff --git a/tests/fifo_client_tests.rs b/tests/fifo_client_tests.rs index 01d631b..6031834 100644 --- a/tests/fifo_client_tests.rs +++ b/tests/fifo_client_tests.rs @@ -7,10 +7,17 @@ // modified, or distributed except according to those terms. use std::io::{self, BufRead, BufReader, BufWriter, Write}; -use std::os::unix::net::{UnixListener, UnixStream}; use std::path::{Path, PathBuf}; use std::thread; +use std::os::unix::net::UnixListener; + +#[cfg(not(feature = "metal-io"))] +use std::os::unix::net::UnixStream; + +#[cfg(feature = "metal-io")] +use mio::net::UnixStream; + use ssip_client::*; struct Server { @@ -75,6 +82,17 @@ impl Server { } } +#[cfg(not(feature = "metal-io"))] +pub fn new_fifo_client

(socket_path: P) -> ClientResult> +where + P: AsRef, +{ + ssip_client::new_fifo_client(&socket_path, None) +} + +#[cfg(feature = "metal-io")] +use ssip_client::new_fifo_client; + /// Create a server and run the client /// /// The communication is an array of (["question", ...], "response") @@ -91,9 +109,12 @@ where let mut process_wrapper = std::panic::AssertUnwindSafe(process); let result = std::panic::catch_unwind(move || { let handle = Server::run(&server_path, communication); - let mut client = - ssip_client::new_fifo_client(&server_path, &ClientName::new("test", "test"), None) - .unwrap(); + let mut client = new_fifo_client(&server_path).unwrap(); + client + .open(ClientName::new("test", "test")) + .unwrap() + .check_status(OK_CLIENT_NAME_SET) + .unwrap(); process_wrapper(&mut client).unwrap(); handle.join().unwrap() }); @@ -115,7 +136,7 @@ fn connect_and_quit() -> io::Result<()> { (&["QUIT\r\n"], "231 HAPPY HACKING\r\n"), ], |client| { - assert_eq!(OK_BYE, client.quit().unwrap().code); + client.quit().unwrap().check_status(OK_BYE).unwrap(); Ok(()) }, ) @@ -133,7 +154,18 @@ fn say_one_line() -> io::Result<()> { ), ], |client| { - assert_eq!("21", client.say_line("Hello, world").unwrap(),); + assert_eq!( + "21", + client + .speak() + .unwrap() + .check_status(OK_RECEIVING_DATA) + .unwrap() + .send_line("Hello, world") + .unwrap() + .receive_message_id() + .unwrap() + ); Ok(()) }, ) @@ -146,8 +178,7 @@ macro_rules! test_setter { test_client( &[SET_CLIENT_COMMUNICATION, (&[$question], $answer)], |client| { - let status = client.$setter($($arg)*).unwrap(); - assert_eq!($code, status.code); + client.$setter($($arg)*).unwrap().check_status($code).unwrap(); Ok(()) }, ) @@ -156,29 +187,32 @@ macro_rules! test_setter { } macro_rules! test_getter { - ($getter:ident, $question:expr, $answer:expr, $value:expr) => { + ($getter:ident, $receive:ident, $question:expr, $answer:expr, $value:expr) => { #[test] fn $getter() -> io::Result<()> { test_client( &[SET_CLIENT_COMMUNICATION, (&[$question], $answer)], |client| { - let value = client.$getter().unwrap(); + let value = client.$getter().unwrap().$receive(251).unwrap(); assert_eq!($value, value); Ok(()) }, ) } }; + ($getter:ident, $question:expr, $answer:expr, $value:expr) => { + test_getter!($getter, receive_string, $question, $answer, $value); + }; } macro_rules! test_list { - ($getter:ident, $question:expr, $answer:expr, $values:expr) => { + ($getter:ident, $question:expr, $answer:expr, $code:expr, $values:expr) => { #[test] fn $getter() -> io::Result<()> { test_client( &[SET_CLIENT_COMMUNICATION, (&[$question], $answer)], |client| { - let values = client.$getter().unwrap(); + let values = client.$getter().unwrap().receive_lines($code).unwrap(); assert_eq!($values, values.as_slice()); Ok(()) }, @@ -206,7 +240,11 @@ fn set_debug() -> io::Result<()> { ), ], |client| { - let output = client.set_debug(true).unwrap(); + let output = client + .set_debug(true) + .unwrap() + .receive_string(OK_DEBUG_SET) + .unwrap(); assert_eq!("/run/user/100/speech-dispatcher/log/debug", output); Ok(()) }, @@ -233,6 +271,7 @@ test_list!( list_output_modules, "LIST OUTPUT_MODULES\r\n", "250-espeak-ng\r\n250-festival\r\n250 OK MODULE LIST SENT\r\n", + 250, &["espeak-ng", "festival"] ); @@ -263,6 +302,7 @@ test_setter!( test_getter!( get_rate, + receive_u8, "GET RATE\r\n", "251-0\r\n251 OK GET RETURNED\r\n", 0 @@ -279,6 +319,7 @@ test_setter!( test_getter!( get_volume, + receive_u8, "GET VOLUME\r\n", "251-100\r\n251 OK GET RETURNED\r\n", 100 @@ -286,6 +327,7 @@ test_getter!( test_getter!( get_pitch, + receive_u8, "GET PITCH\r\n", "251-0\r\n251 OK GET RETURNED\r\n", 0 @@ -355,6 +397,7 @@ test_list!( list_voice_types, "LIST VOICES\r\n", "249-MALE1\r\n249-MALE2\r\n249-FEMALE1\r\n249-FEMALE2\r\n249-CHILD_MALE\r\n249-CHILD_FEMALE\r\n249 OK VOICE LIST SENT\r\n", + 249, &[ "MALE1", "MALE2", "FEMALE1", "FEMALE2", "CHILD_MALE", "CHILD_FEMALE" ] ); @@ -369,7 +412,7 @@ fn list_synthesis_voices() -> io::Result<()> { ), ], |client| { - let voices = client.list_synthesis_voices().unwrap(); + let voices = client.list_synthesis_voices().unwrap().receive_synthesis_voices().unwrap(); let expected_voices: [SynthesisVoice; 3] = [ SynthesisVoice::new("Amharic", Some("am"), None), SynthesisVoice::new("Greek+Auntie", Some("el"), Some("Auntie")), SynthesisVoice::new("Vietnamese (Southern)+shelby", Some("vi-VN-X-SOUTH"), Some("shelby")), @@ -395,7 +438,18 @@ fn receive_notification() -> io::Result<()> { ), ], |client| { - assert_eq!("21", client.say_line("Hello, world").unwrap(),); + assert_eq!( + "21", + client + .speak() + .unwrap() + .check_status(OK_RECEIVING_DATA) + .unwrap() + .send_line("Hello, world") + .unwrap() + .receive_message_id() + .unwrap() + ); match client.receive_event() { Ok(Event { ntype: EventType::Begin,