cstats_core/config/
mod.rs1use std::path::{Path, PathBuf};
4
5use serde::{Deserialize, Serialize};
6
7use crate::{api::types::AnthropicConfig, Error, Result};
8
9#[derive(Debug, Clone, Default, Serialize, Deserialize)]
11pub struct Config {
12 pub database: DatabaseConfig,
14
15 pub api: ApiConfig,
17
18 pub cache: CacheConfig,
20
21 pub stats: StatsConfig,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct DatabaseConfig {
28 pub url: String,
30
31 pub max_connections: u32,
33
34 pub timeout_seconds: u64,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct ApiConfig {
41 pub base_url: Option<String>,
43
44 pub timeout_seconds: u64,
46
47 pub retry_attempts: u32,
49
50 pub anthropic: Option<AnthropicConfig>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct CacheConfig {
57 pub cache_dir: PathBuf,
59
60 pub max_size_bytes: u64,
62
63 pub ttl_seconds: u64,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct StatsConfig {
70 pub default_metrics: Vec<String>,
72
73 pub sampling_rate: f64,
75
76 pub aggregation_window_seconds: u64,
78}
79
80impl Default for DatabaseConfig {
81 fn default() -> Self {
82 Self {
83 url: "sqlite:./cstats.db".to_string(),
84 max_connections: 10,
85 timeout_seconds: 30,
86 }
87 }
88}
89
90impl Default for ApiConfig {
91 fn default() -> Self {
92 Self {
93 base_url: None,
94 timeout_seconds: 30,
95 retry_attempts: 3,
96 anthropic: None,
97 }
98 }
99}
100
101impl Default for CacheConfig {
102 fn default() -> Self {
103 Self {
104 cache_dir: dirs::cache_dir()
105 .unwrap_or_else(std::env::temp_dir)
106 .join("cstats"),
107 max_size_bytes: 100 * 1024 * 1024, ttl_seconds: 3600, }
110 }
111}
112
113impl Default for StatsConfig {
114 fn default() -> Self {
115 Self {
116 default_metrics: vec![
117 "execution_time".to_string(),
118 "memory_usage".to_string(),
119 "cpu_usage".to_string(),
120 ],
121 sampling_rate: 1.0,
122 aggregation_window_seconds: 300, }
124 }
125}
126
127#[cfg(test)]
128mod tests;
129
130impl Config {
131 pub fn default_config_dir() -> Result<PathBuf> {
133 dirs::home_dir()
134 .map(|home| home.join(".cstats"))
135 .ok_or_else(|| Error::config("Unable to determine home directory"))
136 }
137
138 pub fn default_config_path() -> Result<PathBuf> {
140 Ok(Self::default_config_dir()?.join("config.json"))
141 }
142
143 pub async fn load_from_file(path: &Path) -> Result<Self> {
145 if !path.exists() {
146 return Err(Error::config(format!(
147 "Configuration file does not exist: {}",
148 path.display()
149 )));
150 }
151
152 let content = tokio::fs::read_to_string(path)
153 .await
154 .map_err(|e| Error::config(format!("Failed to read config file: {}", e)))?;
155
156 let config: Config = serde_json::from_str(&content)
157 .map_err(|e| Error::config(format!("Failed to parse config file: {}", e)))?;
158
159 Ok(config)
160 }
161
162 pub async fn save_to_file(&self, path: &Path) -> Result<()> {
164 if let Some(parent) = path.parent() {
166 tokio::fs::create_dir_all(parent)
167 .await
168 .map_err(|e| Error::config(format!("Failed to create config directory: {}", e)))?;
169 }
170
171 let content = serde_json::to_string_pretty(self)
172 .map_err(|e| Error::config(format!("Failed to serialize config: {}", e)))?;
173
174 tokio::fs::write(path, content)
175 .await
176 .map_err(|e| Error::config(format!("Failed to write config file: {}", e)))?;
177
178 Ok(())
179 }
180
181 pub async fn load() -> Result<Self> {
183 let mut config = Self::default();
185
186 if let Ok(config_path) = Self::default_config_path() {
188 if config_path.exists() {
189 match Self::load_from_file(&config_path).await {
190 Ok(file_config) => {
191 config = file_config;
192 }
193 Err(e) => {
194 tracing::warn!(
195 "Failed to load config from {}: {}",
196 config_path.display(),
197 e
198 );
199 }
200 }
201 }
202 }
203
204 config.apply_env_overrides();
206
207 Ok(config)
208 }
209
210 pub async fn load_from_path(path: &Path) -> Result<Self> {
212 let mut config = if path.exists() {
213 Self::load_from_file(path).await?
214 } else {
215 Self::default()
216 };
217
218 config.apply_env_overrides();
220
221 Ok(config)
222 }
223
224 fn apply_env_overrides(&mut self) {
226 if let Ok(api_key) = std::env::var("ANTHROPIC_API_KEY") {
228 if !api_key.is_empty() {
229 let mut anthropic_config = self.api.anthropic.clone().unwrap_or_default();
230 anthropic_config.api_key = api_key;
231 self.api.anthropic = Some(anthropic_config);
232 }
233 }
234
235 if let Ok(db_url) = std::env::var("CSTATS_DATABASE_URL") {
237 self.database.url = db_url;
238 }
239
240 if let Ok(base_url) = std::env::var("CSTATS_API_BASE_URL") {
242 self.api.base_url = Some(base_url);
243 }
244 }
245
246 pub fn get_anthropic_api_key(&self) -> Option<&str> {
248 self.api
250 .anthropic
251 .as_ref()
252 .map(|config| config.api_key.as_str())
253 }
254
255 pub fn has_anthropic_api_key(&self) -> bool {
257 if let Ok(api_key) = std::env::var("ANTHROPIC_API_KEY") {
259 if !api_key.is_empty() {
260 return true;
261 }
262 }
263
264 self.api
266 .anthropic
267 .as_ref()
268 .map(|config| !config.api_key.is_empty())
269 .unwrap_or(false)
270 }
271
272 pub fn effective_anthropic_api_key(&self) -> Option<String> {
274 if let Ok(api_key) = std::env::var("ANTHROPIC_API_KEY") {
276 if !api_key.is_empty() {
277 return Some(api_key);
278 }
279 }
280
281 self.api.anthropic.as_ref().and_then(|config| {
283 if config.api_key.is_empty() {
284 None
285 } else {
286 Some(config.api_key.clone())
287 }
288 })
289 }
290
291 pub fn validate(&self) -> Result<()> {
293 let mut errors = Vec::new();
294
295 if self.database.url.is_empty() {
297 errors.push("Database URL cannot be empty".to_string());
298 }
299
300 if self.database.max_connections == 0 {
301 errors.push("Database max_connections must be greater than 0".to_string());
302 }
303
304 if self.database.timeout_seconds == 0 {
305 errors.push("Database timeout_seconds must be greater than 0".to_string());
306 }
307
308 if self.cache.max_size_bytes == 0 {
310 errors.push("Cache max_size_bytes must be greater than 0".to_string());
311 }
312
313 if self.cache.ttl_seconds == 0 {
314 errors.push("Cache ttl_seconds must be greater than 0".to_string());
315 }
316
317 if !(0.0..=1.0).contains(&self.stats.sampling_rate) {
319 errors.push("Stats sampling_rate must be between 0.0 and 1.0".to_string());
320 }
321
322 if self.stats.aggregation_window_seconds == 0 {
323 errors.push("Stats aggregation_window_seconds must be greater than 0".to_string());
324 }
325
326 if let Some(ref anthropic) = self.api.anthropic {
328 if anthropic.timeout_seconds == 0 {
329 errors.push("Anthropic timeout_seconds must be greater than 0".to_string());
330 }
331
332 if anthropic.max_retry_delay_ms < anthropic.initial_retry_delay_ms {
333 errors.push(
334 "Anthropic max_retry_delay_ms must be >= initial_retry_delay_ms".to_string(),
335 );
336 }
337 }
338
339 if !errors.is_empty() {
340 return Err(Error::config(format!(
341 "Configuration validation failed:\n - {}",
342 errors.join("\n - ")
343 )));
344 }
345
346 Ok(())
347 }
348
349 pub fn from_env() -> Result<Self> {
351 let mut config = Self::default();
352 config.apply_env_overrides();
353 Ok(config)
354 }
355}