1use caldata::parser::ContentLine;
2use rustical_xml::{ValueDeserialize, XmlDeserialize};
3use std::borrow::Cow;
4
5#[derive(Clone, Debug, PartialEq, Eq, Default)]
6pub enum TextCollation {
7 #[default]
8 AsciiCasemap,
9 UnicodeCasemap,
10 Octet,
11}
12
13impl TextCollation {
14 #[must_use]
15 pub fn normalise<'a>(&self, value: &'a str) -> Cow<'a, str> {
16 match self {
17 Self::AsciiCasemap => Cow::from(value.to_ascii_uppercase()),
19 Self::UnicodeCasemap => Cow::from(value.to_uppercase()),
20 Self::Octet => Cow::from(value),
21 }
22 }
23}
24
25impl AsRef<str> for TextCollation {
26 fn as_ref(&self) -> &str {
27 match self {
28 Self::AsciiCasemap => "i;ascii-casemap",
29 Self::UnicodeCasemap => "i;unicode-casemap",
30 Self::Octet => "i;octet",
31 }
32 }
33}
34
35impl ValueDeserialize for TextCollation {
36 fn deserialize(val: &str) -> Result<Self, rustical_xml::XmlError> {
37 match val {
38 "i;ascii-casemap" => Ok(Self::AsciiCasemap),
39 "i;unicode-casemap" => Ok(Self::UnicodeCasemap),
40 "i;octet" => Ok(Self::Octet),
41 _ => Err(rustical_xml::XmlError::InvalidVariant(format!(
42 "Invalid collation: {val}"
43 ))),
44 }
45 }
46}
47
48#[derive(Clone, Debug, PartialEq, Eq, Default)]
49pub struct NegateCondition(pub bool);
50
51impl ValueDeserialize for NegateCondition {
52 fn deserialize(val: &str) -> Result<Self, rustical_xml::XmlError> {
53 match val {
54 "yes" => Ok(Self(true)),
55 "no" => Ok(Self(false)),
56 _ => Err(rustical_xml::XmlError::InvalidVariant(format!(
57 "Invalid negate-condition parameter: {val}"
58 ))),
59 }
60 }
61}
62
63#[derive(Clone, Debug, PartialEq, Eq, Default)]
64pub enum MatchType {
65 Equals,
66 #[default]
67 Contains,
68 StartsWith,
69 EndsWith,
70}
71
72impl MatchType {
73 #[must_use]
74 pub fn match_text(&self, collation: &TextCollation, needle: &str, haystack: &str) -> bool {
75 let haystack = collation.normalise(haystack);
76 let needle = collation.normalise(needle);
77
78 match &self {
79 Self::Equals => haystack == needle,
80 Self::Contains => haystack.contains(needle.as_ref()),
81 Self::StartsWith => haystack.starts_with(needle.as_ref()),
82 Self::EndsWith => haystack.ends_with(needle.as_ref()),
83 }
84 }
85}
86
87impl ValueDeserialize for MatchType {
88 fn deserialize(val: &str) -> Result<Self, rustical_xml::XmlError> {
89 Ok(match val {
90 "equals" => Self::Equals,
91 "contains" => Self::Contains,
92 "starts-with" => Self::StartsWith,
93 "ends-with" => Self::EndsWith,
94 _ => {
95 return Err(rustical_xml::XmlError::InvalidVariant(format!(
96 "Invalid match-type parameter: {val}"
97 )));
98 }
99 })
100 }
101}
102
103#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
104#[allow(dead_code)]
105pub struct TextMatchElement {
106 #[xml(ty = "attr", default = "Default::default")]
107 pub collation: TextCollation,
108 #[xml(ty = "attr", default = "Default::default")]
109 pub negate_condition: NegateCondition,
110 #[xml(ty = "attr", default = "Default::default")]
111 pub match_type: MatchType,
112 #[xml(ty = "text")]
113 pub needle: String,
114}
115
116impl TextMatchElement {
117 #[must_use]
118 pub fn match_text(&self, haystack: &str) -> bool {
119 let Self {
120 collation,
121 negate_condition,
122 needle,
123 match_type,
124 } = self;
125
126 let matches = match_type.match_text(collation, needle, haystack);
127 negate_condition.0 ^ matches
129 }
130 #[must_use]
131 pub fn match_property(&self, property: &ContentLine) -> bool {
132 let text = property.value.as_deref().unwrap_or("");
133 self.match_text(text)
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::TextCollation;
140 use crate::xml::MatchType;
141
142 #[test]
143 fn test_collation() {
144 assert!(!MatchType::Contains.match_text(&TextCollation::AsciiCasemap, "GrÜN", "grünsd"));
145 assert!(MatchType::Contains.match_text(&TextCollation::AsciiCasemap, "GrüN", "grün"));
146 assert!(!MatchType::Contains.match_text(&TextCollation::Octet, "GrüN", "grün"));
147 assert!(MatchType::Contains.match_text(&TextCollation::UnicodeCasemap, "GrÜN", "grün"));
148 assert!(MatchType::Contains.match_text(&TextCollation::AsciiCasemap, "GrüN", "grün"));
149 assert!(MatchType::Contains.match_text(&TextCollation::AsciiCasemap, "GrüN", "grün"));
150 assert!(MatchType::StartsWith.match_text(&TextCollation::Octet, "hello", "hello you"));
151 assert!(MatchType::EndsWith.match_text(&TextCollation::Octet, "mama", "joe mama"));
152 assert!(MatchType::Equals.match_text(&TextCollation::UnicodeCasemap, "GrÜN", "grün"));
153 }
154}