support multiple same name components, properties & parameters
This commit is contained in:
parent
6b9720844a
commit
d5a222967d
3 changed files with 244 additions and 110 deletions
|
@ -7,19 +7,18 @@ pub fn is_component_match(
|
|||
filter: &cal::CompFilter,
|
||||
) -> bool {
|
||||
// Find the component among the list
|
||||
//@FIXME do not handle correctly multiple entities (eg. 3 VEVENT)
|
||||
let maybe_comp = components
|
||||
let maybe_comps = components
|
||||
.iter()
|
||||
.find(|candidate| candidate.name.as_str() == filter.name.as_str());
|
||||
.filter(|candidate| candidate.name.as_str() == filter.name.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Filter according to rules
|
||||
match (maybe_comp, &filter.additional_rules) {
|
||||
(Some(_), None) => true,
|
||||
(None, Some(cal::CompFilterRules::IsNotDefined)) => true,
|
||||
(None, None) => false,
|
||||
(Some(_), Some(cal::CompFilterRules::IsNotDefined)) => false,
|
||||
(None, Some(cal::CompFilterRules::Matches(_))) => false,
|
||||
(Some(component), Some(cal::CompFilterRules::Matches(matcher))) => {
|
||||
match (&maybe_comps[..], &filter.additional_rules) {
|
||||
([_, ..], None) => true,
|
||||
([], Some(cal::CompFilterRules::IsNotDefined)) => true,
|
||||
([], None) => false,
|
||||
([_, ..], Some(cal::CompFilterRules::IsNotDefined)) => false,
|
||||
(comps, Some(cal::CompFilterRules::Matches(matcher))) => comps.iter().any(|component| {
|
||||
// check time range
|
||||
if let Some(time_range) = &matcher.time_range {
|
||||
if !is_in_time_range(
|
||||
|
@ -41,7 +40,7 @@ pub fn is_component_match(
|
|||
matcher.comp_filter.iter().all(|inner_filter| {
|
||||
is_component_match(component, component.components.as_ref(), &inner_filter)
|
||||
})
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -71,80 +70,89 @@ fn prop_parse<T: std::str::FromStr>(
|
|||
fn is_properties_match(props: &[icalendar::parser::Property], filters: &[cal::PropFilter]) -> bool {
|
||||
filters.iter().all(|single_filter| {
|
||||
// Find the property
|
||||
let single_prop = props
|
||||
let candidate_props = props
|
||||
.iter()
|
||||
.find(|candidate| candidate.name.as_str() == single_filter.name.0.as_str());
|
||||
match (&single_filter.additional_rules, single_prop) {
|
||||
(None, Some(_)) | (Some(cal::PropFilterRules::IsNotDefined), None) => true,
|
||||
(None, None)
|
||||
| (Some(cal::PropFilterRules::IsNotDefined), Some(_))
|
||||
| (Some(cal::PropFilterRules::Match(_)), None) => false,
|
||||
(Some(cal::PropFilterRules::Match(pattern)), Some(prop)) => {
|
||||
// check value
|
||||
match &pattern.time_or_text {
|
||||
Some(cal::TimeOrText::Time(time_range)) => {
|
||||
let maybe_parsed_date = parser::date_time(prop.val.as_str());
|
||||
.filter(|candidate| candidate.name.as_str() == single_filter.name.0.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let parsed_date = match maybe_parsed_date {
|
||||
None => return false,
|
||||
Some(v) => v,
|
||||
};
|
||||
match (&single_filter.additional_rules, &candidate_props[..]) {
|
||||
(None, [_, ..]) | (Some(cal::PropFilterRules::IsNotDefined), []) => true,
|
||||
(None, []) | (Some(cal::PropFilterRules::IsNotDefined), [_, ..]) => false,
|
||||
(Some(cal::PropFilterRules::Match(pattern)), multi_props) => {
|
||||
multi_props.iter().any(|prop| {
|
||||
// check value
|
||||
match &pattern.time_or_text {
|
||||
Some(cal::TimeOrText::Time(time_range)) => {
|
||||
let maybe_parsed_date = parser::date_time(prop.val.as_str());
|
||||
|
||||
// see if entry is in range
|
||||
let is_in_range = match time_range {
|
||||
cal::TimeRange::OnlyStart(after) => &parsed_date >= after,
|
||||
cal::TimeRange::OnlyEnd(before) => &parsed_date <= before,
|
||||
cal::TimeRange::FullRange(after, before) => {
|
||||
&parsed_date >= after && &parsed_date <= before
|
||||
}
|
||||
};
|
||||
if !is_in_range {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if you are here, this subcondition is valid
|
||||
}
|
||||
Some(cal::TimeOrText::Text(txt_match)) => {
|
||||
//@FIXME ignoring collation
|
||||
let is_match = match txt_match.negate_condition {
|
||||
None | Some(false) => {
|
||||
prop.val.as_str().contains(txt_match.text.as_str())
|
||||
}
|
||||
Some(true) => !prop.val.as_str().contains(txt_match.text.as_str()),
|
||||
};
|
||||
if !is_match {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
None => (), // if not filter on value is set, continue
|
||||
};
|
||||
|
||||
// check parameters
|
||||
pattern.param_filter.iter().all(|single_param_filter| {
|
||||
let maybe_param = prop.params.iter().find(|candidate| {
|
||||
candidate.key.as_str() == single_param_filter.name.as_str()
|
||||
});
|
||||
|
||||
match (maybe_param, &single_param_filter.additional_rules) {
|
||||
(Some(_), None) => true,
|
||||
(None, None) => false,
|
||||
(Some(_), Some(cal::ParamFilterMatch::IsNotDefined)) => false,
|
||||
(None, Some(cal::ParamFilterMatch::IsNotDefined)) => true,
|
||||
(None, Some(cal::ParamFilterMatch::Match(_))) => false,
|
||||
(Some(param), Some(cal::ParamFilterMatch::Match(txt_match))) => {
|
||||
let param_val = match ¶m.val {
|
||||
Some(v) => v,
|
||||
let parsed_date = match maybe_parsed_date {
|
||||
None => return false,
|
||||
Some(v) => v,
|
||||
};
|
||||
|
||||
match txt_match.negate_condition {
|
||||
None | Some(false) => {
|
||||
param_val.as_str().contains(txt_match.text.as_str())
|
||||
// see if entry is in range
|
||||
let is_in_range = match time_range {
|
||||
cal::TimeRange::OnlyStart(after) => &parsed_date >= after,
|
||||
cal::TimeRange::OnlyEnd(before) => &parsed_date <= before,
|
||||
cal::TimeRange::FullRange(after, before) => {
|
||||
&parsed_date >= after && &parsed_date <= before
|
||||
}
|
||||
Some(true) => !param_val.as_str().contains(txt_match.text.as_str()),
|
||||
};
|
||||
if !is_in_range {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if you are here, this subcondition is valid
|
||||
}
|
||||
Some(cal::TimeOrText::Text(txt_match)) => {
|
||||
//@FIXME ignoring collation
|
||||
let is_match = match txt_match.negate_condition {
|
||||
None | Some(false) => {
|
||||
prop.val.as_str().contains(txt_match.text.as_str())
|
||||
}
|
||||
Some(true) => !prop.val.as_str().contains(txt_match.text.as_str()),
|
||||
};
|
||||
if !is_match {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
None => (), // if not filter on value is set, continue
|
||||
};
|
||||
|
||||
// check parameters
|
||||
pattern.param_filter.iter().all(|single_param_filter| {
|
||||
let multi_param = prop
|
||||
.params
|
||||
.iter()
|
||||
.filter(|candidate| {
|
||||
candidate.key.as_str() == single_param_filter.name.as_str()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
match (&multi_param[..], &single_param_filter.additional_rules) {
|
||||
([.., _], None) => true,
|
||||
([], None) => false,
|
||||
([.., _], Some(cal::ParamFilterMatch::IsNotDefined)) => false,
|
||||
([], Some(cal::ParamFilterMatch::IsNotDefined)) => true,
|
||||
(many_params, Some(cal::ParamFilterMatch::Match(txt_match))) => {
|
||||
many_params.iter().any(|param| {
|
||||
let param_val = match ¶m.val {
|
||||
Some(v) => v,
|
||||
None => return false,
|
||||
};
|
||||
|
||||
match txt_match.negate_condition {
|
||||
None | Some(false) => {
|
||||
param_val.as_str().contains(txt_match.text.as_str())
|
||||
}
|
||||
Some(true) => {
|
||||
!param_val.as_str().contains(txt_match.text.as_str())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -554,7 +554,7 @@ fn rfc4791_webdav_caldav() {
|
|||
println!("🧪 rfc4791_webdav_caldav");
|
||||
common::aerogramme_provider_daemon_dev(|_imap, _lmtp, http| {
|
||||
// --- INITIAL TEST SETUP ---
|
||||
// Add entries (3 VEVENT, 1 FREEBUSY, 1 VTODO)
|
||||
// Add entries
|
||||
let resp = http
|
||||
.put("http://localhost:8087/alice/calendar/Personal/rfc1.ics")
|
||||
.header("If-None-Match", "*")
|
||||
|
@ -595,7 +595,14 @@ fn rfc4791_webdav_caldav() {
|
|||
.header("If-None-Match", "*")
|
||||
.body(ICAL_RFC6)
|
||||
.send()?;
|
||||
let _obj6_etag = resp.headers().get("etag").expect("etag must be set");
|
||||
let obj6_etag = resp.headers().get("etag").expect("etag must be set");
|
||||
assert_eq!(resp.status(), 201);
|
||||
let resp = http
|
||||
.put("http://localhost:8087/alice/calendar/Personal/rfc7.ics")
|
||||
.header("If-None-Match", "*")
|
||||
.body(ICAL_RFC7)
|
||||
.send()?;
|
||||
let obj7_etag = resp.headers().get("etag").expect("etag must be set");
|
||||
assert_eq!(resp.status(), 201);
|
||||
|
||||
// A generic function to check a <calendar-data/> query result
|
||||
|
@ -685,8 +692,43 @@ fn rfc4791_webdav_caldav() {
|
|||
//@FIXME not yet supported. returns DAV: 1 ; expects DAV: 1 calendar-access
|
||||
// Not used by any client I know, so not implementing it now.
|
||||
|
||||
// --- REPORT calendar-query ---
|
||||
//@FIXME missing support for calendar-data...
|
||||
// --- REPORT calendar-multiget ---
|
||||
let cal_query = r#"<?xml version="1.0" encoding="utf-8" ?>
|
||||
<C:calendar-multiget xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
|
||||
<D:prop>
|
||||
<D:getetag/>
|
||||
<C:calendar-data/>
|
||||
</D:prop>
|
||||
<D:href>/alice/calendar/Personal/rfc1.ics</D:href>
|
||||
<D:href>/alice/calendar/Personal/rfc3.ics</D:href>
|
||||
</C:calendar-multiget>"#;
|
||||
let resp = http
|
||||
.request(
|
||||
reqwest::Method::from_bytes(b"REPORT")?,
|
||||
"http://localhost:8087/alice/calendar/Personal/",
|
||||
)
|
||||
.body(cal_query)
|
||||
.send()?;
|
||||
assert_eq!(resp.status(), 207);
|
||||
let multistatus = dav_deserialize::<dav::Multistatus<All>>(&resp.text()?);
|
||||
assert_eq!(multistatus.responses.len(), 2);
|
||||
[
|
||||
("/alice/calendar/Personal/rfc1.ics", obj1_etag, ICAL_RFC1),
|
||||
("/alice/calendar/Personal/rfc3.ics", obj3_etag, ICAL_RFC3),
|
||||
]
|
||||
.iter()
|
||||
.for_each(|(ref_path, ref_etag, ref_ical)| {
|
||||
check_cal(
|
||||
&multistatus,
|
||||
(
|
||||
ref_path,
|
||||
Some(ref_etag.to_str().expect("etag header convertible to str")),
|
||||
Some(ref_ical),
|
||||
),
|
||||
)
|
||||
});
|
||||
|
||||
// --- REPORT calendar-query, only filtering ---
|
||||
// 7.8.8. Example: Retrieval of Events Only
|
||||
let cal_query = r#"<?xml version="1.0" encoding="utf-8" ?>
|
||||
<C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
|
||||
|
@ -709,12 +751,13 @@ fn rfc4791_webdav_caldav() {
|
|||
.send()?;
|
||||
assert_eq!(resp.status(), 207);
|
||||
let multistatus = dav_deserialize::<dav::Multistatus<All>>(&resp.text()?);
|
||||
assert_eq!(multistatus.responses.len(), 3);
|
||||
assert_eq!(multistatus.responses.len(), 4);
|
||||
|
||||
[
|
||||
("/alice/calendar/Personal/rfc1.ics", obj1_etag, ICAL_RFC1),
|
||||
("/alice/calendar/Personal/rfc2.ics", obj2_etag, ICAL_RFC2),
|
||||
("/alice/calendar/Personal/rfc3.ics", obj3_etag, ICAL_RFC3),
|
||||
("/alice/calendar/Personal/rfc7.ics", obj7_etag, ICAL_RFC7),
|
||||
]
|
||||
.iter()
|
||||
.for_each(|(ref_path, ref_etag, ref_ical)| {
|
||||
|
@ -788,26 +831,32 @@ fn rfc4791_webdav_caldav() {
|
|||
assert_eq!(resp.status(), 207);
|
||||
let multistatus = dav_deserialize::<dav::Multistatus<All>>(&resp.text()?);
|
||||
assert_eq!(multistatus.responses.len(), 1);
|
||||
check_cal(
|
||||
&multistatus,
|
||||
(
|
||||
"/alice/calendar/Personal/rfc6.ics",
|
||||
Some(obj6_etag.to_str().expect("etag header convertible to str")),
|
||||
Some(ICAL_RFC6),
|
||||
),
|
||||
);
|
||||
|
||||
// 7.8.6. Example: Retrieval of Event by UID
|
||||
// @TODO
|
||||
|
||||
// 7.8.7. Example: Retrieval of Events by PARTSTAT
|
||||
// @TODO
|
||||
|
||||
// 7.8.9. Example: Retrieval of All Pending To-Dos
|
||||
// @TODO
|
||||
|
||||
// --- REPORT calendar-multiget ---
|
||||
let cal_query = r#"<?xml version="1.0" encoding="utf-8" ?>
|
||||
<C:calendar-multiget xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
|
||||
<D:prop>
|
||||
<C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
|
||||
<D:prop xmlns:D="DAV:">
|
||||
<D:getetag/>
|
||||
<C:calendar-data/>
|
||||
</D:prop>
|
||||
<D:href>/alice/calendar/Personal/rfc1.ics</D:href>
|
||||
<D:href>/alice/calendar/Personal/rfc3.ics</D:href>
|
||||
</C:calendar-multiget>"#;
|
||||
<C:filter>
|
||||
<C:comp-filter name="VCALENDAR">
|
||||
<C:comp-filter name="VEVENT">
|
||||
<C:prop-filter name="UID">
|
||||
<C:text-match collation="i;octet">DC6C50A017428C5216A2F1CD@example.com</C:text-match>
|
||||
</C:prop-filter>
|
||||
</C:comp-filter>
|
||||
</C:comp-filter>
|
||||
</C:filter>
|
||||
</C:calendar-query>"#;
|
||||
let resp = http
|
||||
.request(
|
||||
reqwest::Method::from_bytes(b"REPORT")?,
|
||||
|
@ -817,22 +866,61 @@ fn rfc4791_webdav_caldav() {
|
|||
.send()?;
|
||||
assert_eq!(resp.status(), 207);
|
||||
let multistatus = dav_deserialize::<dav::Multistatus<All>>(&resp.text()?);
|
||||
assert_eq!(multistatus.responses.len(), 2);
|
||||
[
|
||||
("/alice/calendar/Personal/rfc1.ics", obj1_etag, ICAL_RFC1),
|
||||
("/alice/calendar/Personal/rfc3.ics", obj3_etag, ICAL_RFC3),
|
||||
]
|
||||
.iter()
|
||||
.for_each(|(ref_path, ref_etag, ref_ical)| {
|
||||
check_cal(
|
||||
&multistatus,
|
||||
(
|
||||
ref_path,
|
||||
Some(ref_etag.to_str().expect("etag header convertible to str")),
|
||||
Some(ref_ical),
|
||||
),
|
||||
assert_eq!(multistatus.responses.len(), 1);
|
||||
check_cal(
|
||||
&multistatus,
|
||||
(
|
||||
"/alice/calendar/Personal/rfc3.ics",
|
||||
Some(obj3_etag.to_str().expect("etag header convertible to str")),
|
||||
Some(ICAL_RFC3),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
// 7.8.7. Example: Retrieval of Events by PARTSTAT
|
||||
let cal_query = r#"<?xml version="1.0" encoding="utf-8" ?>
|
||||
<C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
|
||||
<D:prop xmlns:D="DAV:">
|
||||
<D:getetag/>
|
||||
<C:calendar-data/>
|
||||
</D:prop>
|
||||
<C:filter>
|
||||
<C:comp-filter name="VCALENDAR">
|
||||
<C:comp-filter name="VEVENT">
|
||||
<C:prop-filter name="ATTENDEE">
|
||||
<C:text-match collation="i;ascii-casemap">mailto:lisa@example.com</C:text-match>
|
||||
<C:param-filter name="PARTSTAT">
|
||||
<C:text-match collation="i;ascii-casemap">NEEDS-ACTION</C:text-match>
|
||||
</C:param-filter>
|
||||
</C:prop-filter>
|
||||
</C:comp-filter>
|
||||
</C:comp-filter>
|
||||
</C:filter>
|
||||
</C:calendar-query>"#;
|
||||
let resp = http
|
||||
.request(
|
||||
reqwest::Method::from_bytes(b"REPORT")?,
|
||||
"http://localhost:8087/alice/calendar/Personal/",
|
||||
)
|
||||
});
|
||||
.body(cal_query)
|
||||
.send()?;
|
||||
assert_eq!(resp.status(), 207);
|
||||
let multistatus = dav_deserialize::<dav::Multistatus<All>>(&resp.text()?);
|
||||
assert_eq!(multistatus.responses.len(), 1);
|
||||
check_cal(
|
||||
&multistatus,
|
||||
(
|
||||
"/alice/calendar/Personal/rfc7.ics",
|
||||
Some(obj7_etag.to_str().expect("etag header convertible to str")),
|
||||
Some(ICAL_RFC7),
|
||||
),
|
||||
);
|
||||
|
||||
// 7.8.9. Example: Retrieval of All Pending To-Dos
|
||||
// @TODO
|
||||
|
||||
// --- REPORT calendar-query, with calendar-data tx ---
|
||||
//@FIXME add support for calendar-data...
|
||||
|
||||
Ok(())
|
||||
})
|
||||
|
|
|
@ -175,3 +175,41 @@ END:VALARM
|
|||
END:VTODO
|
||||
END:VCALENDAR
|
||||
"#;
|
||||
|
||||
pub static ICAL_RFC7: &[u8] = br#"BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Example Corp.//CalDAV Client//EN
|
||||
BEGIN:VTIMEZONE
|
||||
LAST-MODIFIED:20040110T032845Z
|
||||
TZID:US/Eastern
|
||||
BEGIN:DAYLIGHT
|
||||
DTSTART:20000404T020000
|
||||
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
|
||||
TZNAME:EDT
|
||||
TZOFFSETFROM:-0500
|
||||
TZOFFSETTO:-0400
|
||||
END:DAYLIGHT
|
||||
BEGIN:STANDARD
|
||||
DTSTART:20001026T020000
|
||||
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
|
||||
TZNAME:EST
|
||||
TZOFFSETFROM:-0400
|
||||
TZOFFSETTO:-0500
|
||||
END:STANDARD
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
ATTENDEE;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:cyrus@example.com
|
||||
ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:lisa@example.com
|
||||
DTSTAMP:20090206T001220Z
|
||||
DTSTART;TZID=US/Eastern:20090104T100000
|
||||
DURATION:PT1H
|
||||
LAST-MODIFIED:20090206T001330Z
|
||||
ORGANIZER:mailto:cyrus@example.com
|
||||
SEQUENCE:1
|
||||
STATUS:TENTATIVE
|
||||
SUMMARY:Event #3
|
||||
UID:DC6C50A017428C5216A2F1CA@example.com
|
||||
X-ABC-GUID:E1CX5Dr-0007ym-Hz@example.com
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
"#;
|
||||
|
|
Loading…
Reference in a new issue