ssip_client/
types.rs

1// ssip-client -- Speech Dispatcher client in Rust
2// Copyright (c) 2021 Laurent Pelecq
3//
4// Licensed under the Apache License, Version 2.0
5// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT
6// license <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
7// option. All files in the project carrying such notice may not be copied,
8// modified, or distributed except according to those terms.
9
10use std::fmt;
11use std::io;
12use std::str::FromStr;
13use thiserror::Error as ThisError;
14
15use strum_macros::Display as StrumDisplay;
16
17/// Return code of SSIP commands
18pub type ReturnCode = u16;
19
20/// Message identifier
21pub type MessageId = u32;
22
23/// Client identifier
24pub type ClientId = u32;
25
26/// Message identifiers
27#[derive(Debug, Clone)]
28pub enum MessageScope {
29    /// Last message from current client
30    Last,
31    /// Messages from all clients
32    All,
33    /// Specific message
34    Message(MessageId),
35}
36
37impl fmt::Display for MessageScope {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        match self {
40            MessageScope::Last => write!(f, "self"),
41            MessageScope::All => write!(f, "all"),
42            MessageScope::Message(id) => write!(f, "{id}"),
43        }
44    }
45}
46
47/// Client identifiers
48#[derive(Debug, Clone)]
49pub enum ClientScope {
50    /// Current client
51    Current,
52    /// All clients
53    All,
54    /// Specific client
55    Client(ClientId),
56}
57
58impl fmt::Display for ClientScope {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        match self {
61            ClientScope::Current => write!(f, "self"),
62            ClientScope::All => write!(f, "all"),
63            ClientScope::Client(id) => write!(f, "{id}"),
64        }
65    }
66}
67
68/// Priority
69#[derive(StrumDisplay, Debug, Clone)]
70pub enum Priority {
71    #[strum(serialize = "progress")]
72    Progress,
73    #[strum(serialize = "notification")]
74    Notification,
75    #[strum(serialize = "message")]
76    Message,
77    #[strum(serialize = "text")]
78    Text,
79    #[strum(serialize = "important")]
80    Important,
81}
82
83/// Punctuation mode.
84#[derive(StrumDisplay, Debug, Clone)]
85pub enum PunctuationMode {
86    #[strum(serialize = "none")]
87    None,
88    #[strum(serialize = "some")]
89    Some,
90    #[strum(serialize = "most")]
91    Most,
92    #[strum(serialize = "all")]
93    All,
94}
95
96/// Capital letters recognition mode.
97#[derive(StrumDisplay, Debug, Clone)]
98pub enum CapitalLettersRecognitionMode {
99    #[strum(serialize = "none")]
100    None,
101    #[strum(serialize = "spell")]
102    Spell,
103    #[strum(serialize = "icon")]
104    Icon,
105}
106
107/// Symbolic key names
108#[derive(StrumDisplay, Debug, Clone)]
109pub enum KeyName {
110    #[strum(serialize = "space")]
111    Space,
112    #[strum(serialize = "underscore")]
113    Underscore,
114    #[strum(serialize = "double-quote")]
115    DoubleQuote,
116    #[strum(serialize = "alt")]
117    Alt,
118    #[strum(serialize = "control")]
119    Control,
120    #[strum(serialize = "hyper")]
121    Hyper,
122    #[strum(serialize = "meta")]
123    Meta,
124    #[strum(serialize = "shift")]
125    Shift,
126    #[strum(serialize = "super")]
127    Super,
128    #[strum(serialize = "backspace")]
129    Backspace,
130    #[strum(serialize = "break")]
131    Break,
132    #[strum(serialize = "delete")]
133    Delete,
134    #[strum(serialize = "down")]
135    Down,
136    #[strum(serialize = "end")]
137    End,
138    #[strum(serialize = "enter")]
139    Enter,
140    #[strum(serialize = "escape")]
141    Escape,
142    #[strum(serialize = "f1")]
143    F1,
144    #[strum(serialize = "f2")]
145    F2,
146    #[strum(serialize = "f3")]
147    F3,
148    #[strum(serialize = "f4")]
149    F4,
150    #[strum(serialize = "f5")]
151    F5,
152    #[strum(serialize = "f6")]
153    F6,
154    #[strum(serialize = "f7")]
155    F7,
156    #[strum(serialize = "f8")]
157    F8,
158    #[strum(serialize = "f9")]
159    F9,
160    #[strum(serialize = "f10")]
161    F10,
162    #[strum(serialize = "f11")]
163    F11,
164    #[strum(serialize = "f12")]
165    F12,
166    #[strum(serialize = "f13")]
167    F13,
168    #[strum(serialize = "f14")]
169    F14,
170    #[strum(serialize = "f15")]
171    F15,
172    #[strum(serialize = "f16")]
173    F16,
174    #[strum(serialize = "f17")]
175    F17,
176    #[strum(serialize = "f18")]
177    F18,
178    #[strum(serialize = "f19")]
179    F19,
180    #[strum(serialize = "f20")]
181    F20,
182    #[strum(serialize = "f21")]
183    F21,
184    #[strum(serialize = "f22")]
185    F22,
186    #[strum(serialize = "f23")]
187    F23,
188    #[strum(serialize = "f24")]
189    F24,
190    #[strum(serialize = "home")]
191    Home,
192    #[strum(serialize = "insert")]
193    Insert,
194    #[strum(serialize = "kp-*")]
195    KpMultiply,
196    #[strum(serialize = "kp-+")]
197    KpPlus,
198    #[strum(serialize = "kp--")]
199    KpMinus,
200    #[strum(serialize = "kp-.")]
201    KpDot,
202    #[strum(serialize = "kp-/")]
203    KpDivide,
204    #[strum(serialize = "kp-0")]
205    Kp0,
206    #[strum(serialize = "kp-1")]
207    Kp1,
208    #[strum(serialize = "kp-2")]
209    Kp2,
210    #[strum(serialize = "kp-3")]
211    Kp3,
212    #[strum(serialize = "kp-4")]
213    Kp4,
214    #[strum(serialize = "kp-5")]
215    Kp5,
216    #[strum(serialize = "kp-6")]
217    Kp6,
218    #[strum(serialize = "kp-7")]
219    Kp7,
220    #[strum(serialize = "kp-8")]
221    Kp8,
222    #[strum(serialize = "kp-9")]
223    Kp9,
224    #[strum(serialize = "kp-enter")]
225    KpEnter,
226    #[strum(serialize = "left")]
227    Left,
228    #[strum(serialize = "menu")]
229    Menu,
230    #[strum(serialize = "next")]
231    Next,
232    #[strum(serialize = "num-lock")]
233    NumLock,
234    #[strum(serialize = "pause")]
235    Pause,
236    #[strum(serialize = "print")]
237    Print,
238    #[strum(serialize = "prior")]
239    Prior,
240    #[strum(serialize = "return")]
241    Return,
242    #[strum(serialize = "right")]
243    Right,
244    #[strum(serialize = "scroll-lock")]
245    ScrollLock,
246    #[strum(serialize = "tab")]
247    Tab,
248    #[strum(serialize = "up")]
249    Up,
250    #[strum(serialize = "window")]
251    Window,
252}
253
254/// Notification type
255#[derive(StrumDisplay, Debug, Clone)]
256pub enum NotificationType {
257    #[strum(serialize = "begin")]
258    Begin,
259    #[strum(serialize = "end")]
260    End,
261    #[strum(serialize = "cancel")]
262    Cancel,
263    #[strum(serialize = "pause")]
264    Pause,
265    #[strum(serialize = "resume")]
266    Resume,
267    #[strum(serialize = "index_mark")]
268    IndexMark,
269    #[strum(serialize = "all")]
270    All,
271}
272
273/// Notification event type (returned by server)
274#[derive(StrumDisplay, Debug, Clone)]
275pub enum EventType {
276    Begin,
277    End,
278    Cancel,
279    Pause,
280    Resume,
281    IndexMark(String),
282}
283
284/// Event identifier
285#[derive(Debug, Clone)]
286pub struct EventId {
287    // Message id
288    pub message: String,
289    // Client id
290    pub client: String,
291}
292
293impl EventId {
294    // New event identifier
295    pub fn new(message: &str, client: &str) -> Self {
296        Self {
297            message: message.to_string(),
298            client: client.to_string(),
299        }
300    }
301}
302
303/// Notification event
304#[derive(Debug, Clone)]
305pub struct Event {
306    pub ntype: EventType,
307    pub id: EventId,
308}
309
310impl Event {
311    pub fn new(ntype: EventType, message: &str, client: &str) -> Event {
312        Event {
313            ntype,
314            id: EventId::new(message, client),
315        }
316    }
317
318    pub fn begin(message: &str, client: &str) -> Event {
319        Event::new(EventType::Begin, message, client)
320    }
321
322    pub fn end(message: &str, client: &str) -> Event {
323        Event::new(EventType::End, message, client)
324    }
325
326    pub fn index_mark(mark: String, message: &str, client: &str) -> Event {
327        Event::new(EventType::IndexMark(mark), message, client)
328    }
329
330    pub fn cancel(message: &str, client: &str) -> Event {
331        Event::new(EventType::Cancel, message, client)
332    }
333
334    pub fn pause(message: &str, client: &str) -> Event {
335        Event::new(EventType::Pause, message, client)
336    }
337
338    pub fn resume(message: &str, client: &str) -> Event {
339        Event::new(EventType::Resume, message, client)
340    }
341}
342
343/// Synthesis voice
344#[derive(Debug, PartialEq)]
345pub struct SynthesisVoice {
346    pub name: String,
347    pub language: Option<String>,
348    pub dialect: Option<String>,
349}
350
351impl SynthesisVoice {
352    pub fn new(name: &str, language: Option<&str>, dialect: Option<&str>) -> SynthesisVoice {
353        SynthesisVoice {
354            name: name.to_string(),
355            language: language.map(|s| s.to_string()),
356            dialect: dialect.map(|s| s.to_string()),
357        }
358    }
359    /// Parse Option::None or string "none" into Option::None
360    fn parse_none(token: Option<&str>) -> Option<String> {
361        match token {
362            Some(s) => match s {
363                "none" => None,
364                s => Some(s.to_string()),
365            },
366            None => None,
367        }
368    }
369}
370
371impl FromStr for SynthesisVoice {
372    type Err = ClientError;
373
374    fn from_str(s: &str) -> Result<Self, Self::Err> {
375        let mut iter = s.split('\t');
376        match iter.next() {
377            Some(name) => Ok(SynthesisVoice {
378                name: name.to_string(),
379                language: SynthesisVoice::parse_none(iter.next()),
380                dialect: SynthesisVoice::parse_none(iter.next()),
381            }),
382            None => Err(ClientError::unexpected_eof("missing synthesis voice name")),
383        }
384    }
385}
386
387/// Command status line
388///
389/// Consists in a 3-digits code and a message. It can be a success or a failure.
390///
391/// Examples:
392/// - 216 OK OUTPUT MODULE SET
393/// - 409 ERR RATE TOO HIGH
394#[derive(Debug, PartialEq)]
395pub struct StatusLine {
396    pub code: ReturnCode,
397    pub message: String,
398}
399
400impl fmt::Display for StatusLine {
401    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
402        write!(f, "{} {}", self.code, self.message)
403    }
404}
405/// Client error, either I/O error or SSIP error.
406#[derive(ThisError, Debug)]
407pub enum ClientError {
408    #[error("I/O: {0}")]
409    Io(io::Error),
410    #[error("Not ready")]
411    NotReady,
412    #[error("SSIP: {0}")]
413    Ssip(StatusLine),
414    #[error("Too few lines")]
415    TooFewLines,
416    #[error("Too many lines")]
417    TooManyLines,
418    #[error("Unexpected status: {0}")]
419    UnexpectedStatus(ReturnCode),
420}
421
422impl ClientError {
423    /// Create I/O error
424    pub(crate) fn io_error(kind: io::ErrorKind, msg: &str) -> Self {
425        Self::Io(io::Error::new(kind, msg))
426    }
427
428    /// Invalid data I/O error
429    pub(crate) fn invalid_data(msg: &str) -> Self {
430        ClientError::io_error(io::ErrorKind::InvalidData, msg)
431    }
432
433    /// Unexpected EOF I/O error
434    pub(crate) fn unexpected_eof(msg: &str) -> Self {
435        ClientError::io_error(io::ErrorKind::UnexpectedEof, msg)
436    }
437}
438
439impl From<io::Error> for ClientError {
440    fn from(err: io::Error) -> Self {
441        if err.kind() == io::ErrorKind::WouldBlock {
442            ClientError::NotReady
443        } else {
444            ClientError::Io(err)
445        }
446    }
447}
448
449/// Client result.
450pub type ClientResult<T> = Result<T, ClientError>;
451
452/// Client result consisting in a single status line
453pub type ClientStatus = ClientResult<StatusLine>;
454
455/// Client name
456#[derive(Debug, Clone)]
457pub struct ClientName {
458    pub user: String,
459    pub application: String,
460    pub component: String,
461}
462
463impl ClientName {
464    pub fn new(user: &str, application: &str) -> Self {
465        ClientName::with_component(user, application, "main")
466    }
467
468    pub fn with_component(user: &str, application: &str, component: &str) -> Self {
469        ClientName {
470            user: user.to_string(),
471            application: application.to_string(),
472            component: component.to_string(),
473        }
474    }
475}
476
477/// Cursor motion in history
478#[derive(StrumDisplay, Debug, Clone)]
479pub enum CursorDirection {
480    #[strum(serialize = "backward")]
481    Backward,
482    #[strum(serialize = "forward")]
483    Forward,
484}
485
486/// Sort direction in history
487#[derive(StrumDisplay, Debug, Clone)]
488pub enum SortDirection {
489    #[strum(serialize = "asc")]
490    Ascending,
491    #[strum(serialize = "desc")]
492    Descending,
493}
494
495/// Property messages are ordered by in history
496#[derive(StrumDisplay, Debug, Clone)]
497pub enum SortKey {
498    #[strum(serialize = "client_name")]
499    ClientName,
500    #[strum(serialize = "priority")]
501    Priority,
502    #[strum(serialize = "message_type")]
503    MessageType,
504    #[strum(serialize = "time")]
505    Time,
506    #[strum(serialize = "user")]
507    User,
508}
509
510/// Sort ordering
511#[derive(StrumDisplay, Debug, Clone)]
512pub enum Ordering {
513    #[strum(serialize = "text")]
514    Text,
515    #[strum(serialize = "sound_icon")]
516    SoundIcon,
517    #[strum(serialize = "char")]
518    Char,
519    #[strum(serialize = "key")]
520    Key,
521}
522
523/// Position in history
524#[derive(Debug, Clone)]
525pub enum HistoryPosition {
526    First,
527    Last,
528    Pos(u16),
529}
530
531impl fmt::Display for HistoryPosition {
532    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
533        match self {
534            HistoryPosition::First => write!(f, "first"),
535            HistoryPosition::Last => write!(f, "last"),
536            HistoryPosition::Pos(n) => write!(f, "pos {n}"),
537        }
538    }
539}
540
541/// History client status
542#[derive(Debug, PartialEq)]
543pub struct HistoryClientStatus {
544    pub id: ClientId,
545    pub name: String,
546    pub connected: bool,
547}
548
549impl HistoryClientStatus {
550    pub fn new(id: ClientId, name: &str, connected: bool) -> Self {
551        Self {
552            id,
553            name: name.to_string(),
554            connected,
555        }
556    }
557}
558
559impl FromStr for HistoryClientStatus {
560    type Err = ClientError;
561
562    fn from_str(s: &str) -> Result<Self, Self::Err> {
563        let mut iter = s.splitn(3, ' ');
564        match iter.next() {
565            Some("") => Err(ClientError::unexpected_eof("expecting client id")),
566            Some(client_id) => match client_id.parse::<u32>() {
567                Ok(id) => match iter.next() {
568                    Some(name) => match iter.next() {
569                        Some("0") => Ok(HistoryClientStatus::new(id, name, false)),
570                        Some("1") => Ok(HistoryClientStatus::new(id, name, true)),
571                        Some(_) => Err(ClientError::invalid_data("invalid client status")),
572                        None => Err(ClientError::unexpected_eof("expecting client status")),
573                    },
574                    None => Err(ClientError::unexpected_eof("expecting client name")),
575                },
576                Err(_) => Err(ClientError::invalid_data("invalid client id")),
577            },
578            None => Err(ClientError::unexpected_eof("expecting client id")),
579        }
580    }
581}
582
583#[cfg(test)]
584mod tests {
585
586    use std::io;
587    use std::str::FromStr;
588
589    use super::{ClientError, HistoryClientStatus, HistoryPosition, MessageScope, SynthesisVoice};
590
591    #[test]
592    fn parse_synthesis_voice() {
593        // Voice with dialect
594        let v1 =
595            SynthesisVoice::from_str("Portuguese (Portugal)+Kaukovalta\tpt\tKaukovalta").unwrap();
596        assert_eq!("Portuguese (Portugal)+Kaukovalta", v1.name);
597        assert_eq!("pt", v1.language.unwrap());
598        assert_eq!("Kaukovalta", v1.dialect.unwrap());
599
600        // Voice without dialect
601        let v2 = SynthesisVoice::from_str("Esperanto\teo\tnone").unwrap();
602        assert_eq!("Esperanto", v2.name);
603        assert_eq!("eo", v2.language.unwrap());
604        assert!(v2.dialect.is_none());
605    }
606
607    #[test]
608    fn format_message_scope() {
609        assert_eq!("self", format!("{}", MessageScope::Last).as_str());
610        assert_eq!("all", format!("{}", MessageScope::All).as_str());
611        assert_eq!("123", format!("{}", MessageScope::Message(123)).as_str());
612    }
613
614    #[test]
615    fn format_history_position() {
616        assert_eq!("first", format!("{}", HistoryPosition::First).as_str());
617        assert_eq!("last", format!("{}", HistoryPosition::Last).as_str());
618        assert_eq!("pos 15", format!("{}", HistoryPosition::Pos(15)).as_str());
619    }
620
621    #[test]
622    fn parse_history_client_status() {
623        assert_eq!(
624            HistoryClientStatus::new(10, "joe:speechd_client:main", false),
625            HistoryClientStatus::from_str("10 joe:speechd_client:main 0").unwrap()
626        );
627        assert_eq!(
628            HistoryClientStatus::new(11, "joe:speechd_client:main", true),
629            HistoryClientStatus::from_str("11 joe:speechd_client:main 1").unwrap()
630        );
631        for line in &[
632            "9 joe:speechd_client:main xxx",
633            "xxx joe:speechd_client:main 1",
634        ] {
635            match HistoryClientStatus::from_str(line) {
636                Ok(_) => panic!("parsing should have failed"),
637                Err(ClientError::Io(err)) if err.kind() == io::ErrorKind::InvalidData => (),
638                Err(_) => panic!("expecting error 'invalid data' parsing \"{}\"", line),
639            }
640        }
641        for line in &["8 joe:speechd_client:main", "8", ""] {
642            match HistoryClientStatus::from_str(line) {
643                Ok(_) => panic!("parsing should have failed"),
644                Err(ClientError::Io(err)) if err.kind() == io::ErrorKind::UnexpectedEof => (),
645                Err(_) => panic!("expecting error 'unexpected EOF' parsing \"{}\"", line),
646            }
647        }
648    }
649}