diff options
author | Quentin Dufour <quentin@deuxfleurs.fr> | 2024-05-25 19:30:59 +0200 |
---|---|---|
committer | Quentin Dufour <quentin@deuxfleurs.fr> | 2024-05-25 19:30:59 +0200 |
commit | 52f870633c2cab8a4aeeec74792774931139b8b5 (patch) | |
tree | 878d4ff16cdebd7fdfc50a278dbadedd7eb63480 /aero-ical/src/query.rs | |
parent | ff823a10f049e06c711537560ba10f3dc826afcd (diff) | |
download | aerogramme-52f870633c2cab8a4aeeec74792774931139b8b5.tar.gz aerogramme-52f870633c2cab8a4aeeec74792774931139b8b5.zip |
add a new aero-ical module
Diffstat (limited to 'aero-ical/src/query.rs')
-rw-r--r-- | aero-ical/src/query.rs | 280 |
1 files changed, 280 insertions, 0 deletions
diff --git a/aero-ical/src/query.rs b/aero-ical/src/query.rs new file mode 100644 index 0000000..5d857bb --- /dev/null +++ b/aero-ical/src/query.rs @@ -0,0 +1,280 @@ +use aero_dav::caltypes as cal; +use crate::parser as parser; + +pub fn is_component_match( + parent: &icalendar::parser::Component, + components: &[icalendar::parser::Component], + filter: &cal::CompFilter, +) -> bool { + // Find the component among the list + let maybe_comp = components + .iter() + .find(|candidate| candidate.name.as_str() == filter.name.as_str()); + + // 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))) => { + // check time range + if let Some(time_range) = &matcher.time_range { + if !is_in_time_range(&filter.name, parent, component.properties.as_ref(), time_range) { + return false; + } + } + + // check properties + if !is_properties_match(component.properties.as_ref(), matcher.prop_filter.as_ref()) { + return false; + } + + // check inner components + matcher.comp_filter.iter().all(|inner_filter| { + is_component_match(component, component.components.as_ref(), &inner_filter) + }) + } + } +} + +fn prop_date( + properties: &[icalendar::parser::Property], + name: &str, +) -> Option<chrono::DateTime<chrono::Utc>> { + properties + .iter() + .find(|candidate| candidate.name.as_str() == name) + .map(|p| p.val.as_str()) + .map(parser::date_time) + .flatten() +} + +fn prop_parse<T: std::str::FromStr>( + properties: &[icalendar::parser::Property], + name: &str, +) -> Option<T> { + properties + .iter() + .find(|candidate| candidate.name.as_str() == name) + .map(|p| p.val.as_str().parse::<T>().ok()) + .flatten() +} + +fn is_properties_match(props: &[icalendar::parser::Property], filters: &[cal::PropFilter]) -> bool { + filters.iter().all(|single_filter| { + // Find the property + let single_prop = 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()); + + let parsed_date = match maybe_parsed_date { + None => return false, + Some(v) => v, + }; + + // 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, + 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()), + } + } + } + }) + } + } + }) +} + +fn resolve_trigger( + parent: &icalendar::parser::Component, + properties: &[icalendar::parser::Property] +) -> Option<chrono::DateTime<chrono::Utc>> { + // A. Do we have a TRIGGER property? If not, returns early + let maybe_trigger_prop = properties + .iter() + .find(|candidate| candidate.name.as_str() == "TRIGGER"); + + let trigger_prop = match maybe_trigger_prop { + None => return None, + Some(v) => v, + }; + + // B.1 Is it an absolute datetime? If so, returns early + let maybe_absolute = trigger_prop.params.iter() + .find(|param| param.key.as_str() == "VALUE") + .map(|param| param.val.as_ref()).flatten() + .map(|v| v.as_str() == "DATE-TIME"); + + if maybe_absolute.is_some() { + return prop_date(properties, "TRIGGER"); + } + + // B.2 Otherwise it's a timedelta relative to a parent field. + // C.1 Parse the timedelta value, returns early if invalid + + // C.2 Get the parent reference absolute datetime, returns early if invalid + let maybe_related_field = trigger_prop + .params + .iter() + .find(|param| param.key.as_str() == "RELATED") + .map(|param| param.val.as_ref()) + .flatten(); + let related_field = maybe_related_field.map(|v| v.as_str()).unwrap_or("DTSTART"); + let parent_date = match prop_date(parent.properties.as_ref(), related_field) { + Some(v) => v, + _ => return None, + }; + + // C.3 Compute the final date from the base date + timedelta + + todo!() +} + +fn is_in_time_range( + component: &cal::Component, + parent: &icalendar::parser::Component, + properties: &[icalendar::parser::Property], + time_range: &cal::TimeRange, +) -> bool { + //@FIXME timezones are not properly handled currently (everything is UTC) + //@FIXME does not support repeat + //ref: https://datatracker.ietf.org/doc/html/rfc4791#section-9.9 + let (start, end) = match time_range { + cal::TimeRange::OnlyStart(start) => (start, &chrono::DateTime::<chrono::Utc>::MAX_UTC), + cal::TimeRange::OnlyEnd(end) => (&chrono::DateTime::<chrono::Utc>::MIN_UTC, end), + cal::TimeRange::FullRange(start, end) => (start, end), + }; + + match component { + cal::Component::VEvent => { + let dtstart = match prop_date(properties, "DTSTART") { + Some(v) => v, + _ => return false, + }; + let maybe_dtend = prop_date(properties, "DTEND"); + let maybe_duration = prop_parse::<i64>(properties, "DURATION").map(|d| chrono::TimeDelta::new(std::cmp::max(d, 0), 0)).flatten(); + + //@FIXME missing "date" management (only support "datetime") + match (&maybe_dtend, &maybe_duration) { + // | Y | N | N | * | (start < DTEND AND end > DTSTART) | + (Some(dtend), _) => start < dtend && end > &dtstart, + // | N | Y | Y | * | (start < DTSTART+DURATION AND end > DTSTART) | + (_, Some(duration)) => *start <= dtstart + *duration && end > &dtstart, + // | N | N | N | Y | (start <= DTSTART AND end > DTSTART) | + _ => start <= &dtstart && end > &dtstart, + } + }, + cal::Component::VTodo => { + let maybe_dtstart = prop_date(properties, "DTSTART"); + let maybe_due = prop_date(properties, "DUE"); + let maybe_completed = prop_date(properties, "COMPLETED"); + let maybe_created = prop_date(properties, "CREATED"); + let maybe_duration = prop_parse::<i64>(properties, "DURATION").map(|d| chrono::TimeDelta::new(d, 0)).flatten(); + + match (maybe_dtstart, maybe_duration, maybe_due, maybe_completed, maybe_created) { + // | Y | Y | N | * | * | (start <= DTSTART+DURATION) AND | + // | | | | | | ((end > DTSTART) OR | + // | | | | | | (end >= DTSTART+DURATION)) | + (Some(dtstart), Some(duration), None, _, _) => *start <= dtstart + duration && (*end > dtstart || *end >= dtstart + duration), + // | Y | N | Y | * | * | ((start < DUE) OR (start <= DTSTART)) | + // | | | | | | AND | + // | | | | | | ((end > DTSTART) OR (end >= DUE)) | + (Some(dtstart), None, Some(due), _, _) => (*start < due || *start <= dtstart) && (*end > dtstart || *end >= due), + // | Y | N | N | * | * | (start <= DTSTART) AND (end > DTSTART) | + (Some(dtstart), None, None, _, _) => *start <= dtstart && *end > dtstart, + // | N | N | Y | * | * | (start < DUE) AND (end >= DUE) | + (None, None, Some(due), _, _) => *start < due && *end >= due, + // | N | N | N | Y | Y | ((start <= CREATED) OR (start <= COMPLETED))| + // | | | | | | AND | + // | | | | | | ((end >= CREATED) OR (end >= COMPLETED))| + (None, None, None, Some(completed), Some(created)) => (*start <= created || *start <= completed) && (*end >= created || *end >= completed), + // | N | N | N | Y | N | (start <= COMPLETED) AND (end >= COMPLETED) | + (None, None, None, Some(completed), None) => *start <= completed && *end >= completed, + // | N | N | N | N | Y | (end > CREATED) | + (None, None, None, None, Some(created)) => *end > created, + // | N | N | N | N | N | TRUE | + _ => true, + } + }, + cal::Component::VJournal => { + let maybe_dtstart = prop_date(properties, "DTSTART"); + match maybe_dtstart { + // | Y | Y | (start <= DTSTART) AND (end > DTSTART) | + Some(dtstart) => *start <= dtstart && *end > dtstart, + // | N | * | FALSE | + None => false, + } + }, + cal::Component::VFreeBusy => { + //@FIXME freebusy is not supported yet + false + }, + cal::Component::VAlarm => { + //@FIXME does not support REPEAT + let maybe_trigger = resolve_trigger(parent, properties); + // (start <= trigger-time) AND (end > trigger-time) + false + }, + _ => false, + } +} |