JMAP for Calendars implementation (part 3)

This commit is contained in:
mdecimus
2025-10-08 19:52:23 +02:00
parent 2fb59edaa7
commit ec4963f46b
44 changed files with 1232 additions and 460 deletions

View File

@@ -40,7 +40,7 @@ pub struct BusyPeriod {
pub event: Option<JSCalendar<'static, Id, BlobId>>,
}
#[derive(Debug, Serialize, Clone)]
#[derive(Debug, Serialize, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum BusyStatus {
Confirmed,

View File

@@ -18,7 +18,7 @@ use std::{
fmt::{self, Display, Formatter},
};
use store::fts::{FilterItem, FilterType, FtsFilter};
use types::{id::Id};
use types::id::Id;
#[derive(Debug, Clone)]
pub struct QueryRequest<T: JmapObject> {
@@ -200,8 +200,8 @@ where
where
V: MapAccess<'de>,
{
let mut filter = T::default();
let mut has_filter = false;
let mut filter = None;
let mut has_multiple_filters = false;
let mut has_conditions = None;
let mut op = None;
@@ -231,13 +231,21 @@ where
self.0.push(Filter::Close);
}
_ => {
filter.deserialize_argument(&key, &mut map)?;
has_filter = true;
if let Some(filter) = filter {
if !has_multiple_filters {
self.0.push(Filter::And);
has_multiple_filters = true;
}
self.0.push(Filter::Property(filter));
}
let mut new_filter = T::default();
new_filter.deserialize_argument(&key, &mut map)?;
filter = Some(new_filter);
}
}
}
if has_filter {
if let Some(filter) = filter {
if has_conditions.is_some() {
return Err(de::Error::custom(
"Cannot mix conditions with property filters",
@@ -245,6 +253,9 @@ where
}
self.0.push(Filter::Property(filter));
if has_multiple_filters {
self.0.push(Filter::Close);
}
}
Ok(())

View File

@@ -7,13 +7,13 @@
use crate::{
object::{AnyId, JmapObject, JmapObjectId},
request::{MaybeInvalid, deserialize::DeserializeArguments},
types::date::UTCDate,
};
use calcard::{
common::timezone::Tz,
jscalendar::{JSCalendarProperty, JSCalendarValue},
jscalendar::{JSCalendarDateTime, JSCalendarProperty, JSCalendarValue},
};
use jmap_tools::{JsonPointerItem, Key};
use mail_parser::DateTime;
use std::{borrow::Cow, str::FromStr};
use types::{blob::BlobId, id::Id};
@@ -81,14 +81,14 @@ impl JmapObjectId for JSCalendarValue<Id, BlobId> {
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CalendarEventFilter {
InCalendar(MaybeInvalid<Id>),
After(UTCDate),
Before(UTCDate),
After(JSCalendarDateTime),
Before(JSCalendarDateTime),
Text(String),
Title(Option<String>),
Description(Option<String>),
Location(Option<String>),
Owner(Option<String>),
Attendee(Option<String>),
Title(String),
Description(String),
Location(String),
Owner(String),
Attendee(String),
Uid(String),
_T(String),
}
@@ -105,8 +105,8 @@ pub enum CalendarEventComparator {
#[derive(Debug, Clone, Default)]
pub struct CalendarEventGetArguments {
pub recurrence_overrides_before: Option<UTCDate>,
pub recurrence_overrides_after: Option<UTCDate>,
pub recurrence_overrides_before: Option<JSCalendarDateTime>,
pub recurrence_overrides_after: Option<JSCalendarDateTime>,
pub reduce_participants: Option<bool>,
pub time_zone: Option<Tz>,
}
@@ -132,28 +132,28 @@ impl<'de> DeserializeArguments<'de> for CalendarEventFilter {
*self = CalendarEventFilter::InCalendar(map.next_value()?);
},
b"after" => {
*self = CalendarEventFilter::After(map.next_value()?);
*self = CalendarEventFilter::After(map.next_value::<LocalTime>()?.0);
},
b"before" => {
*self = CalendarEventFilter::Before(map.next_value()?);
*self = CalendarEventFilter::Before(map.next_value::<LocalTime>()?.0);
},
b"text" => {
*self = CalendarEventFilter::Text(map.next_value::<Cow<str>>()?.to_lowercase());
},
b"title" => {
*self = CalendarEventFilter::Title(map.next_value::<Option<Cow<str>>>()?.map(|s| s.to_lowercase()));
*self = CalendarEventFilter::Title(map.next_value::<Cow<str>>()?.to_lowercase());
},
b"description" => {
*self = CalendarEventFilter::Description(map.next_value::<Option<Cow<str>>>()?.map(|s| s.to_lowercase()));
*self = CalendarEventFilter::Description(map.next_value::<Cow<str>>()?.to_lowercase());
},
b"location" => {
*self = CalendarEventFilter::Location(map.next_value::<Option<Cow<str>>>()?.map(|s| s.to_lowercase()));
*self = CalendarEventFilter::Location(map.next_value::<Cow<str>>()?.to_lowercase());
},
b"owner" => {
*self = CalendarEventFilter::Owner(map.next_value::<Option<Cow<str>>>()?.map(|s| s.to_lowercase()));
*self = CalendarEventFilter::Owner(map.next_value::<Cow<str>>()?.to_lowercase());
},
b"attendee" => {
*self = CalendarEventFilter::Attendee(map.next_value::<Option<Cow<str>>>()?.map(|s| s.to_lowercase()));
*self = CalendarEventFilter::Attendee(map.next_value::<Cow<str>>()?.to_lowercase());
},
b"uid" => {
*self = CalendarEventFilter::Uid(map.next_value()?);
@@ -208,10 +208,10 @@ impl<'de> DeserializeArguments<'de> for CalendarEventGetArguments {
{
hashify::fnc_map!(key.as_bytes(),
b"recurrenceOverridesBefore" => {
self.recurrence_overrides_before = map.next_value()?;
self.recurrence_overrides_before = map.next_value::<Option<LocalTime>>()?.map(|lt| lt.0)
},
b"recurrenceOverridesAfter" => {
self.recurrence_overrides_after = map.next_value()?;
self.recurrence_overrides_after = map.next_value::<Option<LocalTime>>()?.map(|lt| lt.0);
},
b"reduceParticipants" => {
self.reduce_participants = map.next_value()?;
@@ -309,6 +309,29 @@ impl Default for CalendarEventComparator {
}
}
struct LocalTime(JSCalendarDateTime);
impl<'de> serde::Deserialize<'de> for LocalTime {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = <&str>::deserialize(deserializer)?;
if let Some(dt) = DateTime::parse_rfc3339(value) {
Ok(LocalTime(JSCalendarDateTime {
timestamp: dt.to_timestamp_local(),
is_local: true,
}))
} else {
Err(serde::de::Error::custom(format!(
"Invalid datetime: {}",
value
)))
}
}
}
impl JmapObjectId for JSCalendarProperty<Id> {
fn as_id(&self) -> Option<Id> {
if let JSCalendarProperty::IdValue(id) = self {

View File

@@ -561,7 +561,7 @@ mod tests {
"Email/query",
{
"accountId": "0",
"filter": { "conditions": [ { "hasKeyword": "music" }, { "hasKeyword": "video" }, { "operator": "AND", "conditions": [ { "subject": "test" }, { "minSize": 100 } ] } ], "operator": "OR" },
"filter": { "conditions": [ { "hasKeyword": "music", "maxSize": 455 }, { "hasKeyword": "video" }, { "operator": "AND", "conditions": [ { "subject": "test" }, { "minSize": 100 } ] } ], "operator": "OR" },
"sort": [
{
"property": "subject",