mirror of
https://github.com/lennart-k/rustical.git
synced 2026-01-30 12:58:28 +00:00
text-match: Support match types and unicode-casemap collation
This commit is contained in:
@@ -1,23 +1,23 @@
|
||||
use ical::property::Property;
|
||||
use rustical_xml::{ValueDeserialize, XmlDeserialize};
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub enum TextCollation {
|
||||
#[default]
|
||||
AsciiCasemap,
|
||||
UnicodeCasemap,
|
||||
Octet,
|
||||
}
|
||||
|
||||
impl TextCollation {
|
||||
// Check whether a haystack contains a needle respecting the collation
|
||||
#[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 {
|
||||
// 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),
|
||||
Self::AsciiCasemap => Cow::from(value.to_ascii_uppercase()),
|
||||
Self::UnicodeCasemap => Cow::from(value.to_uppercase()),
|
||||
Self::Octet => Cow::from(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ impl AsRef<str> for TextCollation {
|
||||
fn as_ref(&self) -> &str {
|
||||
match self {
|
||||
Self::AsciiCasemap => "i;ascii-casemap",
|
||||
Self::UnicodeCasemap => "i;unicode-casemap",
|
||||
Self::Octet => "i;octet",
|
||||
}
|
||||
}
|
||||
@@ -35,6 +36,7 @@ impl ValueDeserialize for TextCollation {
|
||||
fn deserialize(val: &str) -> Result<Self, rustical_xml::XmlError> {
|
||||
match val {
|
||||
"i;ascii-casemap" => Ok(Self::AsciiCasemap),
|
||||
"i;unicode-casemap" => Ok(Self::UnicodeCasemap),
|
||||
"i;octet" => Ok(Self::Octet),
|
||||
_ => Err(rustical_xml::XmlError::InvalidVariant(format!(
|
||||
"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)]
|
||||
#[allow(dead_code)]
|
||||
pub struct TextMatchElement {
|
||||
@@ -65,6 +106,8 @@ pub struct TextMatchElement {
|
||||
pub collation: TextCollation,
|
||||
#[xml(ty = "attr", default = "Default::default")]
|
||||
pub negate_condition: NegateCondition,
|
||||
#[xml(ty = "attr", default = "Default::default")]
|
||||
pub match_type: MatchType,
|
||||
#[xml(ty = "text")]
|
||||
pub needle: String,
|
||||
}
|
||||
@@ -76,12 +119,13 @@ impl TextMatchElement {
|
||||
collation,
|
||||
negate_condition,
|
||||
needle,
|
||||
match_type,
|
||||
} = self;
|
||||
|
||||
let matches = property
|
||||
.value
|
||||
.as_ref()
|
||||
.is_some_and(|haystack| collation.match_text(needle, haystack));
|
||||
.is_some_and(|haystack| match_type.match_text(collation, needle, haystack));
|
||||
|
||||
// XOR
|
||||
negate_condition.0 ^ matches
|
||||
@@ -90,14 +134,20 @@ impl TextMatchElement {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::xml::MatchType;
|
||||
|
||||
use super::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"));
|
||||
assert!(!MatchType::Contains.match_text(&TextCollation::AsciiCasemap, "GrÜN", "grünsd"));
|
||||
assert!(MatchType::Contains.match_text(&TextCollation::AsciiCasemap, "GrüN", "grün"));
|
||||
assert!(!MatchType::Contains.match_text(&TextCollation::Octet, "GrüN", "grün"));
|
||||
assert!(MatchType::Contains.match_text(&TextCollation::UnicodeCasemap, "GrÜN", "grün"));
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user