dcbor_pattern/pattern/value/
text_pattern.rs

1use dcbor::prelude::*;
2
3use crate::pattern::{Matcher, Path, Pattern, vm::Instr};
4
5/// Pattern for matching text values in dCBOR.
6#[derive(Debug, Clone)]
7pub enum TextPattern {
8    /// Matches any text.
9    Any,
10    /// Matches the specific text.
11    Value(String),
12    /// Matches the regex for a text.
13    Regex(regex::Regex),
14}
15
16impl PartialEq for TextPattern {
17    fn eq(&self, other: &Self) -> bool {
18        match (self, other) {
19            (TextPattern::Any, TextPattern::Any) => true,
20            (TextPattern::Value(a), TextPattern::Value(b)) => a == b,
21            (TextPattern::Regex(a), TextPattern::Regex(b)) => {
22                a.as_str() == b.as_str()
23            }
24            _ => false,
25        }
26    }
27}
28
29impl Eq for TextPattern {}
30
31impl std::hash::Hash for TextPattern {
32    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
33        match self {
34            TextPattern::Any => {
35                0u8.hash(state);
36            }
37            TextPattern::Value(s) => {
38                1u8.hash(state);
39                s.hash(state);
40            }
41            TextPattern::Regex(regex) => {
42                2u8.hash(state);
43                // Regex does not implement Hash, so we hash its pattern string.
44                regex.as_str().hash(state);
45            }
46        }
47    }
48}
49
50impl TextPattern {
51    /// Creates a new `TextPattern` that matches any text.
52    pub fn any() -> Self { TextPattern::Any }
53
54    /// Creates a new `TextPattern` that matches the specific text.
55    pub fn value<T: Into<String>>(value: T) -> Self {
56        TextPattern::Value(value.into())
57    }
58
59    /// Creates a new `TextPattern` that matches the regex for a text.
60    pub fn regex(regex: regex::Regex) -> Self { TextPattern::Regex(regex) }
61}
62
63impl Matcher for TextPattern {
64    fn paths(&self, haystack: &CBOR) -> Vec<Path> {
65        let is_hit = haystack.as_text().is_some_and(|value| match self {
66            TextPattern::Any => true,
67            TextPattern::Value(want) => value == *want,
68            TextPattern::Regex(regex) => regex.is_match(value),
69        });
70
71        if is_hit {
72            vec![vec![haystack.clone()]]
73        } else {
74            vec![]
75        }
76    }
77
78    fn compile(
79        &self,
80        code: &mut Vec<Instr>,
81        literals: &mut Vec<Pattern>,
82        _captures: &mut Vec<String>,
83    ) {
84        let idx = literals.len();
85        literals.push(Pattern::Value(crate::pattern::ValuePattern::Text(
86            self.clone(),
87        )));
88        code.push(Instr::MatchPredicate(idx));
89    }
90}
91
92impl std::fmt::Display for TextPattern {
93    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94        match self {
95            TextPattern::Any => write!(f, "text"),
96            TextPattern::Value(value) => {
97                let escaped = value.replace("\\", "\\\\").replace("\"", "\\\"");
98                write!(f, "\"{}\"", escaped)
99            }
100            TextPattern::Regex(regex) => write!(f, "/{}/", regex),
101        }
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn test_text_pattern_display() {
111        assert_eq!(TextPattern::any().to_string(), "text");
112        assert_eq!(TextPattern::value("Hello").to_string(), "\"Hello\"");
113        assert_eq!(
114            TextPattern::regex(regex::Regex::new(r"^\d+$").unwrap())
115                .to_string(),
116            "/^\\d+$/"
117        );
118    }
119
120    #[test]
121    fn test_text_pattern_matching() {
122        let hello_cbor = "Hello".to_cbor();
123        let world_cbor = "World".to_cbor();
124        let digits_cbor = "12345".to_cbor();
125        let mixed_cbor = "Hello123".to_cbor();
126        let number_cbor = 42.to_cbor();
127
128        // Test Any pattern
129        let any_pattern = TextPattern::any();
130        assert!(any_pattern.matches(&hello_cbor));
131        assert!(any_pattern.matches(&world_cbor));
132        assert!(any_pattern.matches(&digits_cbor));
133        assert!(any_pattern.matches(&mixed_cbor));
134        assert!(!any_pattern.matches(&number_cbor));
135
136        // Test specific value patterns
137        let hello_pattern = TextPattern::value("Hello");
138        assert!(hello_pattern.matches(&hello_cbor));
139        assert!(!hello_pattern.matches(&world_cbor));
140        assert!(!hello_pattern.matches(&number_cbor));
141
142        // Test regex patterns
143        let digits_regex = regex::Regex::new(r"^\d+$").unwrap();
144        let digits_pattern = TextPattern::regex(digits_regex);
145        assert!(!digits_pattern.matches(&hello_cbor));
146        assert!(!digits_pattern.matches(&world_cbor));
147        assert!(digits_pattern.matches(&digits_cbor));
148        assert!(!digits_pattern.matches(&mixed_cbor));
149        assert!(!digits_pattern.matches(&number_cbor));
150
151        let word_regex = regex::Regex::new(r"^[A-Za-z]+$").unwrap();
152        let word_pattern = TextPattern::regex(word_regex);
153        assert!(word_pattern.matches(&hello_cbor));
154        assert!(word_pattern.matches(&world_cbor));
155        assert!(!word_pattern.matches(&digits_cbor));
156        assert!(!word_pattern.matches(&mixed_cbor));
157        assert!(!word_pattern.matches(&number_cbor));
158    }
159
160    #[test]
161    fn test_text_pattern_paths() {
162        let hello_cbor = "Hello".to_cbor();
163        let number_cbor = 42.to_cbor();
164
165        let any_pattern = TextPattern::any();
166        let hello_paths = any_pattern.paths(&hello_cbor);
167        assert_eq!(hello_paths.len(), 1);
168        assert_eq!(hello_paths[0].len(), 1);
169        assert_eq!(hello_paths[0][0], hello_cbor);
170
171        let number_paths = any_pattern.paths(&number_cbor);
172        assert_eq!(number_paths.len(), 0);
173
174        let hello_pattern = TextPattern::value("Hello");
175        let paths = hello_pattern.paths(&hello_cbor);
176        assert_eq!(paths.len(), 1);
177        assert_eq!(paths[0].len(), 1);
178        assert_eq!(paths[0][0], hello_cbor);
179
180        let no_match_paths = hello_pattern.paths(&number_cbor);
181        assert_eq!(no_match_paths.len(), 0);
182    }
183
184    #[test]
185    fn test_text_pattern_equality() {
186        let any1 = TextPattern::any();
187        let any2 = TextPattern::any();
188        let value1 = TextPattern::value("test");
189        let value2 = TextPattern::value("test");
190        let value3 = TextPattern::value("different");
191        let regex1 = TextPattern::regex(regex::Regex::new(r"\d+").unwrap());
192        let regex2 = TextPattern::regex(regex::Regex::new(r"\d+").unwrap());
193        let regex3 = TextPattern::regex(regex::Regex::new(r"[a-z]+").unwrap());
194
195        // Test equality
196        assert_eq!(any1, any2);
197        assert_eq!(value1, value2);
198        assert_eq!(regex1, regex2);
199
200        // Test inequality
201        assert_ne!(any1, value1);
202        assert_ne!(value1, value3);
203        assert_ne!(regex1, regex3);
204        assert_ne!(value1, regex1);
205    }
206
207    #[test]
208    fn test_text_pattern_regex_complex() {
209        let email_cbor = "test@example.com".to_cbor();
210        let not_email_cbor = "not_an_email".to_cbor();
211
212        // Simple email regex pattern
213        let email_regex = regex::Regex::new(r"^[^@]+@[^@]+\.[^@]+$").unwrap();
214        let email_pattern = TextPattern::regex(email_regex);
215
216        assert!(email_pattern.matches(&email_cbor));
217        assert!(!email_pattern.matches(&not_email_cbor));
218    }
219}