1use std::collections::HashMap;
4
5use serde::{Deserialize, Deserializer, Serialize};
6
7pub type ResourceLinks = HashMap<String, String>;
9
10pub(crate) fn null_to_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
22where
23 D: Deserializer<'de>,
24 T: Deserialize<'de> + Default,
25{
26 Ok(Option::<T>::deserialize(deserializer)?.unwrap_or_default())
27}
28
29#[derive(Debug, Clone, Deserialize, Serialize)]
31pub struct MessageResponse {
32 pub message: String,
34}
35
36macro_rules! string_enum {
43 (
44 $(#[$meta:meta])*
45 $name:ident {
46 $($(#[$variant_meta:meta])* $constant:ident => $value:literal),* $(,)?
47 }
48 ) => {
49 $(#[$meta])*
50 #[derive(Debug, Clone, PartialEq, Eq, Hash, ::serde::Serialize, ::serde::Deserialize)]
51 #[serde(transparent)]
52 pub struct $name(::std::borrow::Cow<'static, str>);
53
54 impl $name {
55 $($(#[$variant_meta])* pub const $constant: Self =
56 Self(::std::borrow::Cow::Borrowed($value));)*
57
58 pub fn as_str(&self) -> &str {
60 &self.0
61 }
62 }
63
64 impl ::std::fmt::Display for $name {
65 fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
66 f.write_str(&self.0)
67 }
68 }
69
70 impl AsRef<str> for $name {
71 fn as_ref(&self) -> &str {
72 &self.0
73 }
74 }
75
76 impl From<&str> for $name {
77 fn from(value: &str) -> Self {
78 Self(::std::borrow::Cow::Owned(value.to_string()))
79 }
80 }
81
82 impl From<String> for $name {
83 fn from(value: String) -> Self {
84 Self(::std::borrow::Cow::Owned(value))
85 }
86 }
87 };
88}
89
90pub(crate) use string_enum;
91
92string_enum! {
93 Region {
105 US001 => "us001",
107 UK001 => "uk001",
109 SG001 => "sg001",
111 EU001 => "eu001",
113 UAE001 => "uae001",
115 IN001 => "in001",
117 IN002 => "in002",
119 IN003 => "in003",
121 SG002 => "sg002",
123 US002 => "us002",
125 AUS001 => "aus001",
127 DEV1 => "dev1",
129 }
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
136#[serde(rename_all = "UPPERCASE")]
137pub enum LayoutType {
138 Spotlight,
140 Sidebar,
142 Grid,
144}
145
146#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
148#[serde(rename_all = "UPPERCASE")]
149pub enum LayoutPriority {
150 Speaker,
152 Pin,
154}
155
156#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
158#[serde(rename_all = "lowercase")]
159pub enum Orientation {
160 Portrait,
162 Landscape,
164}
165
166#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
168#[serde(rename_all = "UPPERCASE")]
169pub enum Theme {
170 Dark,
172 Light,
174 Default,
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
180#[serde(rename_all = "lowercase")]
181pub enum CompositionQuality {
182 Low,
184 Med,
186 High,
188 Ultra,
190}
191
192#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
194#[serde(rename_all = "camelCase")]
195pub struct LayoutConfig {
196 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
198 pub kind: Option<LayoutType>,
199 #[serde(skip_serializing_if = "Option::is_none")]
201 pub priority: Option<LayoutPriority>,
202 #[serde(skip_serializing_if = "Option::is_none")]
204 pub grid_size: Option<u32>,
205}
206
207#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
209#[serde(rename_all = "camelCase")]
210pub struct CompositionConfig {
211 #[serde(skip_serializing_if = "Option::is_none")]
213 pub layout: Option<LayoutConfig>,
214 #[serde(skip_serializing_if = "Option::is_none")]
216 pub orientation: Option<Orientation>,
217 #[serde(skip_serializing_if = "Option::is_none")]
219 pub theme: Option<Theme>,
220 #[serde(skip_serializing_if = "Option::is_none")]
222 pub quality: Option<CompositionQuality>,
223}
224
225impl CompositionConfig {
226 pub fn is_empty(&self) -> bool {
228 self.layout.is_none()
229 && self.orientation.is_none()
230 && self.theme.is_none()
231 && self.quality.is_none()
232 }
233}
234
235#[derive(Debug, Clone, Default, Serialize, Deserialize)]
239pub struct SummaryConfig {
240 #[serde(skip_serializing_if = "Option::is_none")]
242 pub enabled: Option<bool>,
243 #[serde(skip_serializing_if = "Option::is_none")]
245 pub prompt: Option<String>,
246}
247
248#[derive(Debug, Clone, Default, Serialize, Deserialize)]
250pub struct TranscriptionConfig {
251 #[serde(skip_serializing_if = "Option::is_none")]
253 pub enabled: Option<bool>,
254 #[serde(skip_serializing_if = "Option::is_none")]
256 pub summary: Option<SummaryConfig>,
257}
258
259#[derive(Debug, Clone, Serialize, Deserialize)]
261#[serde(rename_all = "camelCase")]
262pub struct OnFailureConfig {
263 pub action: String,
265 #[serde(skip_serializing_if = "Option::is_none")]
267 pub wait_time: Option<u32>,
268}
269
270impl OnFailureConfig {
271 pub fn close_room(wait_time: Option<u32>) -> Self {
273 Self {
274 action: "close-room".to_string(),
275 wait_time,
276 }
277 }
278}
279
280#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
284#[serde(rename_all = "lowercase")]
285pub enum RecordingFileFormat {
286 Mp4,
288 Webm,
290}
291
292#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
294#[serde(rename_all = "lowercase")]
295pub enum ParticipantFileFormat {
296 Mp4,
298 Webm,
300 M3u8,
302}
303
304#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
306#[serde(rename_all = "lowercase")]
307pub enum TrackFileFormat {
308 Mp4,
310 Webm,
312 M3u8,
314 Mp3,
316}
317
318#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
320#[serde(rename_all = "snake_case")]
321pub enum TrackKind {
322 Audio,
324 Video,
326 ScreenAudio,
328 ScreenVideo,
330}
331
332#[derive(Debug, Clone, Deserialize)]
334#[serde(rename_all = "camelCase")]
335pub struct RecordingFile {
336 pub file_url: Option<String>,
338 #[serde(flatten)]
340 pub extra: serde_json::Map<String, serde_json::Value>,
341}
342
343#[derive(Debug, Clone, Deserialize)]
345pub struct WebhookDelivery {
346 #[serde(rename = "type")]
348 pub kind: String,
349 pub success: bool,
351}
352
353#[derive(Debug, Clone, Deserialize)]
355#[serde(rename_all = "camelCase")]
356pub struct WebhookDeliverySummary {
357 #[serde(default, deserialize_with = "crate::common::null_to_default")]
359 pub total_count: u32,
360 #[serde(default, deserialize_with = "crate::common::null_to_default")]
362 pub success_count: u32,
363 #[serde(default, deserialize_with = "crate::common::null_to_default")]
365 pub data: Vec<WebhookDelivery>,
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371 use serde_json::json;
372
373 #[test]
374 fn enums_use_the_wire_casing_the_api_expects() {
375 assert_eq!(
376 serde_json::to_value(LayoutType::Grid).unwrap(),
377 json!("GRID")
378 );
379 assert_eq!(
380 serde_json::to_value(LayoutPriority::Speaker).unwrap(),
381 json!("SPEAKER")
382 );
383 assert_eq!(
384 serde_json::to_value(Orientation::Portrait).unwrap(),
385 json!("portrait")
386 );
387 assert_eq!(serde_json::to_value(Theme::Dark).unwrap(), json!("DARK"));
388 assert_eq!(
389 serde_json::to_value(CompositionQuality::Ultra).unwrap(),
390 json!("ultra")
391 );
392 assert_eq!(
393 serde_json::to_value(TrackKind::ScreenAudio).unwrap(),
394 json!("screen_audio")
395 );
396 assert_eq!(
397 serde_json::to_value(TrackFileFormat::M3u8).unwrap(),
398 json!("m3u8")
399 );
400 assert_eq!(
401 serde_json::to_value(RecordingFileFormat::Mp4).unwrap(),
402 json!("mp4")
403 );
404 }
405
406 #[test]
407 fn layout_config_serializes_type_not_kind() {
408 let config = LayoutConfig {
409 kind: Some(LayoutType::Sidebar),
410 priority: Some(LayoutPriority::Pin),
411 grid_size: None,
412 };
413 assert_eq!(
414 serde_json::to_value(config).unwrap(),
415 json!({"type": "SIDEBAR", "priority": "PIN"})
416 );
417 }
418
419 #[test]
420 fn an_empty_composition_config_is_empty() {
421 assert!(CompositionConfig::default().is_empty());
422 assert!(!CompositionConfig {
423 theme: Some(Theme::Light),
424 ..Default::default()
425 }
426 .is_empty());
427 }
428
429 #[test]
430 fn on_failure_helper_builds_the_documented_action() {
431 assert_eq!(
432 serde_json::to_value(OnFailureConfig::close_room(Some(30))).unwrap(),
433 json!({"action": "close-room", "waitTime": 30})
434 );
435 }
436
437 #[test]
438 fn region_is_transparent_on_the_wire() {
439 assert_eq!(serde_json::to_value(Region::IN002).unwrap(), json!("in002"));
440 let parsed: Region = serde_json::from_value(json!("us001")).unwrap();
441 assert_eq!(parsed, Region::US001);
442 }
443
444 #[test]
445 fn an_unknown_region_round_trips() {
446 let parsed: Region = serde_json::from_value(json!("future-region")).unwrap();
447 assert_eq!(parsed.as_str(), "future-region");
448 assert_eq!(
449 serde_json::to_value(&parsed).unwrap(),
450 json!("future-region")
451 );
452 }
453}