Skip to main content

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}