sqry_core/query/builder/regex.rs
1//! Regex builder for constructing regex patterns with flags
2
3use crate::query::{RegexFlags, RegexValue};
4
5/// Builder for regex patterns with compilation flags
6///
7/// Provides a fluent API for constructing regex patterns with optional flags
8/// like case-insensitivity, multiline mode, and dot-all mode.
9///
10/// # Example
11///
12/// ```ignore
13/// use sqry_core::query::builder::RegexBuilder;
14///
15/// // Simple pattern
16/// let regex = RegexBuilder::new("test.*").build()?;
17///
18/// // With flags
19/// let regex = RegexBuilder::new("Test.*")
20/// .case_insensitive()
21/// .multiline()
22/// .build()?;
23/// ```
24#[derive(Clone, Debug)]
25#[must_use = "RegexBuilder does nothing until .build() is called"]
26pub struct RegexBuilder {
27 /// The regex pattern string
28 pub(crate) pattern: String,
29 /// Case-insensitive flag
30 pub(crate) case_insensitive: bool,
31 /// Multiline flag
32 pub(crate) multiline: bool,
33 /// Dot-all flag (dot matches newlines)
34 pub(crate) dot_all: bool,
35}
36
37impl RegexBuilder {
38 /// Create a new regex builder with the given pattern
39 pub fn new(pattern: impl Into<String>) -> Self {
40 Self {
41 pattern: pattern.into(),
42 case_insensitive: false,
43 multiline: false,
44 dot_all: false,
45 }
46 }
47
48 /// Enable case-insensitive matching (flag: i)
49 ///
50 /// # Example
51 ///
52 /// ```ignore
53 /// let regex = RegexBuilder::new("Test")
54 /// .case_insensitive()
55 /// .build()?;
56 /// // Matches "test", "TEST", "Test", etc.
57 /// ```
58 pub fn case_insensitive(mut self) -> Self {
59 self.case_insensitive = true;
60 self
61 }
62
63 /// Enable multiline mode (flag: m)
64 ///
65 /// In multiline mode, `^` and `$` match line boundaries instead of
66 /// just the start and end of the input string.
67 ///
68 /// # Example
69 ///
70 /// ```ignore
71 /// let regex = RegexBuilder::new("^fn ")
72 /// .multiline()
73 /// .build()?;
74 /// // Matches "fn " at the start of any line
75 /// ```
76 pub fn multiline(mut self) -> Self {
77 self.multiline = true;
78 self
79 }
80
81 /// Enable dot-all mode (flag: s)
82 ///
83 /// In dot-all mode, `.` matches newline characters as well as
84 /// other characters.
85 ///
86 /// # Example
87 ///
88 /// ```ignore
89 /// let regex = RegexBuilder::new("fn.*}")
90 /// .dot_all()
91 /// .build()?;
92 /// // Matches function bodies that span multiple lines
93 /// ```
94 pub fn dot_all(mut self) -> Self {
95 self.dot_all = true;
96 self
97 }
98
99 /// Build and validate the regex pattern
100 ///
101 /// This validates the regex syntax with the configured flags applied.
102 /// Returns an error if the pattern is invalid.
103 ///
104 /// # Errors
105 ///
106 /// Returns `regex::Error` if the pattern is syntactically invalid.
107 ///
108 /// # Example
109 ///
110 /// ```ignore
111 /// // Valid pattern
112 /// let regex = RegexBuilder::new("test.*").build()?;
113 ///
114 /// // Invalid pattern returns error
115 /// let result = RegexBuilder::new("[invalid").build();
116 /// assert!(result.is_err());
117 /// ```
118 pub fn build(self) -> Result<RegexValue, regex::Error> {
119 // Validate regex syntax WITH flags applied
120 let mut builder = regex::RegexBuilder::new(&self.pattern);
121 builder.case_insensitive(self.case_insensitive);
122 builder.multi_line(self.multiline);
123 builder.dot_matches_new_line(self.dot_all);
124 builder.build()?;
125
126 Ok(RegexValue {
127 pattern: self.pattern,
128 flags: RegexFlags {
129 case_insensitive: self.case_insensitive,
130 multiline: self.multiline,
131 dot_all: self.dot_all,
132 },
133 })
134 }
135
136 /// Convert to `RegexValue` without validation
137 ///
138 /// This creates a `RegexValue` directly without checking if the pattern
139 /// is syntactically valid. Validation will occur at `QueryBuilder::build()` time.
140 ///
141 /// This is used internally by the `*_matches_with()` methods to defer
142 /// validation to the build phase.
143 pub(crate) fn into_regex_value(self) -> RegexValue {
144 RegexValue {
145 pattern: self.pattern,
146 flags: RegexFlags {
147 case_insensitive: self.case_insensitive,
148 multiline: self.multiline,
149 dot_all: self.dot_all,
150 },
151 }
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn test_simple_pattern() {
161 let regex = RegexBuilder::new("test.*").build().unwrap();
162 assert_eq!(regex.pattern, "test.*");
163 assert!(!regex.flags.case_insensitive);
164 assert!(!regex.flags.multiline);
165 assert!(!regex.flags.dot_all);
166 }
167
168 #[test]
169 fn test_case_insensitive() {
170 let regex = RegexBuilder::new("Test")
171 .case_insensitive()
172 .build()
173 .unwrap();
174 assert!(regex.flags.case_insensitive);
175 assert!(!regex.flags.multiline);
176 assert!(!regex.flags.dot_all);
177 }
178
179 #[test]
180 fn test_multiline() {
181 let regex = RegexBuilder::new("^fn ").multiline().build().unwrap();
182 assert!(!regex.flags.case_insensitive);
183 assert!(regex.flags.multiline);
184 assert!(!regex.flags.dot_all);
185 }
186
187 #[test]
188 fn test_dot_all() {
189 let regex = RegexBuilder::new("fn.*}").dot_all().build().unwrap();
190 assert!(!regex.flags.case_insensitive);
191 assert!(!regex.flags.multiline);
192 assert!(regex.flags.dot_all);
193 }
194
195 #[test]
196 fn test_multiple_flags() {
197 let regex = RegexBuilder::new("Test.*")
198 .case_insensitive()
199 .multiline()
200 .dot_all()
201 .build()
202 .unwrap();
203 assert!(regex.flags.case_insensitive);
204 assert!(regex.flags.multiline);
205 assert!(regex.flags.dot_all);
206 }
207
208 #[test]
209 fn test_invalid_pattern() {
210 let result = RegexBuilder::new("[invalid").build();
211 assert!(result.is_err());
212 }
213
214 #[test]
215 fn test_into_regex_value_no_validation() {
216 // Even invalid patterns work with into_regex_value
217 let regex = RegexBuilder::new("[invalid").into_regex_value();
218 assert_eq!(regex.pattern, "[invalid");
219 }
220
221 #[test]
222 fn test_clone() {
223 let builder = RegexBuilder::new("test").case_insensitive();
224 let cloned = builder.clone();
225 assert_eq!(cloned.pattern, "test");
226 assert!(cloned.case_insensitive);
227 }
228}