use std::fmt;
use std::str::FromStr;
use std::time::Duration;
use crate::client::Response;
use crate::error::Error;
use crate::form::FormResponse;
use crate::generated::types::Recording;
use crate::http::Method;
use crate::observability::OperationInfo;
use crate::services::write_info;
use crate::types::Date;
pub use crate::generated::services::calendar_events::*;
const ATTENDEES: &str = "calendar_event[attendance_email_addresses][]";
const ALL_DAY_REMINDERS: &str = "all_day_reminder_durations[]";
const TIMED_REMINDERS: &str = "timed_reminder_durations[]";
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct EventContent {
pub notes: String,
pub location: String,
pub link: Option<String>,
pub entry_id: Option<i64>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum CountdownUnit {
#[default]
Days = 86_400,
Weeks = 604_800,
Months = 2_629_746,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Countdown {
pub value: u32,
pub unit: CountdownUnit,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum RepeatFrequency {
EveryDay,
EveryWeekday,
EveryWeek,
EveryOtherWeek,
EveryDayOfMonth,
EveryYear,
#[default]
Custom,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RepeatUntil {
Forever,
Date,
Count,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Repeat {
pub frequency: RepeatFrequency,
pub until: Option<RepeatUntil>,
pub until_date: Option<Date>,
pub count: Option<u32>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CreateCalendarEventParams {
pub calendar_id: i64,
pub title: String,
pub starts_at: String,
pub ends_at: String,
pub all_day: bool,
pub start_time: String,
pub end_time: String,
pub start_time_zone: String,
pub end_time_zone: String,
pub time_zone: String,
pub reminders: Vec<Duration>,
pub content: EventContent,
pub attendees: Option<Vec<String>>,
pub highlighted: Option<bool>,
pub countdown: Countdown,
pub repeat: Option<Repeat>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct UpdateCalendarEventParams {
pub calendar_id: Option<i64>,
pub title: Option<String>,
pub starts_at: Option<String>,
pub ends_at: Option<String>,
pub all_day: Option<bool>,
pub start_time: Option<String>,
pub end_time: Option<String>,
pub start_time_zone: Option<String>,
pub end_time_zone: Option<String>,
pub time_zone: Option<String>,
pub reminders: Vec<Duration>,
pub content: EventContent,
pub attendees: Option<Vec<String>>,
pub highlighted: Option<bool>,
pub countdown: Countdown,
pub repeat: Option<Repeat>,
}
pub type UpdateOccurrenceParams = UpdateCalendarEventParams;
#[derive(Debug, Clone, Default, PartialEq)]
pub struct CalendarEventUpdate {
pub title: Option<String>,
pub starts_at: Option<String>,
pub ends_at: Option<String>,
pub all_day: Option<bool>,
pub start_time: Option<String>,
pub end_time: Option<String>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct OccurrenceId {
pub event_id: i64,
pub date: Date,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum OccurrenceScope {
#[default]
ThisOnly,
ThisAndFollowing,
}
impl<'a> CalendarEvents<'a> {
pub async fn create(&self, params: &CreateCalendarEventParams) -> Result<Recording, Error> {
self.write(
Method::POST,
"/calendar/events.json".to_string(),
write_info(
"CalendarEvents",
"CreateCalendarEvent",
"calendar_event",
None,
),
&create_fields(params),
)
.await
}
pub async fn update(&self, event_id: i64, update: &CalendarEventUpdate) -> Result<(), Error> {
let fields = update_fields(update);
let mut operation = self
.client()
.request(Method::PATCH, format!("/calendar/events/{event_id}"));
operation
.info(write_info(
"CalendarEvents",
"UpdateCalendarEvent",
"calendar_event",
Some(event_id),
))
.form(&borrowed(&fields))
.accept("application/json");
self.client().send_unit(operation).await
}
pub async fn update_event(
&self,
event_id: i64,
params: &UpdateCalendarEventParams,
) -> Result<Recording, Error> {
self.write(
Method::PATCH,
format!("/calendar/events/{event_id}.json"),
write_info(
"CalendarEvents",
"UpdateCalendarEvent",
"calendar_event",
Some(event_id),
),
&update_event_fields(params),
)
.await
}
pub async fn update_occurrence(
&self,
occurrence: &OccurrenceId,
scope: OccurrenceScope,
params: &UpdateOccurrenceParams,
) -> Result<Recording, Error> {
let mut fields = update_event_fields(params);
fields.push((
"apply_to_future",
checkbox(scope == OccurrenceScope::ThisAndFollowing),
));
if params.repeat.is_none() {
fields.push(("repeat_frequency", RepeatFrequency::Custom.to_string()));
}
self.write(
Method::PATCH,
occurrence.path(),
write_info(
"CalendarEvents",
"UpdateCalendarEventOccurrence",
"calendar_event",
Some(occurrence.event_id),
),
&fields,
)
.await
}
pub async fn delete_occurrence_scoped(
&self,
occurrence: &OccurrenceId,
scope: OccurrenceScope,
) -> Result<(), Error> {
let params = DeleteCalendarEventOccurrenceParams {
apply_to_future: apply_to_future(scope),
};
self.delete_occurrence(occurrence.event_id, &occurrence.date.to_string(), ¶ms)
.await
}
async fn write(
&self,
method: Method,
path: String,
info: OperationInfo,
fields: &[(&'static str, String)],
) -> Result<Recording, Error> {
let mut operation = self.client().form(method, &path)?;
operation.info(info);
operation.form(&borrowed(fields));
recording_from_form_response(&self.client().execute(operation).await?)
}
}
fn create_fields(params: &CreateCalendarEventParams) -> Vec<(&'static str, String)> {
let mut ends_at = params.ends_at.as_str();
if ends_at.is_empty() {
ends_at = ¶ms.starts_at;
}
let mut fields = vec![
(
"calendar_event[calendar_id]",
params.calendar_id.to_string(),
),
("calendar_event[summary]", params.title.clone()),
("calendar_event[starts_at]", params.starts_at.clone()),
("calendar_event[ends_at]", ends_at.to_string()),
];
push_content(&mut fields, ¶ms.content);
push_attendees(&mut fields, params.attendees.as_deref());
push_highlighted(&mut fields, params.highlighted);
push_countdown(&mut fields, params.countdown);
push_repeat(&mut fields, params.repeat.as_ref());
if params.all_day {
fields.push(("calendar_event[all_day]", checkbox(true)));
push_reminders(&mut fields, ALL_DAY_REMINDERS, ¶ms.reminders);
} else {
fields.push(("calendar_event[all_day]", checkbox(false)));
fields.push((
"calendar_event[starts_at_time]",
format!("{}:00", params.start_time),
));
fields.push((
"calendar_event[ends_at_time]",
format!("{}:00", params.end_time),
));
push_time_zones(
&mut fields,
time_zone_or(¶ms.start_time_zone, ¶ms.time_zone),
time_zone_or(¶ms.end_time_zone, ¶ms.time_zone),
);
push_reminders(&mut fields, TIMED_REMINDERS, ¶ms.reminders);
}
fields
}
fn push_content(fields: &mut Vec<(&'static str, String)>, content: &EventContent) {
fields.push(("calendar_event[description]", content.notes.clone()));
fields.push(("calendar_event[location]", content.location.clone()));
fields.push((
"calendar_event[url]",
content.link.clone().unwrap_or_default(),
));
fields.push((
"calendar_event[entry_id]",
content
.entry_id
.filter(|entry_id| *entry_id != 0)
.map(|entry_id| entry_id.to_string())
.unwrap_or_default(),
));
}
fn push_attendees(fields: &mut Vec<(&'static str, String)>, attendees: Option<&[String]>) {
if let Some(addresses) = attendees {
if addresses.is_empty() {
fields.push((ATTENDEES, String::new()));
} else {
for address in addresses {
fields.push((ATTENDEES, address.clone()));
}
}
}
}
fn push_highlighted(fields: &mut Vec<(&'static str, String)>, highlighted: Option<bool>) {
if let Some(highlighted) = highlighted {
fields.push(("calendar_event[highlighted]", checkbox(highlighted)));
fields.push(("calendar_event[highlight_id]", String::new()));
}
}
fn checkbox(value: bool) -> String {
if value {
"1".to_string()
} else {
"0".to_string()
}
}
fn push_countdown(fields: &mut Vec<(&'static str, String)>, countdown: Countdown) {
if countdown.value > 0 {
fields.push((
"countdown_interval_duration_value",
countdown.value.to_string(),
));
fields.push((
"countdown_interval_duration_unit",
countdown.unit.seconds().to_string(),
));
}
}
fn push_repeat(fields: &mut Vec<(&'static str, String)>, repeat: Option<&Repeat>) {
if let Some(repeat) = repeat {
fields.push(("repeat_frequency", repeat.frequency.to_string()));
if let Some(until) = repeat.until {
fields.push((
"calendar_recurrence_schedule[recurs_until_type]",
until.to_string(),
));
}
if repeat.until == Some(RepeatUntil::Date) {
fields.push((
"calendar_recurrence_schedule[recurs_until_date]",
repeat
.until_date
.map(|date| date.to_string())
.unwrap_or_default(),
));
}
if repeat.until == Some(RepeatUntil::Count) {
fields.push((
"calendar_recurrence_schedule[recurs_count]",
repeat.count.unwrap_or_default().to_string(),
));
}
}
}
fn push_time_zones(fields: &mut Vec<(&'static str, String)>, start: &str, end: &str) {
if start.is_empty() && end.is_empty() {
fields.push(("calendar_event[set_time_zone]", checkbox(false)));
} else {
let mut starts_in = start;
let mut ends_in = end;
if starts_in.is_empty() {
starts_in = ends_in;
}
if ends_in.is_empty() {
ends_in = starts_in;
}
fields.push(("calendar_event[set_time_zone]", checkbox(true)));
fields.push((
"calendar_event[starts_at_time_zone_name]",
starts_in.to_string(),
));
fields.push((
"calendar_event[ends_at_time_zone_name]",
ends_in.to_string(),
));
}
}
fn time_zone_or<'a>(zone: &'a str, both: &'a str) -> &'a str {
if zone.is_empty() { both } else { zone }
}
fn push_reminders(
fields: &mut Vec<(&'static str, String)>,
key: &'static str,
reminders: &[Duration],
) {
for reminder in reminders {
fields.push((key, reminder.as_secs().to_string()));
}
}
fn update_fields(update: &CalendarEventUpdate) -> Vec<(&'static str, String)> {
let mut fields = Vec::new();
if let Some(title) = &update.title {
fields.push(("calendar_event[summary]", title.clone()));
}
if let Some(starts_at) = &update.starts_at {
fields.push(("calendar_event[starts_at]", starts_at.clone()));
}
if let Some(ends_at) = &update.ends_at {
fields.push(("calendar_event[ends_at]", ends_at.clone()));
}
if let Some(all_day) = update.all_day {
fields.push(("calendar_event[all_day]", checkbox(all_day)));
}
if update.all_day != Some(true) {
if let Some(start_time) = &update.start_time {
fields.push(("calendar_event[starts_at_time]", format!("{start_time}:00")));
}
if let Some(end_time) = &update.end_time {
fields.push(("calendar_event[ends_at_time]", format!("{end_time}:00")));
}
}
fields
}
fn update_event_fields(params: &UpdateCalendarEventParams) -> Vec<(&'static str, String)> {
let mut fields = Vec::new();
if let Some(title) = ¶ms.title {
fields.push(("calendar_event[summary]", title.clone()));
}
if let Some(starts_at) = ¶ms.starts_at {
fields.push(("calendar_event[starts_at]", starts_at.clone()));
}
if let Some(ends_at) = ¶ms.ends_at {
fields.push(("calendar_event[ends_at]", ends_at.clone()));
}
if let Some(all_day) = params.all_day {
fields.push(("calendar_event[all_day]", checkbox(all_day)));
}
if params.all_day != Some(true) {
if let Some(start_time) = ¶ms.start_time {
fields.push(("calendar_event[starts_at_time]", format!("{start_time}:00")));
}
if let Some(end_time) = ¶ms.end_time {
fields.push(("calendar_event[ends_at_time]", format!("{end_time}:00")));
}
}
if let Some(calendar_id) = params.calendar_id {
fields.push(("calendar_event[calendar_id]", calendar_id.to_string()));
}
push_content(&mut fields, ¶ms.content);
push_attendees(&mut fields, params.attendees.as_deref());
push_highlighted(&mut fields, params.highlighted);
push_countdown(&mut fields, params.countdown);
push_repeat(&mut fields, params.repeat.as_ref());
let starts_in = params
.start_time_zone
.as_ref()
.or(params.time_zone.as_ref());
let ends_in = params.end_time_zone.as_ref().or(params.time_zone.as_ref());
if starts_in.is_some() || ends_in.is_some() {
push_time_zones(
&mut fields,
starts_in.map(String::as_str).unwrap_or_default(),
ends_in.map(String::as_str).unwrap_or_default(),
);
}
let reminders_key = if params.all_day == Some(true) {
ALL_DAY_REMINDERS
} else {
TIMED_REMINDERS
};
push_reminders(&mut fields, reminders_key, ¶ms.reminders);
fields
}
fn borrowed<'a>(fields: &'a [(&'static str, String)]) -> Vec<(&'static str, &'a str)> {
fields
.iter()
.map(|(name, value)| (*name, value.as_str()))
.collect()
}
fn recording_from_form_response(answered: &Response) -> Result<Recording, Error> {
let written = FormResponse::new(answered);
if written.body.is_empty() {
Ok(Recording {
id: written.extract_id()?,
..Recording::default()
})
} else {
Ok(serde_json::from_str(&written.body)?)
}
}
fn apply_to_future(scope: OccurrenceScope) -> Option<bool> {
Some(scope == OccurrenceScope::ThisAndFollowing)
}
impl CountdownUnit {
pub fn seconds(self) -> u32 {
self as u32
}
}
impl RepeatFrequency {
pub fn as_str(&self) -> &'static str {
match self {
RepeatFrequency::EveryDay => "every_day",
RepeatFrequency::EveryWeekday => "every_weekday",
RepeatFrequency::EveryWeek => "every_week",
RepeatFrequency::EveryOtherWeek => "every_other_week",
RepeatFrequency::EveryDayOfMonth => "every_day_of_month",
RepeatFrequency::EveryYear => "every_year",
RepeatFrequency::Custom => "custom",
}
}
}
impl fmt::Display for RepeatFrequency {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl RepeatUntil {
pub fn as_str(&self) -> &'static str {
match self {
RepeatUntil::Forever => "forever",
RepeatUntil::Date => "date",
RepeatUntil::Count => "count",
}
}
}
impl fmt::Display for RepeatUntil {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl OccurrenceId {
fn path(&self) -> String {
format!(
"/calendar/events/{}/occurrences/{}.json",
self.event_id, self.date
)
}
}
impl FromStr for OccurrenceId {
type Err = Error;
fn from_str(source: &str) -> Result<OccurrenceId, Error> {
let (event, day) = source.split_once('_').ok_or_else(|| {
Error::usage(format!(
"occurrence id {source:?} is not <event id>_<YYYY-MM-DD>"
))
})?;
let event_id = match event.parse() {
Ok(event_id) if event_id > 0 => event_id,
_ => {
return Err(Error::usage(format!(
"occurrence id {source:?} names no event"
)));
}
};
let date = day.parse().map_err(|error| {
Error::usage(format!("occurrence id {source:?} names no date: {error}"))
})?;
Ok(OccurrenceId { event_id, date })
}
}
impl fmt::Display for OccurrenceId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}_{}", self.event_id, self.date)
}
}
impl FromStr for OccurrenceScope {
type Err = Error;
fn from_str(source: &str) -> Result<OccurrenceScope, Error> {
match source {
"this_event" => Ok(OccurrenceScope::ThisOnly),
"this_and_following" => Ok(OccurrenceScope::ThisAndFollowing),
_ => Err(Error::usage(format!(
"occurrence scope {source:?} is neither \"this_event\" nor \"this_and_following\""
))),
}
}
}