initial implementation to connect and quit

main
Laurent Pelecq 3 years ago
commit 0914f8b191

2
.gitignore vendored

@ -0,0 +1,2 @@
/target
Cargo.lock

@ -0,0 +1,13 @@
[package]
name = "ssip-client"
version = "0.1.0"
authors = ["Laurent Pelecq <lpelecq+rust@circoise.eu>"]
edition = "2018"
description = "Client API for Speech Dispatcher"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dirs = "3"
thiserror = "1"

@ -0,0 +1,8 @@
use ssip_client::ClientResult;
fn main() -> ClientResult<()> {
let mut client = ssip_client::new_unix("joe", "list", "main")?;
let status = client.quit()?;
println!("status: {}", status);
Ok(())
}

@ -0,0 +1,76 @@
use std::fmt;
use std::io::{self, Read, Write};
use thiserror::Error as ThisError;
pub type ReturnCode = u16;
/// Command status line
///
/// Consists in a 3-digits code and a message. It can be a success or a failure.
///
/// Examples:
/// - 216 OK OUTPUT MODULE SET
/// - 409 ERR RATE TOO HIGH
#[derive(Debug, PartialEq)]
pub struct StatusLine {
pub code: ReturnCode,
pub message: String,
}
impl fmt::Display for StatusLine {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} {}", self.code, self.message)
}
}
/// Client error, either I/O error or SSIP error.
#[derive(ThisError, Debug)]
pub enum ClientError {
#[error("I/O: {0}")]
Io(io::Error),
#[error("SSIP: {0}")]
Ssip(StatusLine),
}
impl From<io::Error> for ClientError {
fn from(err: io::Error) -> Self {
ClientError::Io(err)
}
}
/// Client result.
pub type ClientResult<T> = Result<T, ClientError>;
/// Client result consisting in a single status line
pub type ClientStatus = ClientResult<StatusLine>;
pub struct Client<S: Read + Write> {
input: io::BufReader<S>,
output: io::BufWriter<S>,
}
impl<S: Read + Write> Client<S> {
pub(crate) fn new(
mut input: io::BufReader<S>,
mut output: io::BufWriter<S>,
user: &str,
application: &str,
component: &str,
) -> ClientResult<Self> {
// https://stackoverflow.com/questions/58467659/how-to-store-tcpstream-with-bufreader-and-bufwriter-in-a-data-structure
execute_command!(
&mut input,
&mut output,
"SET self CLIENT_NAME {}:{}:{}",
user,
application,
component
)?;
Ok(Self { input, output })
}
/// Close the connection
pub fn quit(&mut self) -> ClientStatus {
execute_command!(&mut self.input, &mut self.output, "QUIT")
}
}

@ -0,0 +1,8 @@
#[macro_use]
mod protocol;
mod client;
mod unix;
pub use client::{ClientResult, ClientStatus};
pub use unix::new as new_unix;

@ -0,0 +1,144 @@
use std::io::{self, BufRead, Write};
use crate::client::{ClientError, ClientResult, ClientStatus, StatusLine};
macro_rules! invalid_input {
($msg:expr) => {
ClientError::from(io::Error::new(io::ErrorKind::InvalidInput, $msg))
};
($fmt:expr, $($arg:tt)*) => {
invalid_input!(format!($fmt, $($arg)*).as_str())
};
}
macro_rules! send_command {
($output:expr, $cmd:expr) => {
crate::protocol::send_command($output, $cmd)
};
($output:expr, $fmt:expr, $($arg:tt)*) => {
crate::protocol::send_command($output, format!($fmt, $($arg)*).as_str())
};
}
macro_rules! execute_command {
($input:expr, $output:expr, $cmd:expr) => {
send_command!($output, $cmd)
.and_then(|()|
crate::protocol::receive_answer($input, None))
};
($input:expr, $output:expr, $fmt:expr, $($arg:tt)*) => {
send_command!($output, $fmt, $($arg)*)
.and_then(|()|
crate::protocol::receive_answer($input, None))
};
}
pub(crate) fn send_command(output: &mut dyn Write, command: &str) -> ClientResult<()> {
output.write_all(command.as_bytes())?;
output.write_all(b"\r\n")?;
output.flush()?;
Ok(())
}
/// Parse the status line "OK msg" or "ERR msg"
fn parse_status_line(code: u16, line: &str) -> ClientStatus {
const TOKEN_OK: &str = "OK ";
const OFFSET_OK: usize = TOKEN_OK.len();
const TOKEN_ERR: &str = "ERR ";
const OFFSET_ERR: usize = TOKEN_ERR.len();
if line.starts_with(TOKEN_OK) {
let message = line[OFFSET_OK..].to_string();
Ok(StatusLine { code, message })
} else if line.starts_with(TOKEN_ERR) {
let message = line[OFFSET_ERR..].to_string();
Err(ClientError::Ssip(StatusLine { code, message }))
} else {
let status = StatusLine {
code,
message: line.to_string(),
};
if (300..700).contains(&code) {
Err(ClientError::Ssip(status))
} else {
Ok(status)
}
}
}
pub(crate) fn receive_answer(
input: &mut dyn BufRead,
mut lines: Option<&mut Vec<String>>,
) -> ClientStatus {
loop {
let mut line = String::new();
input.read_line(&mut line).map_err(ClientError::Io)?;
match line.chars().nth(3) {
Some(ch) => match ch {
' ' => match line[0..3].parse::<u16>() {
Ok(code) => return parse_status_line(code, &line[4..].trim_end()),
Err(err) => return Err(invalid_input!(err.to_string())),
},
'-' => match lines {
Some(ref mut lines) => lines.push(line[4..].trim_end().to_string()),
None => return Err(invalid_input!("unexpected line: {}", line)),
},
ch => {
return Err(invalid_input!("expecting space or dash, got {}.", ch));
}
},
None => return Err(invalid_input!("line too short")),
}
}
}
#[cfg(test)]
mod tests {
use std::io::BufReader;
use super::{receive_answer, ClientError};
#[test]
fn single_ok_status_line() {
let mut input = BufReader::new("208 OK CLIENT NAME SET\r\n".as_bytes());
let status = receive_answer(&mut input, None).unwrap();
assert_eq!(208, status.code);
assert_eq!("CLIENT NAME SET", status.message);
}
#[test]
fn single_success_status_line() {
let mut input = BufReader::new("231 HAPPY HACKING\r\n".as_bytes());
let status = receive_answer(&mut input, None).unwrap();
assert_eq!(231, status.code);
assert_eq!("HAPPY HACKING", status.message);
}
#[test]
fn single_err_status_line() {
let mut input = BufReader::new("409 ERR RATE TOO HIGH\r\n".as_bytes());
match receive_answer(&mut input, None).err().unwrap() {
ClientError::Ssip(status) => {
assert_eq!(409, status.code);
assert_eq!("RATE TOO HIGH", status.message);
}
err => panic!("{}: invalid error", err),
}
}
#[test]
fn multi_lines() {
let mut input = BufReader::new(
"249-afrikaans\taf\tnone\r\n249-en-rhotic\ten\tr\r\n249 OK VOICE LIST SENT\r\n"
.as_bytes(),
);
let mut lines = Vec::new();
let status = receive_answer(&mut input, Some(&mut lines)).unwrap();
assert_eq!(249, status.code);
assert_eq!("VOICE LIST SENT", status.message);
assert_eq!(
vec!["afrikaans\taf\tnone", "en-rhotic\ten\tr"],
lines.as_slice()
);
}
}

@ -0,0 +1,29 @@
use std::io;
use std::os::unix::net::UnixStream;
use std::path::PathBuf;
use crate::client::{Client, ClientResult};
const SPEECHD_APPLICATION_NAME: &str = "speech-dispatcher";
const SPEECHD_SOCKET_NAME: &str = "speechd.sock";
/// Return the standard socket according to the [freedesktop.org](https://www.freedesktop.org/) specification.
fn speech_dispatcher_socket() -> io::Result<PathBuf> {
match dirs::runtime_dir() {
Some(runtime_dir) => Ok(runtime_dir
.join(SPEECHD_APPLICATION_NAME)
.join(SPEECHD_SOCKET_NAME)),
None => Err(io::Error::new(
io::ErrorKind::NotFound,
"unix socket not found",
)),
}
}
pub fn new(user: &str, application: &str, component: &str) -> ClientResult<Client<UnixStream>> {
let socket_path = speech_dispatcher_socket()?;
let stream = UnixStream::connect(socket_path)?;
let input = io::BufReader::new(stream.try_clone()?);
let output = io::BufWriter::new(stream);
Client::new(input, output, user, application, component)
}
Loading…
Cancel
Save