commit
0914f8b191
@ -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…
Reference in new issue