use serde::{Deserialize, Serialize};
use crate::visibility::Visibility;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ScheduleWindow {
pub day: String,
pub start: String,
pub end: String,
pub timezone: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BookingEndpoint {
pub id: String,
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub booking_type: Option<String>,
#[serde(default)]
pub visibility: Visibility,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CalendarSection {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub booking_endpoints: Vec<BookingEndpoint>,
#[serde(skip_serializing_if = "Option::is_none")]
pub availability_feed: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scheduling_protocol: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub office_hours: Vec<ScheduleWindow>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timezone: Option<String>,
}
pub mod booking_types {
pub const CONSULTATION: &str = "consultation";
pub const SUPPORT: &str = "support";
pub const MEETING: &str = "meeting";
pub const TASK: &str = "task";
pub const CUSTOM: &str = "custom";
}
pub mod scheduling_protocols {
pub const ICAL_INVITE: &str = "ical-invite";
pub const API: &str = "api";
pub const AGENT_NEGOTIATION: &str = "agent-negotiation";
pub const MANUAL: &str = "manual";
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serde_round_trip() {
let calendar = CalendarSection {
booking_endpoints: vec![BookingEndpoint {
id: "booking-1".to_string(),
url: "https://cal.com/agent/30min".to_string(),
label: Some("30-min call".to_string()),
booking_type: Some(booking_types::CONSULTATION.to_string()),
visibility: Visibility::Public,
}],
availability_feed: Some("https://cal.com/agent/avail.ics".to_string()),
scheduling_protocol: Some(scheduling_protocols::ICAL_INVITE.to_string()),
office_hours: vec![ScheduleWindow {
day: "monday".to_string(),
start: "09:00".to_string(),
end: "17:00".to_string(),
timezone: "UTC".to_string(),
}],
timezone: Some("UTC".to_string()),
};
let json = serde_json::to_string_pretty(&calendar).unwrap_or_default();
let parsed: CalendarSection =
serde_json::from_str(&json).unwrap_or_else(|_| calendar.clone());
assert_eq!(calendar, parsed);
}
#[test]
fn internal_booking_is_redacted() {
let booking = BookingEndpoint {
id: "booking-internal".to_string(),
url: "https://internal.acme.com/schedule".to_string(),
label: Some("Internal only".to_string()),
booking_type: None,
visibility: Visibility::Organization,
};
assert!(booking.visibility.is_redacted_on_dht());
}
#[test]
fn minimal_calendar_deserializes() {
let json = r#"{"timezone":"UTC"}"#;
let cal: Result<CalendarSection, _> = serde_json::from_str(json);
assert!(cal.is_ok());
let cal = cal.unwrap_or_else(|_| CalendarSection {
booking_endpoints: vec![],
availability_feed: None,
scheduling_protocol: None,
office_hours: vec![],
timezone: None,
});
assert!(cal.booking_endpoints.is_empty());
assert_eq!(cal.timezone.as_deref(), Some("UTC"));
}
}