1use regex::Regex;
7
8#[polydat::polydat_node(category = Regex)]
12fn regex_replace(
13 input: &str,
14 pattern: polydat::derive_support::Const<&str>,
15 replacement: polydat::derive_support::Const<&str>,
16 #[poly_const(compile_regex, from = pattern)] re: &Regex,
17) -> String {
18 let _ = pattern;
19 re.replace_all(input, replacement.0).into_owned()
20}
21
22fn compile_regex(pattern: &str) -> Regex {
28 Regex::new(pattern).expect("invalid regex")
29}
30
31#[polydat::polydat_node(category = Regex)]
33fn regex_match(
34 input: &str,
35 pattern: polydat::derive_support::Const<&str>,
36 #[poly_const(compile_regex, from = pattern)] re: &Regex,
37) -> bool {
38 let matched = re.is_match(input);
39 if polydat::library::debug_nodes_enabled() {
40 let snippet: String = input.chars().take(200).collect();
41 let ellipsis = if input.len() > snippet.len() {
42 "…"
43 } else {
44 ""
45 };
46 polydat::library::support::audit::debug(&format!(
47 "regex_match: pattern={:?} input.len={} matched={matched} input.snippet={:?}{ellipsis}",
48 re.as_str(),
49 input.len(),
50 snippet,
51 ));
52 }
53 matched
54}
55
56#[polydat::polydat_node(category = Regex)]
58fn regex_extract(
59 input: &str,
60 pattern: polydat::derive_support::Const<&str>,
61 #[poly_const(compile_regex, from = pattern)] re: &Regex,
62) -> String {
63 if let Some(caps) = re.captures(input) {
64 caps.get(1)
65 .or_else(|| caps.get(0))
66 .map(|m| m.as_str().to_string())
67 .unwrap_or_default()
68 } else {
69 String::new()
70 }
71}
72
73pub use polydat::library::support::pattern::{PatternDialect, compile_pattern, promote_pattern};
83
84fn compile_promoted(pattern: &str) -> Regex {
88 compile_pattern(pattern).unwrap_or_else(|e| panic!("{e}")).0
89}
90
91#[polydat::polydat_node(category = Regex)]
97fn pattern_match(
98 input: &str,
99 pattern: polydat::derive_support::Const<&str>,
100 #[poly_const(compile_promoted, from = pattern)] re: &Regex,
101) -> bool {
102 re.is_match(input)
103}
104
105#[cfg(test)]
106mod pattern_tests {
107 use super::*;
108
109 #[test]
110 fn literal_is_strict_anchored() {
111 let (re, d) = compile_pattern("schema").unwrap();
112 assert_eq!(d, PatternDialect::Literal);
113 assert!(re.is_match("schema"));
114 assert!(!re.is_match("schema_v2"));
115 assert!(!re.is_match("pre_schema"));
116 }
117
118 #[test]
119 fn glob_star_distinguishes_suffix_series() {
120 let (m, dm) = compile_pattern("*m").unwrap();
122 assert_eq!(dm, PatternDialect::Glob);
123 assert!(m.is_match("100m"));
124 assert!(!m.is_match("100mi"));
125 let (mi, _) = compile_pattern("*mi").unwrap();
126 assert!(mi.is_match("100mi"));
127 assert!(!mi.is_match("100m"));
128 }
129
130 #[test]
131 fn regex_metachar_promotes() {
132 let (re, d) = compile_pattern("(100|200)m").unwrap();
133 assert_eq!(d, PatternDialect::Regex);
134 assert!(re.is_match("100m"));
135 assert!(re.is_match("200m"));
136 assert!(!re.is_match("300m"));
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use polydat::ast::{PolydatNode, Value};
144
145 #[test]
146 fn regex_replace_basic() {
147 let node = RegexReplace::new(r"\d+".to_string(), "NUM".to_string());
148 let mut out = [Value::None];
149 node.eval(&[Value::Str("abc 123 def 456".into())], &mut out);
150 assert_eq!(out[0].as_str(), "abc NUM def NUM");
151 }
152
153 #[test]
154 fn regex_replace_no_match() {
155 let node = RegexReplace::new(r"\d+".to_string(), "NUM".to_string());
156 let mut out = [Value::None];
157 node.eval(&[Value::Str("no numbers here".into())], &mut out);
158 assert_eq!(out[0].as_str(), "no numbers here");
159 }
160
161 #[test]
162 fn regex_match_true() {
163 let node = RegexMatch::new(r"^\d{3}-\d{4}$".to_string());
164 let mut out = [Value::None];
165 node.eval(&[Value::Str("123-4567".into())], &mut out);
166 assert!(out[0].as_bool());
167 }
168
169 #[test]
170 fn regex_match_false() {
171 let node = RegexMatch::new(r"^\d{3}-\d{4}$".to_string());
172 let mut out = [Value::None];
173 node.eval(&[Value::Str("hello".into())], &mut out);
174 assert!(!out[0].as_bool());
175 }
176
177 #[test]
178 fn regex_extract_capture_group() {
179 let node = RegexExtract::new(r"name=(\w+)".to_string());
180 let mut out = [Value::None];
181 node.eval(&[Value::Str("name=Alice age=30".into())], &mut out);
182 assert_eq!(out[0].as_str(), "Alice");
183 }
184
185 #[test]
186 fn regex_extract_no_group() {
187 let node = RegexExtract::new(r"\d+".to_string());
188 let mut out = [Value::None];
189 node.eval(&[Value::Str("abc 42 def".into())], &mut out);
190 assert_eq!(out[0].as_str(), "42");
191 }
192
193 #[test]
194 fn regex_extract_no_match() {
195 let node = RegexExtract::new(r"\d+".to_string());
196 let mut out = [Value::None];
197 node.eval(&[Value::Str("no digits".into())], &mut out);
198 assert_eq!(out[0].as_str(), "");
199 }
200}