ssip/
lib.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, PartialEq, Eq, Hash)]
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, Hash, Eq, PartialEq)]
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, PartialEq, Eq, Hash)]
70#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
71#[cfg_attr(feature = "dbus", derive(zvariant::Type))]
72pub enum Priority {
73    #[strum(serialize = "progress")]
74    Progress,
75    #[strum(serialize = "notification")]
76    Notification,
77    #[strum(serialize = "message")]
78    Message,
79    #[strum(serialize = "text")]
80    Text,
81    #[strum(serialize = "important")]
82    Important,
83}
84
85/// Punctuation mode.
86#[derive(StrumDisplay, Debug, Clone, PartialEq, Eq, Hash)]
87#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
88#[cfg_attr(feature = "dbus", derive(zvariant::Type))]
89pub enum PunctuationMode {
90    #[strum(serialize = "none")]
91    None,
92    #[strum(serialize = "some")]
93    Some,
94    #[strum(serialize = "most")]
95    Most,
96    #[strum(serialize = "all")]
97    All,
98}
99
100/// Capital letters recognition mode.
101#[derive(StrumDisplay, Debug, Clone, Hash, Eq, PartialEq)]
102#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
103#[cfg_attr(feature = "dbus", derive(zvariant::Type))]
104pub enum CapitalLettersRecognitionMode {
105    #[strum(serialize = "none")]
106    None,
107    #[strum(serialize = "spell")]
108    Spell,
109    #[strum(serialize = "icon")]
110    Icon,
111}
112
113/// Symbolic key names
114#[derive(StrumDisplay, Debug, Clone, PartialEq, Eq, Hash)]
115#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
116#[cfg_attr(feature = "dbus", derive(zvariant::Type))]
117pub enum KeyName {
118    #[strum(serialize = "space")]
119    Space,
120    #[strum(serialize = "underscore")]
121    Underscore,
122    #[strum(serialize = "double-quote")]
123    DoubleQuote,
124    #[strum(serialize = "alt")]
125    Alt,
126    #[strum(serialize = "control")]
127    Control,
128    #[strum(serialize = "hyper")]
129    Hyper,
130    #[strum(serialize = "meta")]
131    Meta,
132    #[strum(serialize = "shift")]
133    Shift,
134    #[strum(serialize = "super")]
135    Super,
136    #[strum(serialize = "backspace")]
137    Backspace,
138    #[strum(serialize = "break")]
139    Break,
140    #[strum(serialize = "delete")]
141    Delete,
142    #[strum(serialize = "down")]
143    Down,
144    #[strum(serialize = "end")]
145    End,
146    #[strum(serialize = "enter")]
147    Enter,
148    #[strum(serialize = "escape")]
149    Escape,
150    #[strum(serialize = "f1")]
151    F1,
152    #[strum(serialize = "f2")]
153    F2,
154    #[strum(serialize = "f3")]
155    F3,
156    #[strum(serialize = "f4")]
157    F4,
158    #[strum(serialize = "f5")]
159    F5,
160    #[strum(serialize = "f6")]
161    F6,
162    #[strum(serialize = "f7")]
163    F7,
164    #[strum(serialize = "f8")]
165    F8,
166    #[strum(serialize = "f9")]
167    F9,
168    #[strum(serialize = "f10")]
169    F10,
170    #[strum(serialize = "f11")]
171    F11,
172    #[strum(serialize = "f12")]
173    F12,
174    #[strum(serialize = "f13")]
175    F13,
176    #[strum(serialize = "f14")]
177    F14,
178    #[strum(serialize = "f15")]
179    F15,
180    #[strum(serialize = "f16")]
181    F16,
182    #[strum(serialize = "f17")]
183    F17,
184    #[strum(serialize = "f18")]
185    F18,
186    #[strum(serialize = "f19")]
187    F19,
188    #[strum(serialize = "f20")]
189    F20,
190    #[strum(serialize = "f21")]
191    F21,
192    #[strum(serialize = "f22")]
193    F22,
194    #[strum(serialize = "f23")]
195    F23,
196    #[strum(serialize = "f24")]
197    F24,
198    #[strum(serialize = "home")]
199    Home,
200    #[strum(serialize = "insert")]
201    Insert,
202    #[strum(serialize = "kp-*")]
203    KpMultiply,
204    #[strum(serialize = "kp-+")]
205    KpPlus,
206    #[strum(serialize = "kp--")]
207    KpMinus,
208    #[strum(serialize = "kp-.")]
209    KpDot,
210    #[strum(serialize = "kp-/")]
211    KpDivide,
212    #[strum(serialize = "kp-0")]
213    Kp0,
214    #[strum(serialize = "kp-1")]
215    Kp1,
216    #[strum(serialize = "kp-2")]
217    Kp2,
218    #[strum(serialize = "kp-3")]
219    Kp3,
220    #[strum(serialize = "kp-4")]
221    Kp4,
222    #[strum(serialize = "kp-5")]
223    Kp5,
224    #[strum(serialize = "kp-6")]
225    Kp6,
226    #[strum(serialize = "kp-7")]
227    Kp7,
228    #[strum(serialize = "kp-8")]
229    Kp8,
230    #[strum(serialize = "kp-9")]
231    Kp9,
232    #[strum(serialize = "kp-enter")]
233    KpEnter,
234    #[strum(serialize = "left")]
235    Left,
236    #[strum(serialize = "menu")]
237    Menu,
238    #[strum(serialize = "next")]
239    Next,
240    #[strum(serialize = "num-lock")]
241    NumLock,
242    #[strum(serialize = "pause")]
243    Pause,
244    #[strum(serialize = "print")]
245    Print,
246    #[strum(serialize = "prior")]
247    Prior,
248    #[strum(serialize = "return")]
249    Return,
250    #[strum(serialize = "right")]
251    Right,
252    #[strum(serialize = "scroll-lock")]
253    ScrollLock,
254    #[strum(serialize = "tab")]
255    Tab,
256    #[strum(serialize = "up")]
257    Up,
258    #[strum(serialize = "window")]
259    Window,
260}
261
262/// Notification type
263#[derive(StrumDisplay, Debug, Clone, Hash, Eq, PartialEq)]
264#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
265#[cfg_attr(feature = "dbus", derive(zvariant::Type))]
266pub enum NotificationType {
267    #[strum(serialize = "begin")]
268    Begin,
269    #[strum(serialize = "end")]
270    End,
271    #[strum(serialize = "cancel")]
272    Cancel,
273    #[strum(serialize = "pause")]
274    Pause,
275    #[strum(serialize = "resume")]
276    Resume,
277    #[strum(serialize = "index_mark")]
278    IndexMark,
279    #[strum(serialize = "all")]
280    All,
281}
282
283/// Notification event type (returned by server)
284#[derive(StrumDisplay, Debug, Clone)]
285pub enum EventType {
286    Begin,
287    End,
288    Cancel,
289    Pause,
290    Resume,
291    IndexMark(String),
292}
293
294/// Event identifier
295#[derive(Debug, Clone, Hash, Eq, PartialEq)]
296#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
297#[cfg_attr(feature = "dbus", derive(zvariant::Type))]
298pub struct EventId {
299    // Message id
300    pub message: String,
301    // Client id
302    pub client: String,
303}
304
305impl EventId {
306    // New event identifier
307    pub fn new(message: &str, client: &str) -> Self {
308        Self {
309            message: message.to_string(),
310            client: client.to_string(),
311        }
312    }
313}
314
315/// Notification event
316#[derive(Debug, Clone)]
317pub struct Event {
318    pub ntype: EventType,
319    pub id: EventId,
320}
321
322impl Event {
323    pub fn new(ntype: EventType, message: &str, client: &str) -> Event {
324        Event {
325            ntype,
326            id: EventId::new(message, client),
327        }
328    }
329
330    pub fn begin(message: &str, client: &str) -> Event {
331        Event::new(EventType::Begin, message, client)
332    }
333
334    pub fn end(message: &str, client: &str) -> Event {
335        Event::new(EventType::End, message, client)
336    }
337
338    pub fn index_mark(mark: String, message: &str, client: &str) -> Event {
339        Event::new(EventType::IndexMark(mark), message, client)
340    }
341
342    pub fn cancel(message: &str, client: &str) -> Event {
343        Event::new(EventType::Cancel, message, client)
344    }
345
346    pub fn pause(message: &str, client: &str) -> Event {
347        Event::new(EventType::Pause, message, client)
348    }
349
350    pub fn resume(message: &str, client: &str) -> Event {
351        Event::new(EventType::Resume, message, client)
352    }
353}
354
355/// Synthesis voice
356#[derive(Debug, PartialEq, Eq, Clone, Hash)]
357pub struct SynthesisVoice {
358    pub name: String,
359    pub language: Option<String>,
360    pub dialect: Option<String>,
361}
362
363impl SynthesisVoice {
364    pub fn new(name: &str, language: Option<&str>, dialect: Option<&str>) -> SynthesisVoice {
365        SynthesisVoice {
366            name: name.to_string(),
367            language: language.map(|s| s.to_string()),
368            dialect: dialect.map(|s| s.to_string()),
369        }
370    }
371    /// Parse Option::None or string "none" into Option::None
372    fn parse_none(token: Option<&str>) -> Option<String> {
373        match token {
374            Some(s) => match s {
375                "none" => None,
376                s => Some(s.to_string()),
377            },
378            None => None,
379        }
380    }
381}
382
383impl FromStr for SynthesisVoice {
384    type Err = ClientError;
385
386    fn from_str(s: &str) -> Result<Self, Self::Err> {
387        let mut iter = s.split('\t');
388        match iter.next() {
389            Some(name) => Ok(SynthesisVoice {
390                name: name.to_string(),
391                language: SynthesisVoice::parse_none(iter.next()),
392                dialect: SynthesisVoice::parse_none(iter.next()),
393            }),
394            None => Err(ClientError::unexpected_eof("missing synthesis voice name")),
395        }
396    }
397}
398
399/// Command status line
400///
401/// Consists in a 3-digits code and a message. It can be a success or a failure.
402///
403/// Examples:
404/// - 216 OK OUTPUT MODULE SET
405/// - 409 ERR RATE TOO HIGH
406#[derive(Debug, PartialEq, Eq)]
407#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
408#[cfg_attr(feature = "dbus", derive(zvariant::Type))]
409pub struct StatusLine {
410    pub code: ReturnCode,
411    pub message: String,
412}
413
414impl fmt::Display for StatusLine {
415    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
416        write!(f, "{} {}", self.code, self.message)
417    }
418}
419/// Client error, either I/O error or SSIP error.
420#[derive(ThisError, Debug)]
421pub enum ClientError {
422    #[error("I/O: {0}")]
423    Io(io::Error),
424    #[error("Not ready")]
425    NotReady,
426    #[error("SSIP: {0}")]
427    Ssip(StatusLine),
428    #[error("Too few lines")]
429    TooFewLines,
430    #[error("Too many lines")]
431    TooManyLines,
432    #[error("Unexpected status: {0}")]
433    UnexpectedStatus(ReturnCode),
434}
435
436impl ClientError {
437    /// Create I/O error
438    pub fn io_error(kind: io::ErrorKind, msg: &str) -> Self {
439        Self::Io(io::Error::new(kind, msg))
440    }
441
442    /// Invalid data I/O error
443    pub fn invalid_data(msg: &str) -> Self {
444        ClientError::io_error(io::ErrorKind::InvalidData, msg)
445    }
446
447    /// Unexpected EOF I/O error
448    pub fn unexpected_eof(msg: &str) -> Self {
449        ClientError::io_error(io::ErrorKind::UnexpectedEof, msg)
450    }
451}
452
453impl From<io::Error> for ClientError {
454    fn from(err: io::Error) -> Self {
455        if err.kind() == io::ErrorKind::WouldBlock {
456            ClientError::NotReady
457        } else {
458            ClientError::Io(err)
459        }
460    }
461}
462
463/// Client result.
464pub type ClientResult<T> = Result<T, ClientError>;
465
466/// Client result consisting in a single status line
467pub type ClientStatus = ClientResult<StatusLine>;
468
469/// Client name
470#[derive(Debug, Clone, PartialEq, Eq, Hash)]
471#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
472#[cfg_attr(feature = "dbus", derive(zvariant::Type))]
473pub struct ClientName {
474    pub user: String,
475    pub application: String,
476    pub component: String,
477}
478
479impl ClientName {
480    pub fn new(user: &str, application: &str) -> Self {
481        ClientName::with_component(user, application, "main")
482    }
483
484    pub fn with_component(user: &str, application: &str, component: &str) -> Self {
485        ClientName {
486            user: user.to_string(),
487            application: application.to_string(),
488            component: component.to_string(),
489        }
490    }
491}
492
493/// Cursor motion in history
494#[derive(StrumDisplay, Debug, Clone, PartialEq, Eq, Hash)]
495#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
496#[cfg_attr(feature = "dbus", derive(zvariant::Type))]
497pub enum CursorDirection {
498    #[strum(serialize = "backward")]
499    Backward,
500    #[strum(serialize = "forward")]
501    Forward,
502}
503
504/// Sort direction in history
505#[derive(StrumDisplay, Debug, Clone, Eq, PartialEq, Hash)]
506#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
507#[cfg_attr(feature = "dbus", derive(zvariant::Type))]
508pub enum SortDirection {
509    #[strum(serialize = "asc")]
510    Ascending,
511    #[strum(serialize = "desc")]
512    Descending,
513}
514
515/// Property messages are ordered by in history
516#[derive(StrumDisplay, Debug, Clone, PartialEq, Eq, Hash)]
517#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
518#[cfg_attr(feature = "dbus", derive(zvariant::Type))]
519pub enum SortKey {
520    #[strum(serialize = "client_name")]
521    ClientName,
522    #[strum(serialize = "priority")]
523    Priority,
524    #[strum(serialize = "message_type")]
525    MessageType,
526    #[strum(serialize = "time")]
527    Time,
528    #[strum(serialize = "user")]
529    User,
530}
531
532/// Sort ordering
533#[derive(StrumDisplay, Debug, Clone, PartialEq, Eq, Hash)]
534#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
535#[cfg_attr(feature = "dbus", derive(zvariant::Type))]
536pub enum Ordering {
537    #[strum(serialize = "text")]
538    Text,
539    #[strum(serialize = "sound_icon")]
540    SoundIcon,
541    #[strum(serialize = "char")]
542    Char,
543    #[strum(serialize = "key")]
544    Key,
545}
546
547/// Position in history
548#[derive(Debug, Clone, Eq, PartialEq, Hash)]
549pub enum HistoryPosition {
550    First,
551    Last,
552    Pos(u16),
553}
554
555impl fmt::Display for HistoryPosition {
556    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
557        match self {
558            HistoryPosition::First => write!(f, "first"),
559            HistoryPosition::Last => write!(f, "last"),
560            HistoryPosition::Pos(n) => write!(f, "pos {}", n),
561        }
562    }
563}
564
565/// History client status
566#[derive(Debug, PartialEq, Eq, Clone, Hash)]
567#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
568#[cfg_attr(feature = "dbus", derive(zvariant::Type))]
569pub struct HistoryClientStatus {
570    pub id: ClientId,
571    pub name: String,
572    pub connected: bool,
573}
574
575impl HistoryClientStatus {
576    pub fn new(id: ClientId, name: &str, connected: bool) -> Self {
577        Self {
578            id,
579            name: name.to_string(),
580            connected,
581        }
582    }
583}
584
585impl FromStr for HistoryClientStatus {
586    type Err = ClientError;
587
588    fn from_str(s: &str) -> Result<Self, Self::Err> {
589        let mut iter = s.splitn(3, ' ');
590        match iter.next() {
591            Some("") => Err(ClientError::unexpected_eof("expecting client id")),
592            Some(client_id) => match client_id.parse::<u32>() {
593                Ok(id) => match iter.next() {
594                    Some(name) => match iter.next() {
595                        Some("0") => Ok(HistoryClientStatus::new(id, name, false)),
596                        Some("1") => Ok(HistoryClientStatus::new(id, name, true)),
597                        Some(_) => Err(ClientError::invalid_data("invalid client status")),
598                        None => Err(ClientError::unexpected_eof("expecting client status")),
599                    },
600                    None => Err(ClientError::unexpected_eof("expecting client name")),
601                },
602                Err(_) => Err(ClientError::invalid_data("invalid client id")),
603            },
604            None => Err(ClientError::unexpected_eof("expecting client id")),
605        }
606    }
607}
608
609#[derive(Debug, Clone, Hash, PartialEq, Eq)]
610/// Request for SSIP server.
611pub enum Request {
612    SetName(ClientName),
613    // Speech related requests
614    Speak,
615    SendLine(String),
616    SendLines(Vec<String>),
617    SpeakChar(char),
618    SpeakKey(KeyName),
619    // Flow control
620    Stop(MessageScope),
621    Cancel(MessageScope),
622    Pause(MessageScope),
623    Resume(MessageScope),
624    // Setter and getter
625    SetPriority(Priority),
626    SetDebug(bool),
627    SetOutputModule(ClientScope, String),
628    GetOutputModule,
629    ListOutputModules,
630    SetLanguage(ClientScope, String),
631    GetLanguage,
632    SetSsmlMode(bool),
633    SetPunctuationMode(ClientScope, PunctuationMode),
634    SetSpelling(ClientScope, bool),
635    SetCapitalLettersRecognitionMode(ClientScope, CapitalLettersRecognitionMode),
636    SetVoiceType(ClientScope, String),
637    GetVoiceType,
638    ListVoiceTypes,
639    SetSynthesisVoice(ClientScope, String),
640    ListSynthesisVoices,
641    SetRate(ClientScope, i8),
642    GetRate,
643    SetPitch(ClientScope, i8),
644    GetPitch,
645    SetVolume(ClientScope, i8),
646    GetVolume,
647    SetPauseContext(ClientScope, u32),
648    SetNotification(NotificationType, bool),
649    // Blocks
650    Begin,
651    End,
652    // History
653    SetHistory(ClientScope, bool),
654    HistoryGetClients,
655    HistoryGetClientId,
656    HistoryGetClientMsgs(ClientScope, u32, u32),
657    HistoryGetLastMsgId,
658    HistoryGetMsg(MessageId),
659    HistoryCursorGet,
660    HistoryCursorSet(ClientScope, HistoryPosition),
661    HistoryCursorMove(CursorDirection),
662    HistorySpeak(MessageId),
663    HistorySort(SortDirection, SortKey),
664    HistorySetShortMsgLength(u32),
665    HistorySetMsgTypeOrdering(Vec<Ordering>),
666    HistorySearch(ClientScope, String),
667    // Misc.
668    Quit,
669}
670
671#[derive(Debug, Clone, Hash, PartialEq, Eq)]
672/// Response from SSIP server.
673pub enum Response {
674    LanguageSet,                                     // 201
675    PrioritySet,                                     // 202
676    RateSet,                                         // 203
677    PitchSet,                                        // 204
678    PunctuationSet,                                  // 205
679    CapLetRecognSet,                                 // 206
680    SpellingSet,                                     // 207
681    ClientNameSet,                                   // 208
682    VoiceSet,                                        // 209
683    Stopped,                                         // 210
684    Paused,                                          // 211
685    Resumed,                                         // 212
686    Canceled,                                        // 213
687    TableSet,                                        // 215
688    OutputModuleSet,                                 // 216
689    PauseContextSet,                                 // 217
690    VolumeSet,                                       // 218
691    SsmlModeSet,                                     // 219
692    NotificationSet,                                 // 220
693    PitchRangeSet,                                   // 263
694    DebugSet,                                        // 262
695    HistoryCurSetFirst,                              // 220
696    HistoryCurSetLast,                               // 221
697    HistoryCurSetPos,                                // 222
698    HistoryCurMoveFor,                               // 223
699    HistoryCurMoveBack,                              // 224
700    MessageQueued,                                   // 225,
701    SoundIconQueued,                                 // 226
702    MessageCanceled,                                 // 227
703    ReceivingData,                                   // 230
704    Bye,                                             // 231
705    HistoryClientListSent(Vec<HistoryClientStatus>), // 240
706    HistoryMsgsListSent(Vec<String>),                // 241
707    HistoryLastMsg(String),                          // 242
708    HistoryCurPosRet(String),                        // 243
709    TableListSent(Vec<String>),                      // 244
710    HistoryClientIdSent(ClientId),                   // 245
711    MessageTextSent,                                 // 246
712    HelpSent(Vec<String>),                           // 248
713    VoicesListSent(Vec<SynthesisVoice>),             // 249
714    OutputModulesListSent(Vec<String>),              // 250
715    Get(String),                                     // 251
716    InsideBlock,                                     // 260
717    OutsideBlock,                                    // 261
718    NotImplemented,                                  // 299
719    EventIndexMark(EventId, String),                 // 700
720    EventBegin(EventId),                             // 701
721    EventEnd(EventId),                               // 702
722    EventCanceled(EventId),                          // 703
723    EventPaused(EventId),                            // 704
724    EventResumed(EventId),                           // 705
725}
726
727#[cfg(test)]
728mod tests {
729
730    use std::io;
731    use std::str::FromStr;
732
733    use super::{ClientError, HistoryClientStatus, HistoryPosition, MessageScope, SynthesisVoice};
734
735    #[test]
736    fn parse_synthesis_voice() {
737        // Voice with dialect
738        let v1 =
739            SynthesisVoice::from_str("Portuguese (Portugal)+Kaukovalta\tpt\tKaukovalta").unwrap();
740        assert_eq!("Portuguese (Portugal)+Kaukovalta", v1.name);
741        assert_eq!("pt", v1.language.unwrap());
742        assert_eq!("Kaukovalta", v1.dialect.unwrap());
743
744        // Voice without dialect
745        let v2 = SynthesisVoice::from_str("Esperanto\teo\tnone").unwrap();
746        assert_eq!("Esperanto", v2.name);
747        assert_eq!("eo", v2.language.unwrap());
748        assert!(v2.dialect.is_none());
749    }
750
751    #[test]
752    fn format_message_scope() {
753        assert_eq!("self", format!("{}", MessageScope::Last).as_str());
754        assert_eq!("all", format!("{}", MessageScope::All).as_str());
755        assert_eq!("123", format!("{}", MessageScope::Message(123)).as_str());
756    }
757
758    #[test]
759    fn format_history_position() {
760        assert_eq!("first", format!("{}", HistoryPosition::First).as_str());
761        assert_eq!("last", format!("{}", HistoryPosition::Last).as_str());
762        assert_eq!("pos 15", format!("{}", HistoryPosition::Pos(15)).as_str());
763    }
764
765    #[test]
766    fn parse_history_client_status() {
767        assert_eq!(
768            HistoryClientStatus::new(10, "joe:speechd_client:main", false),
769            HistoryClientStatus::from_str("10 joe:speechd_client:main 0").unwrap()
770        );
771        assert_eq!(
772            HistoryClientStatus::new(11, "joe:speechd_client:main", true),
773            HistoryClientStatus::from_str("11 joe:speechd_client:main 1").unwrap()
774        );
775        for line in &[
776            "9 joe:speechd_client:main xxx",
777            "xxx joe:speechd_client:main 1",
778        ] {
779            match HistoryClientStatus::from_str(line) {
780                Ok(_) => panic!("parsing should have failed"),
781                Err(ClientError::Io(err)) if err.kind() == io::ErrorKind::InvalidData => (),
782                Err(_) => panic!("expecting error 'invalid data' parsing \"{}\"", line),
783            }
784        }
785        for line in &["8 joe:speechd_client:main", "8", ""] {
786            match HistoryClientStatus::from_str(line) {
787                Ok(_) => panic!("parsing should have failed"),
788                Err(ClientError::Io(err)) if err.kind() == io::ErrorKind::UnexpectedEof => (),
789                Err(_) => panic!("expecting error 'unexpected EOF' parsing \"{}\"", line),
790            }
791        }
792    }
793}