rustical_caldav/calendar/methods/report/calendar_query/
elements.rs

1use super::comp_filter::{CompFilterElement, CompFilterable};
2use crate::calendar_object::CalendarObjectPropWrapperName;
3use caldata::{component::IcalCalendarObject, parser::ContentLine};
4use rustical_dav::xml::{PropfindType, TextMatchElement};
5use rustical_ical::UtcDateTime;
6use rustical_store::calendar_store::CalendarQuery;
7use rustical_xml::{XmlDeserialize, XmlRootTag};
8
9#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
10#[allow(dead_code)]
11pub struct TimeRangeElement {
12    #[xml(ty = "attr")]
13    pub(crate) start: Option<UtcDateTime>,
14    #[xml(ty = "attr")]
15    pub(crate) end: Option<UtcDateTime>,
16}
17
18#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
19#[allow(dead_code)]
20// https://www.rfc-editor.org/rfc/rfc4791#section-9.7.3
21pub struct ParamFilterElement {
22    #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
23    pub(crate) is_not_defined: Option<()>,
24    #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
25    pub(crate) text_match: Option<TextMatchElement>,
26
27    #[xml(ty = "attr")]
28    pub(crate) name: String,
29}
30
31impl ParamFilterElement {
32    #[must_use]
33    pub fn match_property(&self, prop: &ContentLine) -> bool {
34        let Some(param) = prop.params.get_param(&self.name) else {
35            return self.is_not_defined.is_some();
36        };
37        if self.is_not_defined.is_some() {
38            return false;
39        }
40
41        let Some(text_match) = self.text_match.as_ref() else {
42            return true;
43        };
44        text_match.match_text(param)
45    }
46}
47
48#[derive(XmlDeserialize, XmlRootTag, Clone, Debug, PartialEq)]
49#[xml(root = "filter", ns = "rustical_dav::namespace::NS_CALDAV")]
50#[allow(dead_code)]
51// https://datatracker.ietf.org/doc/html/rfc4791#section-9.7
52pub struct FilterElement {
53    // This comp-filter matches on VCALENDAR
54    #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
55    pub(crate) comp_filter: CompFilterElement,
56}
57
58impl FilterElement {
59    #[must_use]
60    pub fn matches(&self, cal_object: &IcalCalendarObject) -> bool {
61        cal_object.matches(&self.comp_filter)
62    }
63}
64
65impl From<&FilterElement> for CalendarQuery {
66    fn from(value: &FilterElement) -> Self {
67        let comp_filter_vcalendar = &value.comp_filter;
68        for comp_filter in &comp_filter_vcalendar.comp_filter {
69            // A calendar object cannot contain both VEVENT and VTODO, so we only have to handle
70            // whatever we get first
71            if matches!(comp_filter.name.as_str(), "VEVENT" | "VTODO")
72                && let Some(time_range) = &comp_filter.time_range
73            {
74                let start = time_range.start.as_ref().map(|start| start.date_naive());
75                let end = time_range.end.as_ref().map(|end| end.date_naive());
76                return Self {
77                    time_start: start,
78                    time_end: end,
79                };
80            }
81        }
82        Self::default()
83    }
84}
85
86#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
87#[allow(dead_code)]
88// <!ELEMENT calendar-query ((DAV:allprop | DAV:propname | DAV:prop)?, filter, timezone?)>
89pub struct CalendarQueryRequest {
90    #[xml(ty = "untagged")]
91    pub prop: PropfindType<CalendarObjectPropWrapperName>,
92    #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
93    pub(crate) filter: Option<FilterElement>,
94    #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
95    pub(crate) timezone: Option<String>,
96    #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
97    pub(crate) timezone_id: Option<String>,
98}
99
100impl From<&CalendarQueryRequest> for CalendarQuery {
101    fn from(value: &CalendarQueryRequest) -> Self {
102        value.filter.as_ref().map(Self::from).unwrap_or_default()
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use crate::calendar::methods::report::calendar_query::{
109        CompFilterElement, FilterElement, TimeRangeElement,
110    };
111    use chrono::{NaiveDate, TimeZone, Utc};
112    use rustical_ical::UtcDateTime;
113    use rustical_store::calendar_store::CalendarQuery;
114
115    #[test]
116    fn test_filter_element_calendar_query() {
117        let filter = FilterElement {
118            comp_filter: CompFilterElement {
119                name: "VCALENDAR".to_string(),
120                is_not_defined: None,
121                time_range: None,
122                prop_filter: vec![],
123                comp_filter: vec![CompFilterElement {
124                    name: "VEVENT".to_string(),
125                    is_not_defined: None,
126                    time_range: Some(TimeRangeElement {
127                        start: Some(UtcDateTime(
128                            Utc.with_ymd_and_hms(2024, 4, 1, 0, 0, 0).unwrap(),
129                        )),
130                        end: Some(UtcDateTime(
131                            Utc.with_ymd_and_hms(2024, 8, 1, 0, 0, 0).unwrap(),
132                        )),
133                    }),
134                    prop_filter: vec![],
135                    comp_filter: vec![],
136                }],
137            },
138        };
139        let derived_query: CalendarQuery = (&filter).into();
140        let query = CalendarQuery {
141            time_start: Some(NaiveDate::from_ymd_opt(2024, 4, 1).unwrap()),
142            time_end: Some(NaiveDate::from_ymd_opt(2024, 8, 1).unwrap()),
143        };
144        assert_eq!(derived_query, query);
145    }
146}