mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 20:32:48 +00:00
refactor text-match to support collations
This commit is contained in:
@@ -139,8 +139,10 @@ mod tests {
|
|||||||
use rustical_ical::{CalendarObject, UtcDateTime};
|
use rustical_ical::{CalendarObject, UtcDateTime};
|
||||||
|
|
||||||
use crate::calendar::methods::report::calendar_query::{
|
use crate::calendar::methods::report::calendar_query::{
|
||||||
CompFilterable, TextMatchElement, TimeRangeElement, comp_filter::CompFilterElement,
|
CompFilterable, TextMatchElement, TimeRangeElement,
|
||||||
|
comp_filter::CompFilterElement,
|
||||||
prop_filter::PropFilterElement,
|
prop_filter::PropFilterElement,
|
||||||
|
text_match::{NegateCondition, TextCollation},
|
||||||
};
|
};
|
||||||
|
|
||||||
const ICS: &str = r"BEGIN:VCALENDAR
|
const ICS: &str = r"BEGIN:VCALENDAR
|
||||||
@@ -217,8 +219,8 @@ END:VCALENDAR";
|
|||||||
time_range: None,
|
time_range: None,
|
||||||
text_match: Some(TextMatchElement {
|
text_match: Some(TextMatchElement {
|
||||||
needle: "2.0".to_string(),
|
needle: "2.0".to_string(),
|
||||||
collation: None,
|
collation: TextCollation::default(),
|
||||||
negate_condition: None,
|
negate_condition: NegateCondition::default(),
|
||||||
}),
|
}),
|
||||||
param_filter: vec![],
|
param_filter: vec![],
|
||||||
},
|
},
|
||||||
@@ -239,8 +241,8 @@ END:VCALENDAR";
|
|||||||
name: "SUMMARY".to_string(),
|
name: "SUMMARY".to_string(),
|
||||||
time_range: None,
|
time_range: None,
|
||||||
text_match: Some(TextMatchElement {
|
text_match: Some(TextMatchElement {
|
||||||
collation: None,
|
collation: TextCollation::default(),
|
||||||
negate_condition: None,
|
negate_condition: NegateCondition(false),
|
||||||
needle: "weekly".to_string(),
|
needle: "weekly".to_string(),
|
||||||
}),
|
}),
|
||||||
param_filter: vec![],
|
param_filter: vec![],
|
||||||
@@ -326,8 +328,8 @@ END:VCALENDAR";
|
|||||||
name: "TZID".to_string(),
|
name: "TZID".to_string(),
|
||||||
time_range: None,
|
time_range: None,
|
||||||
text_match: Some(TextMatchElement {
|
text_match: Some(TextMatchElement {
|
||||||
collation: None,
|
collation: TextCollation::AsciiCasemap,
|
||||||
negate_condition: None,
|
negate_condition: NegateCondition::default(),
|
||||||
needle: "Europe/Berlin".to_string(),
|
needle: "Europe/Berlin".to_string(),
|
||||||
}),
|
}),
|
||||||
param_filter: vec![],
|
param_filter: vec![],
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
calendar::methods::report::calendar_query::comp_filter::{CompFilterElement, CompFilterable},
|
calendar::methods::report::calendar_query::{
|
||||||
|
TextMatchElement,
|
||||||
|
comp_filter::{CompFilterElement, CompFilterable},
|
||||||
|
},
|
||||||
calendar_object::CalendarObjectPropWrapperName,
|
calendar_object::CalendarObjectPropWrapperName,
|
||||||
};
|
};
|
||||||
use rustical_dav::xml::PropfindType;
|
use rustical_dav::xml::PropfindType;
|
||||||
@@ -29,18 +32,6 @@ pub struct ParamFilterElement {
|
|||||||
pub(crate) name: String,
|
pub(crate) name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub struct TextMatchElement {
|
|
||||||
#[xml(ty = "attr")]
|
|
||||||
pub(crate) collation: Option<String>,
|
|
||||||
#[xml(ty = "attr")]
|
|
||||||
// "yes" or "no", default: "no"
|
|
||||||
pub(crate) negate_condition: Option<String>,
|
|
||||||
#[xml(ty = "text")]
|
|
||||||
pub(crate) needle: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
// https://datatracker.ietf.org/doc/html/rfc4791#section-9.7
|
// https://datatracker.ietf.org/doc/html/rfc4791#section-9.7
|
||||||
|
|||||||
@@ -5,11 +5,14 @@ use rustical_store::CalendarStore;
|
|||||||
mod comp_filter;
|
mod comp_filter;
|
||||||
mod elements;
|
mod elements;
|
||||||
mod prop_filter;
|
mod prop_filter;
|
||||||
|
pub mod text_match;
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
pub use comp_filter::{CompFilterElement, CompFilterable};
|
pub use comp_filter::{CompFilterElement, CompFilterable};
|
||||||
pub use elements::*;
|
pub use elements::*;
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
pub use prop_filter::{PropFilterElement, PropFilterable};
|
pub use prop_filter::{PropFilterElement, PropFilterable};
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
pub use text_match::TextMatchElement;
|
||||||
|
|
||||||
pub async fn get_objects_calendar_query<C: CalendarStore>(
|
pub async fn get_objects_calendar_query<C: CalendarStore>(
|
||||||
cal_query: &CalendarQueryRequest,
|
cal_query: &CalendarQueryRequest,
|
||||||
@@ -36,7 +39,9 @@ mod tests {
|
|||||||
ReportRequest,
|
ReportRequest,
|
||||||
calendar_query::{
|
calendar_query::{
|
||||||
CalendarQueryRequest, FilterElement, ParamFilterElement, TextMatchElement,
|
CalendarQueryRequest, FilterElement, ParamFilterElement, TextMatchElement,
|
||||||
comp_filter::CompFilterElement, prop_filter::PropFilterElement,
|
comp_filter::CompFilterElement,
|
||||||
|
prop_filter::PropFilterElement,
|
||||||
|
text_match::{NegateCondition, TextCollation},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
calendar_object::{CalendarData, CalendarObjectPropName, CalendarObjectPropWrapperName},
|
calendar_object::{CalendarData, CalendarObjectPropName, CalendarObjectPropWrapperName},
|
||||||
@@ -96,8 +101,8 @@ 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: Some("i;ascii-casemap".to_owned()),
|
collation: TextCollation::AsciiCasemap,
|
||||||
negate_condition: None,
|
negate_condition: NegateCondition(false),
|
||||||
needle: "mailto:lisa@example.com".to_string()
|
needle: "mailto:lisa@example.com".to_string()
|
||||||
}),
|
}),
|
||||||
is_not_defined: None,
|
is_not_defined: None,
|
||||||
@@ -105,8 +110,8 @@ mod tests {
|
|||||||
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: Some("i;ascii-casemap".to_owned()),
|
collation: TextCollation::AsciiCasemap,
|
||||||
negate_condition: None,
|
negate_condition: NegateCondition(false),
|
||||||
needle: "NEEDS-ACTION".to_string()
|
needle: "NEEDS-ACTION".to_string()
|
||||||
}),
|
}),
|
||||||
}],
|
}],
|
||||||
|
|||||||
@@ -64,28 +64,10 @@ impl PropFilterElement {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(TextMatchElement {
|
if let Some(text_match) = &self.text_match
|
||||||
collation: _collation,
|
&& !text_match.match_property(property)
|
||||||
negate_condition,
|
|
||||||
needle,
|
|
||||||
}) = &self.text_match
|
|
||||||
{
|
{
|
||||||
let mut matches = property
|
return false;
|
||||||
.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
|
// TODO: param-filter
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
use ical::property::Property;
|
||||||
|
use rustical_xml::{ValueDeserialize, XmlDeserialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Default)]
|
||||||
|
pub enum TextCollation {
|
||||||
|
#[default]
|
||||||
|
AsciiCasemap,
|
||||||
|
Octet,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextCollation {
|
||||||
|
// Check whether a haystack contains a needle respecting the collation
|
||||||
|
pub fn match_text(&self, needle: &str, haystack: &str) -> bool {
|
||||||
|
match self {
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc4790#section-9.2
|
||||||
|
Self::AsciiCasemap => haystack
|
||||||
|
.to_ascii_uppercase()
|
||||||
|
.contains(&needle.to_ascii_uppercase()),
|
||||||
|
Self::Octet => haystack.contains(needle),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValueDeserialize for TextCollation {
|
||||||
|
fn deserialize(val: &str) -> Result<Self, rustical_xml::XmlError> {
|
||||||
|
match val {
|
||||||
|
"i;ascii-casemap" => Ok(Self::AsciiCasemap),
|
||||||
|
"i;octet" => Ok(Self::Octet),
|
||||||
|
_ => Err(rustical_xml::XmlError::InvalidVariant(format!(
|
||||||
|
"Invalid collation: {val}"
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Default)]
|
||||||
|
pub struct NegateCondition(pub bool);
|
||||||
|
|
||||||
|
impl ValueDeserialize for NegateCondition {
|
||||||
|
fn deserialize(val: &str) -> Result<Self, rustical_xml::XmlError> {
|
||||||
|
match val {
|
||||||
|
"yes" => Ok(Self(true)),
|
||||||
|
"no" => Ok(Self(false)),
|
||||||
|
_ => Err(rustical_xml::XmlError::InvalidVariant(format!(
|
||||||
|
"Invalid negate-condition parameter: {val}"
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct TextMatchElement {
|
||||||
|
#[xml(ty = "attr", default = "Default::default")]
|
||||||
|
pub collation: TextCollation,
|
||||||
|
#[xml(ty = "attr", default = "Default::default")]
|
||||||
|
pub(crate) negate_condition: NegateCondition,
|
||||||
|
#[xml(ty = "text")]
|
||||||
|
pub(crate) needle: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextMatchElement {
|
||||||
|
pub fn match_property(&self, property: &Property) -> bool {
|
||||||
|
let Self {
|
||||||
|
collation,
|
||||||
|
negate_condition,
|
||||||
|
needle,
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
let matches = property
|
||||||
|
.value
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|haystack| collation.match_text(needle, haystack));
|
||||||
|
|
||||||
|
// XOR
|
||||||
|
negate_condition.0 ^ matches
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::calendar::methods::report::calendar_query::text_match::TextCollation;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_collation() {
|
||||||
|
assert!(TextCollation::AsciiCasemap.match_text("GrüN", "grün"));
|
||||||
|
assert!(!TextCollation::AsciiCasemap.match_text("GrÜN", "grün"));
|
||||||
|
assert!(!TextCollation::Octet.match_text("GrÜN", "grün"));
|
||||||
|
assert!(TextCollation::Octet.match_text("hallo", "hallo"));
|
||||||
|
assert!(TextCollation::AsciiCasemap.match_text("HaLlo", "hAllo"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user