use std::collections::HashMap;
use serde::{Deserialize, Deserializer, Serialize};
pub type ResourceLinks = HashMap<String, String>;
pub(crate) fn null_to_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: Deserialize<'de> + Default,
{
Ok(Option::<T>::deserialize(deserializer)?.unwrap_or_default())
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct MessageResponse {
pub message: String,
}
macro_rules! string_enum {
(
$(#[$meta:meta])*
$name:ident {
$($(#[$variant_meta:meta])* $constant:ident => $value:literal),* $(,)?
}
) => {
$(#[$meta])*
#[derive(Debug, Clone, PartialEq, Eq, Hash, ::serde::Serialize, ::serde::Deserialize)]
#[serde(transparent)]
pub struct $name(::std::borrow::Cow<'static, str>);
impl $name {
$($(#[$variant_meta])* pub const $constant: Self =
Self(::std::borrow::Cow::Borrowed($value));)*
pub fn as_str(&self) -> &str {
&self.0
}
}
impl ::std::fmt::Display for $name {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
f.write_str(&self.0)
}
}
impl AsRef<str> for $name {
fn as_ref(&self) -> &str {
&self.0
}
}
impl From<&str> for $name {
fn from(value: &str) -> Self {
Self(::std::borrow::Cow::Owned(value.to_string()))
}
}
impl From<String> for $name {
fn from(value: String) -> Self {
Self(::std::borrow::Cow::Owned(value))
}
}
};
}
pub(crate) use string_enum;
string_enum! {
Region {
US001 => "us001",
UK001 => "uk001",
SG001 => "sg001",
EU001 => "eu001",
UAE001 => "uae001",
IN001 => "in001",
IN002 => "in002",
IN003 => "in003",
SG002 => "sg002",
US002 => "us002",
AUS001 => "aus001",
DEV1 => "dev1",
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum LayoutType {
Spotlight,
Sidebar,
Grid,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum LayoutPriority {
Speaker,
Pin,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Orientation {
Portrait,
Landscape,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum Theme {
Dark,
Light,
Default,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CompositionQuality {
Low,
Med,
High,
Ultra,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LayoutConfig {
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub kind: Option<LayoutType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub priority: Option<LayoutPriority>,
#[serde(skip_serializing_if = "Option::is_none")]
pub grid_size: Option<u32>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompositionConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub layout: Option<LayoutConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub orientation: Option<Orientation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub theme: Option<Theme>,
#[serde(skip_serializing_if = "Option::is_none")]
pub quality: Option<CompositionQuality>,
}
impl CompositionConfig {
pub fn is_empty(&self) -> bool {
self.layout.is_none()
&& self.orientation.is_none()
&& self.theme.is_none()
&& self.quality.is_none()
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SummaryConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TranscriptionConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<SummaryConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OnFailureConfig {
pub action: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub wait_time: Option<u32>,
}
impl OnFailureConfig {
pub fn close_room(wait_time: Option<u32>) -> Self {
Self {
action: "close-room".to_string(),
wait_time,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum RecordingFileFormat {
Mp4,
Webm,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ParticipantFileFormat {
Mp4,
Webm,
M3u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TrackFileFormat {
Mp4,
Webm,
M3u8,
Mp3,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TrackKind {
Audio,
Video,
ScreenAudio,
ScreenVideo,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RecordingFile {
pub file_url: Option<String>,
#[serde(flatten)]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct WebhookDelivery {
#[serde(rename = "type")]
pub kind: String,
pub success: bool,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WebhookDeliverySummary {
#[serde(default, deserialize_with = "crate::common::null_to_default")]
pub total_count: u32,
#[serde(default, deserialize_with = "crate::common::null_to_default")]
pub success_count: u32,
#[serde(default, deserialize_with = "crate::common::null_to_default")]
pub data: Vec<WebhookDelivery>,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn enums_use_the_wire_casing_the_api_expects() {
assert_eq!(
serde_json::to_value(LayoutType::Grid).unwrap(),
json!("GRID")
);
assert_eq!(
serde_json::to_value(LayoutPriority::Speaker).unwrap(),
json!("SPEAKER")
);
assert_eq!(
serde_json::to_value(Orientation::Portrait).unwrap(),
json!("portrait")
);
assert_eq!(serde_json::to_value(Theme::Dark).unwrap(), json!("DARK"));
assert_eq!(
serde_json::to_value(CompositionQuality::Ultra).unwrap(),
json!("ultra")
);
assert_eq!(
serde_json::to_value(TrackKind::ScreenAudio).unwrap(),
json!("screen_audio")
);
assert_eq!(
serde_json::to_value(TrackFileFormat::M3u8).unwrap(),
json!("m3u8")
);
assert_eq!(
serde_json::to_value(RecordingFileFormat::Mp4).unwrap(),
json!("mp4")
);
}
#[test]
fn layout_config_serializes_type_not_kind() {
let config = LayoutConfig {
kind: Some(LayoutType::Sidebar),
priority: Some(LayoutPriority::Pin),
grid_size: None,
};
assert_eq!(
serde_json::to_value(config).unwrap(),
json!({"type": "SIDEBAR", "priority": "PIN"})
);
}
#[test]
fn an_empty_composition_config_is_empty() {
assert!(CompositionConfig::default().is_empty());
assert!(!CompositionConfig {
theme: Some(Theme::Light),
..Default::default()
}
.is_empty());
}
#[test]
fn on_failure_helper_builds_the_documented_action() {
assert_eq!(
serde_json::to_value(OnFailureConfig::close_room(Some(30))).unwrap(),
json!({"action": "close-room", "waitTime": 30})
);
}
#[test]
fn region_is_transparent_on_the_wire() {
assert_eq!(serde_json::to_value(Region::IN002).unwrap(), json!("in002"));
let parsed: Region = serde_json::from_value(json!("us001")).unwrap();
assert_eq!(parsed, Region::US001);
}
#[test]
fn an_unknown_region_round_trips() {
let parsed: Region = serde_json::from_value(json!("future-region")).unwrap();
assert_eq!(parsed.as_str(), "future-region");
assert_eq!(
serde_json::to_value(&parsed).unwrap(),
json!("future-region")
);
}
}