1//! The id of a voice, as the voice service names it. It lives in the core because a household setting names one 2//! (the voice a character owns), and the ports' `Voice` speaks in one. 3 4use std::fmt; 5 6use serde::{Deserialize, Serialize}; 7 8/// The most characters of a voice id. 9pub const VOICE_ID_MAX: usize = 64; 10 11/// The id of a voice: letters, digits, `-` and `_`, so one cannot reach a path or a query when it is put in a 12/// 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); 16 17/// Why a text is not a voice id. 18#[derive(Clone, Copy, Debug, PartialEq, Eq)] 19pub struct NotAVoiceId; 20 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 } 38 39 /// Eight hex digits that stand for the id in a log: enough to tell voices apart, not the id itself (a 40 /// 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} 50 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}