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