mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 21:42:34 +00:00
re-implement comp-filter and add property filtering
This commit is contained in:
@@ -1,28 +1,49 @@
|
|||||||
use ical::generator::IcalEvent;
|
|
||||||
use rustical_ical::{CalendarObject, CalendarObjectComponent, CalendarObjectType};
|
|
||||||
|
|
||||||
use crate::calendar::methods::report::calendar_query::{
|
use crate::calendar::methods::report::calendar_query::{
|
||||||
CompFilterElement, PropFilterElement, TimeRangeElement,
|
TimeRangeElement,
|
||||||
|
prop_filter::{PropFilterElement, PropFilterable},
|
||||||
};
|
};
|
||||||
|
use rustical_ical::{CalendarObject, CalendarObjectComponent, CalendarObjectType};
|
||||||
|
use rustical_xml::XmlDeserialize;
|
||||||
|
|
||||||
pub trait CompFilterable {
|
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc4791#section-9.7.1
|
||||||
|
pub struct CompFilterElement {
|
||||||
|
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||||
|
pub(crate) is_not_defined: Option<()>,
|
||||||
|
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||||
|
pub(crate) time_range: Option<TimeRangeElement>,
|
||||||
|
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)]
|
||||||
|
pub(crate) prop_filter: Vec<PropFilterElement>,
|
||||||
|
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)]
|
||||||
|
pub(crate) comp_filter: Vec<CompFilterElement>,
|
||||||
|
|
||||||
|
#[xml(ty = "attr")]
|
||||||
|
pub(crate) name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait CompFilterable: PropFilterable + Sized {
|
||||||
fn get_comp_name(&self) -> &'static str;
|
fn get_comp_name(&self) -> &'static str;
|
||||||
|
|
||||||
fn match_time_range(&self, time_range: &TimeRangeElement) -> bool;
|
fn match_time_range(&self, time_range: &TimeRangeElement) -> bool;
|
||||||
|
|
||||||
fn match_prop_filter(&self, prop_filter: &PropFilterElement) -> bool;
|
|
||||||
|
|
||||||
fn match_subcomponents(&self, comp_filter: &CompFilterElement) -> bool;
|
fn match_subcomponents(&self, comp_filter: &CompFilterElement) -> bool;
|
||||||
|
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc4791#section-9.7.1
|
||||||
|
// The scope of the
|
||||||
|
// CALDAV:comp-filter XML element is the calendar object when used as
|
||||||
|
// a child of the CALDAV:filter XML element. The scope of the
|
||||||
|
// CALDAV:comp-filter XML element is the enclosing calendar component
|
||||||
|
// when used as a child of another CALDAV:comp-filter XML element
|
||||||
fn matches(&self, comp_filter: &CompFilterElement) -> bool {
|
fn matches(&self, comp_filter: &CompFilterElement) -> bool {
|
||||||
let name_matches = self.get_comp_name() != comp_filter.name;
|
let name_matches = self.get_comp_name() == comp_filter.name;
|
||||||
match (comp_filter.is_not_defined.is_some(), name_matches) {
|
match (comp_filter.is_not_defined.is_some(), name_matches) {
|
||||||
// We are the component that's not supposed to be defined
|
// We are the component that's not supposed to be defined
|
||||||
(true, true) => return false,
|
(true, true)
|
||||||
|
// We don't match
|
||||||
|
| (false, false) => return false,
|
||||||
// We shall not be and indeed we aren't
|
// We shall not be and indeed we aren't
|
||||||
(true, false) => return true,
|
(true, false) => return true,
|
||||||
// We don't match
|
|
||||||
(false, false) => return false,
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,31 +54,11 @@ pub trait CompFilterable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for prop_filter in &comp_filter.prop_filter {
|
for prop_filter in &comp_filter.prop_filter {
|
||||||
if !self.match_prop_filter(prop_filter) {
|
if !prop_filter.match_component(self) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// let subcomponents = self.get_subcomponents();
|
|
||||||
// for sub_comp_filter in &comp_filter.comp_filter {
|
|
||||||
// if sub_comp_filter.is_not_defined.is_some() {
|
|
||||||
// // If is_not_defined: Filter shuold match for all
|
|
||||||
// // Confusing logic but matching also means not being the component that
|
|
||||||
// // shouldn't be defined
|
|
||||||
// if subcomponents
|
|
||||||
// .iter()
|
|
||||||
// .any(|sub| !sub.matches(sub_comp_filter))
|
|
||||||
// {
|
|
||||||
// return false;
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// // otherwise if no component matches return false
|
|
||||||
// if !subcomponents.iter().any(|sub| sub.matches(sub_comp_filter)) {
|
|
||||||
// return false;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
comp_filter
|
comp_filter
|
||||||
.comp_filter
|
.comp_filter
|
||||||
.iter()
|
.iter()
|
||||||
@@ -71,14 +72,10 @@ impl CompFilterable for CalendarObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn match_time_range(&self, _time_range: &TimeRangeElement) -> bool {
|
fn match_time_range(&self, _time_range: &TimeRangeElement) -> bool {
|
||||||
|
// VCALENDAR has no concept of time range
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn match_prop_filter(&self, _prop_filter: &PropFilterElement) -> bool {
|
|
||||||
// TODO
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn match_subcomponents(&self, comp_filter: &CompFilterElement) -> bool {
|
fn match_subcomponents(&self, comp_filter: &CompFilterElement) -> bool {
|
||||||
self.get_data().matches(comp_filter)
|
self.get_data().matches(comp_filter)
|
||||||
}
|
}
|
||||||
@@ -90,12 +87,18 @@ impl CompFilterable for CalendarObjectComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn match_time_range(&self, time_range: &TimeRangeElement) -> bool {
|
fn match_time_range(&self, time_range: &TimeRangeElement) -> bool {
|
||||||
// TODO
|
if let Some(start) = &time_range.start
|
||||||
true
|
&& let Some(last_occurence) = self.get_last_occurence().unwrap_or(None)
|
||||||
}
|
&& **start > last_occurence.utc()
|
||||||
|
{
|
||||||
fn match_prop_filter(&self, _prop_filter: &PropFilterElement) -> bool {
|
return false;
|
||||||
// TODO
|
}
|
||||||
|
if let Some(end) = &time_range.end
|
||||||
|
&& let Some(first_occurence) = self.get_first_occurence().unwrap_or(None)
|
||||||
|
&& **end < first_occurence.utc()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,3 +107,179 @@ impl CompFilterable for CalendarObjectComponent {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use chrono::{TimeZone, Utc};
|
||||||
|
use rustical_ical::{CalendarObject, UtcDateTime};
|
||||||
|
|
||||||
|
use crate::calendar::methods::report::calendar_query::{
|
||||||
|
CompFilterable, TextMatchElement, TimeRangeElement, comp_filter::CompFilterElement,
|
||||||
|
prop_filter::PropFilterElement,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ICS: &str = r"BEGIN:VCALENDAR
|
||||||
|
CALSCALE:GREGORIAN
|
||||||
|
VERSION:2.0
|
||||||
|
BEGIN:VTIMEZONE
|
||||||
|
TZID:Europe/Berlin
|
||||||
|
X-LIC-LOCATION:Europe/Berlin
|
||||||
|
END:VTIMEZONE
|
||||||
|
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:318ec6503573d9576818daf93dac07317058d95c
|
||||||
|
DTSTAMP:20250502T132758Z
|
||||||
|
DTSTART;TZID=Europe/Berlin:20250506T090000
|
||||||
|
DTEND;TZID=Europe/Berlin:20250506T092500
|
||||||
|
SEQUENCE:2
|
||||||
|
SUMMARY:weekly stuff
|
||||||
|
TRANSP:OPAQUE
|
||||||
|
RRULE:FREQ=WEEKLY;COUNT=4;INTERVAL=2;BYDAY=TU,TH,SU
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_comp_filter_matching() {
|
||||||
|
let object = CalendarObject::from_ics(ICS.to_string(), None).unwrap();
|
||||||
|
|
||||||
|
let comp_filter = CompFilterElement {
|
||||||
|
is_not_defined: Some(()),
|
||||||
|
name: "VCALENDAR".to_string(),
|
||||||
|
time_range: None,
|
||||||
|
prop_filter: vec![],
|
||||||
|
comp_filter: vec![],
|
||||||
|
};
|
||||||
|
assert!(!object.matches(&comp_filter), "filter: wants no VCALENDAR");
|
||||||
|
|
||||||
|
let comp_filter = CompFilterElement {
|
||||||
|
is_not_defined: None,
|
||||||
|
name: "VCALENDAR".to_string(),
|
||||||
|
time_range: None,
|
||||||
|
prop_filter: vec![],
|
||||||
|
comp_filter: vec![CompFilterElement {
|
||||||
|
name: "VTODO".to_string(),
|
||||||
|
is_not_defined: None,
|
||||||
|
time_range: None,
|
||||||
|
prop_filter: vec![],
|
||||||
|
comp_filter: vec![],
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
assert!(!object.matches(&comp_filter), "filter matches VTODO");
|
||||||
|
|
||||||
|
let comp_filter = CompFilterElement {
|
||||||
|
is_not_defined: None,
|
||||||
|
name: "VCALENDAR".to_string(),
|
||||||
|
time_range: None,
|
||||||
|
prop_filter: vec![],
|
||||||
|
comp_filter: vec![CompFilterElement {
|
||||||
|
name: "VEVENT".to_string(),
|
||||||
|
is_not_defined: None,
|
||||||
|
time_range: None,
|
||||||
|
prop_filter: vec![],
|
||||||
|
comp_filter: vec![],
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
assert!(object.matches(&comp_filter), "filter matches VEVENT");
|
||||||
|
|
||||||
|
let comp_filter = CompFilterElement {
|
||||||
|
is_not_defined: None,
|
||||||
|
name: "VCALENDAR".to_string(),
|
||||||
|
time_range: None,
|
||||||
|
prop_filter: vec![
|
||||||
|
PropFilterElement {
|
||||||
|
is_not_defined: None,
|
||||||
|
name: "VERSION".to_string(),
|
||||||
|
time_range: None,
|
||||||
|
text_match: Some(TextMatchElement {
|
||||||
|
needle: "2.0".to_string(),
|
||||||
|
collation: None,
|
||||||
|
negate_condition: None,
|
||||||
|
}),
|
||||||
|
param_filter: vec![],
|
||||||
|
},
|
||||||
|
PropFilterElement {
|
||||||
|
is_not_defined: Some(()),
|
||||||
|
name: "STUFF".to_string(),
|
||||||
|
time_range: None,
|
||||||
|
text_match: None,
|
||||||
|
param_filter: vec![],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
comp_filter: vec![CompFilterElement {
|
||||||
|
name: "VEVENT".to_string(),
|
||||||
|
is_not_defined: None,
|
||||||
|
time_range: None,
|
||||||
|
prop_filter: vec![PropFilterElement {
|
||||||
|
is_not_defined: None,
|
||||||
|
name: "SUMMARY".to_string(),
|
||||||
|
time_range: None,
|
||||||
|
text_match: Some(TextMatchElement {
|
||||||
|
collation: None,
|
||||||
|
negate_condition: None,
|
||||||
|
needle: "weekly".to_string(),
|
||||||
|
}),
|
||||||
|
param_filter: vec![],
|
||||||
|
}],
|
||||||
|
comp_filter: vec![],
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
assert!(
|
||||||
|
object.matches(&comp_filter),
|
||||||
|
"Some prop filters on VCALENDAR and VEVENT"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn test_comp_filter_time_range() {
|
||||||
|
let object = CalendarObject::from_ics(ICS.to_string(), None).unwrap();
|
||||||
|
|
||||||
|
let comp_filter = CompFilterElement {
|
||||||
|
is_not_defined: None,
|
||||||
|
name: "VCALENDAR".to_string(),
|
||||||
|
time_range: None,
|
||||||
|
prop_filter: vec![],
|
||||||
|
comp_filter: vec![CompFilterElement {
|
||||||
|
name: "VEVENT".to_string(),
|
||||||
|
is_not_defined: None,
|
||||||
|
time_range: Some(TimeRangeElement {
|
||||||
|
start: Some(UtcDateTime(
|
||||||
|
Utc.with_ymd_and_hms(2025, 4, 1, 0, 0, 0).unwrap(),
|
||||||
|
)),
|
||||||
|
end: Some(UtcDateTime(
|
||||||
|
Utc.with_ymd_and_hms(2025, 8, 1, 0, 0, 0).unwrap(),
|
||||||
|
)),
|
||||||
|
}),
|
||||||
|
prop_filter: vec![],
|
||||||
|
comp_filter: vec![],
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
assert!(
|
||||||
|
object.matches(&comp_filter),
|
||||||
|
"event should lie in time range"
|
||||||
|
);
|
||||||
|
|
||||||
|
let comp_filter = CompFilterElement {
|
||||||
|
is_not_defined: None,
|
||||||
|
name: "VCALENDAR".to_string(),
|
||||||
|
time_range: None,
|
||||||
|
prop_filter: vec![],
|
||||||
|
comp_filter: vec![CompFilterElement {
|
||||||
|
name: "VEVENT".to_string(),
|
||||||
|
is_not_defined: None,
|
||||||
|
time_range: Some(TimeRangeElement {
|
||||||
|
start: Some(UtcDateTime(
|
||||||
|
Utc.with_ymd_and_hms(2024, 4, 1, 0, 0, 0).unwrap(),
|
||||||
|
)),
|
||||||
|
end: Some(UtcDateTime(
|
||||||
|
Utc.with_ymd_and_hms(2024, 8, 1, 0, 0, 0).unwrap(),
|
||||||
|
)),
|
||||||
|
}),
|
||||||
|
prop_filter: vec![],
|
||||||
|
comp_filter: vec![],
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
assert!(
|
||||||
|
!object.matches(&comp_filter),
|
||||||
|
"event should not lie in time range"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
use crate::calendar_object::CalendarObjectPropWrapperName;
|
use crate::{
|
||||||
|
calendar::methods::report::calendar_query::comp_filter::{CompFilterElement, CompFilterable},
|
||||||
|
calendar_object::CalendarObjectPropWrapperName,
|
||||||
|
};
|
||||||
use rustical_dav::xml::PropfindType;
|
use rustical_dav::xml::PropfindType;
|
||||||
use rustical_ical::{CalendarObject, UtcDateTime};
|
use rustical_ical::{CalendarObject, UtcDateTime};
|
||||||
use rustical_store::calendar_store::CalendarQuery;
|
use rustical_store::calendar_store::CalendarQuery;
|
||||||
@@ -30,106 +33,12 @@ pub struct ParamFilterElement {
|
|||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub struct TextMatchElement {
|
pub struct TextMatchElement {
|
||||||
#[xml(ty = "attr")]
|
#[xml(ty = "attr")]
|
||||||
pub(crate) collation: String,
|
pub(crate) collation: Option<String>,
|
||||||
#[xml(ty = "attr")]
|
#[xml(ty = "attr")]
|
||||||
// "yes" or "no", default: "no"
|
// "yes" or "no", default: "no"
|
||||||
pub(crate) negate_condition: Option<String>,
|
pub(crate) negate_condition: Option<String>,
|
||||||
}
|
#[xml(ty = "text")]
|
||||||
|
pub(crate) needle: String,
|
||||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
|
|
||||||
#[allow(dead_code)]
|
|
||||||
// https://www.rfc-editor.org/rfc/rfc4791#section-9.7.2
|
|
||||||
pub struct PropFilterElement {
|
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
|
||||||
pub(crate) is_not_defined: Option<()>,
|
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
|
||||||
pub(crate) time_range: Option<TimeRangeElement>,
|
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
|
||||||
pub(crate) text_match: Option<TextMatchElement>,
|
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)]
|
|
||||||
pub(crate) param_filter: Vec<ParamFilterElement>,
|
|
||||||
|
|
||||||
#[xml(ty = "attr")]
|
|
||||||
pub(crate) name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
|
||||||
#[allow(dead_code)]
|
|
||||||
// https://datatracker.ietf.org/doc/html/rfc4791#section-9.7.1
|
|
||||||
pub struct CompFilterElement {
|
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
|
||||||
pub(crate) is_not_defined: Option<()>,
|
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
|
||||||
pub(crate) time_range: Option<TimeRangeElement>,
|
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)]
|
|
||||||
pub(crate) prop_filter: Vec<PropFilterElement>,
|
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)]
|
|
||||||
pub(crate) comp_filter: Vec<CompFilterElement>,
|
|
||||||
|
|
||||||
#[xml(ty = "attr")]
|
|
||||||
pub(crate) name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CompFilterElement {
|
|
||||||
// match the VCALENDAR part
|
|
||||||
pub fn matches_root(&self, cal_object: &CalendarObject) -> bool {
|
|
||||||
let comp_vcal = self.name == "VCALENDAR";
|
|
||||||
match (self.is_not_defined, comp_vcal) {
|
|
||||||
// Client wants VCALENDAR to not exist but we are a VCALENDAR
|
|
||||||
(Some(()), true) |
|
|
||||||
// Client is asking for something different than a vcalendar
|
|
||||||
(None, false) => return false,
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.time_range.is_some() {
|
|
||||||
// <time-range> should be applied on VEVENT/VTODO but not on VCALENDAR
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Implement prop-filter at some point
|
|
||||||
|
|
||||||
// Apply sub-comp-filters on VEVENT/VTODO/VJOURNAL component
|
|
||||||
if self
|
|
||||||
.comp_filter
|
|
||||||
.iter()
|
|
||||||
.all(|filter| filter.matches(cal_object))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
// match the VEVENT/VTODO/VJOURNAL part
|
|
||||||
pub fn matches(&self, cal_object: &CalendarObject) -> bool {
|
|
||||||
let comp_name_matches = self.name == cal_object.get_component_name();
|
|
||||||
match (self.is_not_defined, comp_name_matches) {
|
|
||||||
// Client wants VCALENDAR to not exist but we are a VCALENDAR
|
|
||||||
(Some(()), true) |
|
|
||||||
// Client is asking for something different than a vcalendar
|
|
||||||
(None, false) => return false,
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Implement prop-filter (and comp-filter?) at some point
|
|
||||||
|
|
||||||
if let Some(time_range) = &self.time_range {
|
|
||||||
if let Some(start) = &time_range.start
|
|
||||||
&& let Some(last_occurence) = cal_object.get_last_occurence().unwrap_or(None)
|
|
||||||
&& **start > last_occurence.utc()
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if let Some(end) = &time_range.end
|
|
||||||
&& let Some(first_occurence) = cal_object.get_first_occurence().unwrap_or(None)
|
|
||||||
&& **end < first_occurence.utc()
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||||
@@ -143,7 +52,7 @@ pub struct FilterElement {
|
|||||||
|
|
||||||
impl FilterElement {
|
impl FilterElement {
|
||||||
pub fn matches(&self, cal_object: &CalendarObject) -> bool {
|
pub fn matches(&self, cal_object: &CalendarObject) -> bool {
|
||||||
self.comp_filter.matches_root(cal_object)
|
cal_object.matches(&self.comp_filter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ use rustical_store::CalendarStore;
|
|||||||
|
|
||||||
mod comp_filter;
|
mod comp_filter;
|
||||||
mod elements;
|
mod elements;
|
||||||
|
mod prop_filter;
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
pub use comp_filter::{CompFilterElement, CompFilterable};
|
||||||
pub use elements::*;
|
pub use elements::*;
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
pub use prop_filter::{PropFilterElement, PropFilterable};
|
||||||
|
|
||||||
pub async fn get_objects_calendar_query<C: CalendarStore>(
|
pub async fn get_objects_calendar_query<C: CalendarStore>(
|
||||||
cal_query: &CalendarQueryRequest,
|
cal_query: &CalendarQueryRequest,
|
||||||
@@ -30,8 +35,8 @@ mod tests {
|
|||||||
calendar::methods::report::{
|
calendar::methods::report::{
|
||||||
ReportRequest,
|
ReportRequest,
|
||||||
calendar_query::{
|
calendar_query::{
|
||||||
CalendarQueryRequest, CompFilterElement, FilterElement, ParamFilterElement,
|
CalendarQueryRequest, FilterElement, ParamFilterElement, TextMatchElement,
|
||||||
PropFilterElement, TextMatchElement,
|
comp_filter::CompFilterElement, prop_filter::PropFilterElement,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
calendar_object::{CalendarData, CalendarObjectPropName, CalendarObjectPropWrapperName},
|
calendar_object::{CalendarData, CalendarObjectPropName, CalendarObjectPropWrapperName},
|
||||||
@@ -91,16 +96,18 @@ mod tests {
|
|||||||
prop_filter: vec![PropFilterElement {
|
prop_filter: vec![PropFilterElement {
|
||||||
name: "ATTENDEE".to_owned(),
|
name: "ATTENDEE".to_owned(),
|
||||||
text_match: Some(TextMatchElement {
|
text_match: Some(TextMatchElement {
|
||||||
collation: "i;ascii-casemap".to_owned(),
|
collation: Some("i;ascii-casemap".to_owned()),
|
||||||
negate_condition: None
|
negate_condition: None,
|
||||||
|
needle: "mailto:lisa@example.com".to_string()
|
||||||
}),
|
}),
|
||||||
is_not_defined: None,
|
is_not_defined: None,
|
||||||
param_filter: vec![ParamFilterElement {
|
param_filter: vec![ParamFilterElement {
|
||||||
is_not_defined: None,
|
is_not_defined: None,
|
||||||
name: "PARTSTAT".to_owned(),
|
name: "PARTSTAT".to_owned(),
|
||||||
text_match: Some(TextMatchElement {
|
text_match: Some(TextMatchElement {
|
||||||
collation: "i;ascii-casemap".to_owned(),
|
collation: Some("i;ascii-casemap".to_owned()),
|
||||||
negate_condition: None
|
negate_condition: None,
|
||||||
|
needle: "NEEDS-ACTION".to_string()
|
||||||
}),
|
}),
|
||||||
}],
|
}],
|
||||||
time_range: None
|
time_range: None
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
use ical::{
|
||||||
|
generator::{IcalCalendar, IcalEvent},
|
||||||
|
parser::{
|
||||||
|
Component,
|
||||||
|
ical::component::{IcalJournal, IcalTodo},
|
||||||
|
},
|
||||||
|
property::Property,
|
||||||
|
};
|
||||||
|
use rustical_ical::{CalendarObject, CalendarObjectComponent};
|
||||||
|
use rustical_xml::XmlDeserialize;
|
||||||
|
|
||||||
|
use crate::calendar::methods::report::calendar_query::{
|
||||||
|
ParamFilterElement, TextMatchElement, TimeRangeElement,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc4791#section-9.7.2
|
||||||
|
pub struct PropFilterElement {
|
||||||
|
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||||
|
pub(crate) is_not_defined: Option<()>,
|
||||||
|
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||||
|
pub(crate) time_range: Option<TimeRangeElement>,
|
||||||
|
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||||
|
pub(crate) text_match: Option<TextMatchElement>,
|
||||||
|
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)]
|
||||||
|
pub(crate) param_filter: Vec<ParamFilterElement>,
|
||||||
|
|
||||||
|
#[xml(ty = "attr")]
|
||||||
|
pub(crate) name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PropFilterElement {
|
||||||
|
pub fn match_component(&self, comp: &impl PropFilterable) -> bool {
|
||||||
|
let property = comp.get_property(&self.name);
|
||||||
|
let property = match (self.is_not_defined.is_some(), property) {
|
||||||
|
// We are the component that's not supposed to be defined
|
||||||
|
(true, Some(_))
|
||||||
|
// We don't match
|
||||||
|
| (false, None) => return false,
|
||||||
|
// We shall not be and indeed we aren't
|
||||||
|
(true, None) => return true,
|
||||||
|
(false, Some(property)) => property
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(_time_range) = &self.time_range {
|
||||||
|
// TODO: implement
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(TextMatchElement {
|
||||||
|
collation: _collation,
|
||||||
|
negate_condition,
|
||||||
|
needle,
|
||||||
|
}) = &self.text_match
|
||||||
|
{
|
||||||
|
let mut matches = property
|
||||||
|
.value
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|haystack| haystack.contains(needle));
|
||||||
|
match negate_condition.as_deref() {
|
||||||
|
None | Some("no") => {}
|
||||||
|
Some("yes") => {
|
||||||
|
matches = !matches;
|
||||||
|
}
|
||||||
|
// Invalid value
|
||||||
|
_ => return false,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !matches {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: param-filter
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait PropFilterable {
|
||||||
|
fn get_property(&self, name: &str) -> Option<&Property>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PropFilterable for CalendarObject {
|
||||||
|
fn get_property(&self, name: &str) -> Option<&Property> {
|
||||||
|
Self::get_property(self, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PropFilterable for IcalEvent {
|
||||||
|
fn get_property(&self, name: &str) -> Option<&Property> {
|
||||||
|
Component::get_property(self, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PropFilterable for IcalTodo {
|
||||||
|
fn get_property(&self, name: &str) -> Option<&Property> {
|
||||||
|
Component::get_property(self, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PropFilterable for IcalJournal {
|
||||||
|
fn get_property(&self, name: &str) -> Option<&Property> {
|
||||||
|
Component::get_property(self, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PropFilterable for IcalCalendar {
|
||||||
|
fn get_property(&self, name: &str) -> Option<&Property> {
|
||||||
|
Component::get_property(self, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PropFilterable for CalendarObjectComponent {
|
||||||
|
fn get_property(&self, name: &str) -> Option<&Property> {
|
||||||
|
match self {
|
||||||
|
Self::Event(event, _) => PropFilterable::get_property(&event.event, name),
|
||||||
|
Self::Todo(todo, _) => PropFilterable::get_property(todo, name),
|
||||||
|
Self::Journal(journal, _) => PropFilterable::get_property(journal, name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user