Skip to main content

ryo_analysis/
pattern.rs

1//! Pattern - Pattern matching expressions for symbol discovery.
2//!
3//! Supports glob patterns (`*Config`, `get_*`) and regex patterns.
4//!
5//! # Case Options
6//!
7//! Patterns support two case-related options:
8//! - `ignore_case`: Ignore ASCII case (A-Z == a-z)
9//! - `ignore_word_separate`: Ignore word separators and casing style
10//!   (snake_case == camelCase == PascalCase)
11
12use regex::Regex;
13use serde::{Deserialize, Serialize};
14
15// =============================================================================
16// CaseOptions
17// =============================================================================
18
19/// Case matching options for patterns.
20///
21/// # Examples
22/// ```
23/// use ryo_analysis::pattern::CaseOptions;
24///
25/// // Default: case-sensitive
26/// let opts = CaseOptions::default();
27/// assert!(!opts.ignore_case);
28///
29/// // Case-insensitive
30/// let opts = CaseOptions::new().with_ignore_case();
31/// assert!(opts.ignore_case);
32/// ```
33#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
34pub struct CaseOptions {
35    /// Ignore ASCII case (A-Z == a-z)
36    pub ignore_case: bool,
37    /// Ignore word separators and casing style (snake_case == camelCase == PascalCase)
38    pub ignore_word_separate: bool,
39}
40
41impl CaseOptions {
42    /// Create new case options with defaults (case-sensitive).
43    pub fn new() -> Self {
44        Self::default()
45    }
46
47    /// Enable case-insensitive matching.
48    pub fn with_ignore_case(mut self) -> Self {
49        self.ignore_case = true;
50        self
51    }
52
53    /// Enable word-separate-insensitive matching.
54    pub fn with_ignore_word_separate(mut self) -> Self {
55        self.ignore_word_separate = true;
56        self
57    }
58
59    /// Check if any case option is enabled.
60    pub fn is_default(&self) -> bool {
61        !self.ignore_case && !self.ignore_word_separate
62    }
63}
64
65// =============================================================================
66// Word Normalization Utilities
67// =============================================================================
68
69/// Normalize an identifier to lowercase words.
70///
71/// Splits on underscores and camelCase boundaries, then lowercases.
72///
73/// # Examples
74/// ```
75/// use ryo_analysis::pattern::normalize_to_words;
76///
77/// assert_eq!(normalize_to_words("get_user_name"), vec!["get", "user", "name"]);
78/// assert_eq!(normalize_to_words("getUserName"), vec!["get", "user", "name"]);
79/// assert_eq!(normalize_to_words("GetUserName"), vec!["get", "user", "name"]);
80/// assert_eq!(normalize_to_words("HTTPClient"), vec!["http", "client"]);
81/// assert_eq!(normalize_to_words("parseJSON"), vec!["parse", "json"]);
82/// ```
83pub fn normalize_to_words(s: &str) -> Vec<String> {
84    let mut words = Vec::new();
85    let mut current_word = String::new();
86
87    let chars: Vec<char> = s.chars().collect();
88    let len = chars.len();
89
90    for i in 0..len {
91        let c = chars[i];
92
93        if c == '_' {
94            // Underscore: word boundary
95            if !current_word.is_empty() {
96                words.push(current_word.to_ascii_lowercase());
97                current_word.clear();
98            }
99        } else if c.is_ascii_uppercase() {
100            // Check for camelCase boundary
101            let prev_lower = i > 0 && chars[i - 1].is_ascii_lowercase();
102            let next_lower = i + 1 < len && chars[i + 1].is_ascii_lowercase();
103
104            // Start new word if:
105            // - Previous char was lowercase (camelCase: "getName" -> "get" | "Name")
106            // - Or this is start of a new word in ALLCAPS sequence (HTTPClient -> HTTP | Client)
107            if (prev_lower || (i > 0 && !current_word.is_empty() && next_lower))
108                && !current_word.is_empty()
109            {
110                words.push(current_word.to_ascii_lowercase());
111                current_word.clear();
112            }
113            current_word.push(c);
114        } else {
115            current_word.push(c);
116        }
117    }
118
119    if !current_word.is_empty() {
120        words.push(current_word.to_ascii_lowercase());
121    }
122
123    words
124}
125
126/// Convert a glob pattern to normalized word pattern.
127///
128/// Handles wildcards specially:
129/// - `*` becomes a marker for "match any words"
130/// - `?` becomes a marker for "match any single char in word"
131fn normalize_pattern_to_words(pattern: &str) -> Vec<PatternWord> {
132    let mut result = Vec::new();
133    let mut current = String::new();
134    let mut in_wildcard_seq = false;
135
136    for c in pattern.chars() {
137        match c {
138            '*' => {
139                if !current.is_empty() {
140                    result.extend(
141                        normalize_to_words(&current)
142                            .into_iter()
143                            .map(PatternWord::Literal),
144                    );
145                    current.clear();
146                }
147                if !in_wildcard_seq {
148                    result.push(PatternWord::AnyWords);
149                    in_wildcard_seq = true;
150                }
151            }
152            '?' => {
153                if !current.is_empty() {
154                    result.extend(
155                        normalize_to_words(&current)
156                            .into_iter()
157                            .map(PatternWord::Literal),
158                    );
159                    current.clear();
160                }
161                result.push(PatternWord::AnyChar);
162                in_wildcard_seq = false;
163            }
164            '_' => {
165                // Underscore in pattern is treated as word separator
166                if !current.is_empty() {
167                    result.extend(
168                        normalize_to_words(&current)
169                            .into_iter()
170                            .map(PatternWord::Literal),
171                    );
172                    current.clear();
173                }
174                in_wildcard_seq = false;
175            }
176            _ => {
177                current.push(c);
178                in_wildcard_seq = false;
179            }
180        }
181    }
182
183    if !current.is_empty() {
184        result.extend(
185            normalize_to_words(&current)
186                .into_iter()
187                .map(PatternWord::Literal),
188        );
189    }
190
191    result
192}
193
194/// A word in a normalized pattern.
195#[derive(Debug, Clone, PartialEq, Eq)]
196enum PatternWord {
197    /// Literal word to match
198    Literal(String),
199    /// Match any sequence of words (*)
200    AnyWords,
201    /// Match any single character (?)
202    AnyChar,
203}
204
205/// Match normalized pattern words against target words.
206fn match_word_pattern(pattern: &[PatternWord], target: &[String]) -> bool {
207    match_word_pattern_recursive(pattern, target, 0, 0)
208}
209
210fn match_word_pattern_recursive(
211    pattern: &[PatternWord],
212    target: &[String],
213    pi: usize,
214    ti: usize,
215) -> bool {
216    // Base cases
217    if pi == pattern.len() && ti == target.len() {
218        return true;
219    }
220    if pi == pattern.len() {
221        return false;
222    }
223
224    match &pattern[pi] {
225        PatternWord::AnyWords => {
226            // Try matching 0 or more words
227            for skip in 0..=(target.len() - ti) {
228                if match_word_pattern_recursive(pattern, target, pi + 1, ti + skip) {
229                    return true;
230                }
231            }
232            false
233        }
234        PatternWord::Literal(word) => {
235            if ti < target.len() && target[ti] == *word {
236                match_word_pattern_recursive(pattern, target, pi + 1, ti + 1)
237            } else {
238                false
239            }
240        }
241        PatternWord::AnyChar => {
242            // ? matches any single char - in word context, match word with single char diff
243            // For simplicity, treat as matching any single word
244            if ti < target.len() {
245                match_word_pattern_recursive(pattern, target, pi + 1, ti + 1)
246            } else {
247                false
248            }
249        }
250    }
251}
252
253/// Pattern for matching symbol names.
254///
255/// # Examples
256/// ```
257/// use ryo_analysis::Pattern;
258///
259/// // Glob pattern
260/// let pattern = Pattern::glob("*Config");
261/// assert!(pattern.matches("AppConfig"));
262/// assert!(pattern.matches("UserConfig"));
263/// assert!(!pattern.matches("config"));
264///
265/// // Regex pattern
266/// let pattern = Pattern::regex(r"^(get|set)_.*").unwrap();
267/// assert!(pattern.matches("get_name"));
268/// assert!(pattern.matches("set_value"));
269/// assert!(!pattern.matches("fetch_data"));
270///
271/// // Case-insensitive
272/// let pattern = Pattern::glob_with_options("*config",
273///     ryo_analysis::pattern::CaseOptions::new().with_ignore_case());
274/// assert!(pattern.matches("AppConfig"));
275/// assert!(pattern.matches("APPCONFIG"));
276///
277/// // Word-separate-insensitive
278/// let pattern = Pattern::glob_with_options("get_user*",
279///     ryo_analysis::pattern::CaseOptions::new().with_ignore_word_separate());
280/// assert!(pattern.matches("get_user_name"));
281/// assert!(pattern.matches("getUserName"));
282/// assert!(pattern.matches("GetUserName"));
283/// ```
284#[derive(Debug, Clone)]
285pub enum Pattern {
286    /// Glob pattern (supports `*` and `?` wildcards).
287    Glob(GlobPattern),
288    /// Regular expression pattern.
289    Regex(RegexPattern),
290    /// Exact match.
291    Exact(ExactPattern),
292}
293
294/// Glob pattern implementation.
295#[derive(Debug, Clone)]
296pub struct GlobPattern {
297    pattern: String,
298    compiled: glob::Pattern,
299    case_options: CaseOptions,
300    /// Pre-computed normalized words for word-separate matching
301    normalized_words: Option<Vec<PatternWord>>,
302}
303
304/// Regex pattern implementation.
305#[derive(Debug, Clone)]
306pub struct RegexPattern {
307    pattern: String,
308    compiled: Regex,
309    case_options: CaseOptions,
310}
311
312/// Exact pattern implementation.
313#[derive(Debug, Clone)]
314pub struct ExactPattern {
315    pattern: String,
316    case_options: CaseOptions,
317    /// Pre-computed normalized words for word-separate matching
318    normalized_words: Option<Vec<String>>,
319}
320
321/// Error during pattern compilation.
322#[derive(Debug, Clone, thiserror::Error)]
323pub enum PatternError {
324    /// The supplied glob pattern failed to compile. Carries the rendered
325    /// error from the underlying glob engine.
326    #[error("invalid glob pattern: {0}")]
327    InvalidGlob(String),
328
329    /// The supplied regex pattern failed to compile. Carries the rendered
330    /// error from the underlying regex engine.
331    #[error("invalid regex pattern: {0}")]
332    InvalidRegex(String),
333}
334
335impl Pattern {
336    /// Create a glob pattern.
337    ///
338    /// Supports:
339    /// - `*`: Matches any sequence of characters
340    /// - `?`: Matches any single character
341    /// - `[abc]`: Matches any character in the brackets
342    /// - `[!abc]`: Matches any character not in the brackets
343    ///
344    /// # Examples
345    /// ```
346    /// use ryo_analysis::Pattern;
347    ///
348    /// let pattern = Pattern::glob("*Config");
349    /// assert!(pattern.matches("AppConfig"));
350    /// ```
351    pub fn glob(pattern: impl Into<String>) -> Self {
352        Self::glob_with_options(pattern, CaseOptions::default())
353    }
354
355    /// Create a glob pattern with case options.
356    ///
357    /// # Examples
358    /// ```
359    /// use ryo_analysis::{Pattern, pattern::CaseOptions};
360    ///
361    /// let pattern = Pattern::glob_with_options("*config",
362    ///     CaseOptions::new().with_ignore_case());
363    /// assert!(pattern.matches("AppConfig"));
364    /// assert!(pattern.matches("APPCONFIG"));
365    /// ```
366    pub fn glob_with_options(pattern: impl Into<String>, case_options: CaseOptions) -> Self {
367        let pattern: String = pattern.into();
368
369        // For case-insensitive glob, we need to compile lowercase pattern
370        let compile_pattern = if case_options.ignore_case && !case_options.ignore_word_separate {
371            pattern.to_ascii_lowercase()
372        } else {
373            pattern.clone()
374        };
375
376        match glob::Pattern::new(&compile_pattern) {
377            Ok(compiled) => {
378                let normalized_words = if case_options.ignore_word_separate {
379                    Some(normalize_pattern_to_words(&pattern))
380                } else {
381                    None
382                };
383
384                Pattern::Glob(GlobPattern {
385                    pattern,
386                    compiled,
387                    case_options,
388                    normalized_words,
389                })
390            }
391            Err(_) => Self::exact_with_options(pattern, case_options),
392        }
393    }
394
395    /// Create a regex pattern.
396    ///
397    /// # Errors
398    /// Returns `Err(PatternError::InvalidRegex)` if the pattern is invalid.
399    ///
400    /// # Examples
401    /// ```
402    /// use ryo_analysis::Pattern;
403    ///
404    /// let pattern = Pattern::regex(r"^(get|set)_.*").unwrap();
405    /// assert!(pattern.matches("get_name"));
406    /// ```
407    pub fn regex(pattern: impl Into<String>) -> Result<Self, PatternError> {
408        Self::regex_with_options(pattern, CaseOptions::default())
409    }
410
411    /// Create a regex pattern with case options.
412    ///
413    /// # Examples
414    /// ```
415    /// use ryo_analysis::{Pattern, pattern::CaseOptions};
416    ///
417    /// let pattern = Pattern::regex_with_options(r"config",
418    ///     CaseOptions::new().with_ignore_case()).unwrap();
419    /// assert!(pattern.matches("AppConfig"));
420    /// ```
421    pub fn regex_with_options(
422        pattern: impl Into<String>,
423        case_options: CaseOptions,
424    ) -> Result<Self, PatternError> {
425        let pattern: String = pattern.into();
426
427        let compiled = if case_options.ignore_case {
428            regex::RegexBuilder::new(&pattern)
429                .case_insensitive(true)
430                .build()
431                .map_err(|e| PatternError::InvalidRegex(e.to_string()))?
432        } else {
433            Regex::new(&pattern).map_err(|e| PatternError::InvalidRegex(e.to_string()))?
434        };
435
436        Ok(Pattern::Regex(RegexPattern {
437            pattern,
438            compiled,
439            case_options,
440        }))
441    }
442
443    /// Create an exact match pattern.
444    ///
445    /// # Examples
446    /// ```
447    /// use ryo_analysis::Pattern;
448    ///
449    /// let pattern = Pattern::exact("Config");
450    /// assert!(pattern.matches("Config"));
451    /// assert!(!pattern.matches("config"));
452    /// ```
453    pub fn exact(name: impl Into<String>) -> Self {
454        Self::exact_with_options(name, CaseOptions::default())
455    }
456
457    /// Create an exact match pattern with case options.
458    ///
459    /// # Examples
460    /// ```
461    /// use ryo_analysis::{Pattern, pattern::CaseOptions};
462    ///
463    /// let pattern = Pattern::exact_with_options("config",
464    ///     CaseOptions::new().with_ignore_case());
465    /// assert!(pattern.matches("Config"));
466    /// assert!(pattern.matches("CONFIG"));
467    /// ```
468    pub fn exact_with_options(name: impl Into<String>, case_options: CaseOptions) -> Self {
469        let pattern: String = name.into();
470        let normalized_words = if case_options.ignore_word_separate {
471            Some(normalize_to_words(&pattern))
472        } else {
473            None
474        };
475
476        Pattern::Exact(ExactPattern {
477            pattern,
478            case_options,
479            normalized_words,
480        })
481    }
482
483    /// Check if the pattern matches a string.
484    pub fn matches(&self, s: &str) -> bool {
485        match self {
486            Pattern::Glob(g) => {
487                if g.case_options.ignore_word_separate {
488                    // Word-separate matching
489                    let target_words = normalize_to_words(s);
490                    let word_match = match &g.normalized_words {
491                        Some(pattern_words) => match_word_pattern(pattern_words, &target_words),
492                        None => false,
493                    };
494                    if word_match {
495                        return true;
496                    }
497                    // Fallback: compare pattern against joined target words
498                    // This handles cases like pattern "astregapply" matching "ASTRegApply"
499                    // where the pattern has no word boundaries but target does
500                    let joined_target = target_words.join("");
501                    g.compiled.matches(&joined_target)
502                } else if g.case_options.ignore_case {
503                    // Simple case-insensitive: lowercase both and match
504                    g.compiled.matches(&s.to_ascii_lowercase())
505                } else {
506                    // Default: case-sensitive
507                    g.compiled.matches(s)
508                }
509            }
510            Pattern::Regex(r) => {
511                // Regex handles case_insensitive via compiled regex
512                // word_separate not supported for regex (complex to implement)
513                r.compiled.is_match(s)
514            }
515            Pattern::Exact(e) => {
516                if e.case_options.ignore_word_separate {
517                    // Word-separate matching
518                    let target_words = normalize_to_words(s);
519                    let word_match = match &e.normalized_words {
520                        Some(pattern_words) => &target_words == pattern_words,
521                        None => false,
522                    };
523                    if word_match {
524                        return true;
525                    }
526                    // Fallback: compare pattern against joined target words
527                    // This handles cases like pattern "astregapply" matching "ASTRegApply"
528                    let joined_target = target_words.join("");
529                    if e.case_options.ignore_case {
530                        joined_target.eq_ignore_ascii_case(&e.pattern)
531                    } else {
532                        joined_target == e.pattern
533                    }
534                } else if e.case_options.ignore_case {
535                    // Simple case-insensitive
536                    s.eq_ignore_ascii_case(&e.pattern)
537                } else {
538                    // Default: case-sensitive
539                    s == e.pattern
540                }
541            }
542        }
543    }
544
545    /// Get the pattern string.
546    pub fn as_str(&self) -> &str {
547        match self {
548            Pattern::Glob(g) => &g.pattern,
549            Pattern::Regex(r) => &r.pattern,
550            Pattern::Exact(e) => &e.pattern,
551        }
552    }
553
554    /// Get the case options for this pattern.
555    pub fn case_options(&self) -> CaseOptions {
556        match self {
557            Pattern::Glob(g) => g.case_options,
558            Pattern::Regex(r) => r.case_options,
559            Pattern::Exact(e) => e.case_options,
560        }
561    }
562
563    /// Check if this is a glob pattern.
564    pub fn is_glob(&self) -> bool {
565        matches!(self, Pattern::Glob(_))
566    }
567
568    /// Check if this is a regex pattern.
569    pub fn is_regex(&self) -> bool {
570        matches!(self, Pattern::Regex(_))
571    }
572
573    /// Check if this is an exact match pattern.
574    pub fn is_exact(&self) -> bool {
575        matches!(self, Pattern::Exact(_))
576    }
577
578    /// Check if this pattern contains wildcards.
579    pub fn has_wildcards(&self) -> bool {
580        match self {
581            Pattern::Glob(g) => g.pattern.contains('*') || g.pattern.contains('?'),
582            Pattern::Regex(_) => true, // Regex always has pattern matching
583            Pattern::Exact(_) => false,
584        }
585    }
586}
587
588// Manual Serialize/Deserialize to handle compiled patterns
589impl Serialize for Pattern {
590    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
591    where
592        S: serde::Serializer,
593    {
594        use serde::ser::SerializeStruct;
595
596        // Determine number of fields based on case options
597        let case_opts = self.case_options();
598        let has_case_opts = !case_opts.is_default();
599        let field_count = if has_case_opts { 4 } else { 2 };
600
601        let mut state = serializer.serialize_struct("Pattern", field_count)?;
602        match self {
603            Pattern::Glob(g) => {
604                state.serialize_field("type", "glob")?;
605                state.serialize_field("pattern", &g.pattern)?;
606            }
607            Pattern::Regex(r) => {
608                state.serialize_field("type", "regex")?;
609                state.serialize_field("pattern", &r.pattern)?;
610            }
611            Pattern::Exact(e) => {
612                state.serialize_field("type", "exact")?;
613                state.serialize_field("pattern", &e.pattern)?;
614            }
615        }
616
617        if has_case_opts {
618            state.serialize_field("ignore_case", &case_opts.ignore_case)?;
619            state.serialize_field("ignore_word_separate", &case_opts.ignore_word_separate)?;
620        }
621
622        state.end()
623    }
624}
625
626impl<'de> Deserialize<'de> for Pattern {
627    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
628    where
629        D: serde::Deserializer<'de>,
630    {
631        #[derive(Deserialize)]
632        struct PatternData {
633            #[serde(rename = "type")]
634            pattern_type: String,
635            pattern: String,
636            #[serde(default)]
637            ignore_case: bool,
638            #[serde(default)]
639            ignore_word_separate: bool,
640        }
641
642        let data = PatternData::deserialize(deserializer)?;
643        let case_options = CaseOptions {
644            ignore_case: data.ignore_case,
645            ignore_word_separate: data.ignore_word_separate,
646        };
647
648        match data.pattern_type.as_str() {
649            "glob" => Ok(Pattern::glob_with_options(data.pattern, case_options)),
650            "regex" => Pattern::regex_with_options(data.pattern, case_options)
651                .map_err(serde::de::Error::custom),
652            "exact" => Ok(Pattern::exact_with_options(data.pattern, case_options)),
653            other => Err(serde::de::Error::custom(format!(
654                "unknown pattern type: {}",
655                other
656            ))),
657        }
658    }
659}
660
661#[cfg(test)]
662mod tests {
663    use super::*;
664
665    #[test]
666    fn test_glob_suffix() {
667        let pattern = Pattern::glob("*Config");
668        assert!(pattern.matches("AppConfig"));
669        assert!(pattern.matches("UserConfig"));
670        assert!(pattern.matches("Config"));
671        assert!(!pattern.matches("config"));
672        assert!(!pattern.matches("ConfigManager"));
673    }
674
675    #[test]
676    fn test_glob_prefix() {
677        let pattern = Pattern::glob("get_*");
678        assert!(pattern.matches("get_name"));
679        assert!(pattern.matches("get_value"));
680        assert!(pattern.matches("get_"));
681        assert!(!pattern.matches("set_name"));
682    }
683
684    #[test]
685    fn test_glob_contains() {
686        let pattern = Pattern::glob("*Error*");
687        assert!(pattern.matches("ParseError"));
688        assert!(pattern.matches("ErrorHandler"));
689        assert!(pattern.matches("MyErrorType"));
690        assert!(!pattern.matches("ParseException"));
691    }
692
693    #[test]
694    fn test_glob_question_mark() {
695        let pattern = Pattern::glob("get?");
696        assert!(pattern.matches("get1"));
697        assert!(pattern.matches("getX"));
698        assert!(!pattern.matches("get"));
699        assert!(!pattern.matches("get12"));
700    }
701
702    #[test]
703    fn test_regex_prefix() {
704        let pattern = Pattern::regex(r"^(get|set)_.*").unwrap();
705        assert!(pattern.matches("get_name"));
706        assert!(pattern.matches("set_value"));
707        assert!(!pattern.matches("fetch_data"));
708        assert!(!pattern.matches("reset_name"));
709    }
710
711    #[test]
712    fn test_regex_suffix() {
713        let pattern = Pattern::regex(r".*Error$").unwrap();
714        assert!(pattern.matches("ParseError"));
715        assert!(pattern.matches("NetworkError"));
716        assert!(!pattern.matches("ErrorHandler"));
717    }
718
719    #[test]
720    fn test_exact() {
721        let pattern = Pattern::exact("Config");
722        assert!(pattern.matches("Config"));
723        assert!(!pattern.matches("config"));
724        assert!(!pattern.matches("AppConfig"));
725    }
726
727    #[test]
728    fn test_has_wildcards() {
729        assert!(Pattern::glob("*Config").has_wildcards());
730        assert!(Pattern::glob("get?").has_wildcards());
731        assert!(!Pattern::glob("Config").has_wildcards());
732        assert!(Pattern::regex(r".*").unwrap().has_wildcards());
733        assert!(!Pattern::exact("Config").has_wildcards());
734    }
735
736    #[test]
737    fn test_invalid_regex() {
738        let result = Pattern::regex(r"[invalid");
739        assert!(matches!(result, Err(PatternError::InvalidRegex(_))));
740    }
741
742    #[test]
743    fn test_serde_glob() {
744        let pattern = Pattern::glob("*Config");
745        let json = serde_json::to_string(&pattern).unwrap();
746        let deserialized: Pattern = serde_json::from_str(&json).unwrap();
747        assert!(deserialized.matches("AppConfig"));
748    }
749
750    #[test]
751    fn test_serde_regex() {
752        let pattern = Pattern::regex(r"^get_.*").unwrap();
753        let json = serde_json::to_string(&pattern).unwrap();
754        let deserialized: Pattern = serde_json::from_str(&json).unwrap();
755        assert!(deserialized.matches("get_name"));
756    }
757
758    #[test]
759    fn test_serde_exact() {
760        let pattern = Pattern::exact("Config");
761        let json = serde_json::to_string(&pattern).unwrap();
762        let deserialized: Pattern = serde_json::from_str(&json).unwrap();
763        assert!(deserialized.matches("Config"));
764        assert!(!deserialized.matches("AppConfig"));
765    }
766
767    // ==========================================================================
768    // Case Options Tests
769    // ==========================================================================
770
771    #[test]
772    fn test_normalize_to_words_snake_case() {
773        assert_eq!(
774            normalize_to_words("get_user_name"),
775            vec!["get", "user", "name"]
776        );
777        assert_eq!(normalize_to_words("HTTP_CLIENT"), vec!["http", "client"]);
778    }
779
780    #[test]
781    fn test_normalize_to_words_camel_case() {
782        assert_eq!(
783            normalize_to_words("getUserName"),
784            vec!["get", "user", "name"]
785        );
786        assert_eq!(normalize_to_words("parseJSON"), vec!["parse", "json"]);
787    }
788
789    #[test]
790    fn test_normalize_to_words_pascal_case() {
791        assert_eq!(
792            normalize_to_words("GetUserName"),
793            vec!["get", "user", "name"]
794        );
795        assert_eq!(normalize_to_words("HTTPClient"), vec!["http", "client"]);
796    }
797
798    #[test]
799    fn test_normalize_to_words_mixed() {
800        assert_eq!(
801            normalize_to_words("myHTTPClient"),
802            vec!["my", "http", "client"]
803        );
804        assert_eq!(
805            normalize_to_words("XMLHttpRequest"),
806            vec!["xml", "http", "request"]
807        );
808    }
809
810    #[test]
811    fn test_glob_ignore_case() {
812        let pattern = Pattern::glob_with_options("*config", CaseOptions::new().with_ignore_case());
813        assert!(pattern.matches("AppConfig"));
814        assert!(pattern.matches("APPCONFIG"));
815        assert!(pattern.matches("appconfig"));
816        assert!(pattern.matches("Config"));
817        assert!(!pattern.matches("ConfigManager"));
818    }
819
820    #[test]
821    fn test_glob_ignore_word_separate() {
822        let pattern =
823            Pattern::glob_with_options("get_user*", CaseOptions::new().with_ignore_word_separate());
824        assert!(pattern.matches("get_user_name"));
825        assert!(pattern.matches("getUserName"));
826        assert!(pattern.matches("GetUserName"));
827        assert!(pattern.matches("GET_USER_NAME"));
828        assert!(!pattern.matches("fetch_user_name"));
829    }
830
831    #[test]
832    fn test_glob_ignore_word_separate_suffix() {
833        let pattern =
834            Pattern::glob_with_options("*Config", CaseOptions::new().with_ignore_word_separate());
835        assert!(pattern.matches("AppConfig"));
836        assert!(pattern.matches("app_config"));
837        assert!(pattern.matches("appConfig"));
838        assert!(pattern.matches("APP_CONFIG"));
839    }
840
841    #[test]
842    fn test_exact_ignore_case() {
843        let pattern = Pattern::exact_with_options("config", CaseOptions::new().with_ignore_case());
844        assert!(pattern.matches("Config"));
845        assert!(pattern.matches("CONFIG"));
846        assert!(pattern.matches("config"));
847        assert!(!pattern.matches("AppConfig"));
848    }
849
850    #[test]
851    fn test_exact_ignore_word_separate() {
852        let pattern = Pattern::exact_with_options(
853            "get_user_name",
854            CaseOptions::new().with_ignore_word_separate(),
855        );
856        assert!(pattern.matches("get_user_name"));
857        assert!(pattern.matches("getUserName"));
858        assert!(pattern.matches("GetUserName"));
859        assert!(!pattern.matches("get_user"));
860    }
861
862    #[test]
863    fn test_regex_ignore_case() {
864        let pattern =
865            Pattern::regex_with_options(r"config", CaseOptions::new().with_ignore_case()).unwrap();
866        assert!(pattern.matches("AppConfig"));
867        assert!(pattern.matches("APPCONFIG"));
868        assert!(pattern.matches("appconfig"));
869    }
870
871    #[test]
872    fn test_serde_with_case_options() {
873        let pattern = Pattern::glob_with_options("*config", CaseOptions::new().with_ignore_case());
874        let json = serde_json::to_string(&pattern).unwrap();
875        assert!(json.contains("ignore_case"));
876
877        let deserialized: Pattern = serde_json::from_str(&json).unwrap();
878        assert!(deserialized.matches("AppConfig"));
879        assert!(deserialized.matches("APPCONFIG"));
880    }
881
882    #[test]
883    fn test_case_options_builder() {
884        let opts = CaseOptions::new()
885            .with_ignore_case()
886            .with_ignore_word_separate();
887        assert!(opts.ignore_case);
888        assert!(opts.ignore_word_separate);
889        assert!(!opts.is_default());
890
891        let default = CaseOptions::default();
892        assert!(default.is_default());
893    }
894
895    #[test]
896    fn test_ignore_word_separate_with_no_boundaries_in_pattern() {
897        // Pattern has no word boundaries (all lowercase), but target has PascalCase
898        // Should match by joining target words: ASTRegApply -> ["ast", "reg", "apply"] -> "astregapply"
899        let pattern = Pattern::exact_with_options(
900            "astregapply",
901            CaseOptions::new().with_ignore_word_separate(),
902        );
903        assert!(pattern.matches("ASTRegApply"));
904        assert!(pattern.matches("ast_reg_apply"));
905        assert!(pattern.matches("astRegApply"));
906
907        // Glob pattern should also work
908        let pattern = Pattern::glob_with_options(
909            "astregapply",
910            CaseOptions::new().with_ignore_word_separate(),
911        );
912        assert!(pattern.matches("ASTRegApply"));
913        assert!(pattern.matches("ast_reg_apply"));
914        assert!(pattern.matches("AstRegApply"));
915
916        // With wildcard
917        let pattern =
918            Pattern::glob_with_options("*apply", CaseOptions::new().with_ignore_word_separate());
919        assert!(pattern.matches("ASTRegApply"));
920        assert!(pattern.matches("ast_reg_apply"));
921    }
922}