Skip to main content

chaser_cf/core/
config.rs

1//! Configuration for chaser-cf
2
3use crate::models::Profile;
4use std::env;
5use std::path::PathBuf;
6use std::time::Duration;
7
8/// Configuration for ChaserCF
9#[derive(Debug, Clone)]
10pub struct ChaserConfig {
11    /// Maximum concurrent browser contexts (default: 20)
12    pub context_limit: usize,
13
14    /// Request timeout in milliseconds (default: 60000)
15    pub timeout_ms: u64,
16
17    /// Stealth profile to use (default: Windows)
18    pub profile: Profile,
19
20    /// Whether to defer browser initialization until first use (default: false)
21    pub lazy_init: bool,
22
23    /// Path to Chrome/Chromium binary (default: auto-detect)
24    pub chrome_path: Option<PathBuf>,
25
26    /// Whether to run in headless mode (default: false for stealth)
27    pub headless: bool,
28
29    /// Browser viewport width (default: 1920)
30    pub viewport_width: u32,
31
32    /// Browser viewport height (default: 1080)
33    pub viewport_height: u32,
34
35    /// Extra command-line flags to pass to the Chrome process, appended
36    /// to chaser-cf's defaults (`--disable-blink-features=Automation-
37    /// Controlled`, `--disable-infobars`).
38    ///
39    /// Common values:
40    /// - `--no-sandbox` — required when the host process runs as root
41    ///   (e.g. systemd unit, Docker containers without `--user`),
42    ///   otherwise Chrome refuses to start with the message
43    ///   "Running as root without --no-sandbox is not supported".
44    /// - `--disable-gpu` — for headless servers without a GPU.
45    /// - `--disable-dev-shm-usage` — for /dev/shm-constrained containers.
46    ///
47    /// Default: empty (chaser-cf only sets its own minimum baseline flags).
48    pub extra_args: Vec<String>,
49}
50
51impl Default for ChaserConfig {
52    fn default() -> Self {
53        Self {
54            context_limit: 20,
55            timeout_ms: 60000,
56            profile: Profile::Windows,
57            lazy_init: false,
58            chrome_path: None,
59            headless: false,
60            viewport_width: 1920,
61            viewport_height: 1080,
62            extra_args: Vec::new(),
63        }
64    }
65}
66
67impl ChaserConfig {
68    /// Create configuration from environment variables
69    ///
70    /// Environment variables:
71    /// - `CHASER_CONTEXT_LIMIT`: Max concurrent contexts (default: 20)
72    /// - `CHASER_TIMEOUT`: Timeout in ms (default: 60000)
73    /// - `CHASER_PROFILE`: Profile name (windows/linux/macos)
74    /// - `CHASER_LAZY_INIT`: Enable lazy init (true/false)
75    /// - `CHROME_BIN`: Path to Chrome binary
76    /// - `CHASER_HEADLESS`: Run headless (true/false)
77    /// - `CHASER_EXTRA_ARGS`: Whitespace-separated Chrome flags appended to
78    ///   chaser-cf's defaults (e.g. `--no-sandbox --disable-gpu`)
79    pub fn from_env() -> Self {
80        let mut config = Self::default();
81
82        if let Ok(val) = env::var("CHASER_CONTEXT_LIMIT") {
83            if let Ok(limit) = val.parse() {
84                config.context_limit = limit;
85            }
86        }
87
88        if let Ok(val) = env::var("CHASER_TIMEOUT") {
89            if let Ok(timeout) = val.parse() {
90                config.timeout_ms = timeout;
91            }
92        }
93
94        if let Ok(val) = env::var("CHASER_PROFILE") {
95            if let Some(profile) = Profile::parse(&val) {
96                config.profile = profile;
97            }
98        }
99
100        if let Ok(val) = env::var("CHASER_LAZY_INIT") {
101            config.lazy_init = val.eq_ignore_ascii_case("true") || val == "1";
102        }
103
104        if let Ok(val) = env::var("CHROME_BIN") {
105            config.chrome_path = Some(PathBuf::from(val));
106        }
107
108        if let Ok(val) = env::var("CHASER_HEADLESS") {
109            config.headless = val.eq_ignore_ascii_case("true") || val == "1";
110        }
111
112        if let Ok(val) = env::var("CHASER_EXTRA_ARGS") {
113            config.extra_args = val.split_whitespace().map(|s| s.to_string()).collect();
114        }
115
116        config
117    }
118
119    /// Builder method: set context limit
120    pub fn with_context_limit(mut self, limit: usize) -> Self {
121        self.context_limit = limit;
122        self
123    }
124
125    /// Builder method: set timeout
126    pub fn with_timeout_ms(mut self, timeout: u64) -> Self {
127        self.timeout_ms = timeout;
128        self
129    }
130
131    /// Builder method: set timeout from Duration
132    pub fn with_timeout(mut self, timeout: Duration) -> Self {
133        self.timeout_ms = timeout.as_millis() as u64;
134        self
135    }
136
137    /// Builder method: set profile
138    pub fn with_profile(mut self, profile: Profile) -> Self {
139        self.profile = profile;
140        self
141    }
142
143    /// Builder method: enable lazy initialization
144    pub fn with_lazy_init(mut self, lazy: bool) -> Self {
145        self.lazy_init = lazy;
146        self
147    }
148
149    /// Builder method: set Chrome path
150    pub fn with_chrome_path(mut self, path: impl Into<PathBuf>) -> Self {
151        self.chrome_path = Some(path.into());
152        self
153    }
154
155    /// Builder method: set headless mode
156    pub fn with_headless(mut self, headless: bool) -> Self {
157        self.headless = headless;
158        self
159    }
160
161    /// Builder method: set viewport size
162    pub fn with_viewport(mut self, width: u32, height: u32) -> Self {
163        self.viewport_width = width;
164        self.viewport_height = height;
165        self
166    }
167
168    /// Builder method: replace the extra Chrome args set with the given list.
169    /// Use [`Self::add_extra_arg`] to append a single flag instead.
170    pub fn with_extra_args<I, S>(mut self, args: I) -> Self
171    where
172        I: IntoIterator<Item = S>,
173        S: Into<String>,
174    {
175        self.extra_args = args.into_iter().map(Into::into).collect();
176        self
177    }
178
179    /// Builder method: append a single Chrome flag to the existing extras.
180    /// Useful for chaining, e.g.
181    /// `ChaserConfig::default().add_extra_arg("--no-sandbox")`.
182    pub fn add_extra_arg(mut self, arg: impl Into<String>) -> Self {
183        self.extra_args.push(arg.into());
184        self
185    }
186
187    /// Get timeout as Duration
188    pub fn timeout(&self) -> Duration {
189        Duration::from_millis(self.timeout_ms)
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn test_default_config() {
199        let config = ChaserConfig::default();
200        assert_eq!(config.context_limit, 20);
201        assert_eq!(config.timeout_ms, 60000);
202        assert_eq!(config.profile, Profile::Windows);
203        assert!(!config.lazy_init);
204        assert!(!config.headless);
205        assert!(config.extra_args.is_empty());
206    }
207
208    #[test]
209    fn test_with_extra_args_replaces() {
210        let config = ChaserConfig::default().with_extra_args(["--no-sandbox", "--disable-gpu"]);
211        assert_eq!(config.extra_args, vec!["--no-sandbox", "--disable-gpu"]);
212        // with_extra_args replaces; calling again clears the previous set
213        let config2 = config.with_extra_args(vec!["--foo"]);
214        assert_eq!(config2.extra_args, vec!["--foo"]);
215    }
216
217    #[test]
218    fn test_add_extra_arg_appends() {
219        let config = ChaserConfig::default()
220            .add_extra_arg("--no-sandbox")
221            .add_extra_arg("--disable-gpu");
222        assert_eq!(config.extra_args, vec!["--no-sandbox", "--disable-gpu"]);
223    }
224
225    #[test]
226    fn test_builder_pattern() {
227        let config = ChaserConfig::default()
228            .with_context_limit(10)
229            .with_timeout_ms(30000)
230            .with_profile(Profile::Linux)
231            .with_lazy_init(true)
232            .with_headless(true);
233
234        assert_eq!(config.context_limit, 10);
235        assert_eq!(config.timeout_ms, 30000);
236        assert_eq!(config.profile, Profile::Linux);
237        assert!(config.lazy_init);
238        assert!(config.headless);
239    }
240}