mirror of
https://github.com/lennart-k/rustical.git
synced 2026-01-30 06:58:26 +00:00
text-match: Support match types and unicode-casemap collation
This commit is contained in:
@@ -137,7 +137,7 @@ impl CompFilterable for CalendarObjectComponent {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use chrono::{TimeZone, Utc};
|
use chrono::{TimeZone, Utc};
|
||||||
use rustical_dav::xml::{NegateCondition, TextCollation, TextMatchElement};
|
use rustical_dav::xml::{MatchType, NegateCondition, TextCollation, TextMatchElement};
|
||||||
use rustical_ical::{CalendarObject, UtcDateTime};
|
use rustical_ical::{CalendarObject, UtcDateTime};
|
||||||
|
|
||||||
use crate::calendar::methods::report::calendar_query::{
|
use crate::calendar::methods::report::calendar_query::{
|
||||||
@@ -217,6 +217,7 @@ END:VCALENDAR";
|
|||||||
name: "VERSION".to_string(),
|
name: "VERSION".to_string(),
|
||||||
time_range: None,
|
time_range: None,
|
||||||
text_match: Some(TextMatchElement {
|
text_match: Some(TextMatchElement {
|
||||||
|
match_type: MatchType::Contains,
|
||||||
needle: "2.0".to_string(),
|
needle: "2.0".to_string(),
|
||||||
collation: TextCollation::default(),
|
collation: TextCollation::default(),
|
||||||
negate_condition: NegateCondition::default(),
|
negate_condition: NegateCondition::default(),
|
||||||
@@ -240,6 +241,7 @@ END:VCALENDAR";
|
|||||||
name: "SUMMARY".to_string(),
|
name: "SUMMARY".to_string(),
|
||||||
time_range: None,
|
time_range: None,
|
||||||
text_match: Some(TextMatchElement {
|
text_match: Some(TextMatchElement {
|
||||||
|
match_type: MatchType::Contains,
|
||||||
collation: TextCollation::default(),
|
collation: TextCollation::default(),
|
||||||
negate_condition: NegateCondition(false),
|
negate_condition: NegateCondition(false),
|
||||||
needle: "weekly".to_string(),
|
needle: "weekly".to_string(),
|
||||||
@@ -327,6 +329,7 @@ END:VCALENDAR";
|
|||||||
name: "TZID".to_string(),
|
name: "TZID".to_string(),
|
||||||
time_range: None,
|
time_range: None,
|
||||||
text_match: Some(TextMatchElement {
|
text_match: Some(TextMatchElement {
|
||||||
|
match_type: MatchType::Contains,
|
||||||
collation: TextCollation::AsciiCasemap,
|
collation: TextCollation::AsciiCasemap,
|
||||||
negate_condition: NegateCondition::default(),
|
negate_condition: NegateCondition::default(),
|
||||||
needle: "Europe/Berlin".to_string(),
|
needle: "Europe/Berlin".to_string(),
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ mod tests {
|
|||||||
calendar::methods::report::ReportRequest,
|
calendar::methods::report::ReportRequest,
|
||||||
calendar_object::{CalendarData, CalendarObjectPropName, CalendarObjectPropWrapperName},
|
calendar_object::{CalendarData, CalendarObjectPropName, CalendarObjectPropWrapperName},
|
||||||
};
|
};
|
||||||
use rustical_dav::xml::{NegateCondition, PropElement, TextCollation, TextMatchElement};
|
use rustical_dav::xml::{
|
||||||
|
MatchType, NegateCondition, PropElement, TextCollation, TextMatchElement,
|
||||||
|
};
|
||||||
use rustical_xml::XmlDocument;
|
use rustical_xml::XmlDocument;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -93,6 +95,7 @@ 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 {
|
||||||
|
match_type: MatchType::Contains,
|
||||||
collation: TextCollation::AsciiCasemap,
|
collation: TextCollation::AsciiCasemap,
|
||||||
negate_condition: NegateCondition(false),
|
negate_condition: NegateCondition(false),
|
||||||
needle: "mailto:lisa@example.com".to_string()
|
needle: "mailto:lisa@example.com".to_string()
|
||||||
@@ -102,6 +105,7 @@ 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 {
|
||||||
|
match_type: MatchType::Contains,
|
||||||
collation: TextCollation::AsciiCasemap,
|
collation: TextCollation::AsciiCasemap,
|
||||||
negate_condition: NegateCondition(false),
|
negate_condition: NegateCondition(false),
|
||||||
needle: "NEEDS-ACTION".to_string()
|
needle: "NEEDS-ACTION".to_string()
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ impl Default for SupportedCollationSet {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self(vec![
|
Self(vec![
|
||||||
SupportedCollation(TextCollation::AsciiCasemap),
|
SupportedCollation(TextCollation::AsciiCasemap),
|
||||||
|
SupportedCollation(TextCollation::UnicodeCasemap),
|
||||||
SupportedCollation(TextCollation::Octet),
|
SupportedCollation(TextCollation::Octet),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ END:VCALENDAR
|
|||||||
</supported-calendar-data>
|
</supported-calendar-data>
|
||||||
<supported-collation-set xmlns="urn:ietf:params:xml:ns:caldav">
|
<supported-collation-set xmlns="urn:ietf:params:xml:ns:caldav">
|
||||||
<supported-collation xmlns="urn:ietf:params:xml:ns:caldav">i;ascii-casemap</supported-collation>
|
<supported-collation xmlns="urn:ietf:params:xml:ns:caldav">i;ascii-casemap</supported-collation>
|
||||||
|
<supported-collation xmlns="urn:ietf:params:xml:ns:caldav">i;unicode-casemap</supported-collation>
|
||||||
<supported-collation xmlns="urn:ietf:params:xml:ns:caldav">i;octet</supported-collation>
|
<supported-collation xmlns="urn:ietf:params:xml:ns:caldav">i;octet</supported-collation>
|
||||||
</supported-collation-set>
|
</supported-collation-set>
|
||||||
<max-resource-size xmlns="DAV:">10000000</max-resource-size>
|
<max-resource-size xmlns="DAV:">10000000</max-resource-size>
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
use ical::property::Property;
|
use ical::property::Property;
|
||||||
use rustical_xml::{ValueDeserialize, XmlDeserialize};
|
use rustical_xml::{ValueDeserialize, XmlDeserialize};
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Default)]
|
#[derive(Clone, Debug, PartialEq, Eq, Default)]
|
||||||
pub enum TextCollation {
|
pub enum TextCollation {
|
||||||
#[default]
|
#[default]
|
||||||
AsciiCasemap,
|
AsciiCasemap,
|
||||||
|
UnicodeCasemap,
|
||||||
Octet,
|
Octet,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextCollation {
|
impl TextCollation {
|
||||||
// Check whether a haystack contains a needle respecting the collation
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn match_text(&self, needle: &str, haystack: &str) -> bool {
|
pub fn normalise<'a>(&self, value: &'a str) -> Cow<'a, str> {
|
||||||
match self {
|
match self {
|
||||||
// https://datatracker.ietf.org/doc/html/rfc4790#section-9.2
|
// https://datatracker.ietf.org/doc/html/rfc4790#section-9.2
|
||||||
Self::AsciiCasemap => haystack
|
Self::AsciiCasemap => Cow::from(value.to_ascii_uppercase()),
|
||||||
.to_ascii_uppercase()
|
Self::UnicodeCasemap => Cow::from(value.to_uppercase()),
|
||||||
.contains(&needle.to_ascii_uppercase()),
|
Self::Octet => Cow::from(value),
|
||||||
Self::Octet => haystack.contains(needle),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,6 +26,7 @@ impl AsRef<str> for TextCollation {
|
|||||||
fn as_ref(&self) -> &str {
|
fn as_ref(&self) -> &str {
|
||||||
match self {
|
match self {
|
||||||
Self::AsciiCasemap => "i;ascii-casemap",
|
Self::AsciiCasemap => "i;ascii-casemap",
|
||||||
|
Self::UnicodeCasemap => "i;unicode-casemap",
|
||||||
Self::Octet => "i;octet",
|
Self::Octet => "i;octet",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,6 +36,7 @@ impl ValueDeserialize for TextCollation {
|
|||||||
fn deserialize(val: &str) -> Result<Self, rustical_xml::XmlError> {
|
fn deserialize(val: &str) -> Result<Self, rustical_xml::XmlError> {
|
||||||
match val {
|
match val {
|
||||||
"i;ascii-casemap" => Ok(Self::AsciiCasemap),
|
"i;ascii-casemap" => Ok(Self::AsciiCasemap),
|
||||||
|
"i;unicode-casemap" => Ok(Self::UnicodeCasemap),
|
||||||
"i;octet" => Ok(Self::Octet),
|
"i;octet" => Ok(Self::Octet),
|
||||||
_ => Err(rustical_xml::XmlError::InvalidVariant(format!(
|
_ => Err(rustical_xml::XmlError::InvalidVariant(format!(
|
||||||
"Invalid collation: {val}"
|
"Invalid collation: {val}"
|
||||||
@@ -58,6 +60,45 @@ impl ValueDeserialize for NegateCondition {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Default)]
|
||||||
|
pub enum MatchType {
|
||||||
|
Equals,
|
||||||
|
#[default]
|
||||||
|
Contains,
|
||||||
|
StartsWith,
|
||||||
|
EndsWith,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MatchType {
|
||||||
|
pub fn match_text(&self, collation: &TextCollation, needle: &str, haystack: &str) -> bool {
|
||||||
|
let haystack = collation.normalise(haystack);
|
||||||
|
let needle = collation.normalise(needle);
|
||||||
|
|
||||||
|
match &self {
|
||||||
|
Self::Equals => haystack == needle,
|
||||||
|
Self::Contains => haystack.contains(needle.as_ref()),
|
||||||
|
Self::StartsWith => haystack.starts_with(needle.as_ref()),
|
||||||
|
Self::EndsWith => haystack.ends_with(needle.as_ref()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValueDeserialize for MatchType {
|
||||||
|
fn deserialize(val: &str) -> Result<Self, rustical_xml::XmlError> {
|
||||||
|
Ok(match val {
|
||||||
|
"equals" => Self::Equals,
|
||||||
|
"contains" => Self::Contains,
|
||||||
|
"starts-with" => Self::StartsWith,
|
||||||
|
"ends-with" => Self::EndsWith,
|
||||||
|
_ => {
|
||||||
|
return Err(rustical_xml::XmlError::InvalidVariant(format!(
|
||||||
|
"Invalid match-type parameter: {val}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
|
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub struct TextMatchElement {
|
pub struct TextMatchElement {
|
||||||
@@ -65,6 +106,8 @@ pub struct TextMatchElement {
|
|||||||
pub collation: TextCollation,
|
pub collation: TextCollation,
|
||||||
#[xml(ty = "attr", default = "Default::default")]
|
#[xml(ty = "attr", default = "Default::default")]
|
||||||
pub negate_condition: NegateCondition,
|
pub negate_condition: NegateCondition,
|
||||||
|
#[xml(ty = "attr", default = "Default::default")]
|
||||||
|
pub match_type: MatchType,
|
||||||
#[xml(ty = "text")]
|
#[xml(ty = "text")]
|
||||||
pub needle: String,
|
pub needle: String,
|
||||||
}
|
}
|
||||||
@@ -76,12 +119,13 @@ impl TextMatchElement {
|
|||||||
collation,
|
collation,
|
||||||
negate_condition,
|
negate_condition,
|
||||||
needle,
|
needle,
|
||||||
|
match_type,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
let matches = property
|
let matches = property
|
||||||
.value
|
.value
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|haystack| collation.match_text(needle, haystack));
|
.is_some_and(|haystack| match_type.match_text(collation, needle, haystack));
|
||||||
|
|
||||||
// XOR
|
// XOR
|
||||||
negate_condition.0 ^ matches
|
negate_condition.0 ^ matches
|
||||||
@@ -90,14 +134,20 @@ impl TextMatchElement {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use crate::xml::MatchType;
|
||||||
|
|
||||||
use super::TextCollation;
|
use super::TextCollation;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_collation() {
|
fn test_collation() {
|
||||||
assert!(TextCollation::AsciiCasemap.match_text("GrüN", "grün"));
|
assert!(!MatchType::Contains.match_text(&TextCollation::AsciiCasemap, "GrÜN", "grünsd"));
|
||||||
assert!(!TextCollation::AsciiCasemap.match_text("GrÜN", "grün"));
|
assert!(MatchType::Contains.match_text(&TextCollation::AsciiCasemap, "GrüN", "grün"));
|
||||||
assert!(!TextCollation::Octet.match_text("GrÜN", "grün"));
|
assert!(!MatchType::Contains.match_text(&TextCollation::Octet, "GrüN", "grün"));
|
||||||
assert!(TextCollation::Octet.match_text("hallo", "hallo"));
|
assert!(MatchType::Contains.match_text(&TextCollation::UnicodeCasemap, "GrÜN", "grün"));
|
||||||
assert!(TextCollation::AsciiCasemap.match_text("HaLlo", "hAllo"));
|
assert!(MatchType::Contains.match_text(&TextCollation::AsciiCasemap, "GrüN", "grün"));
|
||||||
|
assert!(MatchType::Contains.match_text(&TextCollation::AsciiCasemap, "GrüN", "grün"));
|
||||||
|
assert!(MatchType::StartsWith.match_text(&TextCollation::Octet, "hello", "hello you"));
|
||||||
|
assert!(MatchType::EndsWith.match_text(&TextCollation::Octet, "joe", "joe mama"));
|
||||||
|
assert!(MatchType::Equals.match_text(&TextCollation::UnicodeCasemap, "GrÜN", "grün"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ END:VCALENDAR
|
|||||||
</CAL:supported-calendar-data>
|
</CAL:supported-calendar-data>
|
||||||
<CAL:supported-collation-set>
|
<CAL:supported-collation-set>
|
||||||
<CAL:supported-collation>i;ascii-casemap</CAL:supported-collation>
|
<CAL:supported-collation>i;ascii-casemap</CAL:supported-collation>
|
||||||
|
<CAL:supported-collation>i;unicode-casemap</CAL:supported-collation>
|
||||||
<CAL:supported-collation>i;octet</CAL:supported-collation>
|
<CAL:supported-collation>i;octet</CAL:supported-collation>
|
||||||
</CAL:supported-collation-set>
|
</CAL:supported-collation-set>
|
||||||
<max-resource-size>10000000</max-resource-size>
|
<max-resource-size>10000000</max-resource-size>
|
||||||
|
|||||||
Reference in New Issue
Block a user