use serde::{Deserialize, Serialize};
use std::sync::OnceLock;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct VoiceInfo {
#[serde(rename = "ShortName")]
short_name: String,
#[serde(rename = "Locale")]
locale: String,
#[serde(rename = "Gender")]
gender: String,
#[serde(rename = "FriendlyName")]
friendly_name: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VoiceEntry {
id: &'static str,
label: &'static str,
}
pub(crate) static DYNAMIC_VOICES: OnceLock<Vec<VoiceInfo>> = OnceLock::new();
impl VoiceInfo {
#[cfg(test)]
pub(crate) fn new(
short_name: String,
locale: String,
gender: String,
friendly_name: String,
) -> Self {
Self {
short_name,
locale,
gender,
friendly_name,
}
}
pub fn short_name(&self) -> &str {
&self.short_name
}
pub fn locale(&self) -> &str {
&self.locale
}
pub fn gender(&self) -> &str {
&self.gender
}
pub fn friendly_name(&self) -> &str {
&self.friendly_name
}
}
impl VoiceEntry {
pub(crate) const fn new(id: &'static str, label: &'static str) -> Self {
Self { id, label }
}
pub fn id(&self) -> &str {
self.id
}
pub fn label(&self) -> &str {
self.label
}
}
pub fn set_dynamic_voices(voices: Vec<VoiceInfo>) {
let _ = DYNAMIC_VOICES.set(voices);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deserialize_voice_info() {
let json = r#"{
"Name": "Microsoft Server Speech Text to Speech Voice (en-US, EmmaMultilingual)",
"ShortName": "en-US-EmmaMultilingualNeural",
"Locale": "en-US",
"Gender": "Female",
"FriendlyName": "Microsoft Emma Multilingual Online (Natural)",
"Status": "GA"
}"#;
let v: VoiceInfo = serde_json::from_str(json).unwrap();
assert_eq!(v.short_name(), "en-US-EmmaMultilingualNeural");
assert_eq!(v.locale(), "en-US");
assert_eq!(v.gender(), "Female");
}
#[test]
fn deserialize_voice_array() {
let json = r#"[
{"ShortName":"en-US-EmmaMultilingualNeural","Locale":"en-US","Gender":"Female","FriendlyName":"Emma"},
{"ShortName":"bn-BD-NabanitaNeural","Locale":"bn-BD","Gender":"Female","FriendlyName":"Nabanita"}
]"#;
let voices: Vec<VoiceInfo> = serde_json::from_str(json).unwrap();
assert_eq!(voices.len(), 2);
assert_eq!(voices.get(1).map(|v| v.locale()), Some("bn-BD"));
}
#[test]
fn deserialize_voice_with_extra_fields() {
let json = r#"{
"ShortName":"ja-JP-NanamiNeural",
"Locale":"ja-JP",
"Gender":"Female",
"FriendlyName":"Nanami",
"VoiceTag":{"ContentCategories":["General"],"VoicePersonalities":["Friendly"]}
}"#;
let v: VoiceInfo = serde_json::from_str(json).unwrap();
assert_eq!(v.short_name(), "ja-JP-NanamiNeural");
}
#[test]
fn voice_entry_accessors() {
let e = VoiceEntry::new("en-US-EmmaMultilingualNeural", "Emma (US)");
assert_eq!(e.id(), "en-US-EmmaMultilingualNeural");
assert_eq!(e.label(), "Emma (US)");
}
#[test]
fn set_dynamic_voices_idempotent() {
let voices = vec![VoiceInfo::new(
"x-Y-VoiceNeural".into(),
"x-Y".into(),
"Female".into(),
"Voice".into(),
)];
set_dynamic_voices(voices);
set_dynamic_voices(Vec::new());
let stored = DYNAMIC_VOICES.get().unwrap();
assert_eq!(stored.len(), 1);
}
}