whiskers.git / crates / whiskers-core / src / voice_id.rs

The id of a voice, as the voice service names it. It lives in the core because a household setting names one (the voice a character owns), and the ports' Voice speaks in one.

4use std::fmt;
6use serde::{Deserialize, Serialize};

The most characters of a voice id.

9pub const VOICE_ID_MAX: usize = 64;

The id of a voice: letters, digits, - and _, so one cannot reach a path or a query when it is put in a request. Read from a document it is checked the same way.

13#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
14#[serde(transparent)]
15pub struct VoiceId(String);

Why a text is not a voice id.

18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub struct NotAVoiceId;
21impl fmt::Display for NotAVoiceId {
22    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23        write!(f, "a voice id is 1 to {VOICE_ID_MAX} letters, digits, - or _")
24    }
25}
26
27impl std::error::Error for NotAVoiceId {}
28
29impl VoiceId {
30    pub fn new(id: &str) -> Result<Self, NotAVoiceId> {
31        let ok = !id.is_empty() && id.len() <= VOICE_ID_MAX && id.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_');
32        if ok { Ok(Self(id.to_owned())) } else { Err(NotAVoiceId) }
33    }
34
35    pub fn as_str(&self) -> &str {
36        &self.0
37    }

Eight hex digits that stand for the id in a log: enough to tell voices apart, not the id itself (a voice id can be a private one).

41    pub fn tag(&self) -> String {
42        let mut h: u32 = 0x811c_9dc5;
43        for b in self.0.bytes() {
44            h ^= u32::from(b);
45            h = h.wrapping_mul(0x0100_0193);
46        }
47        format!("voice#{h:08x}")
48    }
49}
51impl<'de> Deserialize<'de> for VoiceId {
52    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
53        let text = String::deserialize(d)?;
54        VoiceId::new(&text).map_err(serde::de::Error::custom)
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    #[test]
63    fn an_id_is_letters_digits_dash_and_underscore() {
64        assert!(VoiceId::new("cgSgspJ2msm6clMCkdW9").is_ok());
65        for bad in ["", "a b", "a/b", "a?b", &"x".repeat(VOICE_ID_MAX + 1)] {
66            assert_eq!(VoiceId::new(bad), Err(NotAVoiceId), "{bad:?}");
67        }
68        assert!(serde_json::from_str::<VoiceId>("\"a b\"").is_err(), "a document is checked too");
69    }
70
71    #[test]
72    fn the_tag_tells_voices_apart_without_being_the_id() {
73        let (a, b) = (VoiceId::new("one").unwrap(), VoiceId::new("two").unwrap());
74        assert_ne!(a.tag(), b.tag());
75        assert!(!a.tag().contains("one"));
76    }
77}