1use regex::Regex;
13use serde::{Deserialize, Serialize};
14
15#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
34pub struct CaseOptions {
35 pub ignore_case: bool,
37 pub ignore_word_separate: bool,
39}
40
41impl CaseOptions {
42 pub fn new() -> Self {
44 Self::default()
45 }
46
47 pub fn with_ignore_case(mut self) -> Self {
49 self.ignore_case = true;
50 self
51 }
52
53 pub fn with_ignore_word_separate(mut self) -> Self {
55 self.ignore_word_separate = true;
56 self
57 }
58
59 pub fn is_default(&self) -> bool {
61 !self.ignore_case && !self.ignore_word_separate
62 }
63}
64
65pub 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 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 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 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
126fn 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(¤t)
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(¤t)
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 if !current.is_empty() {
167 result.extend(
168 normalize_to_words(¤t)
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(¤t)
186 .into_iter()
187 .map(PatternWord::Literal),
188 );
189 }
190
191 result
192}
193
194#[derive(Debug, Clone, PartialEq, Eq)]
196enum PatternWord {
197 Literal(String),
199 AnyWords,
201 AnyChar,
203}
204
205fn 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 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 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 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#[derive(Debug, Clone)]
285pub enum Pattern {
286 Glob(GlobPattern),
288 Regex(RegexPattern),
290 Exact(ExactPattern),
292}
293
294#[derive(Debug, Clone)]
296pub struct GlobPattern {
297 pattern: String,
298 compiled: glob::Pattern,
299 case_options: CaseOptions,
300 normalized_words: Option<Vec<PatternWord>>,
302}
303
304#[derive(Debug, Clone)]
306pub struct RegexPattern {
307 pattern: String,
308 compiled: Regex,
309 case_options: CaseOptions,
310}
311
312#[derive(Debug, Clone)]
314pub struct ExactPattern {
315 pattern: String,
316 case_options: CaseOptions,
317 normalized_words: Option<Vec<String>>,
319}
320
321#[derive(Debug, Clone, thiserror::Error)]
323pub enum PatternError {
324 #[error("invalid glob pattern: {0}")]
327 InvalidGlob(String),
328
329 #[error("invalid regex pattern: {0}")]
332 InvalidRegex(String),
333}
334
335impl Pattern {
336 pub fn glob(pattern: impl Into<String>) -> Self {
352 Self::glob_with_options(pattern, CaseOptions::default())
353 }
354
355 pub fn glob_with_options(pattern: impl Into<String>, case_options: CaseOptions) -> Self {
367 let pattern: String = pattern.into();
368
369 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 pub fn regex(pattern: impl Into<String>) -> Result<Self, PatternError> {
408 Self::regex_with_options(pattern, CaseOptions::default())
409 }
410
411 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 pub fn exact(name: impl Into<String>) -> Self {
454 Self::exact_with_options(name, CaseOptions::default())
455 }
456
457 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 pub fn matches(&self, s: &str) -> bool {
485 match self {
486 Pattern::Glob(g) => {
487 if g.case_options.ignore_word_separate {
488 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 let joined_target = target_words.join("");
501 g.compiled.matches(&joined_target)
502 } else if g.case_options.ignore_case {
503 g.compiled.matches(&s.to_ascii_lowercase())
505 } else {
506 g.compiled.matches(s)
508 }
509 }
510 Pattern::Regex(r) => {
511 r.compiled.is_match(s)
514 }
515 Pattern::Exact(e) => {
516 if e.case_options.ignore_word_separate {
517 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 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 s.eq_ignore_ascii_case(&e.pattern)
537 } else {
538 s == e.pattern
540 }
541 }
542 }
543 }
544
545 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 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 pub fn is_glob(&self) -> bool {
565 matches!(self, Pattern::Glob(_))
566 }
567
568 pub fn is_regex(&self) -> bool {
570 matches!(self, Pattern::Regex(_))
571 }
572
573 pub fn is_exact(&self) -> bool {
575 matches!(self, Pattern::Exact(_))
576 }
577
578 pub fn has_wildcards(&self) -> bool {
580 match self {
581 Pattern::Glob(g) => g.pattern.contains('*') || g.pattern.contains('?'),
582 Pattern::Regex(_) => true, Pattern::Exact(_) => false,
584 }
585 }
586}
587
588impl 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 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 #[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 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 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 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}