1use std::borrow::Cow;
2
3macro_rules! define_enum {
4 ($name:ident with $($str:literal $($alias:literal)* $variant:ident)* ) => {
5 #[allow(missing_docs)]
6 #[derive(Debug, PartialEq, Eq, Copy, Clone)]
7 pub enum $name {
8 $($variant),*
9 }
10 impl std::fmt::Display for $name {
11 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
12 let s = match self {
13 $( $name::$variant => $str, )*
14 };
15 f.write_str(s)
16 }
17 }
18
19 impl std::str::FromStr for $name {
20 type Err = ();
21 fn from_str(s: &str) -> Result<Self, Self::Err> {
22 let val = match s.to_lowercase().as_str() {
23 $($str $(| $alias)* => $name::$variant,)*
24 _ => return Err(()),
25 };
26 Ok(val)
27 }
28 }
29 };
30}
31
32define_enum!(Codec with
33 "mp3" MP3
34 "wav" WAV
35 "aac" AAC
36 "ogg" OGG
37 "caf" CAF
38);
39impl Default for Codec {
40 fn default() -> Self {
41 Codec::MP3
42 }
43}
44
45define_enum!(Language with
46 "ar-eg" ArabicEgypt
47 "ar-sa" ArabicSaudiArabia
48 "bg-bg" "bg" Bulgarian
49 "ca-es" Catalan
50 "zh-cn" Chinese
51 "zh-hk" ChineseHongKong
52 "zh-tw" ChineseTaiwan
53 "hr-hr" Croatian
54 "cs-cz" Czech
55 "da-dk" Danish
56 "nl-be" DutchBelgium
57 "nl-nl" DutchNetherlands
58 "en-au" EnglishAustralia
59 "en-ca" EnglishCanada
60 "en-gb" EnglishGreatBritain
61 "en-in" EnglishIndia
62 "en-ie" EnglishIreland
63 "en-us" EnglishUnitedStates
64 "fi-fi" "fi" Finnish
65 "fr-ca" FrenchCanada
66 "fr-fr" "fr" French
67 "fr-ch" FrenchSwitzerland
68 "de-de" "de" German
69 "de-at" GermanAustria
70 "de-ch" GermanSwitzerland
71 "el-gr" Greek
72 "he-il" Hebrew
73 "hi-in" Hindi
74 "hu-hu" "hu" Hungarian
75 "id-id" "id" Indonesian
76 "it-it" "it" Italian
77 "ja-jp" Japanese
78 "ko-kr" Korean
79 "ms-my" Malay
80 "nb-no" "no" Norwegian
81 "pl-pl" "pl" Polish
82 "pt-pt" Portuguese
83 "pt-br" PortugueseBrazil
84 "ro-ro" "ro" Romanian
85 "ru-ru" "ru" Russian
86 "sk-sk" "sk" Slovak
87 "sl-si" Slovenian
88 "es-es" "es" Spanish
89 "es-mx" SpanishMexico
90 "sv-se" Swedish
91 "ta-in" Tamil
92 "th-th" "th" Thai
93 "tr-tr" "tr" Turkish
94 "vi-vn" Vietnamese
95);
96impl Default for Language {
97 fn default() -> Self {
98 Language::EnglishUnitedStates
99 }
100}
101
102pub const AUDIO_FORMATS: &[&str] = &[
106 "8khz_8bit_mono",
107 "8khz_8bit_stereo",
108 "8khz_16bit_mono",
109 "8khz_16bit_stereo",
110 "11khz_8bit_mono",
111 "11khz_8bit_stereo",
112 "11khz_16bit_mono",
113 "11khz_16bit_stereo",
114 "12khz_8bit_mono",
115 "12khz_8bit_stereo",
116 "12khz_16bit_mono",
117 "12khz_16bit_stereo",
118 "16khz_8bit_mono",
119 "16khz_8bit_stereo",
120 "16khz_16bit_mono",
121 "16khz_16bit_stereo",
122 "22khz_8bit_mono",
123 "22khz_8bit_stereo",
124 "22khz_16bit_mono",
125 "22khz_16bit_stereo",
126 "24khz_8bit_mono",
127 "24khz_8bit_stereo",
128 "24khz_16bit_mono",
129 "24khz_16bit_stereo",
130 "32khz_8bit_mono",
131 "32khz_8bit_stereo",
132 "32khz_16bit_mono",
133 "32khz_16bit_stereo",
134 "44khz_8bit_mono",
135 "44khz_8bit_stereo",
136 "44khz_16bit_mono",
137 "44khz_16bit_stereo",
138 "48khz_8bit_mono",
139 "48khz_8bit_stereo",
140 "48khz_16bit_mono",
141 "48khz_16bit_stereo",
142 "alaw_8khz_mono",
143 "alaw_8khz_stereo",
144 "alaw_11khz_mono",
145 "alaw_11khz_stereo",
146 "alaw_22khz_mono",
147 "alaw_22khz_stereo",
148 "alaw_44khz_mono",
149 "alaw_44khz_stereo",
150 "ulaw_8khz_mono",
151 "ulaw_8khz_stereo",
152 "ulaw_11khz_mono",
153 "ulaw_11khz_stereo",
154 "ulaw_22khz_mono",
155 "ulaw_22khz_stereo",
156 "ulaw_44khz_mono",
157 "ulaw_44khz_stereo",
158];
159
160#[derive(Default)]
174pub struct VoiceRSSOptions {
175 language: Option<Language>,
176 voice: Option<Cow<'static, str>>,
177 speed: Option<i8>,
178 codec: Option<Codec>,
179 audio_format: Option<Cow<'static, str>>,
180 ssml: Option<bool>,
181 base64: Option<bool>,
182}
183
184impl VoiceRSSOptions {
185 #[allow(missing_docs)]
186 pub fn new() -> Self {
187 Self::default()
188 }
189
190 pub fn language(&mut self, language: Language) -> &mut Self {
192 self.language = Some(language);
193 self
194 }
195
196 pub fn voice(&mut self, voice: impl Into<Cow<'static, str>>) -> &mut Self {
198 self.voice = Some(voice.into());
199 self
200 }
201
202 pub fn speed(&mut self, speed: i8) -> &mut Self {
204 assert!(
205 speed >= -10 && speed <= 10,
206 "speed should be between -10 and 10"
207 );
208 self.speed = Some(speed);
209 self
210 }
211
212 pub fn codec(&mut self, codec: Codec) -> &mut Self {
214 self.codec = Some(codec);
215 self
216 }
217
218 pub fn audio_format(&mut self, audio_format: impl Into<Cow<'static, str>>) -> &mut Self {
220 let format = audio_format.into();
221
222 assert!(AUDIO_FORMATS.iter().any(|&f| f == format));
223
224 self.audio_format = Some(format);
225 self
226 }
227
228 pub fn ssml(&mut self, ssml: bool) -> &mut Self {
230 self.ssml = Some(ssml);
231 self
232 }
233
234 pub fn base64(&mut self, base64: bool) -> &mut Self {
236 self.base64 = Some(base64);
237 self
238 }
239
240 pub fn url(&self, key: &str, text: &str) -> String {
242 assert!(
243 key.chars().all(char::is_alphanumeric),
244 "key should be alphanumeric"
245 );
246
247 let language = self.language.unwrap_or_default();
248 let text = percent_encoding::utf8_percent_encode(text, crate::ENCODE_SET);
249
250 let mut url = format!("http://api.voicerss.org/?key={}&hl={}", key, language);
251
252 if let Some(voice) = &self.voice {
253 url.push_str(&format!("&v={}", voice));
254 }
255 if let Some(speed) = self.speed {
256 url.push_str(&format!("&r={}", speed));
257 }
258 if let Some(codec) = &self.codec {
259 url.push_str(&format!("&c={}", codec));
260 }
261 if let Some(audio_format) = &self.audio_format {
262 url.push_str(&format!("&f={}", audio_format));
263 }
264 if let Some(ssml) = &self.ssml {
265 url.push_str(&format!("&sml={}", ssml));
266 }
267
268 url.push_str(&format!("&src={}", text));
269
270 url
271 }
272}
273
274pub fn url(key: &str, text: &str) -> String {
276 VoiceRSSOptions::default().url(key, text)
277}
278
279#[test]
280#[should_panic]
281fn invalid_speed() {
282 VoiceRSSOptions::new().speed(11);
283}
284
285#[test]
286#[should_panic]
287fn invalid_key() {
288 VoiceRSSOptions::new().url("/?;", "");
289}
290
291#[test]
292fn unicode() {
293 let url = VoiceRSSOptions::new()
294 .language(Language::Russian)
295 .url("key", "Добрый день!");
296
297 assert_eq!(
298 url,
299 "http://api.voicerss.org/?key=key&hl=ru-ru&src=%D0%94%D0%BE%D0%B1%D1%80%D1%8B%D0%B9%20%D0%B4%D0%B5%D0%BD%D1%8C%21"
300 );
301}
302#[test]
303fn language_parse() {
304 assert_eq!(Language::ArabicEgypt, "ar-eg".parse().unwrap());
305}
306
307#[test]
308fn language_alias() {
309 assert_eq!(Language::German, "de".parse().unwrap());
310}