use jmap_types::{GetObject, JmapObject, PatchObject, QueryObject, SetObject};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CalendarProperty {
Id,
Name,
Description,
Color,
SortOrder,
IsSubscribed,
IsVisible,
IsDefault,
IncludeInAvailability,
DefaultAlertsWithTime,
DefaultAlertsWithoutTime,
TimeZone,
ShareWith,
MyRights,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CalendarEventProperty {
Id,
BaseEventId,
CalendarIds,
IsDraft,
IsOrigin,
UtcStart,
UtcEnd,
UseDefaultAlerts,
MayInviteSelf,
MayInviteOthers,
HideAttendees,
BlobId,
Uid,
Title,
Description,
Start,
Duration,
Status,
}
pub const PER_USER_CALENDAR_EVENT_PROPERTIES: &[&str] = &[
"keywords",
"color",
"freeBusyStatus",
"useDefaultAlerts",
"alerts",
];
#[allow(dead_code)]
const PER_USER_PROPERTIES_IN_ENUM: &[&str] = &["useDefaultAlerts"];
#[allow(dead_code)]
const PER_USER_PROPERTIES_NOT_YET_IN_ENUM: &[&str] =
&["keywords", "color", "freeBusyStatus", "alerts"];
#[must_use]
pub fn is_per_user_calendar_event_property(name: &str) -> bool {
PER_USER_CALENDAR_EVENT_PROPERTIES.contains(&name)
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CalendarEventNotificationProperty {
Id,
Created,
ChangedBy,
Comment,
Type,
CalendarEventId,
IsDraft,
Event,
EventPatch,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ParticipantIdentityProperty {
Id,
Name,
CalendarAddress,
IsDefault,
}
impl JmapObject for crate::Calendar {
const TYPE_NAME: &'static str = "Calendar";
type Property = CalendarProperty;
}
impl GetObject for crate::Calendar {}
impl SetObject for crate::Calendar {
type Patch = PatchObject;
}
impl QueryObject for crate::Calendar {
type Filter = crate::CalendarFilterCondition;
type Comparator = serde_json::Value;
}
impl JmapObject for crate::CalendarEvent {
const TYPE_NAME: &'static str = "CalendarEvent";
type Property = CalendarEventProperty;
}
impl GetObject for crate::CalendarEvent {}
impl SetObject for crate::CalendarEvent {
type Patch = PatchObject;
}
impl QueryObject for crate::CalendarEvent {
type Filter = crate::CalendarEventFilterCondition;
type Comparator = crate::CalendarEventComparator;
}
impl JmapObject for crate::CalendarEventNotification {
const TYPE_NAME: &'static str = "CalendarEventNotification";
type Property = CalendarEventNotificationProperty;
}
impl GetObject for crate::CalendarEventNotification {}
impl SetObject for crate::CalendarEventNotification {
type Patch = PatchObject;
}
impl QueryObject for crate::CalendarEventNotification {
type Filter = crate::NotificationFilterCondition;
type Comparator = serde_json::Value;
}
impl JmapObject for crate::ParticipantIdentity {
const TYPE_NAME: &'static str = "ParticipantIdentity";
type Property = ParticipantIdentityProperty;
}
impl GetObject for crate::ParticipantIdentity {}
impl SetObject for crate::ParticipantIdentity {
type Patch = PatchObject;
}
impl QueryObject for crate::ParticipantIdentity {
type Filter = serde_json::Value;
type Comparator = serde_json::Value;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn per_user_calendar_event_properties_match_spec() {
const SPEC_REGISTERED_PER_USER_PROPERTIES_VERBATIM: &str = "\
* keywords
* color
* freeBusyStatus
* useDefaultAlerts
* alerts
";
let spec_list: Vec<&str> = SPEC_REGISTERED_PER_USER_PROPERTIES_VERBATIM
.lines()
.filter_map(|line| line.strip_prefix("* ").map(str::trim))
.filter(|name| !name.is_empty())
.collect();
assert_eq!(
spec_list.len(),
5,
"spec-quote parser sanity: §10.8.2 bullet list has exactly 5 entries; \
parser found {}: {spec_list:?}",
spec_list.len()
);
assert_eq!(
PER_USER_CALENDAR_EVENT_PROPERTIES,
spec_list.as_slice(),
"PER_USER_CALENDAR_EVENT_PROPERTIES must match \
draft-ietf-jmap-calendars-26 §10.8.2 verbatim (including order)"
);
}
#[test]
fn is_per_user_classifies_spec_properties_as_true() {
for name in PER_USER_CALENDAR_EVENT_PROPERTIES {
assert!(
is_per_user_calendar_event_property(name),
"expected {name} to be classified per-user"
);
}
}
#[test]
fn is_per_user_classifies_shared_properties_as_false() {
for shared in &["id", "title", "start", "duration", "calendarIds", "uid"] {
assert!(
!is_per_user_calendar_event_property(shared),
"expected {shared} to be classified shared"
);
}
}
#[test]
fn is_per_user_rejects_unknown_property() {
assert!(!is_per_user_calendar_event_property(""));
assert!(!is_per_user_calendar_event_property("notARealProperty"));
assert!(!is_per_user_calendar_event_property("alerts/abc"));
}
#[test]
fn per_user_const_is_union_of_two_halves() {
use std::collections::BTreeSet;
let in_enum: BTreeSet<&str> = PER_USER_PROPERTIES_IN_ENUM.iter().copied().collect();
let not_yet: BTreeSet<&str> = PER_USER_PROPERTIES_NOT_YET_IN_ENUM
.iter()
.copied()
.collect();
let public: BTreeSet<&str> = PER_USER_CALENDAR_EVENT_PROPERTIES.iter().copied().collect();
let overlap: BTreeSet<&&str> = in_enum.intersection(¬_yet).collect();
assert!(
overlap.is_empty(),
"PER_USER_PROPERTIES_IN_ENUM and PER_USER_PROPERTIES_NOT_YET_IN_ENUM \
must be disjoint, but both contain: {overlap:?}. When you promote a \
property to the enum, move its string from NOT_YET_IN_ENUM to IN_ENUM, \
do not duplicate it."
);
let union: BTreeSet<&str> = in_enum.union(¬_yet).copied().collect();
assert_eq!(
union, public,
"PER_USER_CALENDAR_EVENT_PROPERTIES must equal the union of the two halves"
);
}
#[test]
fn per_user_in_enum_matches_enum_variants() {
use std::collections::BTreeSet;
fn classify(p: &CalendarEventProperty) -> (&'static str, bool) {
match p {
CalendarEventProperty::Id => ("id", false),
CalendarEventProperty::BaseEventId => ("baseEventId", false),
CalendarEventProperty::CalendarIds => ("calendarIds", false),
CalendarEventProperty::IsDraft => ("isDraft", false),
CalendarEventProperty::IsOrigin => ("isOrigin", false),
CalendarEventProperty::UtcStart => ("utcStart", false),
CalendarEventProperty::UtcEnd => ("utcEnd", false),
CalendarEventProperty::UseDefaultAlerts => ("useDefaultAlerts", true),
CalendarEventProperty::MayInviteSelf => ("mayInviteSelf", false),
CalendarEventProperty::MayInviteOthers => ("mayInviteOthers", false),
CalendarEventProperty::HideAttendees => ("hideAttendees", false),
CalendarEventProperty::BlobId => ("blobId", false),
CalendarEventProperty::Uid => ("uid", false),
CalendarEventProperty::Title => ("title", false),
CalendarEventProperty::Description => ("description", false),
CalendarEventProperty::Start => ("start", false),
CalendarEventProperty::Duration => ("duration", false),
CalendarEventProperty::Status => ("status", false),
}
}
let variants = [
CalendarEventProperty::Id,
CalendarEventProperty::BaseEventId,
CalendarEventProperty::CalendarIds,
CalendarEventProperty::IsDraft,
CalendarEventProperty::IsOrigin,
CalendarEventProperty::UtcStart,
CalendarEventProperty::UtcEnd,
CalendarEventProperty::UseDefaultAlerts,
CalendarEventProperty::MayInviteSelf,
CalendarEventProperty::MayInviteOthers,
CalendarEventProperty::HideAttendees,
CalendarEventProperty::BlobId,
CalendarEventProperty::Uid,
CalendarEventProperty::Title,
CalendarEventProperty::Description,
CalendarEventProperty::Start,
CalendarEventProperty::Duration,
CalendarEventProperty::Status,
];
let derived_per_user: BTreeSet<&str> = variants
.iter()
.filter_map(|p| {
let (wire, is_per_user) = classify(p);
is_per_user.then_some(wire)
})
.collect();
let declared_per_user: BTreeSet<&str> =
PER_USER_PROPERTIES_IN_ENUM.iter().copied().collect();
assert_eq!(
derived_per_user, declared_per_user,
"PER_USER_PROPERTIES_IN_ENUM ({declared_per_user:?}) must match the per-user \
variants derived from the CalendarEventProperty match in this test \
({derived_per_user:?}). Update the const, the match, or both as the spec dictates."
);
let mut wire_names = BTreeSet::new();
for p in &variants {
let (wire, _) = classify(p);
assert!(
wire_names.insert(wire),
"duplicate wire-name {wire:?} in classify(); a CalendarEventProperty \
variant has the wrong wire-string"
);
}
}
}