Skip to main content

flat/
config.rs

1use crate::tokens::TokenizerKind;
2use globset::GlobMatcher;
3use std::path::PathBuf;
4
5#[derive(Debug, Clone)]
6pub struct Config {
7    pub path: PathBuf,
8    pub include_extensions: Option<Vec<String>>,
9    pub exclude_extensions: Option<Vec<String>>,
10    pub match_patterns: Option<Vec<GlobMatcher>>,
11    pub output_file: Option<PathBuf>,
12    pub dry_run: bool,
13    pub stats_only: bool,
14    pub gitignore_path: Option<PathBuf>,
15    pub max_file_size: u64,
16    pub compress: bool,
17    pub full_match_patterns: Option<Vec<GlobMatcher>>,
18    pub token_budget: Option<usize>,
19    pub tokenizer: TokenizerKind,
20}
21
22impl Default for Config {
23    fn default() -> Self {
24        Self {
25            path: PathBuf::from("."),
26            include_extensions: None,
27            exclude_extensions: None,
28            match_patterns: None,
29            output_file: None,
30            dry_run: false,
31            stats_only: false,
32            gitignore_path: None,
33            max_file_size: 1024 * 1024, // 1MB
34            compress: false,
35            full_match_patterns: None,
36            token_budget: None,
37            tokenizer: TokenizerKind::default(),
38        }
39    }
40}
41
42impl Config {
43    pub fn should_include_extension(&self, ext: &str) -> bool {
44        // If include list is specified, extension must be in it
45        if let Some(ref include) = self.include_extensions {
46            if !include.iter().any(|e| e.eq_ignore_ascii_case(ext)) {
47                return false;
48            }
49        }
50
51        // If exclude list is specified, extension must not be in it
52        if let Some(ref exclude) = self.exclude_extensions {
53            if exclude.iter().any(|e| e.eq_ignore_ascii_case(ext)) {
54                return false;
55            }
56        }
57
58        true
59    }
60
61    /// Check if a file name matches any of the configured glob patterns.
62    /// Returns true if no patterns are set or if the name matches at least one pattern.
63    pub fn should_include_by_match(&self, file_name: &str) -> bool {
64        match &self.match_patterns {
65            Some(patterns) => patterns.iter().any(|m| m.is_match(file_name)),
66            None => true,
67        }
68    }
69
70    /// Check if a file should always get full content (skip compression).
71    /// Returns true if --full-match patterns are set and the file name matches.
72    pub fn is_full_match(&self, file_name: &str) -> bool {
73        match &self.full_match_patterns {
74            Some(patterns) => patterns.iter().any(|m| m.is_match(file_name)),
75            None => false,
76        }
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use globset::Glob;
84
85    #[test]
86    fn test_include_only() {
87        let config = Config {
88            include_extensions: Some(vec!["rs".to_string(), "toml".to_string()]),
89            ..Default::default()
90        };
91
92        assert!(config.should_include_extension("rs"));
93        assert!(config.should_include_extension("toml"));
94        assert!(!config.should_include_extension("json"));
95    }
96
97    #[test]
98    fn test_exclude_only() {
99        let config = Config {
100            exclude_extensions: Some(vec!["test".to_string(), "json".to_string()]),
101            ..Default::default()
102        };
103
104        assert!(config.should_include_extension("rs"));
105        assert!(!config.should_include_extension("test"));
106        assert!(!config.should_include_extension("json"));
107    }
108
109    #[test]
110    fn test_include_and_exclude() {
111        let config = Config {
112            include_extensions: Some(vec!["rs".to_string(), "toml".to_string()]),
113            exclude_extensions: Some(vec!["toml".to_string()]),
114            ..Default::default()
115        };
116
117        assert!(config.should_include_extension("rs"));
118        assert!(!config.should_include_extension("toml")); // Excluded even though included
119        assert!(!config.should_include_extension("json"));
120    }
121
122    #[test]
123    fn test_match_no_patterns() {
124        let config = Config::default();
125        assert!(config.should_include_by_match("anything.rs"));
126    }
127
128    #[test]
129    fn test_match_single_pattern() {
130        let config = Config {
131            match_patterns: Some(vec![Glob::new("*_test.go").unwrap().compile_matcher()]),
132            ..Default::default()
133        };
134
135        assert!(config.should_include_by_match("user_test.go"));
136        assert!(config.should_include_by_match("auth_test.go"));
137        assert!(!config.should_include_by_match("main.go"));
138        assert!(!config.should_include_by_match("test.rs"));
139    }
140
141    #[test]
142    fn test_match_multiple_patterns() {
143        let config = Config {
144            match_patterns: Some(vec![
145                Glob::new("*_test.go").unwrap().compile_matcher(),
146                Glob::new("*.spec.js").unwrap().compile_matcher(),
147            ]),
148            ..Default::default()
149        };
150
151        assert!(config.should_include_by_match("user_test.go"));
152        assert!(config.should_include_by_match("button.spec.js"));
153        assert!(!config.should_include_by_match("main.go"));
154    }
155}